x402 Explained: HTTP‑Native Micropayments for AI Agents (With Real Code)
Target audience: developers who are building autonomous AI agents and need a lightweight, standards‑based way to charge for each API call.
1. Why look at x402?
AI agents frequently call micro‑services: a language model endpoint, a vector search, a tool that runs a short script, etc. Charging per‑call with traditional invoicing or API‑key tiers adds operational overhead and forces the agent to maintain a balance off‑chain.
x402 repurposes the existing HTTP 402 Payment Required status code to convey a machine‑readable payment request directly in the response headers. The agent can then:
- Read the required amount, token, and network from the header.
- Sign a blockchain transaction that pays the service provider.
- Resend the original request with proof of payment.
All of this happens over plain HTTP/TLS; no new transport protocol is needed. The approach works for any EVM‑compatible chain (including Base) and any ERC‑20 token, making it a good fit for USDC‑based micropayments.
2. The x402 Payment Header
When a server wants to request payment, it returns 402 and includes a Payment header whose value is a JSON object:
{
"scheme": "erc20",
"network": "base",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
"amount": "0.01", // exact amount required for this call
"maxAmount": "0.10", // optional upper bound to protect against over‑charging
"payee": "0x1111…dead", // service provider's wallet
"timeout": 30 // seconds the client has to settle
}
Fields explained
| Field | Meaning |
|---|---|
| scheme | Payment mechanism – erc20 for ERC‑20 tokens, native for ETH, etc. |
| network | Chain identifier (e.g., base, ethereum, polygon). |
| asset | Token contract address (or "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeee" for native ETH). |
| amount | Exact amount the server expects, in the token’s smallest unit (here USDC has 6 decimals, so "0.01" = 10 000 units). |
| maxAmount | Optional ceiling; the client may refuse to pay more. |
| payee | Destination address that must receive the funds. |
| timeout | How long the client has to complete the payment before the request is considered stale. |
If the client already paid, it can resend the request with an X-Payment header containing the transaction hash (or a signed receipt). The server verifies the hash on‑chain and, if valid, processes the request and returns a normal 200.
3. Honest Trade‑offs
| Aspect | Benefit | Cost / Limitation |
|---|---|---|
| Latency | One extra round‑trip (402 → pay → retry) adds ~200‑500 ms on Base, plus wallet signing time. | May be unacceptable for ultra‑low‑latency loops (e.g., real‑time control). |
| UX for agents | Fully programmatic; no UI redirects. | Agents must hold a private key and have access to an RPC endpoint. |
| Gas cost | Paying a tiny amount of USDC means the gas fee is paid in ETH on Base (currently ~$0.0001 per tx). | The gas cost can exceed the service price for sub‑cent calls, making the model uneconomical for very cheap ops. |
| Reliance on RPC | Uses standard JSON‑RPC; any provider works. | If the RPC is censored or down, payments fail. |
| Replay protection | The timeout field and optional nonce in the data field prevent old txs from being reused. |
Developers must still enforce idempotency on the service side. |
| Regulatory | Payments are token transfers; no KYC built‑in. | Depending on jurisdiction, facilitating micro‑transactions may trigger money‑transmitter rules. |
These trade‑offs are realistic; x402 is not a magic bullet but a pragmatic way to embed settlement into HTTP when the overhead is acceptable.
4. Minimal Working Example
Below are two self‑contained snippets: a Node.js/Express server that protects an endpoint with x402, and a browser‑/Node‑compatible client that uses ethers.js to pay and retry.
Assumptions
- Base RPC URL:
https://base-mainnet.g.alchemy.com/v2/<YOUR_KEY>- USDC contract on Base:
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913(6 decimals)- Service wallet (payee):
0x1111111111111111111111111111111111111111(replace with your own)- Private key for the agent: stored securely in an env var
AGENT_PRIVKEY.
4.1 Server (server.js)
javascript
// server.js
import express from 'express';
import { ethers } from 'ethers';
const app = express();
const PORT = 3000;
// Configuration – replace with your own values
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const PAYEE = '0x1111111111111111111111111111111111111111';
const NETWORK = 'base';
const AMOUNT_USDC = '0.01'; // 0.01 USDC per call
const MAX_AMOUNT_USDC = '0.10';
// Helper: format amount as integer units (USDC has 6 decimals)
function toUnits(amountStr) {
return ethers.parseUnits(amountStr, 6);
}
// Middleware that enforces x402 payment
async function requirePayment(req, res, next) {
// Look for proof of payment in X-Payment header
const txHash = req.get('X-Payment');
if (txHash) {
try {
const provider = new ethers.JsonRpcProvider(process.env.BASE_RPC);
const tx = await provider.getTransaction(txHash);
if (!tx) throw new Error('Tx not found');
const receipt = await provider.waitForTransaction(txHash);
if (receipt.status !== 1) throw new Error('Tx failed');
// Basic checks: correct token, recipient, amount
if (tx.to?.toLowerCase() !== USDC_ADDRESS.toLowerCase())
throw new Error('Wrong token contract');
// For ERC-20 transfer we need to inspect input data; here we assume a simple transfer
// In production you'd decode the ERC-20 Transfer event from receipt logs.
// For brevity we skip full validation – see note below.
} catch (e) {
return res.status(402).set('Payment', JSON.stringify({
scheme: 'erc20',
network: NETWORK,
Top comments (0)