Technology Sep 08, 2026 · 6 min read

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

x402 Explained: HTTP-Native Micropayments for AI Agents (With Real Code) An autonomous AI agent often needs to pay for external services—LLM APIs, data feeds, or compute—without a human in the loop. The x402 specification turns ordinary HTTP responses into a payment negotiation channel, l...

DE
DEV Community
by Nikhil Ranka
x402 Explained: HTTP-Native Micropayments for AI Agents (With Real Code)

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

An autonomous AI agent often needs to pay for external services—LLM APIs, data feeds, or compute—without a human in the loop. The x402 specification turns ordinary HTTP responses into a payment negotiation channel, letting an agent settle a micro‑fee directly on‑chain before the server returns the payload. Below is a walk‑through of how x402 works, a minimal implementation in JavaScript/TypeScript, and the practical trade‑offs you’ll face when you adopt it.

How x402 Turns 402 into a Payment Handshake

HTTP already defines status code 402 Payment Required. x402 repurposes it as a negotiation signal:

  1. Client → Server: ordinary request (GET/POST).
  2. Server → Client: if the endpoint requires payment, it returns 402 plus a JSON body that describes the payment request (amount, token, chain, and a nonce).
  3. Client → Server: the client signs a transaction that pays the requested amount, includes the nonce to prevent replay, and sends the signed transaction hash (or a full ERC‑20 approval + transfer) in an X-Payment header on a retry.
  4. Server → Client: validates the payment on‑chain, then replies with 200 and the desired payload.

Because the negotiation lives entirely in HTTP headers and bodies, no new protocol layer is needed—any HTTP client can be extended to understand x402.

Minimal x402 Client (Node.js / TypeScript)

The example below assumes you have an Ethereum wallet (private key or mnemonic) that can sign ERC‑20 transfers on Base. It uses viem for low‑level chain interaction and node-fetch for HTTP.

// x402Client.ts
import { createPublicClient, http, parseAbiItem } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
import fetch from "node-fetch";

// --- CONFIG ----------------------------------------------------
const PRIVATE_KEY = process.env.BASE_PRIVATE_KEY!; // 0x-prefixed
const ACCOUNT = privateKeyToAccount(PRIVATE_KEY);
const PUBLIC_CLIENT = createPublicClient({ chain: base, transport: http() });
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // USDC on Base
const DECIMALS = 6;

// Helper: build and send an ERC‑20 transfer
async function pay(amount: number, nonce: string): Promise<`0x${string}`> {
  const amountWei = amount * 10 ** DECIMALS;
  const data = encodeFunctionData({
    abi: [parseAbiItem("function transfer(address to, uint256 amount) returns (bool)")],
    functionName: "transfer",
    args: [ACCOUNT.address, amountWei],
  });

  const { request } = await PUBLIC_CLIENT.simulateContract({
    address: USDC_ADDRESS as `0x${string}`,
    abi: [parseAbiItem("function transfer(address to, uint256 amount) returns (bool)")],
    functionName: "transfer",
    args: [ACCOUNT.address, amountWei],
  });

  const hash = await PUBLIC_CLIENT.writeContract(request);
  return hash;
}

// Core request with x402 handling
async function x402Get(url: string): Promise<Response> {
  let resp = await fetch(url);
  if (resp.status !== 402) return resp; // no payment needed

  const { amount, token, chainId, nonce } = await resp.json();

  // Basic sanity checks – you may want stricter validation in prod
  if (token.toLowerCase() !== USDC_ADDRESS.toLowerCase())
    throw new Error("Unexpected token");
  if (chainId !== base.id) throw new Error("Wrong chain");

  // Pay and retry
  const txHash = await pay(Number(amount), nonce);
  const retryHeaders = new Headers(resp.headers);
  retryHeaders.set("X-Payment", txHash);
  retryHeaders.set("X-Payment-Nonce", nonce);

  resp = await fetch(url, { method: "GET", headers: retryHeaders });
  if (!resp.ok) throw new Error(`Payment failed: ${resp.status}`);
  return resp;
}

// Example usage
(async () => {
  const resp = await x402Get("https://api.example.com/premium-data");
  const json = await resp.json();
  console.log("Paid data:", json);
})();

