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 agents that need to purchase data, compute, or other services on‑the‑fly.


Why x402 matters for agents

Modern AI agents are often stateless functions that call external APIs to enrich their reasoning (e.g., fetching real‑time price data, invoking a vision model, or pulling a curated knowledge base). In a fully autonomous loop the agent must be able to pay for each call without human intervention, without relying on off‑chain invoicing, and without adding a custom payment layer on top of every service.

The x402 specification solves this by turning the HTTP 402 Payment Required status code into a standardized, wallet‑driven micropayment mechanism that lives entirely in the request/response cycle. No new transport protocol, no side‑channel signatures, and no extra latency beyond the normal round‑trip.


Core ideas of x402

Concept Description
HTTP‑native Uses existing HTTP verbs, headers, and status codes. The only addition is a Payment header that carries a signed payment request.
Stateless The server does not need to store session or balance information; everything needed to verify a payment is embedded in the request.
Wallet‑agnostic Any EIP‑1559‑compatible signer (MetaMask, WalletConnect, a hardware key, or an agent‑controlled key) can produce the required signature.
Fixed‑price or dynamic The server can advertise a static price (x402-price) or compute a price per‑request (e.g., based on data size) and include it in the payment request.
USDC on Base The reference implementation uses the ERC‑20 USDC contract on Base (Chain ID 8453) because it offers low gas and fast finality, but the spec is chain‑agnostic.

The payment flow in detail

  1. Agent makes a normal GET/POST to a protected resource.
  2. If the caller lacks a valid payment, the server replies 402 Payment Required with:
    • Payment header – a base64‑url‑encoded JSON object describing the amount, token, chain, and a nonce.
    • WWW-Authenticate: Bearer realm="x402" – optional, for clients that treat it like auth.
  3. The agent’s wallet signs the payment request (typically an EIP‑712 typed data structure) and returns a new request with:
    • Authorization: Bearer <signature> header.
    • The same Payment header (unchanged) so the server can verify the signature matches the request.
  4. Server validates the signature, checks that the nonce hasn’t been reused (prevents replay), and if everything is ok, processes the original request and returns 200 OK (or another appropriate status).

Because the payment data travels in HTTP headers, intermediaries (proxies, CDNs, API gateways) can forward it unchanged—no special tunneling required.


Minimal working example

Below is a Node.js/Express server that protects a single endpoint (/data) with an x402 payment of 0.01 USDC. The client snippet shows how an agent built with ethers.js can automatically handle the 402 flow.

Server (server.js)

// npm i express ethers dotenv
require('dotenv').config();
const express = require('express');
const { ethers } = require('ethers');
const app = express();
const PORT = 3000;

// USDC on Base (checksum address)
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const USDC_DECIMALS = 6;
const PRICE_USDC = ethers.parseUnits('0.01', USDC_DECIMALS); // 0.01 USDC

// Simple in‑memory nonce store (replace with Redis or DB in prod)
const usedNonces = new Set();

/**
 * Build the payment request object that will be base64‑url encoded
 * and placed in the `Payment` header.
 */
function buildPaymentRequest() {
  const nonce = ethers.randomBytes(8).toString('hex'); // 64‑bit nonce
  return {
    scheme: 'x402',
    network: 8453, // Base chain ID
    token: USDC_ADDRESS,
    amount: PRICE_USDC.toString(),
    nonce,
  };
}

/**
 * Middleware that enforces x402 payment.
 */
function requirePayment(req, res, next) {
  const auth = req.headers.authorization?.split(' ')[1];
  const paymentHeader = req.headers.payment;

  if (!auth || !paymentHeader) {
    const payReq = buildPaymentRequest();
    const encoded = Buffer.from(JSON.stringify(payReq)).toString('base64url');
    res.set('Payment', encoded);
    return res.status(402).send('Payment required');
  }

  // Decode and verify
  let payReq;
  try {
    const json = Buffer.from(paymentHeader, 'base64url').toString();
    payReq = JSON.parse(JSON.parse(json)); // double‑parse because base64url may have been padded
  } catch {
    return res.status(400).send('Invalid Payment header');
  }

  // Basic sanity checks
  if (payReq.network !== 8453 || payReq.token.toLowerCase() !== USDC_ADDRESS.toLowerCase()
      || ethers.parseUnits(payReq.amount, USDC_DECIMALS).neq(PRICE_USDC)) {
    return res.status(400).send('Misconfigured payment request');
  }

  // Replay protection
  if (usedNonces.has(payReq.nonce)) {
    return res.status(409).send('Nonce already used');
  }
  usedNonces.add(payReq.nonce);

  // Verify signature (EIP‑712 typed data)
  const domain = {
    name: 'x402 Payment',
    version: '1',
    chainId: 8453,
    verifyingContract: USDC_ADDRESS,
  };
  const types = {
    PaymentRequest: [
      { name: 'scheme', type: 'string' },
      { name: 'network', type: 'uint256' },
      { name: 'token', type: 'address' },
      { name: 'amount', type: 'uint256' },
      { name: 'nonce', type: 'bytes32' },
    ],
  };
  const value = {
    scheme: payReq.scheme,
    network: BigInt(payReq.network),
    token: payReq.token,
    amount: ethers.BigNumber.from(payReq.amount),
    nonce: '0x' + payReq.nonce,
  };

  try {
    const recovered = ethers.verifyTypedData(domain, types, value, auth);
    if (recovered.toLowerCase() !== ethers.getAddress(process.env.SIGNER_ADDRESS).toLowerCase()) {
      throw new Error('Signature mismatch');
    }
  } catch (e) {
    return res.status(401).send('Invalid signature');
  }

  // Payment verified – fall through to handler
  next();
}

/**
 * Example protected resource: returns a JSON blob of dummy market data.
 */
app.get('/data', requirePayment, (req, res) => {
  res.json({
    timestamp: Date.now(),
    price: Math.random() * 100,
    source: 'x402-demo',
  });
});

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

Key points in the server code

  • The Payment header contains a minimal JSON object; it is intentionally short to keep header size low.
  • Nonce replay protection is done in‑memory for the demo; a production service would store used nonces in a fast KV store (Redis, DynamoDB) with a TTL matching the chain’s finality window.
  • Signature verification uses EIP‑712 typed data, which is the standard way wallets sign structured data and allows the agent to show a clear signing prompt (e.g., “Sign x402 payment of 0.01 USDC for /data”).

Agent client (agent.js)


javascript
// npm i ethers axios
require('dotenv').config();
const { ethers } = require('ethers');
const axios = require('axios');

const provider = new ethers.JsonRpcProvider(process.env.BASE_RPC); // e.g. https://base-mainnet.g.alchemy.com/v2/...
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const USDC_DECIMALS = 6;
const PRICE = ethers.parseUnits('0.01', USDC_DECIMALS);

/**
 * Generic wrapper that automatically retries on 402.
 */
async function x402Get(url) {
  let attempts = 0;
  while (true) {
    try {
      const resp = await axios.get(url);
      return resp.data;
    } catch (err) {
      if (err.response?.status !== 402) throw err;
      attempts++;
      if (attempts > 3) throw new Error('Failed to satisfy payment after retries');

      // Extract payment request
      const payReqB64 = err.response.headers.payment;
      const payReq = JSON.parse(Buffer.from(payReqB64, 'base64url').toString());

      // Build EIP‑712 payload (same as server)
      const domain = {
        name: 'x402 Payment',
        version: '1',
        chainId: 8453,
        verifyingContract: USDC_ADDRESS,
      };
Enter fullscreen mode Exit fullscreen mode

Top comments (0)