SDK reference

Node SDK Reference#

Packages#

PackageDescription
@okxweb3/x402-coreCore: server, facilitator, types
@okxweb3/x402-evmEVM mechanisms: exact, aggr_deferred
@okxweb3/x402-expressExpress middleware (seller)
@okxweb3/x402-nextNext.js middleware (seller)
@okxweb3/x402-honoHono middleware (seller)
@okxweb3/x402-fastifyFastify middleware (seller)
@okxweb3/x402-axiosAxios interceptor (buyer)
@okxweb3/x402-fetchfetch wrapper (buyer)

Core Types#

Network#

typescript
type Network = `${string}:${string}`;
// CAIP-2 format, e.g., "eip155:196"

Money / Price / AssetAmount#

typescript
type Money = string | number;
// User-friendly amount, e.g., "$0.01", "0.01", 0.01

type AssetAmount = {
  asset: string;            // Token contract address
  amount: string;           // Amount in token's smallest unit (e.g., "10000" for 0.01 USDC)
  extra?: Record<string, unknown>;  // Scheme-specific data (e.g., EIP-712 domain)
};

type Price = Money | AssetAmount;
// Either a user-friendly amount or a specific token amount

ResourceInfo#

typescript
interface ResourceInfo {
  url: string;              // Resource URL path
  description?: string;     // Human-readable description
  mimeType?: string;        // Response content type (e.g., "application/json")
}

PaymentRequirements#

Describes what the seller accepts for payment.

type PaymentRequirements = {
  scheme: string;           // Payment scheme: "exact" | "aggr_deferred"
  network: Network;         // CAIP-2 network identifier
  asset: string;            // Token contract address
  amount: string;           // Price in token's smallest unit
  payTo: string;            // Recipient wallet address
  maxTimeoutSeconds: number; // Payment authorization validity window
  extra: Record<string, unknown>; // Scheme-specific data
};

extra fields by scheme:#

SchemeExtra FieldTypeDescription
exact (EIP-3009)extra.eip712.namestringEIP-712 domain name (e.g., "USD Coin")
exact (EIP-3009)extra.eip712.versionstringEIP-712 domain version (e.g., "2")

PaymentRequired#

The HTTP 402 response body sent to clients.

typescript
type PaymentRequired = {
  x402Version: number;       // Protocol version (currently 2)
  error?: string;            // Optional error message
  resource: ResourceInfo;    // Protected resource metadata
  accepts: PaymentRequirements[];  // List of accepted payment options
  extensions?: Record<string, unknown>;  // Optional extension data
};

PaymentPayload#

The client's signed payment submitted in the retry request.

typescript
type PaymentPayload = {
  x402Version: number;       // Must match server's version
  resource?: ResourceInfo;   // Optional resource reference
  accepted: PaymentRequirements;  // The chosen payment option from `accepts`
  payload: Record<string, unknown>;  // Scheme-specific signed data (see below)
  extensions?: Record<string, unknown>;  // Extension data
};

payload fields by scheme:#

For exact (EIP-3009):

typescript
{
  signature: `0x${string}`;     // EIP-712 signature
  authorization: {
    from: `0x${string}`;       // Buyer wallet address
    to: `0x${string}`;         // Seller wallet address
    value: string;              // Amount in smallest unit
    validAfter: string;         // Unix timestamp (start validity)
    validBefore: string;        // Unix timestamp (end validity)
    nonce: `0x${string}`;      // 32-byte unique nonce
  };
}

For aggr_deferred:

typescript
{
  signature: `0x${string}`;     // Session key signature
  authorization: { /* same as EIP-3009 */ };
}

VerifyResponse#

typescript
type VerifyResponse = {
  isValid: boolean;          // Whether signature is valid
  invalidReason?: string;    // Machine-readable reason code
  invalidMessage?: string;   // Human-readable error message
  payer?: string;            // Recovered payer address
  extensions?: Record<string, unknown>;
};

SettleResponse#

