x402 Explained: HTTP‑Native Micropayments for AI Agents (With Real Code)
Why x402 matters for autonomous agents
Autonomous AI agents often need to consume paid services—API calls, data feeds, compute cycles—without human intervention. Traditional payment flows (OAuth, API keys, invoicing) require out‑of‑band setup, manual top‑ups, or trusted intermediaries, which break the “fire‑and‑forget” model agents rely on.
x402 is a lightweight extension to HTTP that turns the existing 402 Payment Required status code into a machine‑readable payment request. When a resource is behind a paywall, the server replies with 402 and includes structured headers that tell a client exactly how to pay (amount, currency, settlement layer, and a nonce to prevent replay). The client can then attach a signed payment proof in a subsequent request, and if the proof validates, the server returns the requested resource with a 200 OK.
Because the protocol lives entirely in HTTP headers, it works with any HTTP client, requires no new transport layer, and can be composed with existing middleware (rate limiting, logging, auth). For AI agents built on top of LLMs or reinforcement learning loops, this means they can discover a service, negotiate price, pay, and continue—all in a single request‑response cycle.
Core components of an x402 exchange
| Piece | Where it lives | What it contains |
|---|---|---|
| 402 Response | Server |
Status: 402 Payment RequiredX-Payment-Info: {"schema":"x402/v1","network":"base","asset":"USDC","amount":"0.05","maxAmount":"0.10","payTo":"0xAbc…","expires":"2025-12-31T23:59:59Z","nonce":"a1b2c3d4"}
|
| Payment Proof | Client (agent) | Signed message (EIP‑712 typedsigned) that includes the nonce, amount, payTo, and chain ID. Sent as X-Payment: <signature> header on the retry. |
| Successful Response | Server |
Status: 200 OKX-Payment-Status: settledResponse body (data, model output, etc.) |
| Failure Responses | Server |
402 with updated maxAmount if underpaid, or 400 Bad Request if proof invalid/missing. |
The flow is deliberately stateless: the server only needs to verify the signature against the nonce it issued. No session storage, no database look‑ups for the payment itself (though you may still keep usage logs for auditing).
Implementing an x402‑protected endpoint (Node.js/Express)
Below is a minimal, production‑ready example that protects a simple “text‑summarizer” microservice. It uses the ethers library for signature verification and assumes the agent holds a USDC balance on Base (Chain ID 8453).
javascript
// server.js
import express from 'express';
import { ethers } from 'ethers';
import cors from 'cors';
const app = express();
app.use(express.json());
app.use(cors());
// Configuration – in practice load from env or secret manager
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // Base USDC
const PAY_TO = '0xYourAgentWalletHere';
const NETWORK = { name: 'base', chainId: 8453 };
const PRICE_USDC = ethers.parseUnits('0.05', 6); // 5 cents USDC (6 decimals)
// Helper: build the EIP‑712 typed data for x402 payment
function buildPaymentData(nonce, amount) {
return {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Payment: [
{ name: 'payTo', type: 'address' },
{ name: 'asset', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'bytes32' },
{ name: 'expires', type: 'uint256' },
],
},
domain: {
name: 'x402 Payment',
version: '1',
chainId: NETWORK.chainId,
verifyingContract: USDC_ADDRESS,
},
primaryType: 'Payment',
message: {
payTo: PAY_TO,
asset: USDC_ADDRESS,
value: amount,
nonce: ethers.keccak256(ethers.toUtf8Bytes(nonce)), // bytes32
expires: Math.floor(Date.now() / 1000) + 3600, // 1 h validity
},
};
}
app.post('/summarize', async (req, res) => {
const { text } = req.body;
if (!text) return res.status(400).json({ error: 'Missing text' });
// 1️⃣ Check for existing payment proof
const paymentHeader = req.headers['x-payment'];
const nonceHeader = req.headers['x-payment-nonce']; // optional, sent by server in 402
if (paymentHeader && nonceHeader) {
try {
// Reconstruct the typed data the server originally sent
// In a real service you’d store the nonce→amount map temporarily (e.g., Redis)
const stored = await getStoredNonce(nonceHeader); // implement your own cache
if (!stored) throw new Error('Nonce unknown or expired');
const domain = {
name: 'x402 Payment',
version: '1',
chainId: NETWORK.chainId,
verifyingContract: USDC_ADDRESS,
};
const typedData = {
types: {
EIP712Domain: [{ name: 'name', type: 'string' }, { name: 'version', type: 'string' }, { name: 'chainId', type: 'uint256' }, { name: 'verifyingContract', type: 'address' }],
Payment: [{ name: 'payTo', type: 'address' }, { name: 'asset', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'nonce', type: 'bytes32' }, { name: 'expires', type: 'uint256' }],
},
domain,
primaryType: 'Payment',
message: {
payTo: PAY_TO,
asset: USDC_ADDRESS,
value: stored.amount,
nonce: ethers.keccak256(ethers.toUtf8Bytes(nonceHeader)),
expires: stored.expires,
},
};
const recovered = ethers.verifyTypedData(typedData.domain, typedData.types, typedData.message, paymentHeader);
if (recovered.toLowerCase() !== PAY_TO.toLowerCase()) throw new Error('Signature mismatch');
// Payment verified – proceed
const summary = await summarizeWithLLM(text); // your LLM call
return res.json({ summary });
} catch (e) {
// Invalid proof – fall through to request payment
console.warn('Payment proof invalid:', e.message);
}
}
// 2️⃣ No valid proof – ask for payment
const nonce = ethers.randomBytes(32).toString('hex');
await storeNonce(nonce, { amount: PRICE_USDC, expires: Math.floor(Date.now() / 1000) + 3600 }); // TTL 1h
const paymentInfo = {
schema: 'x402/v1',
network: NETWORK.name,
asset: USDC_ADDRESS,
amount: ethers.formatUnits(PRICE_USDC, 6), // human‑readable
maxAmount: ethers.formatUnits(PRICE_USDC * 2, 6), // allow a small over‑pay buffer
payTo: PAY_TO,
expires: Math.floor(Date.now() / 1000) + 3600,
nonce,
};
return res
.status(402)
.set('X-Payment-Info', JSON.stringify(paymentInfo))
.json({ error: 'Payment required', paymentInfo });
});
// Dummy storage – replace with Redis, Postgres, etc.
const store = new Map();
async function storeNonce(nonce, data) { store.set(nonce, { ...data, ts: Date.now() }); }
async function getStoredNonce(nonce) {
const entry = store.get(nonce);
if (!entry) return null;
if (Date.now() - entry.ts > 3600 * 1000) { store.delete(nonce); return null; }
return entry;
}
// Fake LLM summarizer – plug in your own provider
async function summarizeWithLLM(text) {
// Example: call OpenAI, Anthropic, or a local model
return text.split('.').slice(0, 2).join('. ') + '.';
}
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log
Top comments (0)