Payment Methods

Pay-as-you-go#

For definitions and the underlying protocol, see Core Concepts · Pay-as-you-go. This page focuses on mechanism and flow.


When it fits#

Your businessSuitable
Per-call consumption is unpredictable (LLM per token / data per byte)
Long-running, high-frequency consumption (subscriptions, streams)
Buyer wants "pay for what you use" with refundable residual
Per-call price is fixed and known❌ Use One-time payment
Tiny unit price + ultra-high frequency❌ Use Batch payment
Buyer only makes one or two calls❌ Channel opening is uneconomical, use One-time payment

Business flow#

Pay-as-you-go has three phases: open the channel → consume and sign → settle / close.

Key design of cumulative Vouchers#

PropertyDescription
Cumulative amount (not incremental)Voucher records "cumulative due X as of now" rather than "deduct Y this time"
Anti-replaycumulativeAmount increases monotonically; older Vouchers are naturally rejected by the on-chain contract
Loss-tolerantEven if some Vouchers are lost in transit, as long as the Seller holds the latest one, full settlement is recoverable
Zero on-chainVouchers do not go on-chain; local signature verification by the Seller is enough

Mid-stream settle and channel close#

Pay-as-you-go offers two ways to realize funds:

OperationTimingChannel stateFund flow
settle (mid-stream)Any timeOPENPays the Seller per the current latest Voucher; remaining funds stay locked
closeBusiness clearly endedOPENCLOSEDFinal settlement + unused residual returned to Buyer

Channel state machine: OPEN (normal use) → CLOSING (Buyer triggers requestClose, enters grace period) → CLOSED (grace ends / Seller or Buyer completes final settlement).

settle is stateless: when calling settle, the Voucher (cumulativeAmount + Buyer signature) is uploaded with the request — the server does not rely on a local DB. As long as the Seller can produce the latest Voucher, settlement succeeds. close is the same, and the contract uses max(submitted amount, on-chain recorded maximum) to protect Seller revenue.

Recommendations
  • Long-lived channel (monthly subscriptions, long-term subscriptions): periodically initiate mid-stream settle to avoid holding too-valuable Vouchers
  • Business clearly ended (Buyer cancels, session ends): proactively close the channel so the Buyer gets the residual back promptly

Forced close (Buyer-side)#

What is the grace period#

The grace period is a delayed-close protection window in the Escrow contract — when the Buyer unilaterally triggers a forced close, the channel does not stop immediately. It first enters the CLOSING state and counts down, giving the Seller a chance to preempt with the latest Voucher and settle. Without this window, the Buyer could instantly close the channel before the Seller submits the latest Voucher on-chain, leaving the Seller's Voucher as worthless paper.

Forced close flow#

The Buyer can unilaterally call requestClose to trigger:

  1. Channel enters the grace period — state changes to CLOSING, countdown starts
  2. During grace, the Seller can preempt with close and settle using the latest Voucher
  3. After grace ends, the Buyer calls withdraw to recover residual funds; the channel terminal state is CLOSED

The grace period is the Seller's last chance. If you don't monitor channel state for long stretches, the Buyer can unilaterally close and recover funds — including amounts you'd theoretically be entitled to but haven't submitted Vouchers for. Be sure to wire up channel-event listening.


Seller integration#

SDK status#

SchemeNode.jsRustGoJava
session (Pay-as-you-go channel)💡

✅ Live · 💡 Coming soon

Seller integration for Pay-as-you-go has four steps:

  1. 1
    Register session payment config

    Declare the billing dimension — billing unit (e.g. llm_token / byte / second), unit price, recommended pre-deposit amount.

  2. 2
    Mount middleware on the metered route

    Per request, the middleware reads the Voucher, verifies the signature locally, and records cumulative usage. Your business code only needs to return the service result based on usage.

  3. 3
    Handle Buyer first-time channel open

    When the Buyer hasn't opened a channel, the first request automatically triggers a 402 Challenge guiding the Buyer to deposit into the Escrow contract. Subsequent requests hit the existing channel — no re-opening needed.

  4. 4
    Mid-stream settle or close the channel

    See Mid-stream settle and channel close above. Choose settle / close based on business duration.

Implementation code#

package.json:

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

const UNIT_PRICE_BASE_UNITS = "100";   // 0.0001 of a 6-decimal token
const UNIT_TYPE = "request";
const SUGGESTED_DEPOSIT = "10000";     // 100× unit price

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

// viem LocalAccount — replace with WalletClient / KMS / HSM signer in production.
// The session method fast-fails on startup if signer.address !== expected payee.
const sellerSigner = privateKeyToAccount(
  process.env.MPP_MERCHANT_PRIVATE_KEY! as `0x${string}`,
);

// Default in-memory store. Pass `store: ...` for SQLite / Redis / Postgres.
const mppx = Mppx.create({
  methods: [session({ saClient, signer: sellerSigner })],
  realm: "test realm",
  secretKey: process.env.MPP_SECRET_KEY!,
});

