Utviklerportal
Tema

One-time Payment#

HTTP Sellers focus on: Business flow → HTTP Seller integration (scheme selection → exact path (incl. Permit2) / charge path / upto path → syncSettle decision) → Advanced (splits, supporting exact + charge simultaneously)

Agent Sellers focus on: Business flow → Agent Seller integration (payment link generation and delivery)

For definitions and the underlying protocol, see Core Concepts · One-time payment. This page focuses on integration.


When it fits#

Your businessSuitable?
Fixed, well-defined amount per call (a report / one inference / one query)
Price known before the call, no further consumption afterward
Non-revocable resource (e.g. file download, report generation)✅ (recommend syncSettle: true)
Per-call cost not knowable upfront, needs settling by actual usage (e.g. LLM inference billed per token)✅ (use upto)

Business flow#

The diagram below is the abstract flow — for HTTP sellers it's "triggered by a client request", for Agent sellers it's "the Agent proactively generates a payment link in the conversation", but the Challenge / Credential message semantics are identical for both. See the seller integration sections below for the concrete transport differences.

KYT (on-chain risk screening) is a compliance check internal to the Broker's Verify phase, not a standalone service.


HTTP Seller integration#

Choosing exact, charge, or upto?#

exact — single recipient, supports both sync and async settlement; collects stablecoins by default, and adding Permit2 extends it to any ERC-20.

charge — besides a single recipient, also supports splits, i.e. paying multiple recipients (≤10) in one payment; sync settlement only (the default and only option).

upto — single recipient, price is a cap, settled by actual usage after the call — suitable when the cost isn't knowable upfront.

Dimensionexactchargeupto
RecipientsSingleSingle / multiple (≤10)Single
When the amount is fixedBefore the callBefore the callAfter the call, by actual (≤ cap)
Settlement timingSync / asyncSync only (default)Sync / async
Pricing tokenEIP-3009 stablecoins (Permit2 → any ERC-20)EIP-3009 stablecoinsAny ERC-20 (Permit2)

Not sure which to pick? Use supporting exact + charge simultaneously to mount both and let the buyer choose.

SDK status#

SchemeNode.jsRustGoJavaPython
exact
chargeComing soon
uptoComing soonComing soonComing soon

Permit2 signing for exact and upto are currently supported only on Node.js / Rust; other languages are coming soon.

exact path#

Each tab contains the full install command + implementation code. The architecture is composed of 4 components: Facilitator client (with OKX API Key) → Resource Server (registers the scheme) → Routes config (the accepts array) → Middleware mount.

bash
npm install express @okxweb3/x402-express @okxweb3/x402-core @okxweb3/x402-evm
npm install -D typescript tsx @types/express @types/node
typescript
import express from "express";
import {
  paymentMiddleware,
  x402ResourceServer,
} from "@okxweb3/x402-express";
import { ExactEvmScheme } from "@okxweb3/x402-evm/exact/server";
import { OKXFacilitatorClient } from "@okxweb3/x402-core";

const app = express();
const NETWORK = "eip155:196";
const PAY_TO = process.env.PAY_TO_ADDRESS || "0xYourSellerWallet";

const facilitatorClient = new OKXFacilitatorClient({
  apiKey: "OKX_API_KEY",
  secretKey: "OKX_SECRET_KEY",
  passphrase: "OKX_PASSPHRASE",
});

const resourceServer = new x402ResourceServer(facilitatorClient);
resourceServer.register(NETWORK, new ExactEvmScheme());

app.use(
  paymentMiddleware(
    {
      "GET /api/premium": {
        accepts: [
          {
            scheme: "exact",
            network: NETWORK,
            payTo: PAY_TO,
            price: "$0.10",
            syncSettle: true,            // Sync settlement: wait for on-chain confirmation
          },
        ],
        description: "Premium API",
        mimeType: "application/json",
      },
    },
    resourceServer,
  ),
);

app.get("/api/premium", (_req, res) => {
  res.json({ data: "premium content" });
});

app.listen(4000, () => {
  console.log("[Seller] listening at http://localhost:4000");
});

Multiple schemes are all declared via the accepts: [...] array; to add aggr_deferred (batch payment), just add another entry to the array — see Batch Payment.

exact + Permit2: support any ERC-20#

By default, exact uses EIP-3009 signing and can only collect stablecoins that natively support EIP-3009 (USD₮0 / USDG); buyers need no on-chain approve and pay with a single off-chain signature.

