x402 Explained: HTTP‑Native Micropayments for AI Agents (With Real Code)
Target audience: developers who are building autonomous agents that need to purchase data, compute, or other services on‑the‑fly.
Why x402 matters for agents
Modern AI agents are often stateless functions that call external APIs to enrich their reasoning (e.g., fetching real‑time price data, invoking a vision model, or pulling a curated knowledge base). In a fully autonomous loop the agent must be able to pay for each call without human intervention, without relying on off‑chain invoicing, and without adding a custom payment layer on top of every service.
The x402 specification solves this by turning the HTTP 402 Payment Required status code into a standardized, wallet‑driven micropayment mechanism that lives entirely in the request/response cycle. No new transport protocol, no side‑channel signatures, and no extra latency beyond the normal round‑trip.
Core ideas of x402
| Concept | Description |
|---|---|
| HTTP‑native | Uses existing HTTP verbs, headers, and status codes. The only addition is a Payment header that carries a signed payment request. |
| Stateless | The server does not need to store session or balance information; everything needed to verify a payment is embedded in the request. |
| Wallet‑agnostic | Any EIP‑1559‑compatible signer (MetaMask, WalletConnect, a hardware key, or an agent‑controlled key) can produce the required signature. |
| Fixed‑price or dynamic | The server can advertise a static price (x402-price) or compute a price per‑request (e.g., based on data size) and include it in the payment request. |
| USDC on Base | The reference implementation uses the ERC‑20 USDC contract on Base (Chain ID 8453) because it offers low gas and fast finality, but the spec is chain‑agnostic. |
The payment flow in detail
- Agent makes a normal GET/POST to a protected resource.
- If the caller lacks a valid payment, the server replies 402 Payment Required with:
-
Paymentheader – a base64‑url‑encoded JSON object describing the amount, token, chain, and a nonce. -
WWW-Authenticate: Bearer realm="x402"– optional, for clients that treat it like auth.
-
- The agent’s wallet signs the payment request (typically an EIP‑712 typed data structure) and returns a new request with:
-
Authorization: Bearer <signature>header. - The same
Paymentheader (unchanged) so the server can verify the signature matches the request.
-
- Server validates the signature, checks that the nonce hasn’t been reused (prevents replay), and if everything is ok, processes the original request and returns 200 OK (or another appropriate status).
Because the payment data travels in HTTP headers, intermediaries (proxies, CDNs, API gateways) can forward it unchanged—no special tunneling required.
Minimal working example
Below is a Node.js/Express server that protects a single endpoint (/data) with an x402 payment of 0.01 USDC. The client snippet shows how an agent built with ethers.js can automatically handle the 402 flow.
Server (server.js)
// npm i express ethers dotenv
require('dotenv').config();
const express = require('express');
const { ethers } = require('ethers');
const app = express();
const PORT = 3000;
// USDC on Base (checksum address)
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const USDC_DECIMALS = 6;
const PRICE_USDC = ethers.parseUnits('0.01', USDC_DECIMALS); // 0.01 USDC
// Simple in‑memory nonce store (replace with Redis or DB in prod)
const usedNonces = new Set();
/**
* Build the payment request object that will be base64‑url encoded
* and placed in the `Payment` header.
*/
function buildPaymentRequest() {
const nonce = ethers.randomBytes(8).toString('hex'); // 64‑bit nonce
return {
scheme: 'x402',
network: 8453, // Base chain ID
token: USDC_ADDRESS,
amount: PRICE_USDC.toString(),
nonce,
};
}
/**
* Middleware that enforces x402 payment.
*/
function requirePayment(req, res, next) {
const auth = req.headers.authorization?.split(' ')[1];
const paymentHeader = req.headers.payment;
if (!auth || !paymentHeader) {
const payReq = buildPaymentRequest();
const encoded = Buffer.from(JSON.stringify(payReq)).toString('base64url');
res.set('Payment', encoded);
return res.status(402).send('Payment required');
}
// Decode and verify
let payReq;
try {
const json = Buffer.from(paymentHeader, 'base64url').toString();
payReq = JSON.parse(JSON.parse(json)); // double‑parse because base64url may have been padded
} catch {
return res.status(400).send('Invalid Payment header');
}
// Basic sanity checks
if (payReq.network !== 8453 || payReq.token.toLowerCase() !== USDC_ADDRESS.toLowerCase()
|| ethers.parseUnits(payReq.amount, USDC_DECIMALS).neq(PRICE_USDC)) {
return res.status(400).send('Misconfigured payment request');
}
// Replay protection
if (usedNonces.has(payReq.nonce)) {
return res.status(409).send('Nonce already used');
}
usedNonces.add(payReq.nonce);
// Verify signature (EIP‑712 typed data)
const domain = {
name: 'x402 Payment',
version: '1',
chainId: 8453,
verifyingContract: USDC_ADDRESS,
};
const types = {
PaymentRequest: [
{ name: 'scheme', type: 'string' },
{ name: 'network', type: 'uint256' },
{ name: 'token', type: 'address' },
{ name: 'amount', type: 'uint256' },
{ name: 'nonce', type: 'bytes32' },
],
};
const value = {
scheme: payReq.scheme,
network: BigInt(payReq.network),
token: payReq.token,
amount: ethers.BigNumber.from(payReq.amount),
nonce: '0x' + payReq.nonce,
};
try {
const recovered = ethers.verifyTypedData(domain, types, value, auth);
if (recovered.toLowerCase() !== ethers.getAddress(process.env.SIGNER_ADDRESS).toLowerCase()) {
throw new Error('Signature mismatch');
}
} catch (e) {
return res.status(401).send('Invalid signature');
}
// Payment verified – fall through to handler
next();
}
/**
* Example protected resource: returns a JSON blob of dummy market data.
*/
app.get('/data', requirePayment, (req, res) => {
res.json({
timestamp: Date.now(),
price: Math.random() * 100,
source: 'x402-demo',
});
});
app.listen(PORT, () => console.log(`x402 demo listening on :${PORT}`));
Key points in the server code
- The
Paymentheader contains a minimal JSON object; it is intentionally short to keep header size low. - Nonce replay protection is done in‑memory for the demo; a production service would store used nonces in a fast KV store (Redis, DynamoDB) with a TTL matching the chain’s finality window.
- Signature verification uses EIP‑712 typed data, which is the standard way wallets sign structured data and allows the agent to show a clear signing prompt (e.g., “Sign x402 payment of 0.01 USDC for /data”).
Agent client (agent.js)
javascript
// npm i ethers axios
require('dotenv').config();
const { ethers } = require('ethers');
const axios = require('axios');
const provider = new ethers.JsonRpcProvider(process.env.BASE_RPC); // e.g. https://base-mainnet.g.alchemy.com/v2/...
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const USDC_DECIMALS = 6;
const PRICE = ethers.parseUnits('0.01', USDC_DECIMALS);
/**
* Generic wrapper that automatically retries on 402.
*/
async function x402Get(url) {
let attempts = 0;
while (true) {
try {
const resp = await axios.get(url);
return resp.data;
} catch (err) {
if (err.response?.status !== 402) throw err;
attempts++;
if (attempts > 3) throw new Error('Failed to satisfy payment after retries');
// Extract payment request
const payReqB64 = err.response.headers.payment;
const payReq = JSON.parse(Buffer.from(payReqB64, 'base64url').toString());
// Build EIP‑712 payload (same as server)
const domain = {
name: 'x402 Payment',
version: '1',
chainId: 8453,
verifyingContract: USDC_ADDRESS,
};
Top comments (0)