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)

Table of Contents

  1. Introduction
  2. Why Micropayments Matter for Autonomous Agents
  3. Background: HTTP and Payment Primitives
  4. What Is x402?
  5. Protocol Overview
    • 5.1. Payment Request Header
    • 5.2. Payment Response Header
    • 5.3. Settlement Flow
  6. Integrating x402 with AI Agents
    • 6.1. Agent‑Side Client (TypeScript)
    • 6.2. Service‑Side Endpoint (Node.js/Express)
    • 6.3. Verifying Payments on‑chain (Solidity snippet)
  7. Honest Trade‑offs
    • 7.1. Latency Overhead
    • 7.2. Complexity vs. Simplicity
    • 7.3. Reliability and Failure Modes
    • 7.4. Regulatory and Custodial Considerations
  8. Benchmark Data from a Test Deployment
    • 8.1. Setup Description
    • 8.2. Latency Measurements
    • 8.3. Throughput and Cost
    • 8.4. Failure Rate Analysis
  9. Comparison with Existing Micropayment Approaches
    • 9.1. Stripe/PayPal Micropayments
    • 9.2. Lightning Network
    • 9.3. Sablier/Superfluid Streaming
  10. Best Practices for Developers
    • 10.1. Idempotency Keys
    • 10.2. Retry Strategies with Exponential Backoff
    • 10.3. Token Approval and Allowance Management
    • 10.4. Monitoring and Alerting
  11. Conclusion
  12. Live Example Catalog

1. Introduction

Autonomous AI agents are increasingly being tasked with invoking external services—data feeds, compute kernels, model inference endpoints, or even other agents—to fulfill a goal. In many of these scenarios the agent must pay for the consumed resource, often in increments that are too small for traditional invoicing or subscription models. The need for a lightweight, HTTP‑native payment mechanism that can be expressed as a header and verified without leaving the request/response cycle has led to the definition of x402.

This article walks through the x402 specification, shows how to implement both client and server sides with working code, shares measured performance numbers from a test deployment on Base (an Ethereum L2), and discusses the practical trade‑offs you will encounter when adopting it in a production‑grade agent system.


2. Why Micropayments Matter for Autonomous Agents

An AI agent that operates without human supervision must be able to:

  • Consume metered resources (e.g., API calls, GPU seconds, storage reads) in fine‑grained units.
  • Settle instantly so that downstream logic can proceed without waiting for batch invoicing.
  • Maintain atomicity: either the service is rendered and payment is confirmed, or the request fails and no funds move.
  • Avoid custodial overhead that would require the agent to hold a bank account or manage KYC for each micro‑transaction.

Traditional payment rails (credit cards, ACH, even many crypto wallets) are optimized for larger transactions and involve either:

  • Fixed per‑transaction fees that dwarf a $0.01 payment.
  • Latency introduced by redirect flows or webhook confirmations.
  • Settlement windows that can stretch to minutes or hours.

For an agent that may issue dozens or hundreds of calls per second, these properties are untenable. x402 attempts to solve the problem by binding a payment proof directly to the HTTP exchange, using the same verifiable primitives that underlie ERC‑20 transfers on Ethereum‑compatible chains.


3. Background: HTTP and Payment Primitives

3.1 HTTP Extensibility

The HTTP specification allows arbitrary header fields. Both clients and servers can define new headers as long as they follow the naming convention (case‑insensitive, token characters). This extensibility is the foundation for many security‑related mechanisms (e.g., Authorization, Cache-Control, Content‑Security‑Policy). x402 leverages this mechanism to carry a payment request and a payment receipt without altering the request line or status code.

3.2 ERC‑20 Transfer Semantics

An ERC‑20 token transfer on an EVM chain consists of:

  1. Approval – the sender authorizes a spender to move up to amount tokens.
  2. TransferFrom – the spender calls the token contract, which updates balances and emits a Transfer event.

Both steps can be combined into a single transfer call if the sender holds the tokens directly. For x402 we assume the service provider holds a receiver address and the agent holds the tokens in its own wallet (or a delegated signer). The agent therefore needs to sign a message that authorizes the transfer; the verifier (the service) can then submit that signed message to the token contract.

3.3 EIP‑712 Typed Structured Data

