x402 Explained: HTTP‑Native Micropayments for AI Agents (With Real Code)
Target audience: developers who are building autonomous AI agents that need to pay for or charge for API usage in a trust‑less, on‑chain way.
Why x402 matters for agents
AI agents today often call third‑party services (LLM inference, data feeds, compute) and either:
- Embed a static API key – creates a single point of failure and forces the agent to trust the provider with long‑lived credentials.
- Rely on off‑chain billing – adds latency, requires account management, and breaks the “stateless” nature of an autonomous agent.
The x402 specification revives the HTTP 402 Payment Required status code and defines a lightweight, stateless way to attach an ERC‑20 payment (USDC on Base in the examples) directly to the request/response cycle. The agent never stores a long‑lived secret; it only needs a wallet capable of signing an EIP‑712 typed message.
The protocol in a nutshell
| Step | Actor | Action |
|---|---|---|
| 1️⃣ | Client (agent) | Sends a normal GET/POST request to a protected resource. |
| 2️⃣ | Server | If no valid payment is present, returns 402 Payment Required with a JSON body that describes the required payment (amount, token, chain, recipient, nonce). |
| 3️⃣ | Client | Uses its wallet to sign an EIP‑712 payment receipt that includes the server’s challenge (nonce, timestamp, amount, etc.). |
| 4️⃣ | Client | Retries the original request, adding the signed receipt in the x402-Pay header. |
| 5️⃣ | Server | Verifies the receipt signature, checks that the ERC‑20 transfer (via approve/transferFrom or a escrow contract) has occurred, then serves the resource (200 OK). |
Because the payment is verified off‑chain via a signature, the actual ERC‑20 transfer can happen on Layer‑2 (Base) with negligible gas cost. The server only needs to confirm that the transfer succeeded—no contract deployment per request is required.
Server side: issuing a 402 challenge
Below is a minimal Express handler that protects a route with x402. It uses the @coinbase/x402 npm package (the reference implementation) to generate the challenge and to verify receipts.
// server.ts
import express from 'express';
import { createX402Middleware, X402Config } from '@coinbase/x402';
import { ethers } from 'ethers';
const app = express();
// 1️⃣ Configuration – adjust for your token/chain/recipient
const config: X402Config = {
// USDC on Base (address on Base mainnet)
tokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
// Chain ID for Base (8453)
chainId: 8453,
// Recipient that should receive the payment
recipient: '0xYourAgentServiceWallet',
// Amount in the token's smallest unit (USDC has 6 decimals)
amount: ethers.parseUnits('0.05', 6).toString(), // $0.05 per call
// Optional: a nonce generator to prevent replays
nonce: () => ethers.randomBytes(32).toString('hex'),
};
app.use('/paid-agent', createX402Middleware(config));
// Protected endpoint – only reachable after a valid payment
app.get('/paid-agent/hello', (req, res) => {
res.json({ message: 'Hello from your paid AI agent!' });
});
app.listen(3000, () => console.log('x402 server listening on :3000'));
What happens under the hood
-
createX402Middlewarechecks for thex402-Payheader. - If missing/invalid, it responds with 402 and a JSON body like:
{
"scheme": "exact",
"network": "base",
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "50000",
"recipient": "0xYourAgentServiceWallet",
"nonce": "a1b2c3d4e5f6..."
}
The client must sign a receipt that covers all those fields plus a timestamp.
Client side: building and sending a payment receipt
The agent needs a signer (e.g., an ethers.js Wallet connected to a private key or a hardware wallet via MetaMask). The following snippet shows a generic fetch wrapper that automatically handles 402 challenges.
// agent-client.ts
import { ethers } from 'ethers';
import fetch from 'node-fetch';
// 1️⃣ Set up signer – replace with your own key management
const privateKey = process.env.PRIVATE_KEY!; // never hard‑code in prod
const signer = new ethers.Wallet(privateKey);
const provider = new ethers.JsonRpcProvider('https://base.mainnet.rpc.url');
const wallet = signer.connect(provider);
// 2️⃣ Helper to build the EIP‑712 typed data expected by x402
function buildPaymentRequest(challenge: any) {
return {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Payment: [
{ name: 'recipient', type: 'address' },
{ name: 'amount', type: 'uint256' },
{ name: 'token', type: 'address' },
{ name: 'nonce', type: 'bytes32' },
{ name: 'timestamp', type: 'uint256' },
],
},
domain: {
name: 'x402 Payment',
version: '1',
chainId: challenge.chainId,
verifyingContract: ethers.ZeroAddress, // no contract; signature only
},
primaryType: 'Payment',
message: {
recipient: challenge.recipient,
amount: challenge.amount,
token: challenge.token,
nonce: challenge.nonce,
timestamp: Math.floor(Date.now() / 1000),
},
};
}
// 3️⃣ Core request function with 402 handling
async function x402Fetch(url: string, init: RequestInit = {}): Promise<Response> {
let response = await fetch(url, init);
// If we got a 402, parse the challenge, sign, and retry
if (response.status === 402) {
const challenge = await response.json();
const payRequest = buildPaymentRequest(challenge);
const signature = await wallet.signTypedData(
payRequest.domain,
payRequest.types,
payRequest.message
);
// Attach the signature as the x402-Pay header
const signedInit = { ...init, headers: { ...init.headers, 'x402-Pay': signature } };
response = await fetch(url, signedInit);
}
return response;
}
// 4️⃣ Example usage – call a paid agent endpoint
(async () => {
const res = await x402Fetch('http://localhost:3000/paid-agent/hello');
if (!res.ok) {
throw new Error(`Agent call failed: ${res.status} ${res.statusText}`);
}
const data = await res.json();
console.log('Agent replied:', data);
})();
Key points in the client code
- The signer only needs to sign a typed message; no on‑chain transaction is sent by the agent.
- The actual USDC transfer is performed off‑chain by the payer (the agent’s wallet) using a simple
transferFromafter the server grants an allowance, or via a escrow contract that the server polls. In many implementations the server runs a
Top comments (0)