A developer's guide to monetizing AI agents with the Nevermined x402 Express middleware
If you've built an AI agent with Express.js, you've probably struggled with the payment problem. How do you:
- Charge users per request?
- Verify they have credits before executing expensive AI calls?
- Track usage and settle payments?
- Handle all the edge cases (expired tokens, insufficient balance, failed settlements)?
The traditional approach involves dozens of lines of boilerplate: checking auth headers, calling payment APIs, wrapping everything in try/catch blocks, and hoping you didn't miss an edge case.
What if you could do it in one line?
Introducing paymentMiddleware
The @nevermined-io/payments package now includes an Express middleware that handles the entire x402 payment flow:
import { paymentMiddleware } from "@nevermined-io/payments/express";
app.use(
paymentMiddleware(payments, {
"POST /ask": { planId: process.env.NVM_PLAN_ID!, credits: 1 },
})
);
That's it. Your /ask endpoint is now payment-protected.
What the Middleware Does
When a request comes in without a payment token:
POST /ask HTTP/1.1
Content-Type: application/json
{"query": "What is the meaning of life?"}
The middleware returns a 402 Payment Required with the payment-required header:
HTTP/1.1 402 Payment Required
payment-required: eyJ4NDAyVmVyc2lvbiI6MiwuLi59
{"error": "Payment Required"}
When the client retries with an x402 token in the payment-signature header:
POST /ask HTTP/1.1
Content-Type: application/json
payment-signature: eyJ4NDAyVmVyc2lvbiI6MiwuLi59
{"query": "What is the meaning of life?"}
The middleware:
- Verifies the token with the Nevermined Facilitator
- Calls your route handler
- Settles the payment (burns credits)
- Returns a settlement receipt in the payment-response header
Your route handler never sees the payment logic—it just processes the request.
Complete Example
Here's a fully working AI agent with payment protection:
// src/agent.ts
import express from "express";
import OpenAI from "openai";
import { Payments } from "@nevermined-io/payments";
import { paymentMiddleware } from "@nevermined-io/payments/express";
const app = express();
app.use(express.json());
// Initialize services
const payments = Payments.getInstance({
nvmApiKey: process.env.NVM_API_KEY!,
environment: "sandbox",
});
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// Add payment protection
app.use(
paymentMiddleware(payments, {
"POST /ask": { planId: process.env.NVM_PLAN_ID!, credits: 1 },
})
);
// Your route handler - no payment logic needed!
app.post("/ask", async (req, res) => {
const { query } = req.body;
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: query }],
});
res.json({ response: completion.choices[0]?.message?.content });
});
// Health check (not payment-protected)
app.get("/health", (req, res) => res.json({ status: "ok" }));
app.listen(3000, () => console.log("Agent running on http://localhost:3000"));
Route Configuration Options
Fixed Credits
Charge the same amount for every request:
paymentMiddleware(payments, {
"POST /ask": { planId: PLAN_ID, credits: 1 },
"POST /generate-image": { planId: PLAN_ID, credits: 10 },
});
Dynamic Credits
Calculate credits based on the request or response:
paymentMiddleware(payments, {
"POST /generate": {
planId: PLAN_ID,
credits: (req, res) => {
// Charge based on token count in response
const tokens = res.locals.tokenCount || 100;
return Math.ceil(tokens / 100);
},
},
});
Path Parameters
Works with Express route patterns:
paymentMiddleware(payments, {
"GET /users/:id": { planId: PLAN_ID, credits: 1 },
"POST /agents/:agentId/task": { planId: PLAN_ID, credits: 5 },
});
Middleware Options
Customize behavior with options:
paymentMiddleware(payments, routes, {
// Custom token header(s)
tokenHeader: "payment-signature",
// Hook before verification
onBeforeVerify: (req, paymentRequired) => {
console.log(`[Payment] Verifying request to ${req.path}`);
},
// Hook after settlement
onAfterSettle: (req, creditsUsed, settlement) => {
console.log(
`[Payment] Settled ${creditsUsed} credits, tx: ${settlement.txHash}`
);
},
// Custom error handler
onPaymentError: (error, req, res) => {
console.error(`[Payment] Error: ${error.message}`);
res.status(402).json({ error: error.message });
},
});
Client Implementation
Here's how clients interact with your payment-protected API:
// src/client.ts
import { Payments } from "@nevermined-io/payments";
import { X402_HEADERS } from "@nevermined-io/payments/express";
const payments = Payments.getInstance({
nvmApiKey: process.env.NVM_API_KEY!,
environment: "sandbox",
});
async function callProtectedAPI() {
const SERVER_URL = "http://localhost:3000";
// Step 1: Request without token → 402
const response1 = await fetch(`${SERVER_URL}/ask`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: "What is 2+2?" }),
});
if (response1.status === 402) {
// Step 2: Decode payment requirements
const paymentRequired = JSON.parse(
Buffer.from(
response1.headers.get(X402_HEADERS.PAYMENT_REQUIRED)!,
"base64"
).toString()
);
const { planId, extra } = paymentRequired.accepts[0];
const agentId = extra?.agentId;
// Step 3: Generate x402 token
const { accessToken } = await payments.x402.getX402AccessToken(
planId,
agentId
);
// Step 4: Request with token → 200
const response2 = await fetch(`${SERVER_URL}/ask`, {
method: "POST",
headers: {
"Content-Type": "application/json",
[X402_HEADERS.PAYMENT_SIGNATURE]: accessToken,
},
body: JSON.stringify({ query: "What is 2+2?" }),
});
const data = await response2.json();
console.log("Response:", data.response);
// Step 5: Decode settlement receipt
const settlement = JSON.parse(
Buffer.from(
response2.headers.get(X402_HEADERS.PAYMENT_RESPONSE)!,
"base64"
).toString()
);
console.log("Credits used:", settlement.creditsRedeemed);
}
}
callProtectedAPI();
x402 Headers Reference
Environment Setup
# .env
NVM_API_KEY=nvm:your-api-key
NVM_ENVIRONMENT=sandbox
NVM_PLAN_ID=your-plan-id
OPENAI_API_KEY=sk-your-openai-api-key
PORT=3000Installation
npm install @nevermined-io/payments express
For TypeScript:
npm install -D typescript @types/express @types/node tsx
Running the Example
# Terminal 1: Start the agent
npx tsx src/agent.ts
# Terminal 2: Run the client
npx tsx src/client.ts
Under the Hood
The middleware implements the x402 HTTP transport spec:
- Request matching: Uses path-to-regexp to match routes
- Token extraction: Reads from payment-signature or x-payment header
- Verification: Calls payments.facilitator.verifyPermissions()
- Route execution: Calls next() to let your handler run
- Settlement: Calls payments.facilitator.settlePermissions()
- Response headers: Sets payment-response with settlement receipt
The middleware wraps res.json() and res.send() to ensure settlement happens after your response is ready but before it's sent to the client.
Error Handling
Advanced: Manual Integration
If you need more control than the middleware provides, you can implement the flow manually:
import { Payments } from "@nevermined-io/payments";
import { X402_HEADERS } from "@nevermined-io/payments/express";
app.post("/ask", async (req, res) => {
const x402Token = req.headers["payment-signature"] as string;
if (!x402Token) {
// Return 402 with payment requirements
const paymentRequired = {
x402Version: 2,
error: "Payment required",
accepts: [
{
scheme: "nvm:erc4337",
network: "eip155:84532",
planId: process.env.NVM_PLAN_ID,
extra: { version: "1" },
},
],
extensions: {},
};
res.set(
X402_HEADERS.PAYMENT_REQUIRED,
Buffer.from(JSON.stringify(paymentRequired)).toString("base64")
);
return res.status(402).json({ error: "Payment Required" });
}
// Verify
const verification = await payments.facilitator.verifyPermissions({
x402AccessToken: x402Token,
maxAmount: BigInt(1),
});
if (!verification.success) {
return res.status(402).json({ error: "Verification failed" });
}
// Execute workload
const result = await generateAIResponse(req.body);
// Settle
const settlement = await payments.facilitator.settlePermissions({
x402AccessToken: x402Token,
maxAmount: BigInt(1),
});
// Return response with settlement receipt
res.set(
X402_HEADERS.PAYMENT_RESPONSE,
Buffer.from(
JSON.stringify({
success: true,
creditsRedeemed: 1,
transactionHash: settlement.txHash,
})
).toString("base64")
);
res.json({ result });
});
Resources
Complete Tutorial: http-simple-agent on GitHub
Express Integration Guide: Nevermined Docs
x402 Protocol Spec: x402 on GitHub
Nevermined x402 Spec: x402 Smart Accounts

See Nevermined
in Action
Real-time payments, flexible pricing, and outcome-based monetization—all in one platform.



