x402 Explained: HTTP‑Native Micropayments for AI Agents (With Real Code)
Autonomous agents often need to consume paid services — API calls, data feeds, or compute — without human intervention. Traditional approaches (API keys, subscription tokens, or off‑chain invoicing) add coupling, require pre‑provisioned balances, or force the agent to manage external billing systems.
The x402 specification repurposes the HTTP 402 Payment Required status code to turn every request into a self‑contained payment negotiation. When a service wants compensation, it answers with 402 and includes the exact amount, currency, and a cryptographic payment request. The agent pays on‑chain, attaches proof, and retries the request — all within the same HTTP flow.
Below is a pragmatic walk‑through of how x402 works, a minimal server implementation (Cloudflare Workers + ethers.js), a client snippet for an AI agent, and an honest assessment of the trade‑offs you’ll face when adopting it.
How x402 Works (in practice)
- Request – The agent sends a normal GET/POST to a protected endpoint.
-
402 Response – If the caller lacks a valid payment proof, the server replies with status
402and these headers (all optional but recommended):-
X-Amount: numeric amount in the smallest unit (e.g., wei for USDC‑6 = 1 × 10⁶ wei per $0.000001). -
X-Currency: token address or symbol (e.g.,0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913for USDC on Base). -
X-Chain-ID: EIP‑155 chain ID (Base = 8453). -
X-Payment-Request: JSON‑encoded EIP‑2718 typed data that describes the payment (to, value, nonce, expiry). -
X-Pay-To: the payee’s wallet address (often the same as the currency contract owner).
-
-
Agent pays – Using a wallet (or a signer) the agent builds and sends the transaction described by
X-Payment-Request. -
Proof attachment – After the transaction is mined, the agent extracts the transaction hash (
txHash) and, optionally, a signature from the payee confirming receipt. It then retries the original request, adding either:- Header
X-Payment-Proof: <txHash>or - Body field
"payment_proof": "<txHash>"(depends on server preference).
- Header
-
Validation – The server verifies that:
- The txHash corresponds to a transaction on the correct chain.
- The transaction sent the expected amount to the payee.
- The nonce (or replay‑protection field) hasn’t been used before.
If all checks pass, the server processes the request and returns
200(or another appropriate status).
Because the payment proof is just a hash, the overhead is tiny, and the flow works with any HTTP client that can add custom headers.
Server‑Side Example (Cloudflare Workers)
Why Cloudflare Workers? They run close to the user, have built‑in Secrets storage for the payee’s private key, and can call external RPC nodes via
fetch. The same logic can be ported to Express, Fastify, or any Node HTTP framework.
// worker.js
import { ethers } from "ethers";
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // Base USDC
const CHAIN_ID = 8453;
const PAYEE_PRIVATE_KEY = USDC_PAYEE_PRIVATE_KEY; // stored in Workers Secrets
const RPC_URL = "https://base-mainnet.g.alchemy.com/v2/<YOUR_KEY>";
// Helper: verify a USDC transfer on Base
async function verifyPayment(txHash, expectedAmountWei) {
const provider = new ethers.JsonRpcProvider(RPC_URL);
const tx = await provider.getTransaction(txHash);
if (!tx) throw new Error("Tx not found");
if (tx.to?.toLowerCase() !== USDC_ADDRESS.toLowerCase())
throw new Error("Wrong recipient");
if (tx.value !== expectedAmountWei)
throw new Error(`Incorrect amount: ${tx.value} vs ${expectedAmountWei}`);
const receipt = await provider.waitForTransaction(txHash);
if (receipt.status !== 1) throw new Error("Tx failed");
}
// Main handler
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Example protected path: /agent/summarize
if (url.pathname.startsWith("/agent/")) {
// Look for payment proof
const proofHeader = request.headers.get("X-Payment-Proof");
if (proofHeader) {
try {
// For demo we expect a fixed price of $0.02 (20000 USDC wei)
await verifyPayment(proofHeader, ethers.parseUnits("0.02", 6));
// Payment good → run the agent logic
const body = await request.json();
const summary = await summarizeText(body.text); // your AI fn
return new Response(JSON.stringify({ summary }), {
headers: { "Content-Type": "application/json" },
});
} catch (e) {
return new Response(
JSON.stringify({ error: "Invalid payment proof", details: e.message }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
}
// No proof → ask for payment
const amountWei = ethers.parseUnits("0.02", 6); // $0.02
const nonce = Date.now(); // simple replay guard; in prod use a DB or cache
const payRequest = {
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
],
Payment: [
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "nonce", type: "uint256" },
],
},
domain: {
name: "x402 Agent Service",
version: "1",
chainId: CHAIN_ID,
verifyingContract: USDC_ADDRESS,
},
primaryType: "Payment",
message: {
to: env.WALLET_ADDRESS, // payee address (set in Secrets)
value: amountWei,
nonce: nonce,
},
};
// Encode as JSON string for the header (base64url safe)
const requestJson = btoa(JSON.stringify(payRequest)).replace(/\+/g, "-").replace(/\//g, "_");
return new Response("", {
status: 402,
headers: {
"X-Amount": amountWei.toString(),
"X-Currency": USDC_ADDRESS,
"X-Chain-ID": CHAIN_ID.toString(),
"X-Payment-Request": requestJson,
"X-Pay-To": env.WALLET_ADDRESS,
"Content-Type": "text/plain",
},
});
}
// fallback for other routes
return new Response("Not Found", { status: 404 });
},
};
What this does
- Returns
402with the exact amount (0.02USDC) and a minimal EIP‑712 typed payment request. - Stores the payee’s private key in Workers Secrets (
USDC_PAYEE_PRIVATE_KEY) and derives the address for verification. - On replay, expects the client to send back the transaction hash in the
X-Payment-Proofheader. - Verifies the transaction on‑chain using Alchemy (or any RPC).
You can replace the fixed amount with a dynamic price based on request length, model used, etc., by computing it before sending the 402.
Client‑Side Example (Node.js AI Agent)
javascript
// agent.js
import { ethers } from "ethers";
import fetch from "node-fetch";
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const RPC_URL = "https://base-mainnet.g.alchemy.com/v2/<YOUR_KEY>";
const PRIVATE_KEY = process.env.AGENT_PRIVATE_KEY; // agent's wallet
const SERVICE_URL = "https://your-x402-service.example.com/agent/summarize";
const provider = new ethers.JsonRpcProvider(RPC_URL);
const signer = new ethers.W
Top comments (0)