x402 Explained: HTTP-Native Micropayments for AI Agents (With Real Code)
TL;DR: x402 is a lightweight extension of HTTP that lets a server request a tiny, verifiable payment before serving a response. For autonomous agents that need to call pay‑per‑use services (LLM inference, data fetches, tool APIs), it removes the need for separate billing SDKs or OAuth flows while keeping the payment semantics visible at the protocol level.
Why a New HTTP Status Code?
Traditional REST APIs rely on out‑of‑band mechanisms—API keys, JWTs, or subscription portals—to gate access. Those work fine for human‑driven apps but add friction for agents that:
- make dozens or hundreds of calls per second,
- cannot store long‑lived secrets securely,
- need to reconcile cost with the value of each individual response.
x402 solves this by defining status code 402 Payment Required (already reserved in RFC 7231) with a standardized Payment-Response header that carries a cryptographic proof‑of‑payment. The client can satisfy the request by attaching a signed payment blob in a subsequent request, all within the same HTTP exchange.
Core Properties
| Property | What it Means for Agents |
|---|---|
| Stateless | No server‑side session needed; each request carries its own payment proof. |
| Atomic | The server either returns the resource or a 402 with a payment request; no partial data leakage. |
| Currency‑agnostic | The payload encodes the amount, token, and chain; agents can pay in USDC, DAI, or any ERC‑20 compatible token. |
| Verifiable | The payment blob includes a signature that the server can check against a known payer address, preventing replay attacks. |
The Wire Format
When a server wants payment, it replies with:
HTTP/1.1 402 Payment Required
Content-Type: application/json
Payment-Request: {
"scheme": "erc20",
"network": "base",
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
"amount": "1000000", // 0.01 USDC (6 decimals)
"payload": "<base64url‑encoded nonce>"
}
The payload is a random nonce that prevents replay. The agent must sign payload || amount || token || network with its private key and return the signature in the next request.
The agent then retries:
GET /resource HTTP/1.1
Authorization: Bearer <jwt-if-needed>
X-Payment: {
"scheme": "erc20",
"network": "base",
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "1000000",
"signature": "0xabcd...", // ECDSA signature over the concatenated fields
"payload": "<same nonce>"
}
If the signature validates and the nonce hasn’t been seen before, the server returns the requested resource (200 OK) and records the nonce to prevent reuse.
Minimal Working Example (Node.js)
Below is a self‑contained Express middleware that enforces x402 for a single route. It uses ethers.js for signature verification and assumes the agent holds a private key stored in an environment variable.
// x402-middleware.js
import express from 'express';
import { ethers } from 'ethers';
import crypto from 'crypto';
const app = express();
// In‑memory nonce store (for demo; use Redis or DB in prod)
const usedNonces = new Set();
/**
* Generate a 402 response with a payment request.
*/
function paymentRequired(res, tokenAddress, amountWei) {
const nonce = crypto.randomBytes(16).toString('base64url');
const request = {
scheme: 'erc20',
network: 'base',
token: tokenAddress,
amount: amountWei.toString(),
payload: nonce,
};
res.set('Payment-Request', JSON.stringify(request));
res.status(402).json({ error: 'payment required', request });
}
/**
* Middleware that checks for a valid X-Payment header.
*/
async function x402(req, res, next, { tokenAddress, priceWei, payerAddress }) {
// If already paid, skip
if (req.headers['x-payment-verified']) return next();
const auth = req.headers['x-payment'];
if (!auth) return paymentRequired(res, tokenAddress, priceWei);
let payload;
try {
payload = JSON.parse(auth);
} catch {
return res.status(400).json({ error: 'malformed X-Payment' });
}
// Basic field checks
const required = ['scheme', 'network', 'token', 'amount', 'signature', 'payload'];
if (!required.every(k => k in payload)) {
return res.status(400).json({ error: 'missing fields in X-Payment' });
}
// Replay protection
if (usedNonces.has(payload.payload)) {
return res.status(409).json({ error: 'nonce already used' });
}
// Verify signature
const msg = ethers.utils.solidityPack(
['string', 'string', 'address', 'uint256'],
[payload.scheme, payload.network, payload.token, payload.amount]
);
const msgHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(msg + payload.payload));
const recovered = ethers.utils.recoverAddress(msgHash, payload.signature);
if (recovered.toLowerCase() !== payerAddress.toLowerCase()) {
return res.status(403).json({ error: 'invalid signature' });
}
// Record nonce and mark request as verified
usedNonces.add(payload.payload);
req.headers['x-payment-verified'] = 'true';
next();
}
/* Example usage */
const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const PRICE_USDC = ethers.utils.parseUnits('0.01', 6); // $0.01
const AGENT_PUBLIC = process.env.AGENT_ADDRESS; // e.g. 0xAbC...
app.get('/price', (req, res) => {
res.json({ usd: 0.01 });
});
app.get('/data', async (req, res) => {
// This route requires payment
await x402(req, res, () => {}, {
tokenAddress: USDC_BASE,
priceWei: PRICE_USDC,
payerAddress: AGENT_PUBLIC,
});
// If we reach here, payment succeeded
res.json({ value: Math.random() });
});
app.listen(3000, () => console.log('Listening on :3000'));
What this code does
- On the first request to
/data, the client receives a 402 with aPayment-Requestheader. - The client builds the signed payload (shown later) and retries.
- The middleware verifies the signature, checks the nonce, and only then calls the handler.
- If verification fails, the client gets a 4xx error with a helpful JSON body.
Client‑Side Payment Construction (JavaScript)
The agent needs to create the X-Payment header. Using the same ethers library:
import { ethers } from 'ethers';
const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const PRICE_USDC = ethers.utils.parseUnits('0.01', 6); // 0.01 USDC
const PRIVATE_KEY = process.env.AGENT_PRIVATE_KEY; // NEVER commit this
const wallet = new ethers.Wallet(PRIVATE_KEY);
// Helper to call an endpoint that may require payment
async function fetchWithX402(url) {
let response = await fetch(url);
if (response.status !== 402) return response;
const requestHeader = response.headers.get('Payment-Request');
const request = JSON.parse(requestHeader);
const msg = ethers.utils.solidityPack(
['string', 'string', 'address', 'uint256'],
[request.scheme, request.network, request.token, request.amount]
);
const msgHash = ethers.utils.keccak256(
ethers.utils.toUtf8Bytes(msg + request.payload)
);
const signature = await wallet.signMessage(ethers.utils.arrayify(msgHash));
const xPayment = {
scheme: request.scheme,
network: request.network,
token: request.token,
amount: request.amount,
signature,
payload: request.payload,
};
// Retry with payment header
response = await fetch(url, {
headers: { 'X-Payment': JSON.stringify(xPayment) },
});
return response;
}
// Example usage
(async () => {
const resp = await fetchWithX402('http://localhost:3000/data');
const data = await resp.json();
console.log('Paid data:', data);
})();
Notes on the client code
- The agent must hold a private key that corresponds to the address the server expects (
payerAddress). In production, you’d use a hardware signer or a managed wallet service (e.g., Coinbase Wallet, Privy) to avoid exposing keys in source. - The nonce is echoed verbatim from the server; replay attacks are prevented by the server storing used nonces.
- The example uses raw
fetch. In a real agent framework you’d wrap this logic in a reusable transport adapter.
Trade‑offs & Honest Assessment
| Aspect | Benefit | Cost / Limitation |
|---|---|---|
| Protocol‑level payment | No extra round‑trip to a billing API; the payment request lives in the same HTTP exchange. | Requires both client and server to understand the Payment-Request/X-Payment headers; existing generic HTTP clients won’t work without middleware. |
| Stateless verification | Server can scale horizontally; only nonces need short‑term storage (Redis with TTL works). | Nonce store must be highly available; loss of nonces could enable replay attacks if |
Top comments (0)