x402

Pay-per-call API access via x402

USDC on Base (CAIP-2 eip155:8453), EIP-3009 transferWithAuthorization, and HTTP payment headers per x402 v2.

How x402 works

x402 is an open payment protocol over HTTP that uses status code 402 Payment Required to challenge unpaid requests with machine-readable payment requirements. Clients construct an authorization for the exact asset amount, submit it in the PAYMENT-SIGNATURE header (v2), and the resource server verifies and settles through a facilitator before returning the resource. USDC on Base uses EIP-3009 transferWithAuthorization; typed signing follows EIP-712.

Stratalize verifies and settles through the Coinbase Developer Platform (CDP) x402 facilitator. Server wiring uses @coinbase/x402 createFacilitatorConfig with @x402/core/server (lib/x402/facilitator.ts). Protocol mechanics and facilitator error codes are defined in CDP x402 documentation.

Quickstart

Pricing and accepted schemes are returned in the 402 challenge and in GET https://www.stratalize.com/api/x402/<tool> metadata. Call without payment to read current requirements. Do not hard-code amounts; select the requirement that matches your wallet network and asset.

Stratalize x402 tool routes use x402 v2 wire headers: PAYMENT-REQUIRED on the challenge response, PAYMENT-SIGNATURE on the retried POST, and PAYMENT-RESPONSE (or legacy X-PAYMENT-RESPONSE) on the paid 200. Legacy v1 X-PAYMENT is not accepted on these routes (app/api/x402/[...tool]/route.ts).

TypeScript (Node 20+)

Dependencies already used by this repo: @x402/core, @x402/evm, viem. Set BASE_RPC_URL (Base mainnet JSON-RPC) and PRIVATE_KEY (funded payer on Base). Uses atomic-tier example tool get_inflation_benchmark; amount is taken from the challenge, not hard-coded.

import { x402Client, x402HTTPClient } from "@x402/core/client";
import { registerExactEvmScheme } from "@x402/evm/exact/client";
import { toClientEvmSigner } from "@x402/evm";
import { privateKeyToAccount } from "viem/accounts";

const RESOURCE =
  "https://www.stratalize.com/api/x402/get_inflation_benchmark";

async function readPaymentRequired(res: Response): Promise<unknown> {
  const header =
    res.headers.get("payment-required") ?? res.headers.get("PAYMENT-REQUIRED");
  if (header) {
    const http = new x402HTTPClient(new x402Client());
    return http.getPaymentRequiredResponse((name) => res.headers.get(name), undefined);
  }
  const copy = res.clone();
  const body = await copy.json().catch(() => null);
  if (
    body &&
    typeof body === "object" &&
    (body as { x402Version?: unknown }).x402Version === 2 &&
    Array.isArray((body as { accepts?: unknown }).accepts)
  ) {
    return body;
  }
  throw new Error("402 without parseable PAYMENT-REQUIRED / x402 v2 body");
}

async function main() {
  const rpcUrl = process.env.BASE_RPC_URL;
  const pk = process.env.PRIVATE_KEY;
  if (!rpcUrl || !pk) {
    throw new Error("Set BASE_RPC_URL and PRIVATE_KEY");
  }

  const account = privateKeyToAccount(pk as `0x${string}`);
  const core = new x402Client();
  registerExactEvmScheme(core, {
    signer: toClientEvmSigner(account),
    networks: ["eip155:8453"],
    schemeOptions: { 8453: { rpcUrl } },
  });
  const http = new x402HTTPClient(core);

  const first = await fetch(RESOURCE, {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({}),
  });

  if (first.status !== 402) {
    const t = await first.text();
    throw new Error(`expected 402, got ${first.status}: ${t.slice(0, 400)}`);
  }

  const paymentRequired = await readPaymentRequired(first);
  const paymentPayload = await http.createPaymentPayload(paymentRequired as any);
  const payHeaders = http.encodePaymentSignatureHeader(paymentPayload);

  const second = await fetch(RESOURCE, {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      ...payHeaders,
    },
    body: JSON.stringify({}),
  });

  const json = await second.json();
  if (second.status !== 200) {
    throw new Error(`tool call failed ${second.status}: ${JSON.stringify(json).slice(0, 500)}`);
  }

  const settleHeader =
    second.headers.get("payment-response") ??
    second.headers.get("PAYMENT-RESPONSE") ??
    second.headers.get("x-payment-response");
  const settled = settleHeader
    ? http.getPaymentSettleResponse((n) => second.headers.get(n))
    : null;

  console.log(JSON.stringify({ settleHeaderPresent: Boolean(settleHeader), settled, json }, null, 2));
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});

curl (unsigned challenge only)

Signing EIP-712 / building PAYMENT-SIGNATURE in shell is impractical; use the TypeScript flow above. curl remains useful to inspect status codes and headers on the unpaid call.

curl -sS -D - \
  -X POST "https://www.stratalize.com/api/x402/get_inflation_benchmark" \
  -H "accept: application/json" \
  -H "content-type: application/json" \
  -d '{}' | head -n 40

Request flow

On the wire, x402 v2 carries the challenge in the PAYMENT-REQUIRED header (base64-encoded JSON). Stratalize may also mirror requirements into the JSON body when the body would otherwise be empty (lib/x402/middleware.ts).

402 challenge (illustrative JSON)

Field names in live challenges follow x402 v2 (amount in atomic units, CAIP-2 network). Amount and copy are defined by the live challenge, not this document.

HTTP/2 402
content-type: application/json
payment-required: <base64-encoded-json>

