DEV Community

Nikhil Ranka
Nikhil Ranka

Posted on

x402 Explained: HTTP-Native Micropayments for AI Agents (With Real Code)

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())
Enter fullscreen mode Exit fullscreen mode

What the code does

  1. Handles a 402 – reads X-Payments-Required, extracts amount, token, chain, nonce, and payee.
  2. Pays – builds a minimal USDC transfer transaction, signs it with the agent’s private key, and sends it to Base.
  3. Retries – adds X-Payment:<txHash> (and optionally the nonce) to the request headers and repeats the call.
  4. 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)