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 of HTTP that lets a server request a tiny, verifiable payment before serving a response. For autonomous agents that need to call pay‑per‑use services (LLM inference, data fetches, tool APIs), it removes the need for separate billing SDKs or OAuth flows while keeping the payment semantics visible at the protocol level.


Why a New HTTP Status Code?

Traditional REST APIs rely on out‑of‑band mechanisms—API keys, JWTs, or subscription portals—to gate access. Those work fine for human‑driven apps but add friction for agents that:

  • make dozens or hundreds of calls per second,
  • cannot store long‑lived secrets securely,
  • need to reconcile cost with the value of each individual response.

x402 solves this by defining status code 402 Payment Required (already reserved in RFC 7231) with a standardized Payment-Response header that carries a cryptographic proof‑of‑payment. The client can satisfy the request by attaching a signed payment blob in a subsequent request, all within the same HTTP exchange.

Core Properties

Property What it Means for Agents
Stateless No server‑side session needed; each request carries its own payment proof.
Atomic The server either returns the resource or a 402 with a payment request; no partial data leakage.
Currency‑agnostic The payload encodes the amount, token, and chain; agents can pay in USDC, DAI, or any ERC‑20 compatible token.
Verifiable The payment blob includes a signature that the server can check against a known payer address, preventing replay attacks.

The Wire Format

When a server wants payment, it replies with:

HTTP/1.1 402 Payment Required
Content-Type: application/json
Payment-Request: {
  "scheme": "erc20",
  "network": "base",
  "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
  "amount": "1000000", // 0.01 USDC (6 decimals)
  "payload": "<base64url‑encoded nonce>"
}
Enter fullscreen mode Exit fullscreen mode

The payload is a random nonce that prevents replay. The agent must sign payload || amount || token || network with its private key and return the signature in the next request.

The agent then retries:

GET /resource HTTP/1.1
Authorization: Bearer <jwt-if-needed>
X-Payment: {
  "scheme": "erc20",
  "network": "base",
  "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "amount": "1000000",
  "signature": "0xabcd...", // ECDSA signature over the concatenated fields
  "payload": "<same nonce>"
}
Enter fullscreen mode Exit fullscreen mode

If the signature validates and the nonce hasn’t been seen before, the server returns the requested resource (200 OK) and records the nonce to prevent reuse.


Minimal Working Example (Node.js)

Below is a self‑contained Express middleware that enforces x402 for a single route. It uses ethers.js for signature verification and assumes the agent holds a private key stored in an environment variable.

// x402-middleware.js
import express from 'express';
import { ethers } from 'ethers';
import crypto from 'crypto';

const app = express();

// In‑memory nonce store (for demo; use Redis or DB in prod)
const usedNonces = new Set();

/**
 * Generate a 402 response with a payment request.
 */
function paymentRequired(res, tokenAddress, amountWei) {
  const nonce = crypto.randomBytes(16).toString('base64url');
  const request = {
    scheme: 'erc20',
    network: 'base',
    token: tokenAddress,
    amount: amountWei.toString(),
    payload: nonce,
  };
  res.set('Payment-Request', JSON.stringify(request));
  res.status(402).json({ error: 'payment required', request });
}

/**
 * Middleware that checks for a valid X-Payment header.
 */
async function x402(req, res, next, { tokenAddress, priceWei, payerAddress }) {
  // If already paid, skip
  if (req.headers['x-payment-verified']) return next();

  const auth = req.headers['x-payment'];
  if (!auth) return paymentRequired(res, tokenAddress, priceWei);

  let payload;
  try {
    payload = JSON.parse(auth);
  } catch {
    return res.status(400).json({ error: 'malformed X-Payment' });
  }

  // Basic field checks
  const required = ['scheme', 'network', 'token', 'amount', 'signature', 'payload'];
  if (!required.every(k => k in payload)) {
    return res.status(400).json({ error: 'missing fields in X-Payment' });
  }

  // Replay protection
  if (usedNonces.has(payload.payload)) {
    return res.status(409).json({ error: 'nonce already used' });
  }

  // Verify signature
  const msg = ethers.utils.solidityPack(
    ['string', 'string', 'address', 'uint256'],
    [payload.scheme, payload.network, payload.token, payload.amount]
  );
  const msgHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(msg + payload.payload));
  const recovered = ethers.utils.recoverAddress(msgHash, payload.signature);

  if (recovered.toLowerCase() !== payerAddress.toLowerCase()) {
    return res.status(403).json({ error: 'invalid signature' });
  }

  // Record nonce and mark request as verified
  usedNonces.add(payload.payload);
  req.headers['x-payment-verified'] = 'true';
  next();
}

