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 the HTTP status code 402 Payment Required. It lets a server signal that a client must attach a verifiable payment before the requested resource can be served. For autonomous AI agents that need to call paid APIs on‑the‑fly, x402 provides a lightweight, standards‑based way to request, negotiate, and settle micropayments without adding a custom payment layer on top of HTTP.


Why x402 matters for AI agents

Autonomous agents frequently need to compose capabilities: call a language model, fetch external data, or trigger a tool. In many cases each of those calls carries a real cost (e.g., GPU time, data licensing, API usage). Traditional approaches either:

  1. Pre‑pay a bulk balance and hope it lasts, or
  2. Embed a proprietary payment token in every request (API key + signature).

Both add friction: pre‑pay requires accounting overhead; proprietary schemes lock you into a vendor’s auth flow and make composability hard.

x402 solves this by keeping the payment negotiation inside the HTTP exchange:

  • The server replies 402 with a Pay header that describes what to pay, how much, and where to send it.
  • The client (your agent) reads the header, constructs a blockchain transaction that satisfies the request, and retries the original request with a Payment header containing the transaction hash (or a signed receipt).
  • If the server validates the payment, it returns 200 and the payload; otherwise it may return another 402 with updated terms.

Because it rides on top of vanilla HTTP, any agent that can make an fetch/axios call can participate—no SDK lock‑in required.


The x402 flow in detail

Step Actor Action HTTP details
1 Client Sends a normal GET/POST to a protected endpoint. GET /agent-tool HTTP/1.1
2 Server Determines payment is required. Responds with 402. 402 Payment Required
Pay: <payment‑payload>
3 Client Parses Pay, builds a transaction that pays the specified amount to the specified address (often an escrow contract).
4 Client Retries the original request, adding a Payment header with the transaction hash (or a signed receipt). Payment: 0xabc123…
5 Server Verifies that the transaction mined on the expected chain, matches the amount, and pays to the correct recipient. If OK → 200 + body; else → another 402 (maybe with a higher price). 200 OK or 402 Payment Required

The Pay header is defined in the x402 spec as a JSON‑encoded string:

{
  "scheme": "exact",               // or "manual"
  "network": "base",               // EVM chain identifier
  "asset": "USDC",                 // token symbol or contract address
  "amount": "0.000005",            // in base units (here 5 µUSDC)
  "payee": "0xAbc…Def",            // recipient address
  "maxTimeout": 86400              // seconds the offer is valid
}
Enter fullscreen mode Exit fullscreen mode

The Payment header mirrors this with the transaction hash (or a signed receipt) so the server can verify on‑chain.


Minimal working example (Node.js)

Below are two self‑contained snippets: a server that protects a dummy AI‑agent endpoint with x402, and a client that acts as an autonomous agent, pays the fee, and retrieves the result.

Assumptions

  • You have an RPC endpoint for Base (e.g., https://base.mainnet.rpc.dev).
  • You hold USDC on Base in a wallet whose private key is stored securely (never hard‑code in production).
  • The USDC contract address on Base is 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913.

Server (Express)

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

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

// ---- CONFIG ----
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const PAYEE        = '0xYourReceiverAddress'; // where you want the funds
const AMOUNT_USDC  = ethers.parseUnits('0.01', 6); // $0.01 USDC (6 decimals)
const RPC_URL      = 'https://base.mainnet.rpc.dev';
const provider     = new ethers.JsonRpcProvider(RPC_URL);
// ----------------

// Helper: build the Pay header JSON as a base64url string (spec‑friendly)
function buildPayHeader() {
  const payload = {
    scheme: 'exact',
    network: 'base',
    asset: USDC_ADDRESS,
    amount: AMOUNT_USDC.toString(),
    payee: PAYEE,
    maxTimeout: 86400
  };
  // spec says base64url without padding
  return Buffer.from(JSON.stringify(payload)).toString('base64url');
}

// Middleware that checks for a valid Payment header
async function requirePayment(req, res, next) {
  const payHeader = req.headers['payment'];
  if (!payHeader) {
    // ask for payment
    res.set('Pay', buildPayHeader());
    return res.status(402).send('Payment Required');
  }

  // Expect payment header to be a transaction hash
  const txHash = payHeader;
  try {
    const tx = await provider.getTransaction(txHash);
    if (!tx) throw new Error('Tx not found');

    // Verify it matches our expectations
    if (tx.to?.toLowerCase() !== PAYEE.toLowerCase()) throw new Error('Wrong payee');
    if (tx.value !== AMOUNT_USDC) throw new Error('Wrong amount');

    // Optionally wait for confirmation (here we accept mempool)
    // await tx.wait(); // uncomment for stricter safety

    // Payment ok – proceed
    next();
  } catch (e) {
    console.error('Payment verification failed:', e);
    res.set('Pay', buildPayHeader());
    return res.status(402).send('Invalid or insufficient payment');
  }
}

// Dummy AI‑agent endpoint: returns a canned "answer"
app.get('/agent/answer', requirePayment, (req, res) => {
  res.json({ answer: 'The sky is blue because of Rayleigh scattering.' });
});

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

What this does

  • The server refuses any request to /agent/answer with 402 and a Pay header describing the exact USDC amount.
  • The client must supply a Payment header containing a transaction hash that pays the expected amount to the payee.
  • The server checks the transaction via an RPC call; if valid, it returns the JSON payload.

Client (agent)


markdown
// agent.js
import { ethers } from 'ethers';
import fetch from 'node-fetch';

// ---- CONFIG ----
const RPC_URL      = 'https://base.mainnet.rpc.dev';
const PRIVATE_KEY  = process.env.PRIVATE_KEY; // load from env, never hard‑code
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const PAYEE        = '0xYourReceiverAddress'; // must match server
const AMOUNT_USDC  = ethers.parseUnits('0.01', 6); // 0.01 USDC
const RPC_PROVIDER = new ethers.JsonRpcProvider(RPC_URL);
const wallet       = new ethers.Wallet(PRIVATE_KEY, RPC_PROVIDER);
const usdcAbi      = ["function transfer(address to, uint256 amount) returns (bool)"];
const usdcContract = new ethers.Contract(USDC_ADDRESS, usdcAbi, wallet);
// -----------------

const SERVER_URL = 'http://localhost:3000/agent/answer';

async function fetchWithPayment() {
  // First attempt – expect 402
  let resp = await fetch(SERVER_URL, { method: 'GET' });
  if (resp.status !== 402) {
    const text = await resp.text();
    throw new Error(`Unexpected status ${resp.status}: ${text}`);
  }

  // Parse Pay header
  const payHeaderRaw = resp.headers.get('pay');
  if (!payHeaderRaw) throw new Error('Missing Pay header');
  const payJson = JSON.parse(Buffer.from(payHeaderRaw, 'base64url').toString('utf8'));

  // Build and send transaction
  const tx = await usdcContract.transfer(
    payJson.payee,
    ethers.parseUnits(payJson.amount, 6) // USDC has 6 decimals
  );
  console.log(`Sent payment tx ${tx.hash}`);
  await tx.wait(); // wait for inclusion (adjust timeout as needed)

  // Retry original request with Payment header
  resp = await fetch(SERVER_URL,
Enter fullscreen mode Exit fullscreen mode

Top comments (0)