typescript
type SettleResponse = {
  success: boolean;          // Whether settlement succeeded
  status?: "pending" | "success" | "timeout";  // OKX extension
  errorReason?: string;      // Machine-readable error code
  errorMessage?: string;     // Human-readable error message
  payer?: string;            // Payer address
  transaction: string;       // On-chain transaction hash (empty for aggr_deferred)
  network: Network;          // Settlement network
  amount?: string;           // Actual settled amount (may differ for "upto")
  extensions?: Record<string, unknown>;
};

SupportedKind / SupportedResponse#

typescript
type SupportedKind = {
  x402Version: number;
  scheme: string;
  network: Network;
  extra?: Record<string, unknown>;
};

type SupportedResponse = {
  kinds: SupportedKind[];
  extensions: string[];      // Supported extension keys
  signers: Record<string, string[]>;  // CAIP family → signer addresses
};

Server API (x402ResourceServer)#

Constructor#

typescript
import { x402ResourceServer } from "@okxweb3/x402-core/server";

const server = new x402ResourceServer(facilitatorClients?);
// facilitatorClients: FacilitatorClient | FacilitatorClient[]

register(network, server)#

Register a server-side scheme. Chainable.

typescript
server
  .register("eip155:196", new ExactEvmScheme())
  .register("eip155:196", new AggrDeferredEvmScheme());

registerExtension(extension)#

typescript
interface ResourceServerExtension {
  key: string;
  enrichDeclaration?: (declaration: unknown, transportContext: unknown) => unknown;
  enrichPaymentRequiredResponse?: (
    declaration: unknown,
    context: PaymentRequiredContext,
  ) => Promise<unknown>;
  enrichSettlementResponse?: (
    declaration: unknown,
    context: SettleResultContext,
  ) => Promise<unknown>;
}

initialize()#

Fetch supported kinds from the facilitator. Call once at startup.

typescript
await server.initialize();

**buildPaymentRequirements(config) → PaymentRequirements[]#

typescript
interface ResourceConfig {
  scheme: string;               // "exact" | "aggr_deferred" | "upto"
  payTo: string;                // Recipient wallet address
  price: Price;                 // "$0.01" or AssetAmount
  network: Network;             // "eip155:196"
  maxTimeoutSeconds?: number;   // Default: 300
  extra?: Record<string, unknown>;
}

const reqs = await server.buildPaymentRequirements({
  scheme: "exact",
  payTo: "0xSeller",
  price: "$0.01",
  network: "eip155:196",
});

**buildPaymentRequirementsFromOptions(options, context) → PaymentRequirements[]#

Dynamic pricing and payTo. Functions receive the context parameter.

typescript
const reqs = await server.buildPaymentRequirementsFromOptions(
  [
    {
      scheme: "exact",
      network: "eip155:196",
      payTo: (ctx) => ctx.sellerId === "A" ? "0xWalletA" : "0xWalletB",
      price: (ctx) => ctx.premium ? "$0.10" : "$0.01",
    },
  ],
  requestContext
);

**verifyPayment(payload, requirements) → VerifyResponse#

typescript
const result = await server.verifyPayment(paymentPayload, requirements);
// result.isValid: boolean

**settlePayment(payload, requirements, ...) → SettleResponse#

typescript
const result = await server.settlePayment(
  paymentPayload,
  requirements,
  declaredExtensions?,     // Extension data from 402 response
  transportContext?,       // HTTP transport context
  settlementOverrides?,    // { amount: "$0.05" } for upto scheme
);

Server Lifecycle Hooks#

HookContextCan Abort/Recover
onBeforeVerify{ paymentPayload, requirements }{ abort: true, reason, message? }
onAfterVerify{ paymentPayload, requirements, result }No
onVerifyFailure{ paymentPayload, requirements, error }{ recovered: true, result }
onBeforeSettle{ paymentPayload, requirements }{ abort: true, reason, message? }
onAfterSettle{ paymentPayload, requirements, result, transportContext? }No
onSettleFailure{ paymentPayload, requirements, error }{ recovered: true, result }
typescript
server.onBeforeVerify(async (ctx) => {
  // Log or gate verification
});

