x402 Explained: HTTP‑Native Micropayments for AI Agents (With Real Code)
TL;DR – x402 is the HTTP status code
402 Payment Required. It lets a server signal that a client must attach a verifiable payment before the requested resource can be served. For autonomous AI agents that need to call paid APIs on‑the‑fly, x402 provides a lightweight, standards‑based way to request, negotiate, and settle micropayments without adding a custom payment layer on top of HTTP.
Why x402 matters for AI agents
Autonomous agents frequently need to compose capabilities: call a language model, fetch external data, or trigger a tool. In many cases each of those calls carries a real cost (e.g., GPU time, data licensing, API usage). Traditional approaches either:
- Pre‑pay a bulk balance and hope it lasts, or
- Embed a proprietary payment token in every request (API key + signature).
Both add friction: pre‑pay requires accounting overhead; proprietary schemes lock you into a vendor’s auth flow and make composability hard.
x402 solves this by keeping the payment negotiation inside the HTTP exchange:
- The server replies
402with aPayheader that describes what to pay, how much, and where to send it. - The client (your agent) reads the header, constructs a blockchain transaction that satisfies the request, and retries the original request with a
Paymentheader containing the transaction hash (or a signed receipt). - If the server validates the payment, it returns
200and the payload; otherwise it may return another402with updated terms.
Because it rides on top of vanilla HTTP, any agent that can make an fetch/axios call can participate—no SDK lock‑in required.
The x402 flow in detail
| Step | Actor | Action | HTTP details |
|---|---|---|---|
| 1 | Client | Sends a normal GET/POST to a protected endpoint. | GET /agent-tool HTTP/1.1 |
| 2 | Server | Determines payment is required. Responds with 402. |
402 Payment RequiredPay: <payment‑payload>
|
| 3 | Client | Parses Pay, builds a transaction that pays the specified amount to the specified address (often an escrow contract). |
— |
| 4 | Client | Retries the original request, adding a Payment header with the transaction hash (or a signed receipt). |
Payment: 0xabc123… |
| 5 | Server | Verifies that the transaction mined on the expected chain, matches the amount, and pays to the correct recipient. If OK → 200 + body; else → another 402 (maybe with a higher price). |
200 OK or 402 Payment Required
|
The Pay header is defined in the x402 spec as a JSON‑encoded string:
{
"scheme": "exact", // or "manual"
"network": "base", // EVM chain identifier
"asset": "USDC", // token symbol or contract address
"amount": "0.000005", // in base units (here 5 µUSDC)
"payee": "0xAbc…Def", // recipient address
"maxTimeout": 86400 // seconds the offer is valid
}
The Payment header mirrors this with the transaction hash (or a signed receipt) so the server can verify on‑chain.
Minimal working example (Node.js)
Below are two self‑contained snippets: a server that protects a dummy AI‑agent endpoint with x402, and a client that acts as an autonomous agent, pays the fee, and retrieves the result.
Assumptions
- You have an RPC endpoint for Base (e.g.,
https://base.mainnet.rpc.dev).- You hold USDC on Base in a wallet whose private key is stored securely (never hard‑code in production).
- The USDC contract address on Base is
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913.
Server (Express)
// server.js
import express from 'express';
import { ethers } from 'ethers';
const app = express();
const PORT = 3000;
// ---- CONFIG ----
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const PAYEE = '0xYourReceiverAddress'; // where you want the funds
const AMOUNT_USDC = ethers.parseUnits('0.01', 6); // $0.01 USDC (6 decimals)
const RPC_URL = 'https://base.mainnet.rpc.dev';
const provider = new ethers.JsonRpcProvider(RPC_URL);
// ----------------
// Helper: build the Pay header JSON as a base64url string (spec‑friendly)
function buildPayHeader() {
const payload = {
scheme: 'exact',
network: 'base',
asset: USDC_ADDRESS,
amount: AMOUNT_USDC.toString(),
payee: PAYEE,
maxTimeout: 86400
};
// spec says base64url without padding
return Buffer.from(JSON.stringify(payload)).toString('base64url');
}
// Middleware that checks for a valid Payment header
async function requirePayment(req, res, next) {
const payHeader = req.headers['payment'];
if (!payHeader) {
// ask for payment
res.set('Pay', buildPayHeader());
return res.status(402).send('Payment Required');
}
// Expect payment header to be a transaction hash
const txHash = payHeader;
try {
const tx = await provider.getTransaction(txHash);
if (!tx) throw new Error('Tx not found');
// Verify it matches our expectations
if (tx.to?.toLowerCase() !== PAYEE.toLowerCase()) throw new Error('Wrong payee');
if (tx.value !== AMOUNT_USDC) throw new Error('Wrong amount');
// Optionally wait for confirmation (here we accept mempool)
// await tx.wait(); // uncomment for stricter safety
// Payment ok – proceed
next();
} catch (e) {
console.error('Payment verification failed:', e);
res.set('Pay', buildPayHeader());
return res.status(402).send('Invalid or insufficient payment');
}
}
// Dummy AI‑agent endpoint: returns a canned "answer"
app.get('/agent/answer', requirePayment, (req, res) => {
res.json({ answer: 'The sky is blue because of Rayleigh scattering.' });
});
app.listen(PORT, () => console.log(`x402 server listening on :${PORT}`));
What this does
- The server refuses any request to
/agent/answerwith402and aPayheader describing the exact USDC amount. - The client must supply a
Paymentheader containing a transaction hash that pays the expected amount to the payee. - The server checks the transaction via an RPC call; if valid, it returns the JSON payload.
Client (agent)
markdown
// agent.js
import { ethers } from 'ethers';
import fetch from 'node-fetch';
// ---- CONFIG ----
const RPC_URL = 'https://base.mainnet.rpc.dev';
const PRIVATE_KEY = process.env.PRIVATE_KEY; // load from env, never hard‑code
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const PAYEE = '0xYourReceiverAddress'; // must match server
const AMOUNT_USDC = ethers.parseUnits('0.01', 6); // 0.01 USDC
const RPC_PROVIDER = new ethers.JsonRpcProvider(RPC_URL);
const wallet = new ethers.Wallet(PRIVATE_KEY, RPC_PROVIDER);
const usdcAbi = ["function transfer(address to, uint256 amount) returns (bool)"];
const usdcContract = new ethers.Contract(USDC_ADDRESS, usdcAbi, wallet);
// -----------------
const SERVER_URL = 'http://localhost:3000/agent/answer';
async function fetchWithPayment() {
// First attempt – expect 402
let resp = await fetch(SERVER_URL, { method: 'GET' });
if (resp.status !== 402) {
const text = await resp.text();
throw new Error(`Unexpected status ${resp.status}: ${text}`);
}
// Parse Pay header
const payHeaderRaw = resp.headers.get('pay');
if (!payHeaderRaw) throw new Error('Missing Pay header');
const payJson = JSON.parse(Buffer.from(payHeaderRaw, 'base64url').toString('utf8'));
// Build and send transaction
const tx = await usdcContract.transfer(
payJson.payee,
ethers.parseUnits(payJson.amount, 6) // USDC has 6 decimals
);
console.log(`Sent payment tx ${tx.hash}`);
await tx.wait(); // wait for inclusion (adjust timeout as needed)
// Retry original request with Payment header
resp = await fetch(SERVER_URL,
Top comments (0)