x402 Explained: HTTP‑Native Micropayments for AI Agents (With Real Code)
Target audience: developers building autonomous AI agents that need to charge or pay for micro‑services without reinventing a billing layer.
Why Micropayments Matter for Agents
Autonomous agents frequently compose capabilities: they call a language model, hit a data‑fetcher, run a sentiment analyzer, etc. Each of those calls is cheap—often a fraction of a cent—but traditional invoicing, API‑key rotation, or subscription models add friction and overhead.
The x402 specification repurposes the HTTP 402 Payment Required status code to let a server signal that a request can be fulfilled only after the client presents a verifiable, on‑chain payment. Because the mechanism lives entirely in the request/response flow, agents can treat paid endpoints just like any other REST resource: they send a GET/POST, receive a 402, attach a payment proof, and retry—no SDK, no account creation, no long‑lived secrets.
Below we walk through the protocol, show a minimal server‑side implementation, demonstrate a client that an AI agent could embed, and discuss the honest trade‑offs you’ll encounter when adopting x402 in production.
The x402 Flow in a Nutshell
| Step | Actor | Action |
|---|---|---|
| 1 | Client | Sends a normal HTTP request (e.g., GET /summary). |
| 2 | Server | If no valid payment is present, returns 402 Payment Required with a WWW-Authenticate: x402 header that contains: • network (e.g., base) • token (e.g., USDC) • amount (in wei) • payload (a hash of the request details) • maxFeePerGas / maxPriorityFeePerGas (optional) |
| 3 | Client | Constructs an EIP‑2718 typed transaction that pays the server’s x402.payTo address the exact amount, signs it with the agent’s wallet, and encodes the signed transaction (or a relayer‑signed meta‑transaction) as the value of an X-Payment header. |
| 4 | Client | Retries the original request, adding the X-Payment header. |
| 5 | Server | Verifies the signature, checks that the nonce matches the payload, confirms sufficient funds were transferred, then processes the request and returns 2xx with the result. |
Because the payment proof is just a signed transaction, the server can verify it off‑chain (e.g., by calling eth_sendRawTransaction to a local node or using a trusted relayer) before executing the business logic, ensuring that no work is done for free.
Minimal Server Implementation (Node.js + Express)
## Server: x402 middleware
The following snippet shows a self‑contained Express middleware that enforces x402 for a route. It assumes:
* The agent’s wallet address is known (`payTo`).
* The token is USDC on Base (contract `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`).
* Amounts are expressed in the token’s smallest unit (6 decimals for USDC).
// x402-middleware.js
const crypto = require('crypto');
const ethers = require('ethers');
// CONFIGURATION -------------------------------------------------
const NETWORK = 'base'; // Chain identifier used in the header
const TOKEN = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // USDC on Base
const PAY_TO = '0xYourAgentWallet'; // Where payments go
// ----------------------------------------------------------------
/**
* Generates the WWW-Authenticate challenge for a 402 response.
* `payload` is a hash of the request method + path + query string.
*/
function challenge(req) {
const payload = crypto
.createHash('sha256')
.update(`${req.method}${req.path}${JSON.stringify(req.query)}`)
.digest('hex');
return `x402 network="${NETWORK}", token="${TOKEN}", amount="1000000", payload="${payload}"`;
// 1,000,000 USDC = 0.001 USDC (6 decimals) → $0.001 ≈ $0.01 after fees
}
/**
* Verifies that the X-Payment header contains a signed transaction
* that pays exactly the challenged amount to PAY_TO.
*/
async function verifyPayment(req, res, next) {
const auth = req.get('WWW-Authenticate') || '';
const match = auth.match(/amount="(\d+)"/);
if (!match) return res.status(400).send('Missing amount in challenge');
const expectedAmount = BigInt(match[1]); // wei‑style integer for the token
const xpay = req.get('X-Payment');
if (!xpay) return res.status(402).set('WWW-Authenticate', challenge(req)).send('Payment Required');
try {
const tx = ethers.Transaction.from(xpay);
// Basic checks
if (tx.to.toLowerCase() !== PAY_TO.toLowerCase())
throw new Error('Wrong payee');
if (tx.value !== expectedAmount)
throw new Error('Incorrect amount');
if (tx.chainId !== ethers.getNetwork(NETWORK).chainId)
throw new Error('Wrong network');
// Optionally, simulate the transaction to ensure it would succeed
const provider = new ethers.JsonRpcProvider('https://base-mainnet.g.alchemy.com/v2/<YOUR_KEY>');
await provider.call(tx); // throws if revert
// Attach the verified tx to request for downstream use (e.g., refunds)
req.x402Tx = tx;
next();
} catch (e) {
console.warn('x402 verification failed:', e.message);
return res.status(402).set('WWW-Authenticate', challenge(req)).send('Invalid payment proof');
}
}
// Example protected route -------------------------------------------------
const express = require('express');
const app = express();
app.get('/summary', verifyPayment, (req, res) => {
// Here you would call your actual AI service (LLM, data fetch, etc.)
const fakeResult = { summary: 'This is a dummy summary of the requested content.' };
res.json(fakeResult);
});
app.listen(3000, () => console.log('x402 demo listening on :3000'));
What this code does
-
Challenge generation – When a request lacks an
X-Paymentheader, the middleware returns 402 with aWWW-Authenticateheader that encodes the amount (here 0.001 USDC) and a payload derived from the request itself. Binding the payload prevents replay attacks across different endpoints. -
Verification – The client must resend the same request with an
X-Paymentheader containing a signed Ethereum transaction (EIP‑1559) that transfers exactly the challenged amount toPAY_TO. The middleware checks the signature, chain ID, nonce (implicit via the transaction), and that the transaction would not revert. - Processing – Only after a valid proof does the route handler run.
Note: In production you’d likely offload transaction submission to a relayer or use ERC‑4337 account abstraction so the agent doesn’t need to hold native Base gas; the example keeps things simple to illustrate the core idea.
Agent‑Side Client (JavaScript/TypeScript)
An autonomous agent can embed the following helper to automatically handle 402 challenges. It uses ethers.js for signing and node-fetch (or the browser’s fetch) for HTTP.
ts
// x402-client.ts
import { ethers } from 'ethers';
import fetch from 'node-fetch';
const PRIVATE_KEY = '0xyour_agent_private_key'; // NEVER hard‑code in prod; use a vault or env
const wallet = new ethers.Wallet(PRIVATE_KEY);
const provider = new ethers.JsonRpcProvider('https://base-mainnet.g.alchemy.com/v2/<YOUR_KEY>');
const signer = wallet.connect(provider);
/**
* Performs a request that may require x402 payment.
* Retries automatically if a 402 is received with a valid challenge.
*/
async function x402Fetch(url: string, init: RequestInit = {}): Promise<Response> {
let attempt = 0;
const maxAttempts = 2; // first try, then one retry after payment
while (attempt < maxAttempts) {
attempt++;
const res = await fetch(url, init);
if (res.status !== 402) return res; // success or other error
// Parse the challenge
const wwwAuth = res.headers.get('WWW-Authenticate') || '';
const match = wwwAuth.match(/amount="(\d+)"/);
if (!match) throw new Error('Malformed 402 challenge');
const amountWei = BigInt(match[1]); // token's smallest unit
const payloadMatch = wwwAuth.match(/payload="([^"]+)"/);
if (!payloadMatch) throw new Error('Missing payload in challenge');
const payload = payloadMatch[1];
// Build a transaction that pays the challenged amount to the server
// In a real implementation you would extract the payee from the challenge
// (x402 allows the server to include a
Top comments (0)