server.onAfterSettle(async (ctx) => {
  console.log(`Settled: ${ctx.result.transaction} on ${ctx.result.network}`);
});

server.onSettleFailure(async (ctx) => {
  if (ctx.error.message.includes("timeout")) {
    return { recovered: true, result: { success: true, transaction: "", network: "eip155:196" } };
  }
});

HTTP Resource Server (x402HTTPResourceServer)#

Higher-level wrapper that handles route matching, paywall, and HTTP-specific logic.

Constructor#

typescript
import { x402HTTPResourceServer } from "@okxweb3/x402-core/http";

const httpServer = new x402HTTPResourceServer(resourceServer, routes);

RoutesConfig#

typescript
type RoutesConfig = Record<string, RouteConfig> | RouteConfig;

interface RouteConfig {
  accepts: PaymentOption | PaymentOption[];  // Accepted payment methods
  resource?: string;           // Override resource name
  description?: string;        // Human-readable description
  mimeType?: string;           // Response MIME type
  customPaywallHtml?: string;  // Custom HTML for browser 402 page
  unpaidResponseBody?: (ctx: HTTPRequestContext) => HTTPResponseBody | Promise<HTTPResponseBody>;
  settlementFailedResponseBody?: (ctx, result) => HTTPResponseBody | Promise<HTTPResponseBody>;
  extensions?: Record<string, unknown>;
}

interface PaymentOption {
  scheme: string;              // "exact" | "aggr_deferred" | "upto"
  payTo: string | DynamicPayTo;  // Static or dynamic recipient
  price: Price | DynamicPrice;   // Static or dynamic price
  network: Network;
  maxTimeoutSeconds?: number;
  extra?: Record<string, unknown>;
}

// Dynamic functions receive HTTPRequestContext
type DynamicPayTo = (context: HTTPRequestContext) => string | Promise<string>;
type DynamicPrice = (context: HTTPRequestContext) => Price | Promise<Price>;

PaywallConfig#

typescript
interface PaywallConfig {
  appName?: string;            // Application name for paywall UI
  appLogo?: string;            // Logo URL
  sessionTokenEndpoint?: string;
  currentUrl?: string;
  testnet?: boolean;           // Show testnet branding
}

onSettlementTimeout(hook)#

typescript
type OnSettlementTimeoutHook = (txHash: string, network: string) => Promise<{ confirmed: boolean }>;

httpServer.onSettlementTimeout(async (txHash, network) => {
  // Custom recovery logic
  return { confirmed: false };
});

onProtectedRequest(hook)#

typescript
type ProtectedRequestHook = (
  context: HTTPRequestContext,
  routeConfig: RouteConfig,
) => Promise<void | { grantAccess: true } | { abort: true; reason: string }>;

httpServer.onProtectedRequest(async (ctx, config) => {
  // Grant free access for certain users
  if (ctx.adapter.getHeader("x-api-key") === "internal") {
    return { grantAccess: true };
  }
});

Middleware Reference#

Express (@okxweb3/x402-express)#

typescript
import {
  paymentMiddleware,
  paymentMiddlewareFromConfig,
  paymentMiddlewareFromHTTPServer,
  setSettlementOverrides,
} from "@okxweb3/x402-express";

// From pre-configured server (recommended)
app.use(paymentMiddleware(routes, server, paywallConfig?, paywall?, syncFacilitatorOnStart?));

// From config (creates server internally)
app.use(paymentMiddlewareFromConfig(routes, facilitatorClients?, schemes?, paywallConfig?, paywall?, syncFacilitatorOnStart?));

// From HTTP server (most control)
app.use(paymentMiddlewareFromHTTPServer(httpServer, paywallConfig?, paywall?, syncFacilitatorOnStart?));

// Settlement override in handler (for "upto" scheme)
app.post("/api/generate", (req, res) => {
  setSettlementOverrides(res, { amount: "$0.05" });
  res.json({ result: "..." });
});
ParameterTypeDefaultDescription
routesRoutesConfigrequiredRoute → payment config mapping
serverx402ResourceServerrequiredPre-configured resource server
paywallConfigPaywallConfigundefinedBrowser paywall settings
paywallPaywallProviderundefinedCustom paywall renderer
syncFacilitatorOnStartbooleantrueFetch supported kinds on first request

