x402 Explained: HTTP‑Native Micropayments for AI Agents (With Real Code)
Audience: developers building autonomous AI agents who need a lightweight, standards‑based way to charge per‑request without reinventing a payment gateway.
1. What is x402?
x402 is an experimental HTTP status code (proposed as 402 Payment Required) that lets a server signal that a client must attach a verifiable payment before the resource can be served. Unlike traditional payment flows that redirect to a checkout page or require out‑of‑band invoicing, x402 keeps everything inside the HTTP request/response cycle:
-
Client sends a normal
GET/POSTrequest. -
Server replies with
402 Payment Requiredand includes aPayment-Headersfield describing:- The payment scheme (e.g.,
erc20-usdcon Base). - The exact amount (in the smallest token unit).
- A nonce or timestamp to prevent replay.
- Optional instructions for constructing the payment proof.
- The payment scheme (e.g.,
-
Client constructs a cryptographic proof (usually a signed ERC‑20 transfer) that satisfies the header, then retries the original request with an
Authorization: Bearer <proof>header (or a customx402-Paymentheader). -
Server validates the proof against the blockchain (or an off‑chain relayer) and, if valid, returns
200 OKwith the requested payload.
Because the negotiation happens via standard headers, any HTTP client—including fetch, Axios, or an AI agent’s internal HTTP wrapper—can participate without SDKs.
2. Why x402 Makes Sense for AI Agents
Agents often need to call paid APIs (LLM inference, data feeds, tool execution) many times per second. Traditional OAuth‑2 or API‑key models either:
- Require static keys that are hard to rotate per‑agent, or
- Introduce latency via redirects or external payment processors.
x402 offers:
| Property | Benefit for Agents |
|---|---|
| Stateless | No server‑side session; each request carries its own payment proof. |
| Atomic | Payment verification and service delivery happen in the same round‑trip. |
| Programmable | Agents can compute the exact cost (e.g., $0.003 per token) and attach the matching amount. |
| Chain‑agnostic | Works with any ERC‑20 compatible token; the example uses USDC on Base for low gas. |
| Open | No proprietary gateway; anyone can implement a 402 responder. |
3. Honest Trade‑offs
| Trade‑off | Explanation |
|---|---|
| Blockchain latency | Proof verification requires reading the latest block (or trusting a relayer). On Base, finality is ~2 seconds; acceptable for many agent workflows but not for sub‑second latency loops. |
| Gas cost for the payer | The agent must fund an ERC‑20 transfer (even if minimal). On Base, a USDC transfer costs ~0.0005 USD in gas, which is negligible compared to the micropayment but still a non‑zero overhead. |
| Replay protection | Servers must store nonces or use timestamps; this adds a small state requirement. Stateless schemes (e.g., using EIP‑712 signed messages with a nonce) can mitigate but increase complexity. |
| Client‑side wallet management | Agents need a private key capable of signing ERC‑20 transfers. In serverless environments, this often means using a managed wallet service or encrypting a key and decrypting at runtime—introducing security considerations. |
| Limited adoption | As of 2025, x402 is still an experimental proposal; not all infrastructure (proxies, CDNs) automatically forwards the 402 status. You may need to patch middleware or run your own gateway. |
If your agent can tolerate a ~1‑second extra round‑trip and you’re comfortable handling a wallet key, x402 is a pragmatic fit. If you need sub‑100 ms latency or cannot manage keys, consider a traditional API‑key with usage‑based billing instead.
4. Minimal Working Example
Below is a Node.js/TypeScript snippet that shows:
- How an AI agent pays for a resource using x402 (USDC on Base).
- How a simple server enforces the payment.
Assumptions
- You have an Ethereum wallet (private key) with USDC on Base.
- You have
ethers@v6installed.- The server runs locally on
http://localhost:3000/price.
4.1 Agent (payer)
// agent.ts
import { ethers } from "ethers";
import fetch from "node-fetch";
// ---------- CONFIG ----------
const PRIVATE_KEY = process.env.PRIVATE_KEY!; // funds USDC on Base
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // Base USDC
const RPC_URL = "https://base-mainnet.infura.io/v3/<PROJECT_ID>";
const AMOUNT_USDC = 0.005; // $0.005 per call (adjust as needed)
const DECIMALS = 6; // USDC
// ---------------------------
const provider = new ethers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const usdc = new ethers.Contract(
USDC_ADDRESS,
["function transfer(address to, uint256 amount) returns (bool)"],
signer
);
async function callProtectedEndpoint() {
// Step 1: Try the request without payment
let resp = await fetch("http://localhost:3000/price", { method: "GET" });
if (resp.status === 200) {
console.log("Already paid? Body:", await resp.text());
return;
}
// Step 2: Expect 402 and parse payment headers
if (resp.status !== 402) {
throw new Error(`Unexpected status: ${resp.status}`);
}
const paymentInfoHeader = resp.headers.get("x402-payment");
if (!paymentInfoHeader) {
throw new Error("Missing x402-payment header");
}
const { scheme, amount, nonce, token } = JSON.parse(paymentInfoHeader);
// Basic validation (in production, verify scheme matches your expectations)
if (scheme !== "erc20-usdc" || token.toLowerCase() !== USDC_ADDRESS.toLowerCase()) {
throw new Error("Unsupported payment scheme");
}
// Step 3: Build the ERC‑20 transfer proof
const amountWei = ethers.parseUnits(amount.toString(), DECIMALS);
const tx = await usdc.transfer(
"0xServerReceiverAddress", // replace with the server's USDC address
amountWei
);
const receipt = await tx.wait();
// Step 4: Retry with proof (using Authorization bearer)
const proof = `${receipt.hash}`; // simple hash‑based proof; replace with your spec
const authHeader = `Bearer ${proof}`;
resp = await fetch("http://localhost:3000/price", {
method: "GET",
headers: { Authorization: authHeader },
});
if (resp.status !== 200) {
throw new Error(`Payment rejected: ${resp.status} ${await resp.text()}`);
}
const body = await resp.text();
console.log("Successful response:", body);
}
callProtectedEndpoint().catch(console.error);
What the agent does
- Performs an optimistic GET.
- On
402, reads the payment instruction (scheme,amount,nonce,token). - Sends a USDC transfer to the server’s address.
- Uses the transaction hash as a proof and retries with an
Authorization: Bearer <hash>header. - If the server validates the hash, it returns
200.
Note: The proof format here is deliberately minimal for illustration. A production implementation would follow the exact
x402spec (e.g., an EIP‑712 signed message containing the request URI, nonce, amount, and timestamp) to prevent replay attacks.
4.2 Simple Server (paywall)
ts
// server.ts
import express from "express";
import { ethers } from "ethers";
const app = express();
const PORT = 3000;
// ---------- CONFIG ----------
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const RPC_URL = "https://base-mainnet.infura.io/v3/<PROJECT_ID>";
const provider = new ethers.JsonRpcProvider(RPC_URL);
// Expected payment: $0.005 USDC (= 5,000,000 wei with 6 decimals)
const PRICE_WEI = ethers.parseUnits("0.005", 6);
// In production, store used nonces in a Redis set or DB to prevent replays.
const usedNonces = new Set<string>();
// ---------------------------
app.get("/price", async (req, res) => {
const auth = req.headers.authorization;
if (!auth?.startsWith("Bearer ")) {
// No proof -> ask for payment
const nonce = ethers.randomBytes(8).toString("hex");
res.set({
"x402-payment": JSON.stringify({
scheme: "erc20-usdc",
amount: ethers.formatUnits(PRICE_WEI, 6), // human readable
token: USDC_ADDRESS,
nonce,
}),
});
return res.status
Top comments (0)