To accept any ERC-20 on X Layer, declare one field in the accepts entry — extra: { assetTransferMethod: "permit2" } — and buyers will sign Permit2 instead. The scheme name stays exact, and the route structure and settlement flow (sync / async) are unchanged; the only difference is that the buyer must do a one-time approve for that token (authorizing the Permit2 contract) on first use.

EIP-3009 (default)Permit2
Token rangeOnly stablecoins that natively support EIP-3009Any ERC-20
Buyer's first-time costZero (pure signature)One-time approve to the Permit2 contract
Subsequent paymentsPure signaturePure signature after approve

package.json:

json
{
  "dependencies": {
    "@okxweb3/x402-core": "^0.2",
    "@okxweb3/x402-evm": "^0.2",
    "@okxweb3/x402-express": "^0.2",
    "express": "^4.19"
  }
}
typescript
import express, { Request, Response } from "express";
import { OKXFacilitatorClient } from "@okxweb3/x402-core";
import { paymentMiddleware, x402ResourceServer } from "@okxweb3/x402-express";
import { ExactEvmScheme } from "@okxweb3/x402-evm/exact/server";

const facilitator = new OKXFacilitatorClient({
  apiKey: process.env.OKX_API_KEY!,
  secretKey: process.env.OKX_SECRET_KEY!,
  passphrase: process.env.OKX_PASSPHRASE!,
});

const resourceServer = new x402ResourceServer(facilitator)
  .register("eip155:196", new ExactEvmScheme());

// The one difference vs plain `exact`:
//   extra.assetTransferMethod = "permit2"
// tells the buyer to sign Permit2 instead of EIP-3009. Same scheme name
// ("exact"), same route shape — only the transfer method changes.
const routes = {
  "GET /api/premium": {
    accepts: {
      scheme: "exact",
      network: "eip155:196",
      payTo: process.env.PAY_TO_ADDRESS!,
      price: "$0.05",
      extra: { assetTransferMethod: "permit2" },
    },
    description: "Premium API — paid via Permit2 (any ERC-20 on X Layer)",
    mimeType: "application/json",
  },
};

const app = express();
app.use(paymentMiddleware(routes, resourceServer));

app.get("/api/premium", (_req: Request, res: Response) => {
  res.json({ data: "premium content" });
});

app.listen(4000, async () => {
  await resourceServer.initialize();
});

Sync vs. async settlement#

After /settle is called, the Facilitator submits the transaction on-chain. The syncSettle field in the route config controls the settlement behavior:

ModeValueFacilitator behaviorWhen to use
SynctrueSubmits the transaction and returns txHash after on-chain confirmationHigh-value transactions that must be confirmed before delivering the resource
AsyncfalseReturns txHash immediately after submitting, without waiting for confirmationMedium value, with response-speed requirements

Async settlement carries a timing risk: the resource may be delivered before the on-chain payment is finally confirmed. Sync settlement is recommended for high-value transactions. The charge scheme supports sync only (the default and only option).

charge path#

SDK status
Rust / Node.js / Go / Python SDKs are live; Java SDK coming soon.

charge doesn't use x402's accepts array; instead it describes each route's price with a ChargeConfig type + MppCharge<T> extractor — the price is returned by an associated function of the type and locked at compile time, so it can't be overridden by the route-config layer.

package.json:

json
{
  "type": "module",
  "dependencies": {
    "@okxweb3/mpp": "^0.1.0"
  }
}
typescript
// server.ts
// Start: node --env-file=.env --experimental-strip-types server.ts
// Or:    npx tsx --env-file=.env server.ts
import * as http from "node:http";
import { Mppx } from "@okxweb3/mpp";
import { charge } from "@okxweb3/mpp/evm/server";
import { SaApiClient } from "@okxweb3/mpp/evm";

// SA-API client (broadcasts EIP-3009 in transaction mode).
const saClient = new SaApiClient({
  apiKey: process.env.OKX_API_KEY!,
  secretKey: process.env.OKX_SECRET_KEY!,
  passphrase: process.env.OKX_PASSPHRASE!,
});

const mppx = Mppx.create({
  methods: [charge({ saClient })],
  realm: "test realm",
  secretKey: process.env.MPP_SECRET_KEY!,
});

// Per-route price (base units; "100" = 0.0001 of a 6-decimal token).
// fee_payer = true → seller broadcasts the EIP-3009 (transaction mode).
const CHARGE = {
  amount: "100",
  currency: "0x...adb21711",                 // currency
  recipient: "0x...378211",                  // receipt
  description: "One premium API call",
  methodDetails: { chainId: 196, feePayer: true },  // X Layer
} as const;

