x402 Explained: HTTP‑Native Micropayments for AI Agents (With Real Code)
Target audience: developers building autonomous AI agents who need a lightweight, standards‑based way to charge for per‑call services.
Why x402 matters for AI agents
Autonomous agents often compose many small‑grained services—LLM inference, data look‑ups, tool wrappers—each of which may cost a fraction of a cent. Traditional billing (API keys, monthly quotas, invoicing) adds operational overhead and makes real‑time composition cumbersome.
The x402 specification repurposes the HTTP 402 Payment Required status code to enable micropayments directly in the request/response cycle. The flow is:
- Agent makes a normal GET/POST to a service endpoint.
- If the caller hasn’t paid, the service replies 402 with a payment request (amount, token, chain, and a payment URL or payload).
- The agent signs and submits the payment (usually a minimal ERC‑20 transfer on an L2 like Base).
- The agent retries the original request, now including a proof‑of‑payment header; the service validates and returns the resource.
Because the payment lives in HTTP headers, no new protocol or SDK is required—just a few lines of extra logic around existing fetch/axios calls.
Core concepts
| Concept | Detail |
|---|---|
| Price advertisement | Server includes X-402-Payment: <scheme>://<payload> in the 402 response. The scheme is usually price for ERC‑20 micro‑transfers. |
| Payment payload | JSON‑encoded: { "network": "base", "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "amount": "1000000000000000" } (USDC with 6 decimals → $0.001). |
| Proof‑of‑payment | After sending the transaction, the client includes X-402-Payment-Response: <txHash> in the retry request. The server checks that the tx matches the expected amount, token, and recipient. |
| Idempotency | Servers should store seen tx hashes (or use a short‑lived nonce) to avoid double‑charging on retries. |
| Gas considerations | On Base, a simple USDC transfer costs ~0.0001 ETH (~$0.15 at current prices). To keep net cost low, agents batch multiple micro‑payments into a single transaction or use relayers that subsidize gas. |
Trade‑offs (honest assessment)
| Advantage | Drawback |
|---|---|
| Standard HTTP – works with any language, proxy, or API gateway. | Extra round‑trip – a failed request → 402 → payment → retry adds latency (≈200‑500 ms on Base). |
| No API‑key management – payment is proof‑based, not secret‑based. | Wallet required – the agent must hold a private key and sign transactions; key management adds security complexity. |
| Fine‑grained pricing – you can charge per token, per second, or per call. | L2 dependency – relies on a Layer‑2 with cheap USDC transfers; if the L2 experiences congestion, fees rise. |
| Transparent pricing – price is visible in the response header, enabling dynamic routing. | Limited tooling – few libraries exist; you often roll your own header handling. |
| Atomicity – payment verification happens before serving the resource, reducing abuse. | Dispute handling – refunds or disputes need off‑chain mechanisms; the protocol itself is trust‑less but not dispute‑resolution aware. |
For most agent‑to‑agent microservices, the latency cost is acceptable compared to the simplification of removing API‑key rotation and usage‑tracking services.
Minimal working example
Below is a Node.js/TypeScript snippet that shows both the client side (agent) and a tiny Express server that implements x402 for a USDC‑priced “summarize‑text” endpoint.
Assumptions
- You have an Ethereum wallet with USDC on Base (contract
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913).- You are using
ethers@v6andexpress.- The server holds the recipient address
0xRecipient…(replace with your own).
1. Server: price advertisement & verification
// server.ts
import express from 'express';
import { ethers } from 'ethers';
import bodyParser from 'body-parser';
const app = express();
app.use(bodyParser.json());
const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // Base USDC
const RECIPIENT = '0xRecipient1111111111111111111111111111111111'; // your address
const PRICE_USDC = ethers.parseUnits('0.01', 6); // $0.01 per call
// In‑memory store of seen tx hashes (replace with Redis/DynamoDB in prod)
const seenTx = new Set<string>();
// Helper: verify that a tx paid the expected amount
async function verifyPayment(txHash: string): Promise<boolean> {
if (seenTx.has(txHash)) return false; // idempotency
const provider = new ethers.JsonRpcProvider('https://base.mainnet.rpc.eu');
const tx = await provider.getTransaction(txHash);
if (!tx) return false;
const receipt = await tx.wait();
if (receipt.status !== 1) return false;
// check ERC20 Transfer event (simplified)
const iface = new ethers.Interface([
"event Transfer(address indexed from, address indexed to, uint256 value)"
]);
const log = receipt.logs.find(l =>
l.address.toLowerCase() === USDC.toLowerCase() &&
l.topics[0] === iface.getEventTopic("Transfer")
);
if (!log) return false;
const [, to, value] = iface.decodeEventLog("Transfer", log.data, log.topics);
if (to.toLowerCase() !== RECIPIENT.toLowerCase()) return false;
if (value !== PRICE_USDC) return false;
seenTx.add(txHash);
return true;
}
// Endpoint that requires payment
app.post('/summarize', async (req, res) => {
const auth = req.headers['x-402-payment-response'];
if (!auth) {
// No proof → ask for payment
const payload = {
network: 'base',
token: USDC,
amount: PRICE_USDC.toString()
};
res.set('X-402-Payment', `price://${Buffer.from(JSON.stringify(payload)).toString('base64')}`);
return res.status(402).send('Payment required');
}
// Verify payment
const ok = await verifyPayment(auth);
if (!ok) {
return res.status(402).send('Invalid or already used payment');
}
// ---- actual business logic ----
const { text } = req.body;
const summary = text.split(' ').slice(0, 20).join(' ') + '…'; // dummy summarize
res.json({ summary });
});
const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => console.log(`x402 server listening on :${PORT}`));
What happens
- First request → no
X-402-Payment-Responseheader → server replies 402 with a base64‑encoded JSON payload describing the USDC amount. - Agent receives the 402, builds a transaction, signs it, sends it to Base, then retries the same POST with
X-402-Payment-Response: <txHash>. - Server validates the tx, stores the hash to prevent replay, then returns the JSON summary.
2. Client (agent) – paying and retrying
ts
// client.ts
import { ethers } from 'ethers';
import fetch from 'node-fetch';
const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const RECIPIENT = '0xRecipient1111111111111111111111111111111111';
const PRICE = ethers.parseUnits('0.01', 6); // $0.01
const provider = new ethers.JsonRpcProvider('https://base.mainnet.rpc.eu');
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const usdc = new ethers.Contract(
USDC,
["function transfer(address to, uint256 amount) returns (bool)"],
wallet
);
async function payAndFetch(url: string, body: any) {
let attempts = 0;
while (true) {
attempts++;
const resp = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (resp.status !== 402) {
const data = await resp.json();
return { status: resp
Top comments (0)