x402 Explained: HTTP‑Native Micropayments for AI Agents (With Real Code)
Target audience: developers who are building autonomous AI agents and need a lightweight way to charge for individual API calls without introducing a separate billing system.
Why look at x402?
Autonomous agents often expose fine‑grained services—think “summarize this paragraph”, “classify this image”, or “fetch the latest price for a token”. Traditional approaches (API keys + monthly invoices, subscription tiers, or ad‑hoc invoicing) add operational overhead that doesn’t scale when an agent might make thousands of micro‑calls per day.
The x402 specification repurposes the HTTP 402 Payment Required status code to turn every request into a self‑contained payment negotiation. If the client hasn’t paid, the server replies with 402 and includes the exact payment details the client must satisfy. Once the payment is verified, the server processes the request and returns the normal 200 response.
Because the payment is expressed as a plain HTTP header, the mechanism works over any transport that supports headers—REST, GraphQL, gRPC‑HTTP/2 bridge, or even WebSockets. No new protocol layers, no side‑channel escrow services, and no need to maintain a separate billing database.
Core components of an x402 flow
| Piece | What it does | Where it lives |
|---|---|---|
| Payment Request | Server‑generated data describing the required amount, token, chain, and payee address. Sent in the Pay response header on a 402. |
Server |
| Payment Proof | Client‑generated data proving that a transaction meeting the request was included on‑chain. Sent in the X-Payment request header. |
Client |
| Verifier | Server‑side code that checks the proof: validates the transaction hash, confirms the correct token amount was transferred to the payee, and ensures the chain ID matches. | Server |
| Wallet/Signer | Client‑side library (e.g., ethers.js) that builds, signs, and broadcasts the payment transaction. | Client |
The spec deliberately stays agnostic about the underlying blockchain; the current reference implementation uses USDC on Base (an Optimistic Rollup) because it offers low fees (~$0.0001) and fast finality (~2 seconds).
Honest trade‑offs
| Advantage | Caveat / Cost |
|---|---|
| Atomicity – payment and service execution are inseparable; you can’t receive the service without paying on‑chain. | On‑chain latency – you must wait for a transaction to be confirmed (or at least seen in the mempool) before the server can verify it. In practice, a 2‑second Base block time is acceptable for most agent workloads, but it adds latency compared to a pure API‑key check. |
| No custodial middleman – funds go directly from the client’s wallet to the service provider’s address. | Wallet UX – the agent must hold a funded wallet and approve the USDC contract (or rely on a paymaster that handles approvals). This adds a small operational step for developers. |
| Granular pricing – you can charge per‑call, per‑token, or any arbitrary amount down to the smallest USDC unit (6 decimals). | Price volatility – if you denominate in a volatile token, the effective cost in fiat can swing. Using a stablecoin (USDC) mitigates this but does not eliminate it entirely (USDC can still de‑peg briefly). |
| Stateless verification – the server only needs to check a transaction receipt; no need to store payment state per user. | Reliance on RPC – verification requires a call to a Base node (or a trusted RPC provider). If the RPC is unavailable or returns stale data, you may incorrectly reject valid payments. |
| Open standard – anyone can implement a verifier; no vendor lock‑in. |
Limited tooling – as of late 2025, only a few helper libraries exist (e.g., @coinbase/x402-sdk). You may need to write or adapt verification logic yourself. |
Overall, x402 shines when the value per call is low (sub‑cent to a few cents) and the call volume is high, making traditional invoicing impractical. If your agents only make a handful of expensive calls per day, the added complexity may not be worth it.
Minimal working example
Below is a TypeScript/Node.js implementation that shows both sides of the flow. It uses ethers.js for wallet interactions and a simple Express server. The code is deliberately kept short; production use would add better error handling, rate limiting, and logging.
1. Server – x402 middleware
ts
// server.ts
import express, { Request, Response, NextFunction } from 'express';
import { ethers } from 'ethers';
import { keccak256, toUtf8Bytes } from 'ethers/lib/utils';
// ---- CONFIG -------------------------------------------------
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // Base USDC
const PAYEE = '0xYourServiceAddressHere'; // where USDC should go
const CHAIN_ID = 8453; // Base
const RPC_URL = 'https://mainnet.base.org'; // public RPC, consider a paid endpoint for prod
const PROVIDER = new ethers.JsonRpcProvider(RPC_URL);
// -----------------------------------------------------------
/**
* Generates a 402 response with a Pay header.
* amount is in USDC (6 decimals).
*/
function paymentRequired(res: Response, amount: number) {
const payHex = ethers.utils
.defaultAbiCoder(
['address', 'uint256', 'address', 'uint256'],
[PAYEE, ethers.parseUnits(String(amount), 6), USDC_ADDRESS, CHAIN_ID]
)
.slice(2); // strip 0x
res.set('Pay', `Bearer ${payHex}`);
res.status(402).send('Payment Required');
}
/**
* Verifies that the X-Payment header contains a valid tx hash.
*/
async function verifyPayment(req: Request): Promise<boolean> {
const auth = req.get('X-Payment');
if (!auth?.startsWith('Bearer ')) return false;
const txHash = auth.slice(7);
if (!ethers.isHexString(txHash, 32)) return false;
const tx = await PROVIDER.getTransaction(txHash);
if (!tx) return false; // pending or unknown
const receipt = await PROVIDER.getTransactionReceipt(txHash);
if (!receipt || receipt.status !== 1) return false; // reverted
// Check that the tx sent USDC to PAYEE for at least the asked amount
// Note: we rely on the ERC‑20 Transfer event; a full verifier would also
// check the token contract's `decimals` and `balanceOf` to avoid replay.
const USDC_ABI = [
"event Transfer(address indexed from, address indexed to, uint256 value)"
];
const iface = new ethers.Interface(USDC_ABI);
const transferLog = receipt.logs
.map(l => ({
address: l.address,
data: l.data,
topics: l.topics,
}))
.find(l => l.address.toLowerCase() === USDC_ADDRESS.toLowerCase()
&& l.topics[0] === iface.getEventTopic('Transfer'));
if (!transferLog) return false;
const [, to, value] = iface.decodeEventLog('Transfer', transferLog.data, transferLog.topics);
return (
to.toLowerCase() === PAYEE.toLowerCase() &&
// value is in wei‑like units (USDC has 6 decimals)
value >= ethers.parseUnits('0.01', 6) // example minimum, adjust per endpoint
);
}
/**
* Express middleware that protects a route with x402.
* amount: price in USDC (e.g., 0.05 for $0.05)
*/
function x402(amount: number) {
return async (req: Request, res: Response, next: NextFunction) => {
const paymentOk = await verifyPayment(req);
if (paymentOk) {
return next();
}
// Not paid yet – ask for payment
return paymentRequired(res, amount);
};
}
// ---- Example service ---------------------------------------
const app = express();
// A dummy "summarize" endpoint that costs $0.02 per call
app.post('/summarize', x402(0.02), (req, res) => {
const text = (req.body as any)?.text ?? '';
// In reality you’d call an LLM here.
const summary = text.split(' ').slice(0,
Top comments (0)