Next.js (@okxweb3/x402-next)#

typescript
import {
  paymentProxy,
  paymentProxyFromConfig,
  paymentProxyFromHTTPServer,
  withX402,
  withX402FromHTTPServer,
} from "@okxweb3/x402-next";

// As global middleware (middleware.ts)
const proxy = paymentProxy(routes, server, paywallConfig?, paywall?, syncFacilitatorOnStart?);
export async function middleware(request: NextRequest) { return proxy(request); }
export const config = { matcher: ["/api/:path*"] };

// Per-route wrapper (app/api/data/route.ts)
export const GET = withX402(handler, routeConfig, server, paywallConfig?, paywall?, syncFacilitatorOnStart?);
export const GET = withX402FromHTTPServer(handler, httpServer, paywallConfig?, paywall?, syncFacilitatorOnStart?);

Hono (@okxweb3/x402-hono)#

typescript
import { paymentMiddleware, paymentMiddlewareFromConfig, paymentMiddlewareFromHTTPServer } from "@okxweb3/x402-hono";

app.use("/*", paymentMiddleware(routes, server, paywallConfig?, paywall?, syncFacilitatorOnStart?));

Fastify (@okxweb3/x402-fastify)#

typescript
import { paymentMiddleware, paymentMiddlewareFromConfig, paymentMiddlewareFromHTTPServer } from "@okxweb3/x402-fastify";

// NOTE: Fastify registers hooks directly, returns void
paymentMiddleware(app, routes, server, paywallConfig?, paywall?, syncFacilitatorOnStart?);

EVM Mechanism Types#

ExactEvmScheme (Server)#

typescript
import { ExactEvmScheme } from "@okxweb3/x402-evm/exact/server";

const scheme = new ExactEvmScheme();  // No constructor args for server-side
scheme.scheme;  // "exact"
// Automatically handles price parsing, EIP-712 domain injection

AggrDeferredEvmScheme (Server)#

typescript
import { AggrDeferredEvmScheme } from "@okxweb3/x402-evm/deferred/server";

const scheme = new AggrDeferredEvmScheme();
scheme.scheme;  // "aggr_deferred"
// Delegates to ExactEvmScheme for price parsing

Client API (Buyer)#

Buyer-side packages auto-handle 402 Payment Required responses: parse the requirements, sign a payment payload via the configured EVM scheme, retry the request with the PAYMENT header attached.

Two transports are provided — pick the one that matches your HTTP client:

PackageWrapsWhen to use
@okxweb3/x402-axiosAxiosInstanceExisting Axios codebases; access to interceptors / instance config
@okxweb3/x402-fetchglobalThis.fetchfetch-based runtimes (browser, edge, Node 18+)

Both expose the same surface: wrapXxxWithPayment(client_or_fetch, x402Client) and wrapXxxWithPaymentFromConfig(client_or_fetch, config).

Axios — @okxweb3/x402-axios#

bash
npm install @okxweb3/x402-axios @okxweb3/x402-evm @okxweb3/x402-core axios
typescript
import axios from "axios";
import { wrapAxiosWithPaymentFromConfig } from "@okxweb3/x402-axios";
import { ExactEvmScheme, toClientEvmSigner } from "@okxweb3/x402-evm";
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { xLayer } from "viem/chains";

// Build a viem signer from the buyer's private key
const signer = toClientEvmSigner(
  createWalletClient({
    account: privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`),
    chain: xLayer,
    transport: http(),
  }),
);

const api = wrapAxiosWithPaymentFromConfig(axios.create(), {
  schemes: [
    {
      network: "eip155:196", // X Layer; use "eip155:*" to match any EVM chain
      client: new ExactEvmScheme(signer),
    },
  ],
});

// 402 → sign → retry, all transparent to the caller
const response = await api.get("https://api.example.com/paid-endpoint");

Fetch — @okxweb3/x402-fetch#

bash
npm install @okxweb3/x402-fetch @okxweb3/x402-evm @okxweb3/x402-core
typescript
import { wrapFetchWithPaymentFromConfig } from "@okxweb3/x402-fetch";
import { ExactEvmScheme, toClientEvmSigner } from "@okxweb3/x402-evm";
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { xLayer } from "viem/chains";

const signer = toClientEvmSigner(
  createWalletClient({
    account: privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`),
    chain: xLayer,
    transport: http(),
  }),
);

