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)

By a senior engineer who’s actually tried it


Why x402 matters for autonomous agents

When an AI agent needs to call another service—say a language model, a data fetch, or a compute‑heavy function—it usually does so over HTTP. Traditional monetisation (API keys, subscription tiers, or invoicing) forces the agent to carry static credentials, manage billing cycles, or wait for human‑approved invoices. None of those fit a truly stateless agent that may spin up, make a single request, and disappear.

The x402 proposal (an extension of the HTTP 402 Payment Required status) solves this by turning payment into a first‑class HTTP concern:

  1. The server replies 402 Payment Required with a Pay header that describes how much and in what token to pay.
  2. The client (the agent) attaches a signed payment proof in the Pay header of the retry request.
  3. If the proof validates, the server returns the requested resource with a normal 200 OK.

Because the whole flow lives in HTTP headers, no new transport protocol, SDK, or language‑specific library is required—any HTTP client can participate.


The wire format in practice

A minimal Pay header looks like:

Pay: scheme="x402-version-1"
     network="base"
     token="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"   # USDC on Base
     amount="1000000"                                 # 0.01 USDC (6 decimals)
     payload="base64url(<request‑id>|<timestamp>)"
Enter fullscreen mode Exit fullscreen mode
  • scheme tells the client which version of the spec to follow.
  • network and token identify the blockchain and ERC‑20 contract.
  • amount is the smallest unit (USDC uses 6 decimals).
  • payload is opaque data the server can bind to a nonce or request ID to prevent replay attacks.

When the client retries, it adds a Pay header containing a signed version of the same fields plus a signature parameter:

Pay: scheme="x402-version-1"
     network="base"
     token="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
     amount="1000000"
     payload="base64url(<request‑id>|<timestamp>)"
     signature="0xabcdef…"
Enter fullscreen mode Exit fullscreen mode

The signature is an ECDSA‑secp256k1 signature over the concatenated UTF‑8 bytes of all preceding parameters (scheme, network, token, amount, payload). The server recovers the signer address and checks that it holds enough USDC (via a quick view‑call or an indexer) before serving the payload.


Honest trade‑offs

Aspect Benefit Cost / Complexity
Statelessness Agents need no long‑lived API keys; payment proves identity per request. Requires the agent to manage a wallet and sign transactions for every call.
Latency Only one extra round‑trip (402 → retry) if the client lacks funds or signature. In practice, the extra TLS handshake + signature verification adds ~50‑150 ms on top of normal latency.
Fee volatility Using a stablecoin (USDC) shields the agent from price swings. You must hold USDC on Base; bridging from other chains incurs its own cost and delay.
Integration effort Works with any HTTP client; no custom SDK needed. Server side must implement the 402 flow, signature verification, and token‑balance checks (a modest amount of boilerplate).
Security Cryptographic proof prevents replay; server‑side eliminates API‑key leakage. If the agent’s private key is compromised, an attacker can spend its USDC until revoked. Key management remains a concern.
Ecosystem maturity Draft spec is stable; several testnets and a few production services already use it. Not yet a universal standard; you may need to convince upstream providers to adopt x402.

In short, x402 trades a small amount of operational overhead (wallet management + signing) for a clean, usage‑based pricing model that fits the ephemeral nature of AI agents.


Minimal working example: agent → x402‑protected service

Below is a self‑contained Python snippet that demonstrates the full client flow. It assumes:

  • You have an Ethereum‑compatible wallet (private key) with USDC on Base.
  • You’re using the eth-account library for signing.
  • The target service follows the spec described above.
# -------------------------------------------------
# x402 micropayment client for AI agents (Python 3.11+)
# -------------------------------------------------
import base64
import json
import time
import requests
from eth_account import Account
from eth_account.messages import encode_defunct

# ---- CONFIGURATION -------------------------------------------------
PRIVATE_KEY = "0xYOUR_PRIVATE_KEY_HERE"          # keeper of USDC on Base
USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"  # USDC on Base (checksum)
NETWORK = "base"
SCHEME = "x402-version-1"
# -------------------------------------------------------------------

def build_payload(request_id: str) -> str:
    """Create the base64url payload the server expects."""
    raw = f"{request_id}|{int(time.time())}"
    return base64.urlsafe_b64encode(raw.encode()).rstrip(b"=").decode()

def sign_payload(scheme, network, token, amount, payload_b64):
    """Sign the concatenated fields as per x402 spec."""
    message = f"{scheme}{network}{token}{amount}{payload_b64}"
    encoded = encode_defunct(text=message)
    signed = Account.sign_message(encoded, private_key=PRIVATE_KEY)
    return signed.signature.hex()

def call_agent_service(endpoint: str, request_id: str):
    """Perform the 402 flow and return the JSON response."""
    url = f"https://example-x402.service{endpoint}"
    headers = {"Accept": "application/json"}

    # 1️⃣ First request – may trigger 402
    resp = requests.get(url, headers=headers, timeout=10)
    if resp.status_code != 402:
        resp.raise_for_status()
        return resp.json()

    # 2️⃣ Parse the challenge
    pay_header = resp.headers["Pay"]
    challenge = dict(part.split('=', 1) for part in pay_header.split(', '))
    # Strip quotes
    challenge = {k: v.strip('"') for k, v in challenge.items()}

    # 3️⃣ Build our signed response
    payload_b64 = challenge["payload"]
    amount = challenge["amount"]
    signature = sign_payload(
        scheme=challenge["scheme"],
        network=challenge["network"],
        token=challenge["token"],
        amount=amount,
        payload_b64=payload_b64,
    )

    pay_retry = (
        f'scheme="{challenge["scheme"]}", '
        f'network="{challenge["network"]}", '
        f'token="{challenge["token"]}", '
        f'amount="{amount}", '
        f'payload="{payload_b64}", '
        f'signature="0x{signature}"'
    )
    headers["Pay"] = pay_retry

    # 4️⃣ Retry with payment proof
    resp = requests.get(url, headers=headers, timeout=10)
    resp.raise_for_status()
    return resp.json()


# ---- Example usage -------------------------------------------------
if __name__ == "__main__":
    # Imagine this agent needs a sentiment‑analysis model
    result = call_agent_service("/sentiment?text=I%20love%20x402", request_id="agent-42")
    print(json.dumps(result, indent=2))
Enter fullscreen mode Exit fullscreen mode

What this code does

  1. Sends a plain GET. If the service isn’t paid, it replies 402.
  2. Extracts the challenge from the Pay header, builds a payload that includes a timestamp‑based nonce, and signs it with the agent’s private key.
  3. Retries the request with a Pay header containing the signature.
  4. Returns the JSON payload once the server validates the proof.

You can drop this into any agent framework (LangChain, LlamaIndex, AutoGPT, etc.) and replace the endpoint with whatever service you need.


Server‑side sketch (Cloudflare Workers)

If you’re providing a service, the implementation is equally short. Below is a Worker that protects a /summarize endpoint:


javascript
// x402-protected Worker (CF Workers, JavaScript)
import { ethers } from "ethers";

const USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // Base USDC
const MIN_AMOUNT = BigInt("1000000"); // 0.01 USDC (6 decimals)

async function handle(request) {
  const url = new URL(request.url);
  if (url.pathname !== "/summarize") return new Response("Not found", {status: 404});

  const auth = request.headers.get("
Enter fullscreen mode Exit fullscreen mode

Top comments (0)