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)

Autonomous agents often need to call external services—LLM inference, data feeds, tool wrappers—without a human in the loop to manage billing. Traditional API‑key models force developers to either pre‑pay for bulk quotas or embed complex subscription logic inside the agent. The x402 proposal repurposes the rarely used HTTP status code 402 Payment Required to make payment a first‑class part of the request/response cycle, letting agents pay for each call exactly as they would pay for bandwidth or compute.

What x402 Actually Is

x402 is not a new protocol; it is a convention that extends HTTP/1.1 semantics:

  • A server that wishes to charge for a resource replies with 402 Payment Required when the caller has not provided sufficient proof of payment.
  • The response includes a Payment-Request header that describes what must be paid (amount, currency, payee, and a nonce to prevent replay).
  • The client, upon receiving 402, obtains payment proof (e.g., a signed transaction or a receipt from a payment processor) and retries the request, adding a Payment header containing that proof.
  • If the proof validates, the server processes the request and returns 200 OK (or another appropriate status).

Because the exchange stays within HTTP, any client library, proxy, or middleware that understands headers can participate without code changes beyond attaching the payment proof.

Why HTTP‑Native Matters for Agents

  1. Statelessness – Agents already treat each HTTP call as an isolated interaction. Adding a payment header does not require maintaining a session or token store beyond the nonce.
  2. Interoperability – Proxies, API gateways, and service meshes that forward headers automatically forward payment data, enabling zero‑trust billing at the edge.
  3. Granularity – Payments can be as small as a fraction of a cent, matching the fine‑grained usage patterns of agent‑to‑agent calls (e.g., “$0.003 per token”).
  4. No Vendor Lock‑In – The scheme is agnostic to the settlement layer; you can use USDC on Base, fiat via Stripe, or even a proprietary ledger, as long as both sides agree on the verification method.

A Minimal Working Example

Below is a self‑contained Node.js/Express server that enforces x402 payments in USDC on the Base L2. It assumes the client holds a USDC balance and can produce an EIP‑712 signed attestation that references a nonce supplied by the server. The verification is done off‑chain for speed; in production you would settle the signed message on‑chain or via a custodial service.

Server (payment‑verifying middleware)

// x402-middleware.js
const crypto = require('crypto');
const { ethers } = require('ethers');

// Expected USDC contract on Base (address is placeholder)
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const PAYEE = '0xYourAgentServiceAddress'; // replace with your address

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

function generateNonce() {
  return crypto.randomBytes(16).toString('hex');
}

/**
 * Middleware that checks for a valid x402 payment proof.
 * Expects header: Payment: <signature> <nonce> <amount>
 * where signature is an EIP‑712 signed message over:
 *   {
 *     types: {
 *       EIP712Domain: [{name:'name',type:'string'},{name:'version',type:'string'}],
 *       Payment: [{name:'nonce',type:'bytes32'},{name:'amount',type:'uint256'},{name:'token',type:'address'}]
 *     },
 *     domain: {name:'x402', version:'1'},
 *     message: {nonce:<bytes32>, amount:<uint256>, token:USDC_ADDRESS}
 *   }
 */
function x402PaymentRequired(req, res, next) {
  const auth = req.get('Payment');
  if (!auth) {
    const nonce = generateNonce();
    nonces.set(nonce, Date.now() + 5 * 60 * 1000); // 5‑minute expiry
    res.set('Payment-Request', `USDC ${USDC_ADDRESS} ${PAYEE} 0.01 ${nonce}`);
    return res.status(402).send('Payment required');
  }

  const [signature, nonceHex, amountStr] = auth.split(' ');
  if (!signature || !nonceHex || !amountStr) {
    return res.status(400).send('Malformed Payment header');
  }

  const expiry = nonces.get(nonceHex);
  if (!expiry || expiry < Date.now()) {
    nonces.delete(nonceHex);
    return res.status(402).send('Expired or unknown nonce; retry');
  }

  // Reconstruct the signed message
  const amount = ethers.parseUnits(amountStr, 6); // USDC has 6 decimals
  const domain = {
    name: 'x402',
    version: '1',
  };
  const types = {
    Payment: [
      { name: 'nonce', type: 'bytes32' },
      { name: 'amount', type: 'uint256' },
      { name: 'token', type: 'address' },
    ],
  };
  const value = {
    nonce: nonceHex,
    amount: amount,
    token: USDC_ADDRESS,
  };

  try {
    const recovered = ethers.verifyTypedData(domain, types, value, signature);
    if (recovered.toLowerCase() !== PAYEE.toLowerCase()) {
      throw new Error('Signature mismatch');
    }
    // Payment validated – clean nonce and proceed
    nonces.delete(nonceHex);
    next();
  } catch (e) {
    return res.status(402).send('Invalid payment proof: ' + e.message);
  }
}

// Example protected route
const express = require('express');
const app = express();
app.use(express.json());

app.get('/agent/tool', x402PaymentRequired, (req, res) => {
  // Simulate some work
  const result = { output: Math.random() };
  res.json(result);
});

app.listen(3000, () => console.log('x402 server listening on :3000'));
Enter fullscreen mode Exit fullscreen mode

Explanation of the flow

  1. Client calls GET /agent/tool without a Payment header.
  2. Server replies 402 Payment Required and sets Payment-Request: USDC <token> <payee> 0.01 <nonce>.
  3. Client reads the nonce, asks its wallet to sign the EIP‑712 structure (nonce, amount=0.01 USDC, token address) and sends back Payment: <signature> <nonce> 0.01.
  4. Server verifies the signature, checks the nonce hasn’t been used, and if ok, processes the request.

Client (Python) – attaching the proof


python
# x402_client.py
import time
import requests
from eth_account.messages import encode_typed_data
from eth_account import Account
from web3 import Web3

# Configuration
PRIVATE_KEY = "0xYOUR_PRIVATE_KEY"          # never commit this in prod
ACCOUNT = Account.from_key(PRIVATE_KEY)
PAYEE = "0xYourAgentServiceAddress"
USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
BASE_RPC = "https://base-mainnet.infura.io/v3/YOUR_INFURA_KEY"
w3 = Web3(Web3.HTTPProvider(BASE_RPC))

def fetch_nonce(url):
    resp = requests.get(url)
    if resp.status_code != 402:
        resp.raise_for_status()
    # Payment-Request: USDC <token> <payee> <amount> <nonce>
    _, _, _, amount, nonce = resp.headers["Payment-Request"].split()
    return amount, nonce, resp.headers["Payment-Request"]

def sign_payment(nonce_hex, amount_str):
    """Return EIP‑712 signature as hex string."""
    domain = {
        "name": "x402",
        "version": "1",
    }
    types = {
        "Payment": [
            {"name": "nonce", "type": "bytes32"},
            {"name":
Enter fullscreen mode Exit fullscreen mode

Top comments (0)