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)

Audience: developers building autonomous AI agents who need a lightweight, standards‑based way to charge per‑request without reinventing a payment gateway.


1. What is x402?

x402 is an experimental HTTP status code (proposed as 402 Payment Required) that lets a server signal that a client must attach a verifiable payment before the resource can be served. Unlike traditional payment flows that redirect to a checkout page or require out‑of‑band invoicing, x402 keeps everything inside the HTTP request/response cycle:

  1. Client sends a normal GET/POST request.
  2. Server replies with 402 Payment Required and includes a Payment-Headers field describing:
    • The payment scheme (e.g., erc20-usdc on Base).
    • The exact amount (in the smallest token unit).
    • A nonce or timestamp to prevent replay.
    • Optional instructions for constructing the payment proof.
  3. Client constructs a cryptographic proof (usually a signed ERC‑20 transfer) that satisfies the header, then retries the original request with an Authorization: Bearer <proof> header (or a custom x402-Payment header).
  4. Server validates the proof against the blockchain (or an off‑chain relayer) and, if valid, returns 200 OK with the requested payload.

Because the negotiation happens via standard headers, any HTTP client—including fetch, Axios, or an AI agent’s internal HTTP wrapper—can participate without SDKs.


2. Why x402 Makes Sense for AI Agents

Agents often need to call paid APIs (LLM inference, data feeds, tool execution) many times per second. Traditional OAuth‑2 or API‑key models either:

  • Require static keys that are hard to rotate per‑agent, or
  • Introduce latency via redirects or external payment processors.

x402 offers:

Property Benefit for Agents
Stateless No server‑side session; each request carries its own payment proof.
Atomic Payment verification and service delivery happen in the same round‑trip.
Programmable Agents can compute the exact cost (e.g., $0.003 per token) and attach the matching amount.
Chain‑agnostic Works with any ERC‑20 compatible token; the example uses USDC on Base for low gas.
Open No proprietary gateway; anyone can implement a 402 responder.

3. Honest Trade‑offs

Trade‑off Explanation
Blockchain latency Proof verification requires reading the latest block (or trusting a relayer). On Base, finality is ~2 seconds; acceptable for many agent workflows but not for sub‑second latency loops.
Gas cost for the payer The agent must fund an ERC‑20 transfer (even if minimal). On Base, a USDC transfer costs ~0.0005 USD in gas, which is negligible compared to the micropayment but still a non‑zero overhead.
Replay protection Servers must store nonces or use timestamps; this adds a small state requirement. Stateless schemes (e.g., using EIP‑712 signed messages with a nonce) can mitigate but increase complexity.
Client‑side wallet management Agents need a private key capable of signing ERC‑20 transfers. In serverless environments, this often means using a managed wallet service or encrypting a key and decrypting at runtime—introducing security considerations.
Limited adoption As of 2025, x402 is still an experimental proposal; not all infrastructure (proxies, CDNs) automatically forwards the 402 status. You may need to patch middleware or run your own gateway.

If your agent can tolerate a ~1‑second extra round‑trip and you’re comfortable handling a wallet key, x402 is a pragmatic fit. If you need sub‑100 ms latency or cannot manage keys, consider a traditional API‑key with usage‑based billing instead.


4. Minimal Working Example

Below is a Node.js/TypeScript snippet that shows:

  1. How an AI agent pays for a resource using x402 (USDC on Base).
  2. How a simple server enforces the payment.

Assumptions

  • You have an Ethereum wallet (private key) with USDC on Base.
  • You have ethers@v6 installed.
  • The server runs locally on http://localhost:3000/price.

4.1 Agent (payer)

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

// ---------- CONFIG ----------
const PRIVATE_KEY = process.env.PRIVATE_KEY!; // funds USDC on Base
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // Base USDC
const RPC_URL = "https://base-mainnet.infura.io/v3/<PROJECT_ID>";
const AMOUNT_USDC = 0.005; // $0.005 per call (adjust as needed)
const DECIMALS = 6; // USDC
// ---------------------------

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

async function callProtectedEndpoint() {
  // Step 1: Try the request without payment
  let resp = await fetch("http://localhost:3000/price", { method: "GET" });
  if (resp.status === 200) {
    console.log("Already paid? Body:", await resp.text());
    return;
  }

  // Step 2: Expect 402 and parse payment headers
  if (resp.status !== 402) {
    throw new Error(`Unexpected status: ${resp.status}`);
  }
  const paymentInfoHeader = resp.headers.get("x402-payment");
  if (!paymentInfoHeader) {
    throw new Error("Missing x402-payment header");
  }
  const { scheme, amount, nonce, token } = JSON.parse(paymentInfoHeader);

  // Basic validation (in production, verify scheme matches your expectations)
  if (scheme !== "erc20-usdc" || token.toLowerCase() !== USDC_ADDRESS.toLowerCase()) {
    throw new Error("Unsupported payment scheme");
  }

  // Step 3: Build the ERC‑20 transfer proof
  const amountWei = ethers.parseUnits(amount.toString(), DECIMALS);
  const tx = await usdc.transfer(
    "0xServerReceiverAddress", // replace with the server's USDC address
    amountWei
  );
  const receipt = await tx.wait();

  // Step 4: Retry with proof (using Authorization bearer)
  const proof = `${receipt.hash}`; // simple hash‑based proof; replace with your spec
  const authHeader = `Bearer ${proof}`;

  resp = await fetch("http://localhost:3000/price", {
    method: "GET",
    headers: { Authorization: authHeader },
  });

  if (resp.status !== 200) {
    throw new Error(`Payment rejected: ${resp.status} ${await resp.text()}`);
  }
  const body = await resp.text();
  console.log("Successful response:", body);
}

callProtectedEndpoint().catch(console.error);
Enter fullscreen mode Exit fullscreen mode

What the agent does

  • Performs an optimistic GET.
  • On 402, reads the payment instruction (scheme, amount, nonce, token).
  • Sends a USDC transfer to the server’s address.
  • Uses the transaction hash as a proof and retries with an Authorization: Bearer <hash> header.
  • If the server validates the hash, it returns 200.

Note: The proof format here is deliberately minimal for illustration. A production implementation would follow the exact x402 spec (e.g., an EIP‑712 signed message containing the request URI, nonce, amount, and timestamp) to prevent replay attacks.

4.2 Simple Server (paywall)


ts
// server.ts
import express from "express";
import { ethers } from "ethers";

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

// ---------- CONFIG ----------
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const RPC_URL = "https://base-mainnet.infura.io/v3/<PROJECT_ID>";
const provider = new ethers.JsonRpcProvider(RPC_URL);
// Expected payment: $0.005 USDC (= 5,000,000 wei with 6 decimals)
const PRICE_WEI = ethers.parseUnits("0.005", 6);
// In production, store used nonces in a Redis set or DB to prevent replays.
const usedNonces = new Set<string>();
// ---------------------------

app.get("/price", async (req, res) => {
  const auth = req.headers.authorization;
  if (!auth?.startsWith("Bearer ")) {
    // No proof -> ask for payment
    const nonce = ethers.randomBytes(8).toString("hex");
    res.set({
      "x402-payment": JSON.stringify({
        scheme: "erc20-usdc",
        amount: ethers.formatUnits(PRICE_WEI, 6), // human readable
        token: USDC_ADDRESS,
        nonce,
      }),
    });
    return res.status
Enter fullscreen mode Exit fullscreen mode

Top comments (0)