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 AI agents that need to pay for or charge for API usage in a trust‑less, on‑chain way.


Why x402 matters for agents

AI agents today often call third‑party services (LLM inference, data feeds, compute) and either:

  1. Embed a static API key – creates a single point of failure and forces the agent to trust the provider with long‑lived credentials.
  2. Rely on off‑chain billing – adds latency, requires account management, and breaks the “stateless” nature of an autonomous agent.

The x402 specification revives the HTTP 402 Payment Required status code and defines a lightweight, stateless way to attach an ERC‑20 payment (USDC on Base in the examples) directly to the request/response cycle. The agent never stores a long‑lived secret; it only needs a wallet capable of signing an EIP‑712 typed message.


The protocol in a nutshell

Step Actor Action
1️⃣ Client (agent) Sends a normal GET/POST request to a protected resource.
2️⃣ Server If no valid payment is present, returns 402 Payment Required with a JSON body that describes the required payment (amount, token, chain, recipient, nonce).
3️⃣ Client Uses its wallet to sign an EIP‑712 payment receipt that includes the server’s challenge (nonce, timestamp, amount, etc.).
4️⃣ Client Retries the original request, adding the signed receipt in the x402-Pay header.
5️⃣ Server Verifies the receipt signature, checks that the ERC‑20 transfer (via approve/transferFrom or a escrow contract) has occurred, then serves the resource (200 OK).

Because the payment is verified off‑chain via a signature, the actual ERC‑20 transfer can happen on Layer‑2 (Base) with negligible gas cost. The server only needs to confirm that the transfer succeeded—no contract deployment per request is required.


Server side: issuing a 402 challenge

Below is a minimal Express handler that protects a route with x402. It uses the @coinbase/x402 npm package (the reference implementation) to generate the challenge and to verify receipts.

// server.ts
import express from 'express';
import { createX402Middleware, X402Config } from '@coinbase/x402';
import { ethers } from 'ethers';

const app = express();

// 1️⃣ Configuration – adjust for your token/chain/recipient
const config: X402Config = {
  // USDC on Base (address on Base mainnet)
  tokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
  // Chain ID for Base (8453)
  chainId: 8453,
  // Recipient that should receive the payment
  recipient: '0xYourAgentServiceWallet',
  // Amount in the token's smallest unit (USDC has 6 decimals)
  amount: ethers.parseUnits('0.05', 6).toString(), // $0.05 per call
  // Optional: a nonce generator to prevent replays
  nonce: () => ethers.randomBytes(32).toString('hex'),
};

app.use('/paid-agent', createX402Middleware(config));

// Protected endpoint – only reachable after a valid payment
app.get('/paid-agent/hello', (req, res) => {
  res.json({ message: 'Hello from your paid AI agent!' });
});

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

What happens under the hood

  • createX402Middleware checks for the x402-Pay header.
  • If missing/invalid, it responds with 402 and a JSON body like:
{
  "scheme": "exact",
  "network": "base",
  "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "amount": "50000",
  "recipient": "0xYourAgentServiceWallet",
  "nonce": "a1b2c3d4e5f6..."
}
Enter fullscreen mode Exit fullscreen mode

The client must sign a receipt that covers all those fields plus a timestamp.


Client side: building and sending a payment receipt

The agent needs a signer (e.g., an ethers.js Wallet connected to a private key or a hardware wallet via MetaMask). The following snippet shows a generic fetch wrapper that automatically handles 402 challenges.

// agent-client.ts
import { ethers } from 'ethers';
import fetch from 'node-fetch';

// 1️⃣ Set up signer – replace with your own key management
const privateKey = process.env.PRIVATE_KEY!; // never hard‑code in prod
const signer = new ethers.Wallet(privateKey);
const provider = new ethers.JsonRpcProvider('https://base.mainnet.rpc.url');
const wallet = signer.connect(provider);

// 2️⃣ Helper to build the EIP‑712 typed data expected by x402
function buildPaymentRequest(challenge: any) {
  return {
    types: {
      EIP712Domain: [
        { name: 'name', type: 'string' },
        { name: 'version', type: 'string' },
        { name: 'chainId', type: 'uint256' },
        { name: 'verifyingContract', type: 'address' },
      ],
      Payment: [
        { name: 'recipient', type: 'address' },
        { name: 'amount', type: 'uint256' },
        { name: 'token', type: 'address' },
        { name: 'nonce', type: 'bytes32' },
        { name: 'timestamp', type: 'uint256' },
      ],
    },
    domain: {
      name: 'x402 Payment',
      version: '1',
      chainId: challenge.chainId,
      verifyingContract: ethers.ZeroAddress, // no contract; signature only
    },
    primaryType: 'Payment',
    message: {
      recipient: challenge.recipient,
      amount: challenge.amount,
      token: challenge.token,
      nonce: challenge.nonce,
      timestamp: Math.floor(Date.now() / 1000),
    },
  };
}

// 3️⃣ Core request function with 402 handling
async function x402Fetch(url: string, init: RequestInit = {}): Promise<Response> {
  let response = await fetch(url, init);

  // If we got a 402, parse the challenge, sign, and retry
  if (response.status === 402) {
    const challenge = await response.json();
    const payRequest = buildPaymentRequest(challenge);
    const signature = await wallet.signTypedData(
      payRequest.domain,
      payRequest.types,
      payRequest.message
    );

    // Attach the signature as the x402-Pay header
    const signedInit = { ...init, headers: { ...init.headers, 'x402-Pay': signature } };
    response = await fetch(url, signedInit);
  }
  return response;
}

// 4️⃣ Example usage – call a paid agent endpoint
(async () => {
  const res = await x402Fetch('http://localhost:3000/paid-agent/hello');
  if (!res.ok) {
    throw new Error(`Agent call failed: ${res.status} ${res.statusText}`);
  }
  const data = await res.json();
  console.log('Agent replied:', data);
})();
Enter fullscreen mode Exit fullscreen mode

Key points in the client code

  • The signer only needs to sign a typed message; no on‑chain transaction is sent by the agent.
  • The actual USDC transfer is performed off‑chain by the payer (the agent’s wallet) using a simple transferFrom after the server grants an allowance, or via a escrow contract that the server polls. In many implementations the server runs a

Top comments (0)