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 API calls.
1. Why look at x402?
Autonomous agents frequently need to consume paid services — LLMs, data feeds, compute — without a human in the loop to manage subscriptions or API keys. Traditional approaches (API keys, OAuth, prepaid balances) either require out‑of‑band account management or add latency because the agent must first query a billing service.
The x402 proposal repurposes the HTTP status code 402 Payment Required to turn every request into a self‑contained payment negotiation. If the client can satisfy the payment challenge, the server immediately serves the resource; otherwise the client receives a clear, machine‑readable instruction on how to pay.
Because the protocol lives entirely in HTTP headers, it works with any language, any transport (including HTTP/2 or QUIC), and does not require a separate billing microservice. The downside is that the agent must be able to sign and broadcast a blockchain transaction, which adds complexity and depends on chain finality.
2. The x402 flow in a nutshell
| Step | Actor | Action |
|---|---|---|
| 1 | Client | Sends a normal GET/POST to the protected endpoint. |
| 2 | Server | If no valid payment proof is present, returns 402 with a Payment-Required header that contains a JSON‑encoded invoice: amount, asset, chain, destination, nonce, expiry, and a server signature. |
| 3 | Client | Parses the invoice, builds and signs a transaction that pays the exact amount to the destination, waits for inclusion (or a configurable confirmation depth), then extracts the transaction hash. |
| 4 | Client | Retries the original request, adding a Payment header whose value is the transaction hash (or a signed receipt). |
| 5 | Server | Verifies the payment on‑chain (matching amount, asset, destination, nonce, and that the transaction is sufficiently confirmed). If valid, returns 200 and the requested data; otherwise another 402 with an error code. |
The protocol is deliberately stateless on the server side: the nonce prevents replay attacks, and the signature guarantees the invoice really came from the claimed service.
3. Minimal server implementation (Node.js + Express)
Below is a compact, production‑ready sketch that you can drop into an existing Express app. It assumes you have a wallet whose private key is stored in an environment variable (SERVER_PRIVATE_KEY) and that you want to accept USDC on Base (Chain ID 8453).
// x402-server.js
require('dotenv').config();
const express = require('express');
const { ethers } = require('ethers');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// Configuration
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // Base USDC
const CHAIN_ID = 8453;
const SERVER_ADDRESS = new ethers.Wallet(process.env.SERVER_PRIVATE_KEY).address;
// Helper: create a deterministic nonce per (path, method, ip)
function makeNonce(req) {
const hash = crypto.createHash('sha256')
.update(`${req.method}|${req.path}|${req.ip}`)
.digest('hex');
return `0x${hash.slice(0, 16)}`; // 8‑byte hex
}
// Helper: sign the invoice
async function signInvoice(invoice) {
const wallet = new ethers.Wallet(process.env.SERVER_PRIVATE_KEY);
const msgHash = ethers.utils.solidityKeccak256(
['address', 'uint256', 'address', 'uint256', 'bytes32', 'uint256'],
[
invoice.destination,
invoice.amount,
invoice.asset,
invoice.chainId,
ethers.utils.keccak256(ethers.utils.toUtf8Bytes(invoice.nonce)),
invoice.expiry,
]
);
const signature = await wallet.signMessage(ethers.utils.arrayify(msgHash));
return signature;
}
// Middleware that enforces x402
async function requirePayment(req, res, next) {
const auth = req.headers['payment'];
if (auth) {
// Verify payment proof
const txHash = auth.trim();
try {
const provider = new ethers.providers.JsonRpcProvider(
process.env.BASE_RPC_URL
);
const tx = await provider.getTransaction(txHash);
if (!tx) throw new Error('tx not found');
const receipt = await tx.wait();
if (receipt.status !== 1) throw new Error('tx failed');
// Basic on‑chain checks
if (tx.to.toLowerCase() !== SERVER_ADDRESS.toLowerCase())
throw new Error('wrong recipient');
if (tx.chainId !== CHAIN_ID) throw new Error('wrong chain');
const [, , asset, amountStr] = await provider.call({
to: USDC_ADDRESS,
data: '0x70a08231000000000000000000000000' + tx.from.slice(2).padStart(64, '0'),
});
// In a real service you'd decode the ERC‑20 transfer event; omitted for brevity.
// Here we trust the amount matches the invoice (the client must include it).
// For brevity we skip full ERC‑20 verification; production code should verify.
return next(); // payment ok
} catch (e) {
return res.status(402).json({ error: 'invalid payment', details: e.message });
}
}
// No payment proof → issue an invoice
const nonce = makeNonce(req);
const expiry = Math.floor(Date.now() / 1000) + 300; // 5 min
const amount = ethers.utils.parseUnits('0.05', 6); // $0.05 USDC (6 decimals)
const invoice = {
destination: SERVER_ADDRESS,
amount: amount.toString(),
asset: USDC_ADDRESS,
chainId: CHAIN_ID,
nonce,
expiry,
};
const signature = await signInvoice(invoice);
const paymentRequired = {
invoice,
signature,
// Optional: a human‑readable description
description: 'Access to AI‑agent endpoint',
};
res.set('Payment-Required', JSON.stringify(paymentRequired));
return res.status(402).send('Payment required');
}
// Example protected route
app.get('/agent/analyze', requirePayment, (req, res) => {
// Your agent logic goes here
res.json({ result: 'analysis complete', timestamp: Date.now() });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`x402 server listening on ${PORT}`));
What this code does
- Generates a reproducible nonce from the request’s method, path, and client IP to prevent replay.
- Builds an invoice that specifies the exact USDC amount, the Base contract address, chain ID, nonce, and expiry.
- Signs the invoice with the server’s ECDSA key so the client can verify authenticity.
- On receipt of a
Paymentheader, it extracts the transaction hash, fetches the transaction and receipt from an RPC node, and performs minimal sanity checks (recipient, chain, status). A production implementation would also decode the ERC‑20Transferevent to confirm the amount and asset. - Returns
402with a JSON‑encodedPayment-Requiredheader when no proof is present.
4. Client‑side handling (Python + viem)
Below is a minimal autonomous‑agent snippet that calls the protected endpoint, interprets the 402 response, builds and signs a USDC transfer on Base using a private key, and retries the request.
python
# x402-client.py
import os, json, time, requests
from eth_account import Account
from eth_account.messages import encode_defunct
from web3 import Web3
BASE_RPC = os.getenv("BASE_RPC_URL")
USDC = Web3.to_checksum_address("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")
PRIVATE_KEY = os.getenv("AGENT_PRIVATE_KEY") # agent's wallet
ACCOUNT = Account.from_key(PRIVATE_KEY)
WEB3 = Web3(Web3.HTTPProvider(BASE_RPC))
def build_usdc_transfer(to, amount_wei):
"""
Returns a raw ERC‑20 transfer transaction (E
Top comments (0)