x402 Explained: HTTP‑Native Micropayments for AI Agents (With Real Code)
TL;DR – x402 repurposes the HTTP 402 Payment Required status code to let a service request payment before delivering a resource. An autonomous AI agent can treat the code as a normal HTTP response, fetch a payment URL from a wallet, sign a minimal USDC transfer on Base, and retry the request with a proof‑of‑payment header. The flow stays within the standard request/response model—no new protocol, just a convention.
1. Why Micropayments Matter for Agents
Autonomous agents often need to call external APIs: data feeds, LLMs, image generators, or even other agents. When those calls have a cost (e.g., $0.005 per LLM token), you want:
- Atomicity – the agent either gets the resource and pays, or gets nothing.
- Statelessness – no long‑lived session or API‑key management per vendor.
- Compatibility – the agent can reuse its existing HTTP client stack.
Traditional API‑key billing solves the first two points but forces you to manage secrets, rate‑limit handling, and often a monthly invoice that doesn’t map cleanly to per‑call usage. x402 flips the model: the server asks for payment when it’s needed, and the client (your agent) supplies it on the fly.
2. The x402 Flow, Step‑by‑Step
| Step | Actor | Action | HTTP Details |
|---|---|---|---|
| 1 | Client (agent) | GET https://api.example.com/translate |
No auth headers |
| 2 | Server | If payment required → 402 Payment Required + X-Payments-Required: <payload>
|
Payload is a JSON object describing amount, token, chain, and a nonce |
| 3 | Client | Parse payload, build a minimal ERC‑20 transfer transaction, sign it with its wallet (USDC on Base) |
Transaction data: to: USDC contract, value: amount * 1e6, data: erc20Transfer(...)
|
| 4 | Client | Submit transaction to an RPC (or use a relayer) → get tx hash | Optionally wait for confirmation (1‑2 blocks on Base is ~2 s) |
| 5 | Client | Retry original request with X-Payment: <txHash> header |
Server verifies tx on‑chain, checks amount, nonce, and if OK returns 200 + resource |
| 6 | Server | (Optional) cache verified tx hashes to prevent replay | Nonce in payload guarantees uniqueness per request |
The only new header is X‑Payment (client→server) and X‑Payments-Required (server→client). Everything else is vanilla HTTP/1.1 or HTTP/2.
3. Minimal Working Example (Python + httpx + web3.py)
Below is a self‑contained snippet you can drop into an agent that needs to call a hypothetical x402‑protected translation service. It assumes:
- You have a wallet private key stored in an environment variable
AGENT_PRIV_KEY. - USDC contract address on Base:
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913(the canonical USDC on Base). - Base RPC endpoint:
https://base-mainnet.infura.io/v3/<YOUR_INFURA_ID>(or any public RPC).
# --------------------------------------------------------------
# x402 client helper for autonomous agents
# --------------------------------------------------------------
import os
import json
import time
from typing import Any, Dict
import httpx
from web3 import Web3
from eth_account import Account
# ---- Configuration ------------------------------------------------
BASE_RPC = os.getenv("BASE_RPC", "https://base-mainnet.infura.io/v3/<YOUR_INFURA_ID>")
USDC_ADDRESS = Web3.to_checksum_address(
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
)
AGENT_PRIV_KEY = os.getenv("AGENT_PRIV_KEY")
if not AGENT_PRIV_KEY:
raise RuntimeError("Set AGENT_PRIV_KEY env var")
w3 = Web3(Web3.HTTPProvider(BASE_RPC))
account = Account.from_key(AGENT_PRIV_KEY)
# Minimal ERC‑20 ABI for transfer(uint256 amount)
ERC20_ABI = json.loads(
'''[
{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],
"name":"transfer","outputs":[{"name":"","type":"bool"}],"type":"function"},
{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"type":"function"}
]'''
)
usdc_contract = w3.eth.contract(address=USDC_ADDRESS, abi=ERC20_ABI)
# --------------------------------------------------------------
# Helper: sign & send a USDC transfer, wait for receipt
# --------------------------------------------------------------
def pay_usdc(to: str, amount_usdc: float) -> str:
"""
Sends `amount_usdc` USDC to `to` address.
Returns the transaction hash (hex).
"""
decimals = usdc_contract.functions.decimals().call()
amount = int(amount_usdc * (10 ** decimals))
# Build transaction
tx = usdc_contract.functions.transfer(
Web3.to_checksum_address(to), amount
).build_transaction({
"chainId": w3.eth.chain_id,
"from": account.address,
"nonce": w3.eth.get_transaction_count(account.address),
"gas": 100_000, # USDC transfer is cheap; safe upper bound
"maxFeePerGas": w3.to_wei(2, "gwei"),
"maxPriorityFeePerGas": w3.to_wei(1, "gwei"),
})
# Sign & send
signed = account.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed.rawTransaction)
# Wait for inclusion (2 blocks on Base ≈ 2 s)
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=30)
if receipt.status != 1:
raise RuntimeError(f"USDC transfer failed: {receipt}")
return tx_hash.hex()
# --------------------------------------------------------------
# x402‑aware GET request
# --------------------------------------------------------------
def x402_get(url: str, max_retries: int = 3) -> httpx.Response:
"""
Performs a GET request, handling 402 responses by paying with USDC.
Retries up to `max_retries` times on payment failure.
"""
headers = {}
for attempt in range(max_retries):
resp = httpx.get(url, headers=headers, timeout=10.0)
if resp.status_code != 402:
return resp # success or other error
# ----- Parse payment payload -----
try:
payload = json.loads(resp.headers["X-Payments-Required"])
except (KeyError, json.JSONDecodeError) as exc:
raise RuntimeError(f"Malformed 402 header: {exc}") from exc
# Expected fields: amount (USDC), token, chain, nonce, payee
amount_usdc = float(payload["amount"])
token = payload["token"] # e.g. "USDC"
chain = payload["chain"] # e.g. "base"
payee = payload["payee"] # recipient address
nonce = payload["nonce"] # server‑generated UUID
if token != "USDC" or chain != "base":
raise RuntimeError("Unsupported token/chain in 402 payload")
# ----- Pay -----
try:
tx_hash = pay_usdc(payee, amount_usdc)
except Exception as exc:
# If payment fails, backoff and retry (maybe gas spike)
time.sleep(2 ** attempt)
continue
# ----- Retry original request with proof -----
headers = {"X-Payment": tx_hash}
# Optionally include nonce to help server deduplicate
headers["X-Payment-Nonce"] = nonce
# If we exit loop, we failed after max_retries
raise RuntimeError(f"Failed to satisfy x402 payment after {max_retries} attempts")
# --------------------------------------------------------------
# Example usage
# --------------------------------------------------------------
if __name__ == "__main__":
translation_url = "https://api.example.com/translate?text=Hello%20world&target=es"
response = x402_get(translation_url)
print("Status:", response.status_code)
print("Body:", response.json())
What the code does
-
Handles a 402 – reads
X-Payments-Required, extracts amount, token, chain, nonce, and payee. -
Pays – builds a minimal USDC
transfertransaction, signs it with the agent’s private key, and sends it to Base. -
Retries – adds
X-Payment:<txHash>(and optionally the nonce) to the request headers and repeats the call. - Fails gracefully – if the server rejects the proof (e.g., nonce replay, insufficient funds), you get a non‑200 response you can log and act on.
Note: This example keeps the transaction simple (fixed gas limit, EIP‑1559 fee fields). In production you may want to estimate gas dynamically, use a relayer to avoid needing ETH for gas, or batch multiple micropayments into a single transaction to save on Base’s ~$0.0001 per tx cost.
4. Honest Trade‑offs & Practical Considerations
| Aspect | Benefit |
Top comments (0)