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 building autonomous AI agents


Why look at 402?

Most AI‑agent workflows today rely on API keys, subscription tiers, or ad‑hoc invoicing to pay for third‑party services. Those approaches work, but they introduce coupling: the agent must manage credentials, handle billing cycles, and often trust a central broker to mediate payments.

The HTTP status code 402 Payment Required was defined in the original HTTP/1.0 spec, but it has never been standardized for actual use. The x402 draft (see the W3C‑community group “HTTP Payments”) proposes a lightweight, stateless way to attach a micropayment to a single HTTP request‑response pair. If you are building agents that need to call many fine‑grained, pay‑per‑use services (e.g., tokenizers, model inference, data look‑ups), x402 lets you keep the payment logic inside the protocol itself rather than layering it on top of OAuth or custom invoicing.

Below we walk through the protocol flow, show minimal working code for both payer (agent) and payee (service), and discuss the practical trade‑offs you’ll encounter.


1. The x402 request/response cycle

Step Actor Action HTTP details
1 Client (agent) Sends a normal GET/POST to a resource. No payment header yet.
2 Server Determines that the caller has not paid (or payment is insufficient). Returns 402 Payment Required with a Payment header that describes the required amount, asset, network, and a nonce. HTTP/1.1 402 Payment Required
Payment: amount=0.005; asset=USDC; network=base; nonce=7a3f9c…
3 Client Constructs a transaction that pays the server the exact amount (including any fee), signs it with its wallet, and includes the signed transaction (or a hash+signature) in a Payment header on a retry of the original request. Payment: tx=0x…; sig=0x…
4 Server Verifies the payment (checks signature, nonce, amount, asset, network). If valid, processes the request and returns 200 OK (or another appropriate status). HTTP/1.1 200 OK
Content-Type: application/json
5 Client Uses the response body as normal.

The key property is statelessness: the server does not need to keep a session or invoice record beyond the nonce, which prevents replay attacks. The client only needs a wallet that can sign a transaction on the specified chain.


2. Minimal payer implementation (Node.js)

The following snippet assumes you have an Ethereum‑compatible wallet (private key or mnemonic) and the ethers library installed (npm i ethers). It shows how an AI agent would call a hypothetical /summarize endpoint that charges 0.005 USDC per request on Base.

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

// ----------------- Configuration -----------------
const RPC_URL = "https://base.mainnet.rpc.dev"; // public Base RPC
const PRIVATE_KEY = "0xYOUR_PRIVATE_KEY";      // agent's wallet
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // Base USDC
const SUMMARIZE_ENDPOINT = "https://api.example.com/summarize";
const REQUIRED_AMOUNT = ethers.parseUnits("0.005", 6); // USDC has 6 decimals
// ------------------------------------------------

const provider = new ethers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const usdc = new ethers.Contract(
  USDC_ADDRESS,
  ["function balanceOf(address) view returns (uint256)",
   "function transfer(address to, uint256 amount) returns (bool)"],
  signer
);

// Helper: build the payment header value from a signed tx
function paymentHeader(tx) {
  // The x402 draft suggests a compact form: tx=<hex>,sig=<hex>
  return `tx=${tx};sig=${signer.signMessage(ethers.getBytes(tx)).slice(2)}`;
}

async function callSummarize(text) {
  let attempt = 0;
  while (true) {
    attempt++;
    const resp = await fetch(SUMMARIZE_ENDPOINT, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        // No payment header on first try
      },
      body: JSON.stringify({ text }),
    });

    if (resp.ok) {
      const data = await resp.json();
      return data.summary; // success path
    }

    if (resp.status !== 402) {
      throw new Error(`Unexpected status ${resp.status}: ${await resp.text()}`);
    }

    // ----- 402 received -----
    const paymentHeaderRaw = resp.headers.get("Payment");
    if (!paymentHeaderRaw) {
      throw new Error("402 without Payment header");
    }
    // Parse server's challenge (amount, asset, network, nonce)
    const challenge = Object.fromEntries(
      paymentHeaderRaw.split(";").map(part => part.trim().split("="))
    );
    if (challenge.asset !== USDC_ADDRESS.toLowerCase() ||
        challenge.network !== "base") {
      throw new Error("Server requested unsupported asset/network");
    }
    const amountRequired = ethers.parseUnits(challenge.amount, 6);
    if (!amountRequired.eq(REQUIRED_AMOUNT)) {
      throw new Error("Amount mismatch");
    }

    // Build a simple USDC transfer transaction
    const nonce = await provider.getTransactionCount(signer.address);
    const tx = {
      to: USDC_ADDRESS,
      data: usdc.interface.encodeFunctionData("transfer", [
        signer.address, // we will proxy via the server? Actually we need to pay the service.
        amountRequired
      ]),
      chainId: 8453, // Base
      nonce,
      gasLimit: 100000,
      // gasPrice can be fetched from provider; we use a simple fallback
      gasPrice: await provider.getFeeData().then(f => f.gasPrice || ethers.parseUnits("0.1", 9))
    };
    const signedTx = await signer.signTransaction(tx);
    // Retry with payment header
    const payment = paymentHeader(signedTx);
    // loop continues; server will now see the header and verify
  }
}

// Example usage
(async () => {
  try {
    const summary = await callSummarize("Explain quantum entanglement in two sentences.");
    console.log("Summary:", summary);
  } catch (e) {
    console.error("Agent call failed:", e);
  }
})();
Enter fullscreen mode Exit fullscreen mode

What the code does

  1. First request – no payment header, expecting a 402.
  2. Parse the 402 – extracts amount, asset (USDC on Base), and a nonce (here we rely on the server’s amount field; a real implementation would also include a nonce to avoid replay).
  3. Build a USDC transfer – signs a transaction that sends the exact amount to the server’s address (the server would have published its USDC receiving address in its documentation or via a well‑known .well-known/x402 endpoint).
  4. Retry – attaches the signed transaction in a Payment header; the server verifies it and, if good, returns the summarised text.

Note – In a production agent you would likely cache the server’s payee address, estimate gas dynamically, and handle transaction failures (e.g., insufficient balance, nonce gaps). The example keeps those concerns out to stay focused on the protocol flow.


3. Minimal payee implementation (Express middleware)

The service side only needs to:

  • Detect missing or invalid payment.
  • Respond with 402 and a Payment header that tells the client what to pay.
  • Verify a presented payment before invoking the actual handler.

js
// server.js
import express from "express";
import { ethers } from "ethers";
import crypto from "crypto";

const app = express();
app.use(express.json());

// ----------------- Configuration -----------------
const RPC_URL = "https://base.mainnet.rpc.dev";
const provider = new ethers.JsonRpcProvider(RPC_URL);
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
// The service's own USDC receiving address (must be funded)
const PAYEE_ADDRESS = "0xYourServiceUSDCAddress";
// Price per call in USDC (6 decimals)
const PRICE_PER_CALL = ethers.parseUnits("0.01", 6);
// ------------------------------------------------

// Generate a server‑side nonce to prevent replays
function makeNonce() {
  return crypto.randomBytes(16).toString("hex");
}

// Middleware that enforces x402 payment
function requirePayment(req, res
Enter fullscreen mode Exit fullscreen mode

Top comments (0)