{
  "x402Version": 2,
  "error": "Payment required",
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:8453",
      "amount": "<atomic units; per current pricing>",
      "resource": "https://www.stratalize.com/api/x402/get_inflation_benchmark",
      "description": "<tool description>",
      "mimeType": "application/json",
      "payTo": "0x5d51ECC50813E0937a5c43be298E9146796ad60d",
      "maxTimeoutSeconds": 300,
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "extra": { "name": "USD Coin", "version": "2" },
      "extensions": {
        "bazaar": {
          "discoverable": true,
          "category": "finance",
          "tags": ["attested", "finance", "inflation-benchmark"],
          "inputSchema": { "type": "object", "properties": {} }
        }
      }
    }
  ]
}

200 OK (attested tool payload)

HTTP/2 200
content-type: application/json
payment-response: <base64-settlement-receipt>

{
  "data": { "example": "tool-specific fields omitted" },
  "_stratalize": {
    "signed_at": "2026-04-30T12:34:56.789Z",
    "signature": "<base64-ed25519-signature>",
    "public_key_url": "https://www.stratalize.com/api/trust/signing-key",
    "canonicalization": "JCS-RFC8785",
    "version": "1"
  }
}

Attestation envelope is documented in /docs/attestation.

Settlement

Settlement occurs after successful tool execution and before the HTTP response is returned to the client. Stratalize does not settle when execution fails validation, returns a non-billable degraded path, or errors before a billable response is produced; the client receives an error status and no successful settlement receipt for that attempt.

Settlement is recorded on-chain through the CDP facilitator. The facilitator settlement object is exposed to HTTP clients in PAYMENT-RESPONSE / X-PAYMENT-RESPONSE (decode per @x402/core/http); use the transaction identifier returned there for on-chain reconciliation.

Failure modes

Status and machine-readable error fields may come from Stratalize route handlers, the x402 resource server, or the CDP facilitator verify/settle steps. Retry idempotent GETs for pricing; use exponential backoff on 503/5xx.

HTTPSignalMeaning / action
402Missing PAYMENT-SIGNATUREInitial challenge: unpaid POST to a protected x402 route. Read PAYMENT-REQUIRED / body, build v2 PAYMENT-SIGNATURE, retry.
402Facilitator verify rejectionAuthorization failed CDP verify (signature, nonce, expiry, asset, amount, or network mismatch). Inspect JSON body and CDP invalidReason / invalidMessage when present; re-sign against the latest challenge.
402Authorization time windowAuthorization validAfter/validBefore outside facilitator rules or maxTimeoutSeconds. Re-fetch challenge; re-sign with a fresh timestamp window.
402Insufficient authorization amountSigned value below required atomic amount for the selected accepts entry. Fetch a new challenge; sign for the full required amount.
402Facilitator settle rejectionVerify succeeded but on-chain settle failed or was rejected (success:false from facilitator). Inspect errorReason / errorMessage; do not retry blindly if the debit may be ambiguous—fetch a fresh challenge.
404Unknown x402 SLA toolPath not in the x402 SLA catalog (see /.well-known/x402.json). Tool may be MCP-only or misspelled. Use GET /api/x402/<tool> to confirm availability.
402 / JSON-RPCx402_required (MCP public surface)Paid tool called on /api/mcp-public without payment. Use POST /api/x402/<tool> with PAYMENT-SIGNATURE per this page; discovery metadata at GET /api/x402/<tool>.
503x402_init_failedFacilitator client failed to initialize (missing or invalid CDP_API_KEY_ID / CDP_API_KEY_SECRET on the deployment). Transient from caller perspective; retry with backoff.
503service_degraded / service degradedTool execution could not return billable data (e.g., upstream unavailable) or quality gate failed. No settlement for a failed billable path; read JSON reason and degraded flags.
500Server misconfigurationInternal dependency missing (e.g., signing or persistence). Retry later; no client-side payment fix.
5xxOther internal errorsUnhandled failure during execution or settlement attachment. Retry with backoff; treat response as untrusted until a successful 200 with attestation.

Bazaar extensions

Each Stratalize x402 SLA tool registers Bazaar discovery metadata on the resource server. Discovery JSON is attached under extensions.bazaar on payment requirements (see lib/x402/sla-catalog.ts).

Example: finance tool discovery fragment

Category values follow the Stratalize catalog: governance, finance, healthcare, realestate, crypto, intelligence. Tags are indexed for search; live tags vary by tool name.

"extensions": {
  "bazaar": {
    "discoverable": true,
    "category": "finance",
    "tags": ["attested", "finance", "inflation-benchmark"],
    "inputSchema": { "type": "object", "properties": { /* MCP-shaped */ } }
  }
}

Every successful JSON tool response includes an Ed25519-signed _stratalize envelope (RFC 8032, RFC 8785 JCS) per /docs/attestation.

SLO commitments

  • 99.9% monthly uptime on the x402 paid surface.
  • p95 latency under 800ms for atomic-tier tools; p95 under 2000ms for standard-tier tools (server-side processing, excluding client wallet and chain confirmation time).
  • Authorization validity bounded by facilitator rules and by accepts.maxTimeoutSeconds in the challenge (Stratalize SLA routes currently advertise 300 seconds in route config).
  • Settlement confirmation within 30 seconds under normal Base mainnet conditions after verify succeeds.
  • Status page: status.stratalize.com (planned; not shipped at time of this document).

Related: MCP (OAuth), Tool catalog, Docs home.