

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:
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?
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.
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:
Your route handler never sees the payment logic—it just processes the request.
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"));
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 },
});
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 });
},
});
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();
# .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=3000npm install @nevermined-io/payments express
For TypeScript:
npm install -D typescript @types/express @types/node tsx
# Terminal 1: Start the agent
npx tsx src/agent.ts
# Terminal 2: Run the client
npx tsx src/client.ts
The middleware implements the x402 HTTP transport spec:
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.
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 });
});
Complete Tutorial: http-simple-agent on GitHub
Express Integration Guide: Nevermined Docs
x402 Protocol Spec: x402 on GitHub
Nevermined x402 Spec: x402 Smart Accounts

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