/* Example usage */
const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const PRICE_USDC = ethers.utils.parseUnits('0.01', 6); // $0.01
const AGENT_PUBLIC = process.env.AGENT_ADDRESS; // e.g. 0xAbC...

app.get('/price', (req, res) => {
  res.json({ usd: 0.01 });
});

app.get('/data', async (req, res) => {
  // This route requires payment
  await x402(req, res, () => {}, {
    tokenAddress: USDC_BASE,
    priceWei: PRICE_USDC,
    payerAddress: AGENT_PUBLIC,
  });
  // If we reach here, payment succeeded
  res.json({ value: Math.random() });
});

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

What this code does

  1. On the first request to /data, the client receives a 402 with a Payment-Request header.
  2. The client builds the signed payload (shown later) and retries.
  3. The middleware verifies the signature, checks the nonce, and only then calls the handler.
  4. If verification fails, the client gets a 4xx error with a helpful JSON body.

Client‑Side Payment Construction (JavaScript)

The agent needs to create the X-Payment header. Using the same ethers library:

import { ethers } from 'ethers';

const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const PRICE_USDC = ethers.utils.parseUnits('0.01', 6); // 0.01 USDC
const PRIVATE_KEY = process.env.AGENT_PRIVATE_KEY; // NEVER commit this
const wallet = new ethers.Wallet(PRIVATE_KEY);

// Helper to call an endpoint that may require payment
async function fetchWithX402(url) {
  let response = await fetch(url);
  if (response.status !== 402) return response;

  const requestHeader = response.headers.get('Payment-Request');
  const request = JSON.parse(requestHeader);

  const msg = ethers.utils.solidityPack(
    ['string', 'string', 'address', 'uint256'],
    [request.scheme, request.network, request.token, request.amount]
  );
  const msgHash = ethers.utils.keccak256(
    ethers.utils.toUtf8Bytes(msg + request.payload)
  );
  const signature = await wallet.signMessage(ethers.utils.arrayify(msgHash));

  const xPayment = {
    scheme: request.scheme,
    network: request.network,
    token: request.token,
    amount: request.amount,
    signature,
    payload: request.payload,
  };

  // Retry with payment header
  response = await fetch(url, {
    headers: { 'X-Payment': JSON.stringify(xPayment) },
  });
  return response;
}

// Example usage
(async () => {
  const resp = await fetchWithX402('http://localhost:3000/data');
  const data = await resp.json();
  console.log('Paid data:', data);
})();
Enter fullscreen mode Exit fullscreen mode

Notes on the client code

  • The agent must hold a private key that corresponds to the address the server expects (payerAddress). In production, you’d use a hardware signer or a managed wallet service (e.g., Coinbase Wallet, Privy) to avoid exposing keys in source.
  • The nonce is echoed verbatim from the server; replay attacks are prevented by the server storing used nonces.
  • The example uses raw fetch. In a real agent framework you’d wrap this logic in a reusable transport adapter.

Trade‑offs & Honest Assessment

Aspect Benefit Cost / Limitation
Protocol‑level payment No extra round‑trip to a billing API; the payment request lives in the same HTTP exchange. Requires both client and server to understand the Payment-Request/X-Payment headers; existing generic HTTP clients won’t work without middleware.
Stateless verification Server can scale horizontally; only nonces need short‑term storage (Redis with TTL works). Nonce store must be highly available; loss of nonces could enable replay attacks if

Top comments (0)