const fetchWithPayment = wrapFetchWithPaymentFromConfig(fetch, {
  schemes: [
    {
      network: "eip155:196",
      client: new ExactEvmScheme(signer),
    },
  ],
});

const response = await fetchWithPayment("https://api.example.com/paid-endpoint");

Builder pattern with x402Client#

Use the explicit builder when registering multiple schemes / networks, sharing one client across transports, or composing payment selectors.

typescript
import axios from "axios";
import { wrapAxiosWithPayment, x402Client } from "@okxweb3/x402-axios";
import { ExactEvmScheme, toClientEvmSigner } from "@okxweb3/x402-evm";

const client = new x402Client()
  .register("eip155:196", new ExactEvmScheme(signer));

const api = wrapAxiosWithPayment(axios.create(), client);

x402Client is also re-exported from @okxweb3/x402-fetch, so the same instance works for both transports.

Reading the payment receipt#

After a successful retry, the server returns a PAYMENT-RESPONSE header with the on-chain receipt (txHash, settled amount, etc.). Decode it with decodePaymentResponseHeader:

typescript
import { decodePaymentResponseHeader } from "@okxweb3/x402-axios"; // or "@okxweb3/x402-fetch"

// Axios
const paymentResponse = response.headers["payment-response"];
// Fetch
// const paymentResponse = response.headers.get("PAYMENT-RESPONSE");

if (paymentResponse) {
  const receipt = decodePaymentResponseHeader(paymentResponse);
  console.log("Payment receipt:", receipt);
}

x402ClientConfig#

FieldTypeDescription
schemesSchemeRegistration[]Required. Each entry pairs a network (e.g. "eip155:196", "eip155:*") with a scheme client (e.g. new ExactEvmScheme(signer)).
policiesPaymentPolicy[]Optional. See Policies below. Applied in order to filter / transform accepts before selection.
paymentRequirementsSelectorSelectPaymentRequirementsOptional. Picks one option from the filtered list. Defaults to (version, accepts) => accepts[0].

Selection pipeline#

When a 402 arrives, the client decides which payment option to use in three steps:

  1. Filter by registered schemes — only accepts whose network + scheme were registered via register() survive.
  2. Apply policies in registration order — each PaymentPolicy further filters / transforms the list.
  3. Selector picks the single requirement to sign against.

If step 1 or step 2 leaves the list empty, the client throws — payment is never attempted.

Policies — PaymentPolicy#

typescript
type PaymentPolicy = (
  x402Version: number,
  paymentRequirements: PaymentRequirements[],
) => PaymentRequirements[];

A policy is a pure function: take the current accepts, return the filtered subset (or a transformed copy). Use it for spend caps, network whitelists, scheme preferences, etc.

typescript
import {
  wrapAxiosWithPaymentFromConfig,
  type PaymentPolicy,
} from "@okxweb3/x402-axios";

// Reject any option that asks for more than 1 USDT (1_000_000 atomic units, 6 decimals)
const maxAmountPolicy: PaymentPolicy = (_version, reqs) =>
  reqs.filter(r => BigInt(r.amount) <= 1_000_000n);

// Only allow X Layer mainnet
const xLayerOnlyPolicy: PaymentPolicy = (_version, reqs) =>
  reqs.filter(r => r.network === "eip155:196");

// Prefer "exact" over "aggr_deferred" if both are offered
const preferExactPolicy: PaymentPolicy = (_version, reqs) => {
  const exact = reqs.filter(r => r.scheme === "exact");
  return exact.length > 0 ? exact : reqs;
};

