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 each inference, tool call, or data fetch without reinventing a payment gateway.
Why HTTP 402?
The HTTP status code 402 Payment Required has existed since the original spec, but it was never widely used. The x402 proposal (see the IETF draft draft-ietf-httpbis‑x402‑00) revives it as a native mechanism for attaching a verifiable payment to a single HTTP request/response exchange.
Key properties that make x402 attractive for AI agents:
| Property | Why it matters for agents |
|---|---|
| Stateless | No session cookies or OAuth flows; each request carries its own proof of payment. |
| Header‑based | Payment data lives in a request header (X-Payment) and the server replies with a 402 that includes a payment request object. |
| Chain‑agnostic | The spec only requires a signed payload; you can plug in USDC on Base, ERC‑20 on Polygon, or even a custodial API key. |
| Cache‑friendly | If a response is cacheable, the payment proof can be validated once and reused (subject to freshness rules). |
In practice, an agent sends a request, receives a 402 if it hasn’t paid, attaches a signed payment proof, and retries. The server validates the proof, processes the request, and returns the normal 2xx response.
The Payment Proof Format
x402 defines a JSON object that must be JWS‑signed (JSON Web Signature) by the payer’s private key. The unsigned payload looks like:
{
"scheme": "exact",
"network": "base",
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC contract on Base
"amount": "1000000", // 1 USDC = 1e6 (6 decimals)
"payload": "<base64url‑encoded request‑specific data>",
"exp": 1735689600 // Unix timestamp, optional expiration
}
`payload` is a hash of the request (method, URL, body) that prevents replay attacks. The server can recompute it and verify the signature matches the address that signed the JWS.
Note: The spec leaves the exact hashing algorithm to the implementation; the examples below use SHA‑256 of the canonical request string.
Server‑Side: Enforcing x402 in Python (FastAPI)
Below is a minimal FastAPI app that protects an /infer endpoint with x402. It expects a USDC payment of $0.01 (10 000 wei‑scaled units) per call.
# file: agent_server.py
import os
import json
import base64
import hashlib
from datetime import datetime, timezone
from fastapi import FastAPI, Request, HTTPException, Header
from pydantic import BaseModel
from eth_account.messages import encode_defunct
from eth_account import Account
app = FastAPI()
# ---- Configuration -------------------------------------------------
USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" # Base USDC
REQUIRED_AMOUNT = "1000000" # 1 USDC (6 decimals) → $0.01 if 1 USDC = $1
CHAIN_ID = 8453 # Base
# In a real service you would verify the signature against a known payer
# address or allow any address that pays the required amount.
# For demo purposes we accept any valid signature.
# --------------------------------------------------------------------
def _canonical_request(req: Request) -> str:
"""
Build a deterministic string that represents the request.
Must match exactly what the client signed.
"""
method = req.method.upper()
url = str(req.url)
# Note: we ignore query ordering for simplicity; production should sort.
body = b""
# Consume body without breaking downstream handlers
async def receive():
nonlocal body
chunk = await req.stream()
body += chunk
return chunk
# We'll read the body now; in a real ASGI app you'd use a middleware.
import asyncio
asyncio.run(_read_body(req))
body_str = body.decode()
return f"{method}\n{url}\n{body_str}"
async def _read_body(req: Request):
body = b""
async for chunk in req.stream():
body += chunk
req._body = body # type: ignore
def _verify_jws(jws: str, payload: str) -> str:
"""
Very light JWS verification: expects `header.payload.signature`
with ES256K (ECDSA secp256k1) signature.
Returns the signer address if valid, else raises.
"""
try:
header_b64, payload_b64, sig_b64 = jws.split(".")
except ValueError:
raise HTTPException(status_code=400, detail="Malformed JWS")
# decode
header = json.loads(base64.urlsafe_b64decode(header_b64 + "=="))
if header.get("alg") != "ES256K":
raise HTTPException(status_code=400, detail="Unsupported alg")
# Reconstruct signing input
signing_input = f"{header_b64}.{payload_b64}".encode()
signature = base64.urlsafe_b64decode(sig_b64 + "==")
# Recover address from signature
message = encode_defunct(text=payload)
# eth_account expects the raw payload (not the JWS payload)
# Here we assume the JWS payload is the exact string we signed.
recovered = Account.recover_message(message, signature=signature)
return recovered.lower()
@app.post("/infer")
async def infer(
request: Request,
x_payment: str = Header(None, alias="X-Payment"),
):
# 1️⃣ If no payment header, ask for payment via 402
if not x_payment:
payment_request = {
"scheme": "exact",
"network": str(CHAIN_ID),
"token": USDC_ADDRESS,
"amount": REQUIRED_AMOUNT,
# payload is a hash of the request the client will sign
"payload": base64.urlsafe_b64encode(
hashlib.sha256(_canonical_request(request).encode()).digest()
).decode(),
"exp": int((datetime.now(timezone.utc).timestamp()) + 300), # 5‑min window
}
# The server signs this with its own key to prove authenticity (optional)
# For brevity we omit server signature; clients trust the URL.
raise HTTPException(
status_code=402,
detail=json.dumps(payment_request),
headers={"Content-Type": "application/json"},
)
# 2️⃣ Verify the JWS
try:
payer = _verify_jws(x_payment, _canonical_request(request))
except HTTPException:
raise # re‑raise validation errors
# 3️⃣ (Optional) Check that the payer actually sent enough funds.
# In a production system you would query a blockchain indexer or
# rely on a custodial service that validates the payment off‑chain.
# Here we accept any valid signature as proof of payment.
# 4️⃣ Process the request (dummy AI inference)
data = await request.json()
prompt = data.get("prompt", "")
# Replace with real model call
answer = f"Echo: {prompt[::-1]}" # just reverse the string for demo
return {"answer": answer, "paid_by": payer}
What the server does
-
Missing
X-Payment→ returns 402 with a JSON payment request object. - Client receives 402, builds a JWS signed with their wallet, retries.
- Server verifies the signature and (optionally) checks the amount/chain.
- If everything matches, the request is processed and a normal 200 is returned.
Trade‑off: The server must implement JWS verification and a way to check that the paid amount matches the request. Doing this on‑chain adds latency and cost; doing it off‑chain requires trust in a payment processor or indexer.
Client‑Side: Paying an x402‑Protected Endpoint (JavaScript)
The following snippet shows how an autonomous agent can automatically handle a 402 response, sign the required payload, and retry the request. It uses ethers.js for signing and assumes
Top comments (0)