// Per-route session config. Charged per call; voucher accumulates;
// settle batches on /session/manage close action.
const SESSION = {
  amount: UNIT_PRICE_BASE_UNITS,
  currency: "0x...adb21711",                 // currency
  recipient: "0x...378211",                  // receipt
  description: "Pay-per-use API",
  unitType: UNIT_TYPE,
  suggestedDeposit: SUGGESTED_DEPOSIT,
  methodDetails: {
    chainId: 196,                            // X Layer
    escrowContract: process.env.MPP_ESCROW!, // 40-hex escrow address
    feePayer: true,
    minVoucherDelta: "0",
  },
} as const;

// Routes by `payload.action`: open / voucher / topUp / close.
// mppx.session(...)(request) handles all four uniformly:
//   - 402 → challenge response
//   - 200 → action-specific result; withReceipt() attaches Payment-Receipt
async function manage(request: Request): Promise<Response> {
  const result = await mppx.session(SESSION)(request);
  if (result.status === 402) return result.challenge;
  // open / topUp / close → empty 204; voucher → resource body.
  return result.withReceipt(Response.json({ status: "ok" }));
}

http.createServer(async (req, res) => {
  const url = `http://${req.headers.host ?? "localhost:4023"}${req.url}`;
  const webReq = new Request(url, {
    method: req.method,
    headers: new Headers(req.headers as Record<string, string>),
  });
  const path = new URL(url).pathname;
  const webRes =
    path === "/session/manage"
      ? await manage(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(4023);

EvmSessionMethod / SessionMethodDetails field reference:

FieldMeaningNotes
with_escrowEscrow contract addressRequired; channel funds are locked in this contract
with_signerSeller signerAccepts any alloy::signers::Signer: PrivateKeySigner / AwsSigner / LedgerSigner / custom remote signer
verify_payeeStartup-time check that signer.address() == expected recipientFail-fast at startup beats account drift at runtime
currencyPricing token contract addressRequired; currently only USDG / USD₮0 and other EIP-3009-compatible stablecoins are supported
recipientPrimary payee addressRequired, EIP-55-checksummed 40-hex address
chain_idChain ID196 = X Layer
fee_payerSome(true) Seller pays gas (transaction mode)Recommended true so Buyers don't need to hold X Layer gas
min_voucher_deltaMinimum increment per Voucher (base units)"0" accepts any; raising it lowers the verify cost of high-frequency tiny Vouchers
unit_typeBilling unit nameFree-form: request / llm_token / byte / second
unit_priceUnit price (base units)6-decimal stablecoin: "100" = 0.0001
suggested_depositRecommended pre-deposit (base units)Typically unit price × 100, covering one session's worth
realmNamespace isolationUse distinct realms per business line to prevent credential cross-use
secret_keySeller's key for signing ChallengesInject via MPP_SECRET_KEY env var, never hardcode

Buyer integration#

Pay-as-you-go does not require Agentic Walletany EVM wallet that supports EIP-712 signing works. We recommend Agentic Wallet for the smoothest automation experience.

  1. 1
    First request triggers channel open

    On the first request, the Buyer receives a 402 Challenge; the wallet prompts "open a pre-deposit channel, deposit X USD₮0". After signing the authorization, the Broker submits the open transaction on the Buyer's behalf.

  2. 2
    Sign Vouchers on each call

    The Agent / wallet signs cumulative Vouchers automatically on subsequent requests; the wallet UI clearly shows "you're signing a cumulative bill (cumulative due X USD₮0 as of now)".

  3. 3
    Top up and resume

    Call topUp whenever funds run low — no need to re-open the channel. New sessions can specify an existing channelId in the 402 to reuse the same channel.

  4. 4
    Proactively close

    At the end of the business, call close (cooperative) or requestClose (forced + grace period); residual funds are auto-refunded.


Limits and trade-offs#

When not to use Pay-as-you-go
  • Fixed and known price: Channel opening costs one on-chain action → use One-time payment
  • Tiny unit price + ultra-high frequency: Channel signing / reconciliation overhead is worse than just using Batch payment aggregation
  • Buyer only makes one or two calls: Channel opening is uneconomical; One-time payment is lighter
  • Need escrow release (no payout before delivery): use Escrow payment

Advanced (already supported at the protocol layer)#

Reuse a channel#

channelId is precomputed by the client (bytes32) from salt and other parameters. After one open, the client holds a channel identified by that channelId; subsequent sessions can keep signing Vouchers on the same channel without re-opening. When funds run low, use topUp instead.

Channel event listening#

On-chain Escrow events triggered unilaterally by the Payer must be consumed, otherwise you'll miss settlement windows or drift in reconciliation:

EventTriggerPriorityAction
requestClosePayerVery highImmediately call close and settle using the latest Voucher (grace period countdown has already started)
withdrawPayerHighAfter grace ends the user withdraws; channel terminal state is CLOSED, sync local state

Agent Seller (coming soon)#

The Agent Seller version is coming soon. The Agent Seller scenario is carried by an OKX extension on top of the protocol (independent at the underlying layer from HTTP Sellers), but the semantic layer (Challenge / Credential) and field structure remain identical to the HTTP Seller.

DimensionHTTP SellerAgent Seller (coming soon)
Challenge carrierHTTP 402 responseMessaging channel message body
Channel-open triggerClient first requestAgent initiates in dialogue
Voucher submissionHTTP request headerMessage reply
Business driverAPI callsAgent dialogue

Next#