const api = wrapAxiosWithPaymentFromConfig(axios.create(), {
  schemes: [{ network: "eip155:196", client: new ExactEvmScheme(signer) }],
  policies: [maxAmountPolicy, xLayerOnlyPolicy, preferExactPolicy],
});

Policies run in array order, so put narrowing filters (caps, whitelists) before preference reorderers.

Custom selector — SelectPaymentRequirements#

typescript
type SelectPaymentRequirements = (
  x402Version: number,
  paymentRequirements: PaymentRequirements[],
) => PaymentRequirements;

Runs after policies. Use it when policies returned multiple equally-valid options and you want explicit picking logic (e.g., cheapest first):

typescript
const cheapestFirst: SelectPaymentRequirements = (_version, reqs) =>
  [...reqs].sort((a, b) => Number(BigInt(a.amount) - BigInt(b.amount)))[0];

const api = wrapAxiosWithPaymentFromConfig(axios.create(), {
  schemes: [{ network: "eip155:*", client: new ExactEvmScheme(signer) }],
  paymentRequirementsSelector: cheapestFirst,
});

Lifecycle hooks#

x402Client exposes three lifecycle hooks for instrumentation, last-mile gating, and recovery. Use the builder form to register them:

typescript
import { wrapAxiosWithPayment, x402Client } from "@okxweb3/x402-axios";
import { ExactEvmScheme } from "@okxweb3/x402-evm";

const client = new x402Client()
  .register("eip155:196", new ExactEvmScheme(signer))
  // 1. Before signing — can abort the payment entirely
  .onBeforePaymentCreation(async ({ paymentRequired, selectedRequirements }) => {
    const tooExpensive = BigInt(selectedRequirements.amount) > 5_000_000n;
    if (tooExpensive) {
      return { abort: true, reason: "Amount exceeds buyer policy" };
    }
  })
  // 2. After successful sign — pure observation (logging, metrics)
  .onAfterPaymentCreation(async ({ paymentPayload }) => {
    console.log("Signed payload nonce:", paymentPayload.payload?.authorization?.nonce);
  })
  // 3. On signing failure — can recover by returning a manually-built payload
  .onPaymentCreationFailure(async ({ error }) => {
    console.error("Payment creation failed:", error.message);
    // return { recovered: true, payload: fallbackPayload };
  });

const api = wrapAxiosWithPayment(axios.create(), client);
HookWhenReturn semantics
onBeforePaymentCreationAfter selection, before scheme signsvoid to continue · { abort: true, reason } to cancel and reject
onAfterPaymentCreationAfter scheme returns the signed payloadvoid only (observation)
onPaymentCreationFailureWhen scheme throws during signingvoid to re-throw · { recovered: true, payload } to substitute a payload

Hooks within the same stage run in registration order.

Client extensions — registerExtension#

Use when a PaymentRequired response carries an extensions field that needs mechanism-specific payload enrichment (e.g., gas-sponsoring permits). The extension's enrichPaymentPayload hook only runs when its key matches a key in paymentRequired.extensions.

typescript
client.registerExtension({
  key: "eip2612GasSponsoring",
  async enrichPaymentPayload(payload, paymentRequired) {
    // Sign an EIP-2612 permit and attach to payload.extensions
    return { ...payload, extensions: { ...payload.extensions, /* ... */ } };
  },
});

Error Classes#

typescript
class VerifyError extends Error {
  readonly invalidReason?: string;
  readonly invalidMessage?: string;
  readonly payer?: string;
  readonly statusCode: number;
}

class SettleError extends Error {
  readonly errorReason?: string;
  readonly errorMessage?: string;
  readonly payer?: string;
  readonly transaction: string;
  readonly network: Network;
  readonly statusCode: number;
}

class FacilitatorResponseError extends Error {}

class RouteConfigurationError extends Error {
  readonly errors: RouteValidationError[];
}

interface RouteValidationError {
  routePattern: string;
  scheme: string;
  network: Network;
  reason: "missing_scheme" | "missing_facilitator";
  message: string;
}