// Runs only after verify + settle.
async function premium(request: Request): Promise<Response> {
  const result = await mppx.charge(CHARGE)(request);
  if (result.status === 402) return result.challenge;
  return result.withReceipt(Response.json({ data: "premium content" }));
}

// node:http ↔ Web Standards bridge (10 lines).
http.createServer(async (req, res) => {
  const url = `http://${req.headers.host ?? "localhost:4000"}${req.url}`;
  const webReq = new Request(url, {
    method: req.method,
    headers: new Headers(req.headers as Record<string, string>),
  });
  const webRes =
    new URL(url).pathname === "/api/premium"
      ? await premium(webReq)
      : new Response("not found", { status: 404 });
  res.statusCode = webRes.status;
  webRes.headers.forEach((v, k) => res.setHeader(k, v));
  res.end(await webRes.text());
}).listen(4000);

upto path#

upto is a variant of one-time payment — the buyer first authorizes a payment cap, and after the server completes the request it settles the actual amount (≤ cap) based on the real cost; the over-authorized portion is not charged. If the request ultimately has no billable output, the actual amount can be 0, in which case no on-chain transaction is initiated at all.

Suitable for scenarios that are a single call but whose final amount can only be determined after the request finishes executing, for example: a data query billed by the number of rows actually returned, or a batch validation billed by the number of entries actually processed successfully.

Key differences from exact:

  • price is a cap, not the amount actually charged; the real amount is filled in by the handler after the request is processed.
  • upto is based on Permit2 signing (not EIP-3009), but the settlement structure is still "one settlement per request".

upto is based on Permit2 signing; buyers can use any EVM wallet. As with exact + Permit2, the buyer must do a one-time approve for that token (authorizing the Permit2 contract) on first use.

bash
npm install express @okxweb3/x402-express @okxweb3/x402-core @okxweb3/x402-evm
npm install -D typescript tsx @types/express @types/node
typescript
import express, { Request, Response } from "express";
import { OKXFacilitatorClient } from "@okxweb3/x402-core";
import {
  paymentMiddleware,
  setSettlementOverrides,
  x402ResourceServer,
} from "@okxweb3/x402-express";
import { UptoEvmScheme } from "@okxweb3/x402-evm/upto/server";

const facilitator = new OKXFacilitatorClient({
  apiKey: process.env.OKX_API_KEY!,
  secretKey: process.env.OKX_SECRET_KEY!,
  passphrase: process.env.OKX_PASSPHRASE!,
});

const resourceServer = new x402ResourceServer(facilitator)
  .register("eip155:196", new UptoEvmScheme());

// price is the cap, not the actual charge; the handler decides the actual amount.
const routes = {
  "GET /api/usage": {
    accepts: [
      {
        scheme: "upto",
        network: "eip155:196",
        payTo: process.env.PAY_TO_ADDRESS!,
        price: "$0.10",              // The cap the buyer authorizes
        maxTimeoutSeconds: 300,
        // extra omitted — UptoEvmScheme auto-injects the required fields
      },
    ],
    description: "Pay-by-actual-usage demo (upto)",
    mimeType: "application/json",
  },
};

const app = express();
app.use(express.json());
app.use(paymentMiddleware(routes, resourceServer));

app.get("/api/usage", (_req: Request, res: Response) => {
  // Compute the real cost of this request (e.g. tokens × unit price)
  const actualAmount = "$0.034";

  // The middleware reads this header, settles the override amount, and strips the header before responding.
  // amount accepts four formats:
  //   "1234000"  base units
  //   "50%"      percent of the cap
  //   "$0.034"   dollar string (same syntax as price)
  //   "0"        short-circuit — no on-chain transaction
  setSettlementOverrides(res, { amount: actualAmount });

  res.json({ report: { tokens_used: 1342, model: "demo" }, billed: actualAmount });
});

app.listen(4000, async () => {
  await resourceServer.initialize();
});

upto is currently supported only on the Node.js / Rust SDKs; Go / Java / Python are coming soon.

Advanced#

Applies to HTTP sellers only — the SDK code blocks below all mount on HTTP middleware; Agent sellers generate payment links via the Skill and don't touch this layer.

1. Splits (only charge)#

A single payment is automatically split to up to 10 recipients. Common scenarios: platform commission, multi-party revenue sharing, referral commissions.

Hard constraints:

  • sum(splits.amount) < ChargeConfig::amount() (the total of splits must be strictly less than the primary amount)
  • splits.len() ≤ 10
  • Each recipient must be an EIP-55 checksummed 40-hex address

