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 discover, call, and pay for micro‑services without building a custom billing layer.


Why HTTP‑402 for Micropayments?

The HTTP status code 402 Payment Required was reserved in the original spec but never widely used. The x402 proposal (see x402.org) repurposes it as a lightweight, standards‑based way to request payment in‑band with an HTTP request/response cycle.

Key properties that make it attractive for agent‑to‑agent interactions:

Property What it means for agents
Stateless No session cookies or OAuth dances; each request carries enough info to prove payment.
Currency‑agnostic The spec only requires a payment pointer (e.g., a USDC address) and an amount; the actual settlement can happen on any compatible chain.
Built on existing infrastructure Works with any HTTP client/server, proxies, CDNs, and API gateways that understand status codes.
Atomic per‑call Payment is verified before the handler runs, eliminating “pay‑after‑use” trust issues.

In practice, an agent makes a normal GET/POST. If the resource requires payment, the server replies 402 with a header (Pay) that tells the client how and how much to pay. The client then fulfills the payment (usually via a blockchain transaction) and retries the request with a proof‑of‑payment header (X-Payment). The server validates the proof and, if correct, returns 200 with the payload.


The x402 Flow, Step‑by‑Step

  1. Client → Server (initial request)
   GET /agent/summarize HTTP/1.1
   Host: api.example.com
   Accept: application/json
Enter fullscreen mode Exit fullscreen mode
  1. Server → Client (payment request)
   HTTP/1.1 402 Payment Required
   Content-Type: application/json
   Pay: ["http://payment.example.com/invoice", {"scheme":"erc20","network":"base","asset":"USDC","amount":"0.05"}]
Enter fullscreen mode Exit fullscreen mode
  • Pay is a JSON array: [paymentPointer, metadata].
  • paymentPointer is a URL where the client can fetch a payment invoice (or construct one directly if the metadata is sufficient).
  1. Client → Blockchain (settle the invoice)

    The client creates a transaction that transfers the exact amount of USDC to the address encoded in the pointer, includes a nonce or memo to make it unique, and waits for confirmation.

  2. Client → Server (retry with proof)

   GET /agent/summarize HTTP/1.1
   Host: api.example.com
   X-Payment: {"tx":"0xabc…def","chainId":8453,"blockNumber":12345678}
   Accept: application/json
Enter fullscreen mode Exit fullscreen mode
  1. Server → Client (service)
   HTTP/1.1 200 OK
   Content-Type: application/json
   {"summary":"…"}
Enter fullscreen mode Exit fullscreen mode

If the proof is invalid, missing, or replayed, the server returns 402 again (or 400 Bad Request) and the loop can repeat.


Minimal Working Example (Node.js + ethers.js)

Below is a self‑contained example that shows both sides of the flow. It uses the Base testnet (you can swap to mainnet) and USDC (0x036CbD53842c5426634e7929541eC2318f3dCF7e). The code is deliberately terse to highlight the protocol, not production‑grade security.

1. Server (Express)


js
// server.js
require('dotenv').config();
const express = require('express');
const { ethers } = require('ethers');
const app = express();
app.use(express.json());

const USDC_ADDRESS = '0x036CbD53842c5426634e7929541eC2318f3dCF7e'; // Base testnet USDC
const PAYEE = process.env.PAYEE_ADDRESS; // your agent's wallet
const provider = new ethers.JsonRpcProvider(process.env.BASE_RPC);
const usdc = new ethers.Contract(USDC_ADDRESS, ['function balanceOf(address) view returns (uint256)'], provider);

// Middleware that checks for a valid payment proof
async function requirePayment(req, res, next) {
  const proofHeader = req.headers['x-payment'];
  if (!proofHeader) {
    // No proof → ask for payment
    const amount = '0.05'; // USDC, 6 decimals
    const payPtr = {
      scheme: 'erc20',
      network: 'base',
      asset: USDC_ADDRESS,
      amount,
    };
    return res.status(402)
      .set('Pay', JSON.stringify(['https://example.com/invoice', payPtr]))
      .json({ error: 'payment required' });
  }

  let proof;
  try { proof = JSON.parse(proofHeader); } catch { return res.status(400).json({ error: 'bad X-Payment' }); }

  // Basic validation: tx exists, sends correct amount to PAYEE
  try {
    const tx = await provider.getTransaction(proof.tx);
    if (!tx) throw new Error('tx not found');
    if (tx.to?.toLowerCase() !== PAYEE.toLowerCase()) throw new Error('wrong recipient');
    const receipt = await tx.wait();
    if (receipt.status !== 1) throw new Error('tx failed');

    // fetch USDC amount from tx data (simplified: assume plain transfer)
    if (tx.data !== '0xa9059cbb000000000000000000000000' + PAYEE.slice(2) + '0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Enter fullscreen mode Exit fullscreen mode

Top comments (0)