One-time Payment#

HTTP Sellers focus on: Business flow → HTTP Seller integration (scheme selection → exact path / charge 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
Single-call amount is fixed and clear (one report / one inference / one query)
Price is determined before the call, no follow-on consumption after
Resource is non-revocable (file download, report generation, etc.)✅ (recommend syncSettle: true)

Business flow#

The diagram below is the abstract-level flow — HTTP Sellers are "triggered by client request", Agent Sellers are "triggered by the Agent generating a payment link in the dialogue", but the Challenge / Credential message semantics are identical. The carrier differences are covered in the seller integration sections below.

KYT (on-chain risk screening) is a compliance check inside the Broker's Verify stage — not a separate service.


HTTP Seller integration#

exact or charge?#

exact — single recipient, supports both sync and async settlement.

charge — supports a single recipient too, plus splits, i.e. one payment to multiple addresses (≤10). Defaults to and only supports sync settlement.

Dimensionexactcharge
RecipientSingleSingle / one-payment-to-multiple-addresses (≤10)
Settlement timingSync / asyncDefault and sync only

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

SDK status#

SchemeNode.jsRustGoJava
exact
chargeComing soon

exact path#

Each Tab includes the full install command + implementation code. The architecture has 4 components: Facilitator client (with OKX API Key) → Resource Server (registers the scheme) → Routes config (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 settle: 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");
});

Multi-scheme declarations all go through the accepts: [...] array; to append aggr_deferred (Batch payment), just add another entry to the array — see Batch payment.

Sync vs. async settlement#

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

ModeValueFacilitator behaviorUse case
SynctrueSubmits the transaction and waits for on-chain confirmation before returning txHashHigh-value transactions; need confirmation before delivering the resource
AsyncfalseReturns txHash immediately after submission, without waiting for confirmationMedium-value, response-speed sensitive

Async settlement carries a timing risk: the resource may be delivered before the on-chain payment is finalized. Sync settlement is recommended for high-value transactions. The charge scheme defaults to and only supports sync.

charge path#

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

charge does not use x402's accepts array; instead it uses a ChargeConfig type + MppCharge<T> extractor to describe each route's price — the price is returned by an associated function on the type and locked at compile time, not overridable at the route-config layer.

package.json:

json
{
  "type": "module",
  "dependencies": {
    "@okxweb3/mpp": "^0.1.0"
  }
}
typescript
// server.ts
// Run: 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);

Advanced#

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

1. Splits (only charge)#

One payment is automatically split among up to 10 recipients. Common scenarios: platform commission, multi-party revenue split, referral payout.

Hard constraints:

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

Signing burden: the buyer signs one EIP-3009 per split — 1 to the primary recipient + 1 per split — and the seller submits them all together at /settle.

package.json:

json
{
  "type": "module",
  "dependencies": {
    "@okxweb3/mpp": "^0.1.0"
  }
}
typescript
// server.ts
// Run: 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);

The current charge split implementation uses explicit amounts — each split is in absolute base units, not a percentage.

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
// Run: 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 Onchain OS Skill

    Send the prompt below to your AI Agent and follow the guided install:

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

    See Agent Seller quickstart.

  2. 2
    Generate a payment link

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

    text
    Buyer Agent: Translate this 3000-word document for me
    Seller Agent: Sure, translation service 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 URL returned by the Skill (in the form https://pay.okx.com/p/a2a_xxx) as a text message to the buyer Agent. The other side parses the URL and uses Agentic Wallet to sign.

  4. 4
    Poll payment status

    The Skill polls GET /payment/{paymentId}/status automatically. When 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 in their Agent — installing the Skill automatically configures Agentic Wallet as the underlying signing wallet, no separate install. The Skill auto-detects HTTP 402 responses or payment URLs in the messaging channel, calls the wallet to sign, and replays the request — all without manual intervention. Full integration steps in 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
  • Mutually distrustful parties needing acceptance before payout: use Escrow payment

Next#