Signing overhead: the buyer signs one EIP-3009 per split — 1 for the primary recipient + 1 per split — all submitted together by the seller at /settle.

package.json:

json
{
  "type": "module",
  "dependencies": {
    "@okxweb3/mpp": "^0.1.0"
  }
}
typescript
// server.ts
// Start: npx tsx --env-file=.env server.ts
import * as http from "node:http";
import { Mppx } from "@okxweb3/mpp";
import { charge } from "@okxweb3/mpp/evm/server";
import { SaApiClient } from "@okxweb3/mpp/evm";

const saClient = new SaApiClient({
  apiKey: process.env.OKX_API_KEY!,
  secretKey: process.env.OKX_SECRET_KEY!,
  passphrase: process.env.OKX_PASSPHRASE!,
});

const mppx = Mppx.create({
  methods: [charge({ saClient })],
  realm: "test realm",
  secretKey: process.env.MPP_SECRET_KEY!,
});

// Total 100 base units; primary keeps 50, splits take 30 + 20.
// Constraints: sum(splits) < amount; splits.length <= 10;
//              recipient must be 40-hex EIP-55.
// Buyer signs one EIP-3009 per split (one to primary + one per entry).
const splits = [
  { amount: "30", recipient: "0x....321a1308", memo: "partner-a" },
  { amount: "20", recipient: "0x....d31a6608", memo: "partner-b" },
];

// Only difference vs single charge: methodDetails.splits.
const CHARGE = {
  amount: "100",
  currency: "0x...adb21711",                 // currency
  recipient: "0x...378211",                  // primary receipt
  description: "One premium API call (split)",
  methodDetails: { chainId: 196, feePayer: true, splits },
} as const;

async function premium(request: Request): Promise<Response> {
  const result = await mppx.charge(CHARGE)(request);
  if (result.status === 402) return result.challenge;
  return result.withReceipt(Response.json({ data: "premium content" }));
}

http.createServer(async (req, res) => {
  const url = `http://${req.headers.host ?? "localhost:4000"}${req.url}`;
  const webReq = new Request(url, {
    method: req.method,
    headers: new Headers(req.headers as Record<string, string>),
  });
  const webRes =
    new URL(url).pathname === "/api/premium"
      ? await premium(webReq)
      : new Response("not found", { status: 404 });
  res.statusCode = webRes.status;
  webRes.headers.forEach((v, k) => res.setHeader(k, v));
  res.end(await webRes.text());
}).listen(4000);

charge split is currently implemented as explicit amounts — each split is an absolute base-units value, not a ratio.

2. Supporting exact + charge simultaneously#

package.json:

json
{
  "type": "module",
  "dependencies": {
    "@okxweb3/payment-router": "^0.1.0",
    "@okxweb3/mpp": "^0.1.0",
    "@okxweb3/x402-core": "^0.1.0",
    "@okxweb3/x402-evm": "^0.1.0"
  }
}
typescript
// server.ts
// Start: npx tsx --env-file=.env server.ts
import * as http from "node:http";

import { Mppx } from "@okxweb3/mpp";
import { charge as mppCharge } from "@okxweb3/mpp/evm/server";
import { SaApiClient } from "@okxweb3/mpp/evm";

import { OKXFacilitatorClient } from "@okxweb3/x402-core";
import {
  x402HTTPResourceServer,
  x402ResourceServer,
} from "@okxweb3/x402-core/server";
import { ExactEvmScheme } from "@okxweb3/x402-evm/exact/server";

import {
  MppAdapter,
  X402Adapter,
  paymentRouter,
} from "@okxweb3/payment-router";

// —— MPP setup ——
const saClient = new SaApiClient({
  apiKey: process.env.OKX_API_KEY!,
  secretKey: process.env.OKX_SECRET_KEY!,
  passphrase: process.env.OKX_PASSPHRASE!,
});
const mppx = Mppx.create({
  methods: [mppCharge({ saClient })],
  realm: "test realm",
  secretKey: process.env.MPP_SECRET_KEY!,
});

// —— x402 setup (facilitator + scheme; routes are declared on the router) ——
const NETWORK = "eip155:196"; // X Layer Mainnet
const x402Server = new x402ResourceServer(
  new OKXFacilitatorClient({
    apiKey: process.env.OKX_API_KEY!,
    secretKey: process.env.OKX_SECRET_KEY!,
    passphrase: process.env.OKX_PASSPHRASE!,
  }),
).register(NETWORK, new ExactEvmScheme());