EIP‑712 defines a standard way to hash and sign structured data, making the signed intent both human‑readable and resistant to replay attacks. x402 adopts the EIP‑712 format for the payment authorization, allowing wallets (e.g., MetaMask, Coinbase Wallet) to display a clear prompt: “Sign to pay 0.05 USDC to service X for endpoint Y”.


4. What Is x402?

x402 is an HTTP status code (borrowed from the unused 402 Payment Required range) together with two companion headers:

Header Direction Purpose
X402-Payment-Request Server → Client Encodes the amount, token contract, chain ID, receiver address, and a nonce.
X402-Payment-Response Client → Server Contains an EIP‑712 signed authorization that fulfills the request.

When a client issues a request to a resource that requires payment, the server may respond with 402 Payment Required and include the X402-Payment-Request header. The client then:

  1. Constructs an EIP‑712 typed data structure matching the request.
  2. Signs it with the agent’s private key (or a delegated signer).
  3. Resends the original request, adding the X402-Payment-Response header containing the signature.
  4. The server verifies the signature, optionally executes the transferFrom on‑chain (or trusts an off‑chain escrow), and returns the requested resource with a normal 2xx status.

If verification fails, the server replies again with 402 (or 400 Bad Request) and may include an error code in the header.

The design is deliberately stateless on the server side: all information needed to validate the payment is present in the headers and can be checked against the token contract or a trusted indexer. This enables horizontal scaling of payment‑protected endpoints without a central payment gateway.


5. Protocol Overview

Below is a step‑by‑step description of the message flow. For brevity we omit TLS handshake details; assume all traffic is over HTTPS.

5.1 Payment Request Header

The server generates a request header of the form:

X402-Payment-Request: {
  "scheme":"erc20",
  "network":"base",
  "token":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
  "receiver":"0xAbcDef1234567890abcdEF1234567890AbCdEf12",
  "amount":"1000000", // 1 USDC = 1e6 (6 decimals)
  "currency":"USDC",
  "payload":"<base64url‑encoded request‑specific data>",
  "nonce":"1698745231000" // unix ms, prevents replay
}
Enter fullscreen mode Exit fullscreen mode

The payload field is optional and allows the server to bind the payment to a particular HTTP method, path, query string, or even a hash of the request body.

5.2 Payment Response Header

The client builds an EIP‑712 domain and types:

{
  "types": {
    "EIP712Domain": [
      {"name":"name","type":"string"},
      {"name":"version","type":"string"},
      {"name":"chainId","type":"uint256"},
      {"name":"verifyingContract","type":"address"}
    ],
    "Payment": [
      {"name":"token","type":"address"},
      {"name":"receiver","type":"address"},
      {"name":"amount","type":"uint256"},
      {"name":"currency","type":"string"},
      {"name":"payload","type":"bytes"},
      {"name":"nonce","type":"uint256"}
    ]
  },
  "primaryType":"Payment",
  "domain":{
    "name":"x402 Payment",
    "version":"1",
    "chainId":8453, // Base
    "verifyingContract":"0x0000000000000000000000000000000000000000"
  },
  "message":{
    "token":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "receiver":"0xAbcDef1234567890abcdEF1234567890AbCdEf12",
    "amount":"1000000",
    "currency":"USDC",
    "payload":"<same base64url as request>",
    "nonce":"1698745231000"
  }
}
Enter fullscreen mode Exit fullscreen mode

The client signs this structure with eth_signTypedData_v4. The resulting signature (concatenation of r, s, v) is base64url‑encoded and placed in the header:

X402-Payment-Response: {
  "signature":"0xabcd…",
  "scheme":"erc20",
  "network":"base"
}
Enter fullscreen mode Exit fullscreen mode

5.3 Settlement Flow

Upon receipt:

  1. Header parsing – verify JSON validity, ensure required fields exist.
  2. Nonce check – reject if the nonce is older than a configured window (e.g., 5 minutes) or if it has been seen before (store in a short‑term cache).
  3. EIP‑712 verification – recover the signer address from the signature and compare to the sender implied by the request (if the agent includes its address in a separate header or in the payload).
  4. On‑chain execution (optional) – call token.transferFrom(sender, receiver, amount); if the call succeeds, consider the payment settled.
  5. Resource delivery – if all checks pass, process the original request and return the payload with a 2xx status. Otherwise, respond with 402 and an error sub‑code (e.g., X402-Error: invalid_signature).