What the code does

  1. Detects 402 – If the server asks for payment, we parse the JSON body.
  2. Validates the request – Checks token, chain, and amount to avoid paying the wrong contract.
  3. Executes an ERC‑20 transfer – Uses viem to simulate, then signs and sends the transaction on Base.
  4. Retries with proof – Sends the transaction hash (and nonce) in X-Payment / X-Payment-Nonce headers.
  5. Returns the payload – Only after the server verifies the payment on‑chain does it respond with 200.

You can adapt the same pattern for POST/PUT bodies or for approval‑then‑transfer flows if the server prefers you to approve a spender first.

Server‑Side Sketch (Express)

The counterpart is tiny: detect missing/invalid payment, return 402 with a challenge, and verify on‑chain before responding.

// x402Server.js
import express from "express";
import { parseUnits } from "viem";
import { base } from "viem/chains";
import fetch from "node-fetch";

const app = express();
const USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const REQUIRED_AMOUNT = 0.01; // USDC

app.get("/premium-data", async (req, res) => {
  const txHash = req.headers["x-payment"];
  const nonce = req.headers["x-payment-nonce"];

  if (!txHash || !nonce) {
    return res.status(402).json({
      amount: REQUIRED_AMOUNT,
      token: USDC,
      chainId: base.id,
      nonce: Math.random().toString(36).substring(2),
    });
  }

  // Verify transaction on Base
  try {
    const tx = await fetch(`https://api.basescan.org/api?module=transaction&action=gettxinfo&txhash=${txHash}&apikey=YOUR_KEY`);
    const txInfo = await tx.json();
    if (txInfo.result.to.toLowerCase() !== USDC.toLowerCase()) throw new Error("Wrong recipient");
    if (BigInt(txInfo.result.value) < parseUnits(String(REQUIRED_AMOUNT), 6)) throw new Error("Insufficient amount");
    // Optional: check nonce against a short‑lived store to prevent replay
  } catch (e) {
    return res.status(402).json({ error: "Invalid payment", detail: e.message });
  }

  // Payment OK – return actual resource
  res.json({ message: "Here is your premium data", timestamp: Date.now() });
});

app.listen(3000, () => console.log("x402 server listening on :3000"));

Key points

  • The server never holds funds; it only validates that a transaction with sufficient value reached the USDC contract.
  • The nonce prevents replay attacks; in production you’d store used nonces for a few minutes (e.g., in Redis).
  • If you want to avoid a full transfer each request, you could have the client approve a spender and let the server pull the amount via transferFrom; the core flow stays the same.

Honest Trade‑offs

Aspect Benefit Cost / Limitation
Atomicity Payment and service delivery are inseparable; the agent never gets data without paying. Requires an on‑chain confirmation (≈2 s on Base) before the server can release the payload, adding latency.
Universality Works with any HTTP client; no new ports, WebSocket, or gRPC needed. The client must hold a wallet and pay gas for each micro‑transaction; gas on Base is cheap (~$0.0001) but not zero.
Granularity Enables true pay‑per‑call pricing (e.g., $0.01 per LLM token bundle). Very small amounts (< $0.001) become impractical once gas is factored; you may need to batch or use escrow.
Security Nonce + on‑chain verification eliminates replay and spoofing if implemented correctly. Developers must handle chain reorganizations, failed txs, and edge cases (e.g., out‑of‑gas). Proper error handling adds code complexity.
Operational overhead No need to maintain invoicing, subscription DBs, or fiat rails for each agent. You must monitor the blockchain for failed payments, manage wallet key security, and decide on a strategy for refunds or dispute resolution.

In practice, many teams adopt x402 for high‑frequency, low‑value calls where the cost of maintaining a traditional billing system outweighs the on‑chain overhead. For bulk data transfers or long‑running compute jobs, a prepaid escrow or subscription model may still be preferable.

Closing Note

A live catalog of x402‑paid agent services—26 endpoints priced between $

DE
Source

This article was originally published by DEV Community and written by Nikhil Ranka.

Read original article on DEV Community
Back to Discover

Reading List