x402 Explained: HTTP‑Native Micropayments for AI Agents (With Real Code)
Target audience: developers who are building autonomous AI agents that need to discover, call, and pay for micro‑services without building a custom billing layer.
Why HTTP‑402 for Micropayments?
The HTTP status code 402 Payment Required was reserved in the original spec but never widely used. The x402 proposal (see x402.org) repurposes it as a lightweight, standards‑based way to request payment in‑band with an HTTP request/response cycle.
Key properties that make it attractive for agent‑to‑agent interactions:
| Property | What it means for agents |
|---|---|
| Stateless | No session cookies or OAuth dances; each request carries enough info to prove payment. |
| Currency‑agnostic | The spec only requires a payment pointer (e.g., a USDC address) and an amount; the actual settlement can happen on any compatible chain. |
| Built on existing infrastructure | Works with any HTTP client/server, proxies, CDNs, and API gateways that understand status codes. |
| Atomic per‑call | Payment is verified before the handler runs, eliminating “pay‑after‑use” trust issues. |
In practice, an agent makes a normal GET/POST. If the resource requires payment, the server replies 402 with a header (Pay) that tells the client how and how much to pay. The client then fulfills the payment (usually via a blockchain transaction) and retries the request with a proof‑of‑payment header (X-Payment). The server validates the proof and, if correct, returns 200 with the payload.
The x402 Flow, Step‑by‑Step
- Client → Server (initial request)
GET /agent/summarize HTTP/1.1
Host: api.example.com
Accept: application/json
- Server → Client (payment request)
HTTP/1.1 402 Payment Required
Content-Type: application/json
Pay: ["http://payment.example.com/invoice", {"scheme":"erc20","network":"base","asset":"USDC","amount":"0.05"}]
-
Payis a JSON array:[paymentPointer, metadata]. -
paymentPointeris a URL where the client can fetch a payment invoice (or construct one directly if the metadata is sufficient).
Client → Blockchain (settle the invoice)
The client creates a transaction that transfers the exact amount of USDC to the address encoded in the pointer, includes a nonce or memo to make it unique, and waits for confirmation.Client → Server (retry with proof)
GET /agent/summarize HTTP/1.1
Host: api.example.com
X-Payment: {"tx":"0xabc…def","chainId":8453,"blockNumber":12345678}
Accept: application/json
- Server → Client (service)
HTTP/1.1 200 OK
Content-Type: application/json
{"summary":"…"}
If the proof is invalid, missing, or replayed, the server returns 402 again (or 400 Bad Request) and the loop can repeat.
Minimal Working Example (Node.js + ethers.js)
Below is a self‑contained example that shows both sides of the flow. It uses the Base testnet (you can swap to mainnet) and USDC (0x036CbD53842c5426634e7929541eC2318f3dCF7e). The code is deliberately terse to highlight the protocol, not production‑grade security.
1. Server (Express)
js
// server.js
require('dotenv').config();
const express = require('express');
const { ethers } = require('ethers');
const app = express();
app.use(express.json());
const USDC_ADDRESS = '0x036CbD53842c5426634e7929541eC2318f3dCF7e'; // Base testnet USDC
const PAYEE = process.env.PAYEE_ADDRESS; // your agent's wallet
const provider = new ethers.JsonRpcProvider(process.env.BASE_RPC);
const usdc = new ethers.Contract(USDC_ADDRESS, ['function balanceOf(address) view returns (uint256)'], provider);
// Middleware that checks for a valid payment proof
async function requirePayment(req, res, next) {
const proofHeader = req.headers['x-payment'];
if (!proofHeader) {
// No proof → ask for payment
const amount = '0.05'; // USDC, 6 decimals
const payPtr = {
scheme: 'erc20',
network: 'base',
asset: USDC_ADDRESS,
amount,
};
return res.status(402)
.set('Pay', JSON.stringify(['https://example.com/invoice', payPtr]))
.json({ error: 'payment required' });
}
let proof;
try { proof = JSON.parse(proofHeader); } catch { return res.status(400).json({ error: 'bad X-Payment' }); }
// Basic validation: tx exists, sends correct amount to PAYEE
try {
const tx = await provider.getTransaction(proof.tx);
if (!tx) throw new Error('tx not found');
if (tx.to?.toLowerCase() !== PAYEE.toLowerCase()) throw new Error('wrong recipient');
const receipt = await tx.wait();
if (receipt.status !== 1) throw new Error('tx failed');
// fetch USDC amount from tx data (simplified: assume plain transfer)
if (tx.data !== '0xa9059cbb000000000000000000000000' + PAYEE.slice(2) + '0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Top comments (0)