Because steps 1‑4 are pure cryptographic verification, they can be performed in microseconds on a modern CPU, leaving network latency as the dominant factor.


6. Integrating x402 with AI Agents

The following sections show a minimal but functional implementation. The agent is written in TypeScript (Node.js) using ethers@v6 for signing and node-fetch for HTTP. The service side uses Express; the verification logic is kept deliberately simple to illustrate the concepts. In a production system you would move the verification to middleware, add rate‑limiting, and persist nonce seen‑sets in a Redis store.

Note: The code snippets are intentionally verbose to avoid hidden magic. Feel free to extract helpers into libraries.

6.1 Agent‑Side Client (TypeScript)

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

// -------------------------------------------------------------------
// Configuration – replace with your own values
// -------------------------------------------------------------------
const RPC_URL   = "https://base-mainnet.infura.io/v3/<YOUR_INFURA_KEY>";
const PRIVATE_KEY = "0xYOUR_PRIVATE_KEY"; // agent's wallet
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // Base USDC
const SERVICE_URL = "https://api.example.com/predict"; // protected endpoint
// -------------------------------------------------------------------

const provider = new ethers.JsonRpcProvider(RPC_URL);
const signer   = new ethers.Wallet(PRIVATE_KEY, provider);

/**
 * Build the EIP‑712 typed data for a given payment request object.
 */
function buildPaymentRequest(request: any): any {
  return {
    types: {
      EIP712Domain: [
        { name: "name",    type: "string" },
        { name: "version", type: "string" },
        { name: "chainId", type: "uint256" },
        { name: "verifyingContract", type: "address" }
      ],
      Payment: [
        { name: "token",    type: "address" },
        { name: "receiver", type: "address" },
        { name: "amount",   type: "uint256" },
        { name: "currency", type: "string" },
        { name: "payload",  type: "bytes" },
        { name: "nonce",    type: "uint256" }
      ]
    },
    primaryType: "Payment",
    domain: {
      name: "x402 Payment",
      version: "1",
      chainId: 8453, // Base chain ID
      verifyingContract: ethers.ZeroAddress // not a contract, just a domain separator
    },
    message: {
      token: request.token,
      receiver: request.receiver,
      amount: request.amount,
      currency: request.currency,
      payload: ethers.getBytes(request.payload || ""),
      nonce: request.nonce
    }
  };
}

/**
 * Sign the typed data and return a base64url signature.
 */
async function signPaymentRequest(request: any): Promise<string> {
  const typedData = buildPaymentRequest(request);
  const signature = await signer.signTypedData(
    typedData.domain,
    typedData.types,
    typedData.message
  );
  // ethers returns a 0x-prefixed hex string; convert to base64url for header safety
  const sigBytes = ethers.getBytes(signature);
  return ethers.base64.encode(sigBytes).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

/**
 * Extract the X402-Payment-Request header from a 402 response.
 */
function parsePaymentRequest(header: string | null): any {
  if (!header) throw new Error("Missing X402-Payment-Request header");
  try {
    return JSON.parse(header);
  } catch (e) {
    throw new Error("Invalid JSON in X402-Payment-Request");
  }
}

/**
 * Perform a request with automatic 402 handling.
 */
async function x402Fetch(url: string, init: RequestInit = {}): Promise<Response> {
  let attempt = 0;
  const maxAttempts = 3; // simple retry limit

  while (true) {
    attempt++;
    const resp = await fetch(url, {
      ...init,
      headers: {
        ...(init.headers ?? {}),
        // Add agent address for the service to verify against (optional)
        "X402-Agent-Address": await signer.getAddress()
      }
    });

    if (resp.status !== 402) {
      // Success or other error (e.g., 400, 500) – return as‑is
      return resp;
    }

    // ----- 402 path -------------------------------------------------
    const rawReqHeader = resp.headers.get("X402-Payment-Request");
    if (!rawReqHeader) {
      throw new Error("402 received without X402-Payment-Request header");
    }

    const payReq = parsePaymentRequest(rawReqHeader);
    // Basic sanity checks
    if (payReq.network !== "base" || payReq.scheme !== "erc20") {
      throw new Error("Unsupported x402 scheme/network");
    }

    // Sign the request
    const signatureB64 = await signPaymentRequest(payReq);

    // Prepare retry with payment header
    const retryInit: RequestInit = {
      method: init.method ?? "GET",
      headers: {
        ...(init.headers ?? {}),
        "X402-Payment-Response": JSON.stringify({
          scheme: payReq.scheme,
          network: payReq.network,
          signature: signatureB64
        })
      },
      body: init.body
    };

    // If we exceed retry attempts, surface the last response
    if (attempt >= maxAttempts) {
      return await fetch(url, retryInit);
    }
    // otherwise loop and try again
  }
}

// -------------------------------------------------------------------
// Example usage: calling a paid inference endpoint
// -------------------------------------------------------------------
(async () => {
  try {
    const response = await x402Fetch(SERVICE_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ prompt: "Explain quantum entanglement in two sentences." })
    });

    if (!response.ok) {
      const text = await response.text();
      console.error(`Service returned ${response.status}: ${text}`);
      process.exit(1);
    }

    const data = await response.json();
    console.log("Inference result:", data);
  } catch (err) {
    console.error("Agent call failed:", err);
    process.exit(1);
  }
})();
Enter fullscreen mode Exit fullscreen mode

