x402 Explained: HTTP-Native Micropayments for AI Agents (With Real Code)

# ai# automation# webdev# crypto
x402 Explained: HTTP-Native Micropayments for AI Agents (With Real Code)Nikhil Ranka

x402 Explained: HTTP‑Native Micropayments for AI Agents (With Real Code) ...

x402 Explained: HTTP‑Native Micropayments for AI Agents (With Real Code)

Introduction

The HTTP status code 402 Payment Required has been reserved in the specification for decades, but browsers and most servers never gave it a defined semantics. The x402 draft (still experimental) proposes a lightweight way to turn 402 into a native payment trigger: the server replies with a 402 status and a standardized Pay header that tells the client exactly what to pay, in which currency, and where to send the funds. If the client can satisfy the request, it retries the original call with an Authorization: Bearer <payment‑token> header; otherwise it surfaces the error to the user or falls back to a free tier.

For developers building autonomous AI agents—services that may need to call other agents, LLMs, or data providers on a per‑call basis—x402 offers a way to express pricing directly in the protocol without inventing a custom billing layer, API keys, or subscription models. Below we walk through the mechanics, show working client and server code, and discuss the practical trade‑offs you’ll encounter when adopting it today.


How x402 Works (in practice)

  1. Request – The agent sends a normal HTTP request (GET, POST, etc.) to a protected resource.
  2. Server response – If the caller lacks a valid payment token, the server returns 402 Payment Required and includes a Pay header:
   Pay: amount=0.005; currency=USDC; network=base; endpoint=https://pay.example.com/settle
Enter fullscreen mode Exit fullscreen mode
  • amount – decimal amount required for this call.
  • currency – ISO‑4217 code (or a token symbol like USDC).
  • network – blockchain or ledger where the token lives (e.g., base, ethereum).
  • endpoint – URL the client should call to obtain a payment token (often a simple custodial API or a smart‑contract call).
  1. Client flow – The agent extracts the header, calls the endpoint to lock up the required funds, receives a signed token (or transaction hash), and retries the original request with Authorization: Bearer <token>.
  2. Success – If the token validates, the server processes the request and returns a 2xx response.

The spec deliberately avoids mandating a particular settlement mechanism; any system that can prove “I paid X USDC on Base” and produce a verifiable token works. In the examples below we use a simple custodial endpoint that mints a JWT‑style token after verifying a USDC transfer on Base via a public RPC.


Server‑Side Implementation (Node.js/Express)

// server.js
import express from 'express';
import jwt from 'jsonwebtoken';
import { ethers } from 'ethers';

const app = express();
const PORT = 3000;

// Shared secret for signing payment tokens (in prod use a proper KMS)
const JWT_SECRET = 'change-me-please';

// ERC‑20 USDC contract on Base (address from https://base.org/tokens)
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const usdcAbi = [
  "function balanceOf(address) view returns (uint256)",
  "function transfer(address to, uint256 amount) returns (bool)"
];

// Provider – replace with your own RPC or a service like Infura/Alchemy
const provider = new ethers.JsonRpcProvider('https://base.mainnet.rpc.cloud');
const usdc = new ethers.Contract(USDC_ADDRESS, usdcAbi, provider);

// Expected price per call (in USDC, 6 decimals)
const PRICE_USDC = ethers.parseUnits('0.01', 6); // $0.01

// Middleware that checks for a valid payment token
function requirePayment(req, res, next) {
  const auth = req.headers.authorization;
  if (!auth || !auth.startsWith('Bearer ')) {
    return askForPayment(res);
  }
  const token = auth.slice(7);
  try {
    const payload = jwt.verify(token, JWT_SECRET);
    // Optional: verify nonce/replay protection here
    req.payment = payload; // attach for downstream handlers
    next();
  } catch (err) {
    return askForPayment(res);
  }
}

