x402 Explained: HTTP-Native Micropayments for AI Agents (With Real Code)
Target audience: developers who are building autonomous AI agents that need to charge for API calls, data look‑ups, or compute resources in a trust‑less way.
1. Why Micropayments Matter for Agents
An AI agent that can act on behalf of a user often needs to consume external services—LLM inference, vector search, weather data, or even simple HTTP hooks. Traditional payment flows (credit‑card gateways, invoicing, or subscription dashboards) add friction:
- They require out‑of‑band user interaction (login, OTP, redirect).
- They introduce latency that breaks the “instant‑response” expectation of an agent loop.
- They lock the agent into a specific billing provider, making multi‑tenant composability hard.
x402 proposes to keep the payment inside the HTTP request/response cycle by using a new status code (402 Payment Required) and a standardized header format for the payment request and proof. The agent can then automatically settle the charge with a blockchain transaction before proceeding, all without leaving the HTTP stack.
2. The x402 Flow (HTTP‑Native)
| Step | Actor | Message | Meaning |
|---|---|---|---|
| 1 | Client (agent) | GET /resource HTTP/1.1 |
Normal request |
| 2 | Server |
402 Payment RequiredPaywall: <payment‑request‑json>
|
Server says “pay X to continue” |
| 3 | Client | Constructs a blockchain transaction that satisfies the request (amount, token, destination, nonce, etc.) and signs it. | Agent pays |
| 4 | Client |
GET /resource HTTP/1.1Paywall-Proof: <signed‑tx‑hex>
|
Provides proof of payment |
| 5 | Server | Verifies the proof (on‑chain or via an indexer). If valid, returns 200 OK with the payload. |
Service delivered |
| 6 | (Optional) Server | May include a Paywall-Receipt header for the client’s records. |
Auditable trail |
The Paywall header contains a JSON object (base64‑url encoded) with fields such as:
{
"version": "1",
"schema": "x402",
"network": "base",
"currency": "usdc",
"amount": "0.005", // in smallest unit (e.g., 5 mUSDC)
"payee": "0xAbC...def",
"nonce": "2024-09-26T12:34:56Z",
"metadata": { "service": "text‑embedding", "version": "2.1" }
}
The Paywall-Proof header is simply the raw transaction hex (or a compact representation like EIP‑2718 typed transaction) that the server can replay against an RPC endpoint or a trusted indexer.
3. Minimal Working Example (Python + httpx + web3.py)
Below is a self‑contained snippet that shows how an AI agent can:
- Call a protected endpoint.
- Interpret a
402response. - Build and sign a USDC transfer on Base.
- Retry the request with the proof.
Note: This example uses the Base testnet for simplicity. Swap the RPC URL, chain ID, and contract address for mainnet when you go live.
# agent_x402.py
import json
import base64
import os
from typing import Dict
import httpx
from web3 import Web3
from eth_account import Account
from eth_account.messages import encode_defunct
# ----------------------------------------------------------------------
# Configuration – replace with your own values
# ----------------------------------------------------------------------
BASE_RPC = "https://base.sepolia.org" # Sepolia testnet Base RPC
USDC_ADDRESS = "0x036CbD53842c5426634e7929541cE260a48f5e9E" # USDC on Base Sepolia
CHAIN_ID = 84532 # Base Sepolia
PRIVATE_KEY = os.getenv("AGENT_PRIVATE_KEY") # 0x-prefixed hex
ACCOUNT = Account.from_key(PRIVATE_KEY)
W3 = Web3(Web3.HTTPProvider(BASE_RPC))
# ----------------------------------------------------------------------
# Helper: decode base64url Paywall header
# ----------------------------------------------------------------------
def _b64url_decode(s: str) -> dict:
# Add padding if needed
padding = 4 - len(s) % 4
if padding != 4:
s += "=" * padding
return json.loads(base64.urlsafe_b64decode(s))
# ----------------------------------------------------------------------
# Step 1: make the initial request
# ----------------------------------------------------------------------
def call_protected(url: str) -> httpx.Response:
return httpx.get(url, timeout=10.0)
# ----------------------------------------------------------------------
# Step 2: if we get 402, build a payment transaction
# ----------------------------------------------------------------------
def build_usdc_transfer(paywall: Dict) -> str:
"""
Returns a signed transaction hex (EIP‑2718 type 0) that transfers
the requested USDC amount to the payee.
"""
amount_wei = int(float(paywall["amount"]) * 1e6) # USDC has 6 decimals
nonce = W3.eth.get_transaction_count(ACCOUNT.address)
tx = {
"to": Web3.to_checksum_address(USDC_ADDRESS),
"value": 0,
"data": _build_transfer_data(
Web3.to_checksum_address(paywall["payee"]),
amount_wei,
),
"chainId": CHAIN_ID,
"nonce": nonce,
"gas": 100_000,
"maxFeePerGas": W3.to_wei(2, "gwei"),
"maxPriorityFeePerGas": W3.to_wei(1, "gwei"),
}
signed = ACCOUNT.sign_transaction(tx)
return signed.raw_transaction.hex()
def _build_transfer_data(to: str, amount: int) -> str:
"""
ERC‑20 transfer ABI: 0xa9059cbb + addr padded + amount padded
"""
selector = Web3.keccak(text="transfer(address,uint256)")[:4].hex()
addr_padded = Web3.to_hex(Web3.to_bytes(hexstr=to).rjust(32, b'\0'))
amount_padded = Web3.to_hex(Web3.to_bytes(amount).rjust(32, b'\0'))
return "0x" + selector + addr_padded[2:] + amount_padded[2:]
# ----------------------------------------------------------------------
# Step 3: retry with proof
# ----------------------------------------------------------------------
def call_with_proof(url: str, proof_tx_hex: str) -> httpx.Response:
headers = {"Paywall-Proof": proof_tx_hex}
return httpx.get(url, headers=headers, timeout=10.0)
# ----------------------------------------------------------------------
# Orchestrator
# ----------------------------------------------------------------------
def fetch_resource(url: str) -> str:
resp = call_protected(url)
if resp.status_code == 402:
# Parse Paywall header
raw = resp.headers.get("Paywall")
if not raw:
raise RuntimeError("402 received without Paywall header")
paywall = _b64url_decode(raw)
# Build & sign payment
proof_hex = build_usdc_transfer(paywall)
# Retry with proof
resp = call_with_proof(url, proof_hex)
if resp.status_code != 200:
raise RuntimeError(f"Failed: {resp.status_code} {resp.text}")
return resp.text
# ----------------------------------------------------------------------
# Example usage
# ----------------------------------------------------------------------
if __name__ == "__main__":
protected_endpoint = "https://api.example.com/v1/embed"
try:
result = fetch_resource(protected_endpoint)
print("Success:", result[:200])
except Exception as e:
print("Error:", e)
What the code does
-
First GET – if the service expects payment, it returns
402with aPaywallheader. - Parse the header to extract amount, token, payee, and network.
-
Construct an ERC‑20
transfertransaction for USDC (6 decimals) and sign it with the agent’s private key. -
Retry the same GET, adding the signed transaction hex as
Paywall-Proof. - The service verifies that the transaction:
- was sent from the agent’s address,
- transferred the exact amount to the expected payee,
- occurred after the nonce timestamp (to prevent replay),
- and succeeded on-chain (via an RPC call or indexer).
- On success, the service replies with
200and the requested payload.
4. Honest Trade‑offs & Practical Considerations
| Aspect | Benefit | Cost / Risk |
|---|---|---|
| Atomicity | Payment verification happens before the service does any work, eliminating the need for escrow or post‑hoc refunds. | Requires the service to run a node or rely on a trusted indexer; otherwise you introduce centralization or extra latency. |
| Latency | One extra round‑trip (the retry) is unavoidable; however, the payment |
Top comments (0)