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 is a lightweight extension to HTTP that lets a server respond with 402 Payment Required and a JSON‑encoded payment request. An AI agent can fulfill the request by signing a transaction with a crypto wallet, then retry the original request with a proof‑of‑payment header. The flow stays inside the normal request/response loop, so existing HTTP tooling (proxies, caches, middleware) works unchanged.


1. Why a payment‑status code?

HTTP already defines status codes for client (4xx) and server (5xx) errors. 402 Payment Required was reserved in RFC 7231 but never standardized for a concrete protocol. x402 fills that gap with a minimal, extensible payload:

Header Meaning
402 Payment Required Server cannot service the request until payment is supplied.
X-PAYMENT-REQUEST Base64‑url JSON describing amount, currency, destination address, and a nonce.
X-PAYMENT-RESPONSE (client → server) Base64‑url JSON containing a signed transaction hash (or any proof the verifier accepts).
X-PAYMENT-VERIFIER (optional) URL of a service that can validate the proof (useful for off‑chain verification).

Because the payment data lives in headers, the body of the original request/response is untouched—ideal for agents that already exchange JSON or binary payloads.


2. End‑to‑end flow

  1. Agent → Server (GET/POST …)

    No payment header → server returns 402 + X-PAYMENT-REQUEST.

  2. Agent decodes the request, builds a transaction that pays the specified amount to the destination address (usually a smart‑contract escrow or a simple EOA), signs it with its wallet, and extracts the transaction hash (or a Merkle proof if using roll‑ups).

  3. Agent → Server (retry)

    Adds X-PAYMENT-RESPONSE: <base64url(signedProof)> header and repeats the original request.

  4. Server verifies the proof (on‑chain call or off‑chain verifier). If valid, it processes the request and returns 200 OK (or another appropriate status). If invalid, it returns 402 again with a new nonce to prevent replay attacks.

The entire exchange is stateless from the server’s perspective except for the nonce, which prevents replay without requiring server‑side session storage.


3. Minimal server implementation (Node.js + Express)

// server.js
const express = require('express');
const crypto = require('crypto');
const app = express();
const PORT = 3000;

// In‑memory nonce store – replace with Redis or DB in prod
const nonces = new Map();

// Helper: generate a payment request payload
function makePaymentRequest() {
  const nonce = crypto.randomBytes(16).toString('hex');
  nonces.set(nonce, Date.now() + 5 * 60 * 1000); // 5‑min TTL
  return Buffer.from(
    JSON.stringify({
      amount: '0.05',          // USDC amount (string to avoid float issues)
      currency: 'USDC',
      chainId: 8453,           // Base
      destination: '0x1234...abcd', // your wallet or escrow contract
      nonce,
    })
  ).toString('base64url');
}

// Middleware to verify payment response
function verifyPayment(req, res, next) {
  const respHeader = req.get('X-PAYMENT-RESPONSE');
  if (!respHeader) return next(); // let the route handler decide

  try {
    const payload = JSON.parse(Buffer.from(respHeader, 'base64url').toString());
    const { nonce, txHash } = payload;
    const expiry = nonces.get(nonce);
    if (!expiry || expiry < Date.now())
      return res.status(402).set('X-PAYMENT-REQUEST', makePaymentRequest()).send('Expired or unknown nonce');

    // ----> REAL VERIFICATION LOGIC <----
    // For demo we just check that txHash looks like a hex string.
    // In production you would:
    //   1. Call a JSON‑RPC endpoint (Base) to get transaction receipt.
    //   2. Confirm status == 1, to == destination, value == amount (in wei).
    //   3. Optionally verify via an off‑chain verifier service.
    if (!/^0x[a-fA-F0-9]{64}$/.test(txHash))
      throw new Error('malformed txHash');

    // Consume nonce to prevent replay
    nonces.delete(nonce);
    next(); // payment verified
  } catch (e) {
    return res.status(402)
      .set('X-PAYMENT-REQUEST', makePaymentRequest())
      .send('Invalid payment proof');
  }
}