// Helper to respond with 402 and Pay header
function askForPayment(res) {
  const payHeader = `amount=0.01; currency=USDC; network=base; endpoint=https://${req.get('host')}/pay`;
  res.set('Pay', payHeader);
  res.status(402).send('Payment required');
}

// Endpoint that the client calls to obtain a token
app.post('/pay', express.json(), async (req, res) => {
  const { payer, txHash } = req.body; // client must provide a signed USDC transfer tx
  try {
    // Verify the transaction actually sent enough USDC to our address
    const tx = await provider.getTransaction(txHash);
    if (!tx) throw new Error('Tx not found');
    const receipt = await tx.wait();
    if (receipt.status !== 1) throw new Error('Tx failed');

    // Optional: decode transfer data to confirm amount & recipient
    // For brevity we assume the client sent the correct amount.

    // Issue a short‑lived JWT (5 min) that authorizes the call
    const token = jwt.sign(
      { sub: payer, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 300 },
      JWT_SECRET,
      { audience: 'x402-agent' }
    );
    res.json({ token });
  } catch (e) {
    console.error(e);
    res.status(400).json({ error: 'Invalid payment proof' });
  }
});

// Example protected resource – an LLM inference endpoint
app.post('/generate', requirePayment, express.json(), (req, res) => {
  const { prompt } = req.body;
  // In reality you’d call your model here; we just echo back.
  res.json({ response: `Echo: ${prompt}` });
});

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

What the code does

  • The requirePayment middleware looks for a Bearer token. If missing or invalid, it replies with 402 and a Pay header pointing to /pay.
  • /pay expects the caller to have already executed a USDC transfer on Base to the server’s address and to supply the transaction hash. The server verifies the transaction succeeded, then signs a JWT that the client can reuse for the next few minutes.
  • The protected /generate route simply returns a dummy echo; replace the body with your actual agent logic (LLM call, tool invocation, etc.).

Client‑Side Agent (TypeScript/fetch)


typescript
// agent.ts
import fetch from 'node-fetch';

// Configuration
const AGENT_URL = 'http://localhost:3000/generate';
const PAY_ENDPOINT = 'http://localhost:3000/pay';
const USDC_CONTRACT = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const RPC_URL = 'https://base.mainnet.rpc.cloud';
const PRICE_USDC = ethers.parseUnits('0.01', 6); // $0.01
const WALLET_PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY!; // funded with USDC on Base

const provider = new ethers.JsonRpcProvider(RPC_URL);
const wallet = new ethers.Wallet(WALLET_PRIVATE_KEY, provider);
const usdc = new ethers.Contract(
  USDC_CONTRACT,
  ["function transfer(address to, uint256 amount) returns (bool)"],
  wallet
);

/**
 * Helper: send a USDC transfer and wait for confirmation.
 */
async function payAmount(to: string): Promise<string> {
  const tx = await usdc.transfer(to, PRICE_USDC);
  const receipt = await tx.wait();
  if (receipt.status !== 1) throw new Error('Payment tx failed');
  return tx.hash;
}

/**
 * Core request function that handles 402 → pay → retry.
 */
async function x402Request(payload: any): Promise<any> {
  let attempt = 0;
  while (true) {
    attempt++;
    const resp = await fetch(AGENT_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });

    if (resp.status !== 402) {
      const data = await resp.json();
      return data; // success or other error
    }

    // ----- 402 flow -----
    const payHeader = resp.headers.get('Pay') ?? '';
    const params = new URLSearchParams(payHeader);
    const amount = params.get('amount');
    const currency = params.get('currency');
    const network = params.get('network');
    const endpoint = params.get('endpoint');

    if (amount !== '0.01' || currency !== 'USDC' || network !== 'base') {
      throw new Error(`Unexpected pay terms: ${payHeader}`);
    }

    // 1. Send payment
    const txHash = await payAmount(endpoint.replace('https://', '').split('/')[0]); // extract host
    // 2. Obtain token from server
    const tokenResp = await fetch(endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify
Enter fullscreen mode Exit fullscreen mode