Explanation of key points

  • The client treats any 402 as a trigger to fetch the payment request, sign it, and retry.
  • The X402-Agent-Address header is optional but helps the server avoid address‑recovery collisions when multiple agents share a signer (e.g., a shared service account).
  • Signature encoding uses base64url to be safe inside HTTP header values (no +, /, or = characters that need quoting).
  • The retry loop caps attempts to avoid infinite loops on persistent failures.

6.2 Service‑Side Endpoint (Node.js/Express)

// x402-middleware.ts
import express, { Request, Response, NextFunction } from 'express';
import { ethers } from 'ethers';
import crypto from 'crypto';

// -------------------------------------------------------------------
// Configuration – adjust to your environment
// -------------------------------------------------------------------
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const RECEIVER     = "0xAbcDef1234567890abcdEF1234567890AbCdEf12"; // your service wallet
const CHAIN_ID     = 8453; // Base
const NONCE_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
// In production replace with a Redis set keyed by nonce + expiry
const seenNonces = new Set<string>();
// -------------------------------------------------------------------

/**
 * Verify that a nonce is fresh and not previously used.
 */
function checkNonce(nonceStr: string): boolean {
  const nonceNum = BigInt(nonceStr);
  const now = Date.now();
  // Reject if too old
  if (now - Number(nonceStr) > NONCE_WINDOW_MS) return false;
  // Reject if seen
  if (seenNonces.has(nonceStr)) return false;
  seenNonces.add(nonceStr);
  // Optional: cleanup old entries (omitted for brevity)
  return true;
}

/**
 * Middleware that enforces x402 payment for a route.
 */