// Example protected endpoint
app.get('/ai/generate', verifyPayment, (req, res) => {
  // Your actual AI work goes here – placeholder:
  const result = { text: 'Generated response for paid request.' };
  res.json(result);
});

// Public health endpoint (no payment needed)
app.get('/health', (req, res) => res.send('OK'));

app.listen(PORT, () => console.log(`x402 server listening on :${PORT}`));
Enter fullscreen mode Exit fullscreen mode

Key points

  • The server never stores wallet keys or signs transactions – it only validates proofs.
  • Nonces are short‑lived; a production deployment would push them to a fast KV store (Redis, DynamoDB) with automatic expiry.
  • The verification step is deliberately marked as a placeholder; the real work is an on‑chain call or a call to a trusted verifier service.

4. Agent client snippet (Python + web3.py)

# agent.py
import os
import base64
import json
import time
import requests
from web3 import Web3
from eth_account import Account

# Configuration
BASE_RPC = os.getenv("BASE_RPC", "https://base.mainnet.rpc.cloud")
WALLET_PRIVKEY = os.getenv("WALLET_PRIVKEY")   # never commit this!
ACCOUNT = Account.from_key(WALLET_PRIVKEY)
WEB3 = Web3(Web3.HTTPProvider(BASE_RPC))
SERVER = "http://localhost:3000/ai/generate"

def b64url_encode(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

def b64url_decode(s: str) -> bytes:
    padding = 4 - len(s) % 4
    if padding == 4:
        padding = 0
    return base64.urlsafe_b64decode(s + '=' * payment)

def fetch_payment_request() -> dict:
    """First call – expect 402 with X-PAYMENT-REQUEST."""
    r = requests.get(SERVER, timeout=10)
    if r.status_code != 402:
        raise RuntimeError(f"Unexpected status {r.status_code}: {r.text}")
    req_b64 = r.headers["X-PAYMENT-REQUEST"]
    return json.loads(b64url_decode(req_b64))

def build_and_sign_tx(req: dict) -> str:
    """Create a USDC transfer transaction on Base and sign it."""
    usdc_address = Web3.to_checksum_address("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")  # USDC on Base
    usdc_abi = [{"constant":False,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"type":"function"}]
    usdc = WEB3.eth.contract(address=usdc_address, abi=usdc_abi)

    amount_wei = int(float(req["amount"]) * (10 ** 6))   # USDC has 6 decimals
    txn = usdc.functions.transfer(
        Web3.to_checksum_address(req["destination"]),
        amount_wei
    ).build_transaction({
        "chainId": req["chainId"],
        "gas": 80000,
        "gasPrice": WEB3.eth.gas_price,
        "nonce": WEB3.eth.get_transaction_count(ACCOUNT.address),
    })
    signed = ACCOUNT.sign_transaction(txn)
    return signed.transactionHash.hex()

def send_payment_proof(proof: dict):
    """Retry original request with X-PAYMENT-RESPONSE."""
    headers = {"X-PAYMENT-RESPONSE": b64url_encode(json.dumps(proof).encode())}
    r = requests.get(SERVER, headers=headers, timeout=10)
    r.raise_for_status()
    return r.json()

def main():
    req = fetch_payment_request()
    print("Payment request:", req)
    tx_hash = build_and_sign_tx(req)
    print(f"Submitted tx {tx_hash}")
    # In a real agent you would wait for confirmation or rely on a mempool listener.
    # For demo we assume instant inclusion (not safe on mainnet!).
    proof = {"nonce": req["nonce"], "txHash": tx_hash}
    result = send_payment_proof(proof)
    print("AI response:", result)

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Explanation of the client

  • The first GET triggers the 402. The agent parses the header, builds a USDC transfer, signs it, and extracts the hash.
  • The proof consists of the nonce (to bind the proof to the request) and the transaction hash.
  • The agent retries the request with X-PAYMENT-RESPONSE. The server verifies the hash on‑chain (in a real implementation) and, if successful, returns the AI output.

Note

Top comments (0)