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)

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 Required
Paywall: <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.1
Paywall-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" }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Call a protected endpoint.
  2. Interpret a 402 response.
  3. Build and sign a USDC transfer on Base.
  4. 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)
Enter fullscreen mode Exit fullscreen mode

What the code does

  1. First GET – if the service expects payment, it returns 402 with a Paywall header.
  2. Parse the header to extract amount, token, payee, and network.
  3. Construct an ERC‑20 transfer transaction for USDC (6 decimals) and sign it with the agent’s private key.
  4. Retry the same GET, adding the signed transaction hex as Paywall-Proof.
  5. 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).
  6. On success, the service replies with 200 and 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)