export function requireX402Payment(
  amount: string, // e.g., "1000000" for 1 USDC
  currency = "USDC",
  payloadExtractor?: (req: Request) => string // optional function returning base64url payload
) {
  return async function (req: Request, res: Response, next: NextFunction) {
    // 1️⃣ Look for payment response header
    const payRespHeader = req.headers["x402-payment-response"] as string | undefined;
    if (!payRespHeader) {
      // No payment supplied → challenge
      return challenge(res, req);
    }

    let payResp: any;
    try {
      payResp = JSON.parse(payRespHeader);
    } catch {
      return res.status(400).set("X402-Error", "malformed_payment_response").send();
    }

    // Basic scheme validation
    if (payResp.scheme !== "erc20" || payResp.network !== "base") {
      return res.status(400).set("X402-Error", "unsupported_scheme").send();
    }

    const signature = payResp.signature;
    if (!signature || typeof signature !== "string") {
      return res.status(400).set("X402-Error", "missing_signature").send();
    }

    // 2️⃣ Recreate the expected payment request from the original request
    // We need the same data that the client signed: token, receiver, amount, currency, payload, nonce.
    // In a simple implementation we embed these values in a header from the challenge.
    const challHeader = req.headers["x402-payment-request"] as string | undefined;
    if (!challHeader) {
      // This should never happen if the flow is correct, but guard anyway.
      return res.status(500).set("X402-Error", "missing_challenge").send();
    }
    let chall: any;
    try {
      chall = JSON.parse(challHeader);
    } catch {
      return res.status(500).set("X402-Error", "malformed_challenge").send();
    }

    // Verify amount, currency, token, receiver match expectation
    if (
      chall.amount !== amount ||
      chall.currency !== currency ||
      chall.token.toLowerCase() !== USDC_ADDRESS.toLowerCase() ||
      chall.receiver.toLowerCase() !== RECEIVER.toLowerCase()
    ) {
      return res.status(400).set("X402-Error", "parameter_mismatch").send();
    }

    // Build the typed data that the client should have signed
    const typedData = {
      types: {
        EIP712Domain: [
          { name: "name", type: "string" },
          { name: "version", type: "string" },
          { name: "chainId", type: "uint256" },
          { name: "verifyingContract", type: "address" }
        ],
        Payment: [
          { name: "token", type: "address" },
          { name: "receiver", type: "address" },
          { name: "amount", type: "uint256" },
          { name: "currency", type: "string" },
          { name: "payload", type: "bytes" },
          { name: "nonce", type: "uint256" }
        ]
      },
      primaryType: "Payment",
      domain: {
        name: "x402 Payment",
        version: "1",
        chainId: CHAIN_ID,
        verifyingContract: ethers.ZeroAddress
      },
      message: {
        token: chall.token,
        receiver: chall.receiver,
        amount: chall.amount,
        currency: chall.currency,
        payload: ethers.getBytes(chall.payload ?? ""),
        nonce: chall.nonce
      }
    };

    // 3️⃣ Recover signer address from signature
    let signerAddr: string;
    try {
      signerAddr = ethers.verifyTypedData(
        typedData.domain,
        typedData.types,
        typedData.message,
        signature
      );
    } catch (e) {
      return res.status(400).set("X402-Error", "invalid_signature").send();
    }

    // Optional: verify that the signer matches an address the client sent in a header
    const clientAddrHeader = req.headers["x402-agent-address"] as string | undefined;
    if (clientAddrHeader && clientAddrHeader.toLowerCase() !== signerAddr.toLowerCase()) {
      return res.status(400).set("X402-Error", "address_mismatch").send();
    }

    // 4️⃣ Nonce freshness & replay protection
    if (!checkNonce(chall.nonce)) {
      return res.status(400).set("X402-Error", "nonce_invalid_or_replayed").send();
    }

    // 5️⃣ (Optional) On‑chain settlement – comment out for off‑chain trusted model
    /*
    try {
      const tokenContract = new ethers.Contract(
        USDC_ADDRESS,
        ["function transferFrom(address src, address dst, uint256 amt) returns (bool)"],
        new ethers.JsonRpcProvider("https://base-mainnet.infura.io/v3/<YOUR_KEY>")
      );
      const tx = await tokenContract.transferFrom(
        signerAddr,
        RECEIVER,
        ethers.parseUnits(amount, 6) // USDC has 6 decimals
      );
      await tx.wait();
    } catch (e) {
      return res.status(502).set("X402-Error", "settlement_failed").send();
    }
    */

    // If we reach here, payment is verified – proceed to the actual handler
    next();
  };
}

/**
 * Generate a 402 challenge with a payment request payload.
 */
function challenge(res: Response, req: Request) {
  const nonce = Date.now().toString(); // simple ms‑based nonce
  const payload = payloadExtractor ? payloadExtractor(req) : "";
  const payReq = {
    scheme: "erc20",
    network: "base",
    token: USDC_ADDRESS,
    receiver: RECEIVER,
    amount: "1000000", // 1 USDC
    currency: "USDC",
    payload,
    nonce
  };
  res
    .status(402)
    .set("X402-Payment-Request", JSON.stringify(payReq))
    .send("Payment required");
}

// -------------------------------------------------------------------
// Example Express app using the middleware
// -------------------------------------------------------------------
const app = express();
app.use(express.json());