// Built-in priorities: MPP=10, x402=20 (MPP wins when both headers present).
// Custom adapters should start at priority ≥ 100.
const protect = paymentRouter({
  adapters: [
    new MppAdapter({ mppx }),
    new X402Adapter({
      resourceServer: x402Server,
      httpResourceServerCtor: x402HTTPResourceServer,
    }),
  ],
  routes: {
    "GET /generateImg": {
      description: "AI Image Generation Service",
      adapterConfigs: {
        mpp: {
          intent: "charge",
          amount: "10000",
          currency: "0x...adb21711",         // currency
          recipient: "0x...378211",          // receipt
          description: "AI Image Generation Service",
          methodDetails: { chainId: 196, feePayer: true },
        },
        x402: {
          scheme: "exact",
          network: NETWORK,
          payTo: "0x...378211",              // receipt
          price: "$0.01",
          description: "AI Image Generation Service",
          mimeType: "application/json",
        },
      },
    },
  },
});

// Protocol-agnostic. Runs only after one of the adapters has verified payment.
const handler = protect(async () =>
  Response.json({
    imageUrl: "https://placehold.co/512x512/png?text=AI+Generated",
    prompt: "a sunset over mountains",
  }),
);

http.createServer(async (req, res) => {
  const url = `http://${req.headers.host ?? "localhost:4000"}${req.url}`;
  const webReq = new Request(url, {
    method: req.method,
    headers: new Headers(req.headers as Record<string, string>),
  });
  const webRes =
    new URL(url).pathname === "/generateImg" && req.method === "GET"
      ? await handler(webReq)
      : new Response("not found", { status: 404 });
  res.statusCode = webRes.status;
  webRes.headers.forEach((v, k) => res.setHeader(k, v));
  res.end(await webRes.text());
}).listen(4000);

Agent Seller integration#

Agent Sellers don't "passively mount middleware and wait for buyer requests" — instead, the Agent actively generates a payment link in the dialogue when it needs to charge, and sends it through a messaging channel (XMTP / Telegram / etc.).

This section focuses on payment-link generation and delivery; for how two Agents establish a session through Telegram / XMTP / etc., see Quickstart · I'm an Agent Seller — Configure messaging channel gateway.

  1. 1
    Install the OnchainOS Skill

    Send the following prompt to your AI Agent and follow the guided steps to finish installation:

    text
    Please help me install the Onchain OS Payment Skill so my Agent can generate payment links and charge externally.
    My receiving wallet address: 0xYourSellerWallet

    See the Agent Seller Quickstart.

  2. 2
    Generate a payment link

    When it needs to charge, the seller Agent calls the Skill to generate a one-time payment link:

    text
    Buyer Agent: Please translate this 3000-word document for me.
    Seller Agent: Sure, translation service is 50 USD₮0.
           [Skill call: createPayment({type:'charge', amount:'50000000', recipient:'0x...'})]
           Payment link: https://pay.okx.com/p/a2a_01HZX8Q9RK3JWYV7M2N5T8P4AB

    Each link is one-time and expires automatically after 30 minutes by default.

  3. 3
    Send the payment link

    Send the payment URL returned by the Skill (in the form https://pay.okx.com/p/a2a_xxx) to the buyer Agent as a text message; the other side parses the URL and completes signing via Agentic Wallet.

  4. 4
    Poll payment status

    The Skill automatically polls GET /payment/{paymentId}/status; once the status becomes completed, it notifies the Agent to deliver the service.

    text
    Skill: Payment completed (tx: 0xabc...)
    Agent: Payment received, starting translation...

Buyer integration#

Buyers integrate by installing the Onchain OS Skill on their Agent — installing the Skill automatically configures Agentic Wallet as the underlying signing wallet, with no separate installation needed. The Skill automatically detects HTTP 402 responses or payment URLs in message channels, completes signing via the wallet, then replays the request — no manual buyer involvement at any point. For the full integration steps, see Agent Buyer.


Limits and trade-offs#

When not to use one-time payment
  • Tiny single-call price + ultra-high call frequency: per-call on-chain settlement is uneconomical → use Batch payment
  • Long-running relationship + repeated cumulative billing (subscription APIs / Agent multi-step tasks): one channel beats per-call settlement → use Pay-as-you-go
  • Single call, cost only known afterward: just use upto on this page; only "charging accumulated across many calls" needs Pay-as-you-go
  • Mutually distrustful parties needing acceptance before payout: use Escrow payment

Next#