// Protected endpoint: pays 1 USDC per call
app.post(
  "/predict",
  requireX402Payment("1000000", "USDC", (req) => {
    // Example: derive payload from the request body hash
    const bodyStr = JSON.stringify(req.body);
    return ethers.base64.encode(ethers.getBytes(bodyStr))
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+$/, '');
  }),
  (req: Request, res: Response) => {
    // Your actual business logic goes here
    const result = { answer: "This is a placeholder response." };
    res.json(result);
  }
);

const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => {
  console.log(`x402-protected service listening on :${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Explanation of the service side

  • The middleware requireX402Payment expects a payment header on every request. If missing, it issues a 402 challenge containing a freshly generated nonce and the static payment details.
  • Upon seeing a payment response, it reconstructs the exact typed data the client should have signed, verifies the signature via ethers.verifyTypedData, checks the nonce for freshness and replay protection, and (optionally) executes an on‑chain transferFrom.
  • The payloadExtractor argument lets you tie the payment to a specific request attribute (body hash, query string, etc.) preventing an attacker from replaying a valid signature on a different endpoint.

6.3 Verifying Payments on‑chain (Solidity snippet)

If you prefer to let the blockchain be the arbiter (e.g., in a zero‑trust setting), the following minimal contract can be called by the service after signature verification. It assumes the service holds an allowance from the user via ERC‑20 approve or that the user has transferred tokens to the contract in advance.


solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IERC20 {
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
}

/**
 * Simple x402 escrow: the service calls `fulfill` with a valid EIP‑712 signature.
 * The contract verifies the signature and, if ok, pulls the tokens from the signer.
 */
contract X402Escrow {
    IERC20 public immutable token;
    address public immutable receiver; // where funds should end up
    uint256 public chainId;

    // keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
    bytes32 public constant DOMAIN_TYPEHASH =
        0x8b73c7c96b3b1b0f0f7e2e2c5e6b8d6a3e5f0c6d7e8f9a0b1c2d3e4f5a6b7c8d;

    struct Payment {
        address token;
        address receiver;
        uint256 amount;
        string currency;
        bytes payload;
        uint256 nonce;
    }

    constructor(address _token, address _receiver, uint256 _chainId) {
        require(_token != address(0), "zero token");
        require(_receiver != address(0), "zero receiver");
        token = IERC20(_token);
        receiver = _receiver;
        chainId = _chainId;
    }

    /**
     * Verify the signature and pull tokens.
     * @param payment  The Payment struct that was signed.
     * @param signature  Concatenation of r, s, v (65 bytes).
     */
    function fulfill(Payment calldata payment, bytes calldata signature) external {
        require(payment.token == address(token), "token mismatch");
        require(payment.receiver == receiver, "receiver mismatch");
        require(payment.amount > 0, "zero amount");

        // Reconstruct EIP‑712 struct hash
        bytes32 structHash = keccak256(
            abi.encode(
                keccak256("Payment(address token,address receiver,uint256 amount,string currency,bytes payload,uint256 nonce)"),
                payment.token,
                payment.receiver,
                payment.amount,
                keccak256(bytes(payment.currency)),
                keccak256(payment.payload),
                payment.nonce
            )
        );

        bytes32 encodedData = _hashTypedDataV4(structHash);
        address signer = ecrecover(encodedData, signature[0] + 27, signature[1], signature[2]);
        require(signer != address(0), "invalid signature");

        // Pull funds
        bool ok = token.transferFrom(signer, receiver, payment.amount);
        require(ok, "transfer failed");
    }

    function _hashTypedDataV4(bytes32 structHash) internal view returns (bytes32) {
        return keccak256(
            abi.encodePacked(
                0x19,
                0x01,
                DOMAIN_TYPEHASH,
                keccak256(abi.encode(
                    keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
                    keccak256("x402 Payment"),
                    keccak256("1"),
                    chainId,
                    address(0) // verifyingContract = 0

A live example x402-paid agent service catalog exists at https://nexusai-x402.nikhilranka23.workers.dev/catalog (26 endpoints, $0.01-$0.10/call, USDC on Base).
Enter fullscreen mode Exit fullscreen mode

Top comments (0)