_shared/amount-display.md
# Amount display
All user-facing amounts in BOTH human and atomic form: `<human> (<atomic>)`, e.g. `0.0004 USDC (400)`, `1.5 ETH (1500000000000000000)`. Compute `human = atomic / 10^decimals` from the challenge `currency` token.
| Token | Decimals | 1 unit in minimal | Example |
|---|---|---|---|
| USDC | 6 | `1000000` | `1000000` → 1.00 USDC |
| USDT | 6 | `1000000` | `2500000` → 2.50 USDT |
| USDG | 6 | `1000000` | `500000` → 0.50 USDG |
| ETH | 18 | `1000000000000000000` | `10000000000000000` → 0.01 ETH |
**Unknown symbol** (not in the table): never assume — query `okx-dex-market` for its decimals first. If you can't resolve them, render `<atomic> <symbol>` and append `unknown decimals — please double-check the seller-provided amount`. Do not block the flow. (`a2a-pay` overrides this — see `references/a2a_charge.md`.)
_shared/preflight.md
# Preflight Checks
> At the start of each thread, run the following checks in order.
1. Run: `onchainos preflight --skill-version <this skill's frontmatter version>`
2. Read `data.action` from the JSON it prints:
- **null** → continue silently; don't echo routine output.
- **non-null** → show it to the user and do exactly what it says
## Global Notes
- **`onchainos preflight` fails with `command not found` or `unrecognized subcommand 'preflight'`** → install/replace the CLI directly:
- macOS/Linux: `curl -sSL https://raw.githubusercontent.com/okx/onchainos-skills/main/install.sh | sh`
- Windows: `irm https://raw.githubusercontent.com/okx/onchainos-skills/main/install.ps1 | iex`
Then re-run `onchainos preflight --skill-version <this skill's frontmatter version>` and continue the original request. Stop only if installation fails.
- **A global install printed `PromptScript does not support global skill installation`** (only with `npx skills add … --yes -g`) → known upstream `npx skills` limitation: the skill files installed correctly. Tell the user it's safe to ignore.
references/a2a_charge.md
# a2a_charge — agent-to-agent payment links (`onchainos payment a2a-pay`)
> **CLI down-sink:** don't self-sleep/poll for status — use
> `onchainos payment a2a-pay status --payment-id <id> --wait` to poll internally
> (3s interval, 60s ceiling) until a terminal state; read `data.terminal` /
> `data.timed_out`. create/pay NL→command routing stays here.
> Loaded from `../SKILL.md` when the user mentions a paymentId, an `a2a_...` link, "create payment link", or asks to check a2a payment status. Unlike the HTTP 402 paths (`accepts`-based and `WWW-Authenticate: Payment`), a2a is **not triggered by an HTTP 402 response** — it's invoked by name, with a paymentId or a seller's create-link request.
Wraps `onchainos payment a2a-pay` for seller (`create`) and buyer (`pay` / `status`) roles. Buyer-side trust is **delegated upstream** (see Trust model below).
## Pre-flight
`create` and `pay` need a live wallet session — the dispatcher's Step B2 already checked it. If you entered here directly, run `onchainos wallet status` first; not logged in → `onchainos wallet login`. Never sign without a live session.
---
## Seller — Create a Payment Link
**Inputs**:
- **Required**: `--amount` (decimal, e.g. `"0.01"`), `--symbol` (e.g. `"USDT"`), `--recipient` (0x... EVM address — seller wallet)
- **Optional**: `--description`, `--realm`, `--expires-in` (seconds, default 1800)
**Steps**:
1. Run pre-flight (see above).
2. Shell out:
```bash
onchainos payment a2a-pay create \
--amount <amount> --symbol <symbol> --recipient <recipient> \
[--description <text> --realm <domain> --expires-in <seconds>]
```
3. Parse the response — only `payment_id` and `deliveries.url` (optional) are present. The CLI no longer returns `amount` / `currency`; echo the seller's input args back for display.
4. Display:
> Payment link created.
> • paymentId: `<id>`
> • Amount: `<amount input> <symbol input>` (decimal as you submitted)
> • Recipient: `<recipient input>`
> • Share with buyer: `<deliveries.url>` (if returned by the server) or `paymentId=<id>`
5. Suggest next: poll status anytime with `onchainos payment a2a-pay status --payment-id <id>` once the buyer is expected to have paid.
---
## Buyer — Pay a Payment Link
**Required input**: `paymentId` only. The CLI fetches the seller-issued challenge from the server and signs whatever amount / currency / recipient the challenge declares.
> **Trust model**: the buyer signs the seller's challenge as-is. Verifying that the challenge matches what the buyer agreed to pay is the **upstream caller's responsibility** — the user (or the upstream skill) MUST cross-check the seller's `paymentId` / `deliveries.url` against their out-of-band agreement (chat, task spec, prior negotiation) **before** calling this skill. Once invoked, the skill signs whatever the on-server challenge declares.
### Step 1 — Sign and submit
The skill does not run its own preview / yes-no gate; trust is delegated upstream. Shell out directly:
```bash
onchainos payment a2a-pay pay --payment-id <paymentId>
```
The CLI fetches the on-server challenge, TEE-signs the EIP-3009 authorization, and submits the credential. Two outcomes:
**Accepted** — `ok:true`, exit 0; `data` carries `payment_id` / `status` / `tx_hash` / `signature`. Proceed to Step 2 (auto-poll).
**Rejected** — server returned `data.success:false` (e.g. `errorReason:"insufficient_balance"`). CLI surfaces it as a hard failure: `ok:false`, exit code 1, message embeds the reason verbatim:
```json
{
"ok": false,
"error": "payment a2a_xxx rejected (reason=<errorReason>)"
}
```
**Treat as terminal — do NOT retry `pay`.** Every retry produces a fresh EIP-3009 nonce + signature; if the reason is `insufficient_balance` or similar, retrying wastes a signature without changing the outcome. Tell the user what failed, suggest the obvious remedy (top up balance / ask the seller for a new link), and stop.
### Step 2 — Auto-poll status to terminal
Status classification:
- **Non-terminal** (poll): `pending`, `settling`
- **Terminal** (stop): `completed`, `failed`, `expired`, `cancelled`
If `status` is already terminal → render the result and stop.
If non-terminal → poll every **3 seconds**, up to a **60-second** total budget:
```bash
onchainos payment a2a-pay status --payment-id <paymentId>
```
- As soon as a terminal status is observed → render full result (status + tx_hash + block_number) and stop.
- If 60 seconds elapse and the status is still non-terminal → return the current `status` plus the paymentId, and tell the user: "Status is still `<status>` after 60s; you can run `status` again later."
**Terminal display strings**:
| status | Display |
|---|---|
| `completed` | "✅ Payment confirmed on-chain. tx_hash: `<tx_hash>` block: `<block_number>`" |
| `failed` | "❌ Payment failed. (include the server-provided reason if any)" |
| `expired` | "⌛ Payment link expired before settlement. Ask the seller for a new one." |
| `cancelled` | "🚫 Seller cancelled this payment." |
---
## Status — Query Payment State
**Input**: `paymentId`.
```bash
onchainos payment a2a-pay status --payment-id <paymentId>
```
Map the returned `status` to a human-readable line:
| status | Meaning | Display |
|---|---|---|
| `pending` | Awaiting buyer signature | "⏳ Awaiting buyer signature." |
| `settling` | Credential received, settling on-chain | "🔄 Settling on-chain (credential submitted, awaiting confirmation)." |
| `completed` | Confirmed on-chain | "✅ Confirmed on-chain. tx_hash: `<tx_hash>` block: `<block_number>` fee: `<fee_decimal> <fee_symbol>`" |
| `failed` | Payment failed | "❌ Failed. (include the server-provided reason if any)" |
| `expired` | Expired before settlement | "⌛ Expired before settlement." |
| `cancelled` | Seller cancelled | "🚫 Cancelled by seller." |
**Rendering the fee**: the CLI returns `fee_amount` as a top-level string in minimal units (and `fee_bps` as the basis-points used). To compute `<fee_decimal>`, look up the token decimals (see Amount Display Rules below). For `<fee_symbol>`, reuse the `--symbol` the seller passed to `create` for the same `paymentId` — the upstream caller (or the seller flow that issued the link) is the source of truth; the `status` response itself does not echo it back. If neither is available, display `fee_amount` minimal units as-is.
**Suggest next**:
- `pending` / `settling` → "Check again in a few moments" or wait briefly and re-run `status`.
- `completed` → recommend `okx-agentic-wallet` to verify post-payment balance delta.
- `failed` → recommend checking buyer balance via `okx-agentic-wallet`, and if `tx_hash` is present, inspect it via `okx-agentic-wallet` (`onchainos security tx-scan`).
---
## Amount Display Rules
Convert `amount` / `fee_amount` per **`../_shared/amount-display.md`**.
**a2a exception (unlisted symbol):** a2a delegates trust upstream, so do **NOT** query `okx-dex-market` and do **NOT** block — use the unknown-decimals fallback (`<atomic> <symbol>` + "double-check") directly.
---
## Edge cases
| Scenario | Handling |
|---|---|
| `onchainos wallet status` reports not logged in | Prompt user to run `onchainos wallet login`. Never attempt to sign without a live session. |
| User provides no `paymentId` | STOP and ask the user for the seller-issued paymentId. |
| CLI reports `payment ... not payable` / expired challenge / unsupported intent | Relay the error verbatim and surface as a **terminal failure** — do NOT retry signing. |
| CLI reports `payment ... rejected (reason=<errorReason>)` (post-signing credential refusal — `insufficient_balance`, etc.) | Relay verbatim and surface as a **terminal failure**. Map common `errorReason` values to user remedies: `insufficient_balance` → top up wallet via `okx-agentic-wallet`; otherwise relay the reason and ask seller for a new link. **Do NOT retry `pay`** — burns a fresh nonce + signature without changing the outcome. |
| `paymentId` not found / 404 from server | Relay the error and ask the user to confirm the paymentId with the seller or upstream caller. |
| `pay` succeeded but status still `pending` / `settling` after 60s poll budget | Return the current status verbatim + paymentId; tell the user `Status is still <status> after 60s; you can run status again later`. |
| Server returns 5xx | Surface status code and any `errorMessage` verbatim. **Do not auto-retry `pay`** — every retry produces a fresh EIP-3009 nonce + signature; let the upstream decide. `status` is read-only and safe to retry manually. |
| `--symbol` is not in the hardcoded decimals table | Apply the unknown-decimals fallback (see Amount Display Rules). Do not block. |
| `--expires-in` was set too short and the link is now past its window | `status` returns `expired`; ask the seller to create a new link. |
---
## CLI Reference
### `onchainos payment a2a-pay create`
```bash
onchainos payment a2a-pay create \
--amount <decimal> --symbol <symbol> --recipient <address> \
[--description <text>] [--realm <domain>] [--expires-in <seconds>]
```
| Param | Required | Default | Description |
|---|---|---|---|
| `--amount` | Yes | - | Decimal token amount (e.g. `"50"` or `"0.01"`) |
| `--symbol` | Yes | - | ERC-20 token symbol (e.g. `"USDT"`) |
| `--recipient` | Yes | - | Seller wallet address (= EIP-3009 `to`) |
| `--description` | No | - | Human-readable description shown to the buyer |
| `--realm` | No | - | Seller / provider domain (e.g. `provider.example.com`) |
| `--expires-in` | No | 1800 | Payment-link expiration window in seconds |
**Return fields**: `payment_id`, `deliveries` (object containing `url` when issued by the server).
### `onchainos payment a2a-pay pay`
```bash
onchainos payment a2a-pay pay --payment-id <id>
```
| Param | Required | Default | Description |
|---|---|---|---|
| `--payment-id` | Yes | - | Seller-issued paymentId |
**Return fields**: `payment_id`, `status`, `tx_hash` (optional), `valid_after`, `valid_before`, `signature`.
### `onchainos payment a2a-pay status`
```bash
onchainos payment a2a-pay status --payment-id <id>
```
| Param | Required | Default | Description |
|---|---|---|---|
| `--payment-id` | Yes | - | The paymentId to query |
**Return fields**: `payment_id`, `status`, `tx_hash` (optional), `block_number` (optional), `block_timestamp` (optional), `fee_amount` (optional, minimal units), `fee_bps` (optional).
## Quickstart
```bash
onchainos payment a2a-pay create --amount 0.01 --symbol USDT --recipient 0xSeller # → { "payment_id": "a2a_xxx", "deliveries": {...} }
onchainos payment a2a-pay pay --payment-id a2a_xxx # buyer signs on-server challenge as-is
onchainos payment a2a-pay status --payment-id a2a_xxx # auto-polled ~60s after pay if non-terminal
```
references/a2mcp-mcp.md
# A2MCP over MCP transport (Streamable HTTP / SSE)
## When this applies
`payment quote` returned `data.mcpTools[]`, OR the endpoint URL ends in `/mcp`/`/sse`, OR the bare
probe returned `Content-Type: text/event-stream` or a JSON-RPC body. The paywall is at the
`tools/call` layer — a bare probe / `tools/list` returns no 402; only a real `tools/call` does.
The CLI does the whole `initialize → tools/list → tools/call` JSON-RPC handshake and SSE parsing
internally. Do NOT hand-write JSON-RPC or parse SSE yourself.
## Three-step flow
1. **Discover** — `onchainos payment quote <url>`
→ returns `data.mcpTools[]` (each `{name, description?, inputSchema?}`); no `paymentId`; free.
2. **Trigger 402** — pick a tool per the user's intent (`AskUserQuestion` if ambiguous), assemble
`--param key=value` from the tool's `inputSchema`, then
`onchainos payment quote <url> --tool <name> --param k=v …`
→ the CLI issues `tools/call`. A paid tool returns 402 → `data.{paymentId,accepts,candidates}`
(identical to a REST quote). A free / first-N-free tool returns `data.result` instead.
3. **Pay** — confirm the amount/scheme (Step A3), then
`onchainos payment pay --payment-id <id> [--selected-index <n>] --yes`
→ the CLI TEE-signs and replays the SAME `tools/call` with a `PAYMENT-SIGNATURE` header, then
parses the SSE response + `PAYMENT-RESPONSE` receipt.
(`--selected-index <n>` picks an `accepts[]` entry when the 402 offered multiple schemes.)
## `--param` coercion (must match the tool's `inputSchema`)
The CLI coerces each `--param` value per `inputSchema.properties[key].type`:
- `integer` / `number` → JSON number • `boolean` → JSON bool • `object` / `array` → parsed JSON
- other type / no schema / parse failure → kept as a string
Example: `zip` is declared `string` → `--param zip=01234` stays `"01234"`; `n` is declared
`integer` → `--param n=5` becomes JSON `5`. The coerced values are persisted in the paymentId state
and replayed verbatim by `payment pay`.
## SSE / tiered billing
The response is a Streamable-HTTP SSE stream (`event: message` / `data:` lines). In-stream
notifications (e.g. `progress`) that arrive before the response are skipped; the first `data:` line
carrying a JSON-RPC `result`/`error` is taken. Tiered billing is supported: `tools/list` is free,
a paid `tools/call` returns 402, and a "first N calls free" tool returns no 402 — that non-402
`tools/call` is a **free result** surfaced as `data.result`.
## Do NOT hallucinate a payment
A bare probe / `tools/list` returning no 402 does **NOT** mean the service is free — on an MCP
endpoint the paywall lives at the `tools/call` layer, so a tool's price only surfaces when you
actually invoke it. Never invent or assume a payment. If a real `tools/call` genuinely returns no
402 (a non-402 `data.result`), report the endpoint as **free / not x402-enabled** and stop — do not
fabricate a `paymentId`, an `accepts[]` challenge, or a signing step that the server never asked for.
## Error tokens (grep-able first word of `.error`)
- `endpoint_unreachable` — `initialize` / `tools/list` / `tools/call` transport failure; or a bare
405 on a non-`mcp|sse` URL with no `--tool` (retry with `--tool <name>` or `--method POST`).
- `invalid_input` — `--tool` names a tool not in the discovered catalog (the message lists the
available tool names).
- `unsupported` — the 402 challenge's `accepts[]` has no known payment scheme.
The destructive `payment pay` confirming gate (exit 2, `{confirming,…}`) and all REST error tokens
are reused unchanged.
## Not in scope
stdio / local MCP transport; paying for MCP `resources` / `prompts`; non-x402 MCP payment schemes;
cross-process MCP session caching (`quote` and `pay` each re-handshake).
references/accepts-schemes.md
# `accepts`-based schemes — `exact` / `aggr_deferred` / `upto` (+ Permit2)
> **CLI down-sink:** the primary path for **every** `accepts`-based 402 —
> single scheme (`exact` / `exact`+Permit2 / `upto` / `aggr_deferred`) or multi-scheme —
> is now `onchainos payment quote <url>` → confirm → `onchainos payment pay
> --payment-id --yes` (SKILL.md Path A). `quote` accepts a single-element `accepts[]`
> exactly like a multi-scheme one (it rejects only an empty array), runs the same
> mandatory confirm gate, and `pay --payment-id` signs + **replays** + returns the
> settled receipt — so a single scheme is **not** a shortcut for skipping `quote`, you
> do **not** load this file on the success path, and you do **not** hand-assemble a
> header. This file is retained only for: post-pay scheme-specific receipt reading, the
> one-time Permit2 approve, the `pay-local` local-key fallback, the explicit `pay
> --payload` sign-only compat, and legacy x402 v1.
> Loaded from `../SKILL.md` **only on a failure / legacy / compat path** — never on the
> primary `quote → pay --payment-id` success path. Load it when: `pay` returns
> `Permit2 allowance insufficient` (one-time approve), you need to interpret a
> scheme-specific settlement result, a legacy x402 v1 raw proof arrives, or you must
> fall back to the explicit `pay --payload` sign-only path because `payment quote` is
> genuinely unavailable.
**Compat sign-only surface (`pay --payload`).** When you must bypass `quote` (explicit legacy request, or `quote` unavailable), all three schemes share one signing surface: `onchainos payment pay --payload '<base64 PAYMENT-REQUIRED>' [--selected-index <n>]` decodes the payload, signs the chosen `accepts` entry via TEE, **assembles the header itself** (embedding `sessionCert` into `accepted.extra` for `aggr_deferred` only — without clobbering `name` / `version`), and returns `{authorization_header, header_name, scheme, wallet}`; you then replay it yourself. You never assemble or merge anything. On the primary Path A flow `pay --payment-id` does this signing **and** the replay for you. The local-key fallback `pay-local` signs **`exact + EIP-3009`, `exact + Permit2`, and `upto`** locally — only `aggr_deferred` is unsupported (it needs a TEE-resident session key).
## Interpreting the settlement result
On the primary Path A flow the settled receipt comes straight from `payment pay --payment-id` (or `payment decode-receipt --header <b64> | --receipt <json>`). On the compat `pay --payload` path, Replay = resend the original request with `<header_name>: <authorization_header>` (here `PAYMENT-SIGNATURE`), expect `HTTP 200`, then decode the `PAYMENT-RESPONSE` header locally (`echo '<value>' | base64 -d | jq .`). Either way, read the result by scheme:
| `scheme` | How to read the result |
|---|---|
| `exact` | Settles immediately. `status` / `transaction` / `amount` / `payer` are final. |
| `aggr_deferred` | `status` may be `pending` — facilitator settles asynchronously, the chain tx appears later. Report as "settling", **not** a failure. |
| `upto` | `amount` is the **actual settled amount (≤ the signed cap)** — report this, not the cap. May be `0` (zero-settle: the request consumed no metered resource; the buyer was **not** charged). |
## `upto` / `exact`+Permit2 — one-time Permit2 approve
`upto`, and `exact` whose chosen entry has `extra.assetTransferMethod = "permit2"`, are Permit2-based (the wire carries `permit2Authorization`). Before the buyer's **first** Permit2 payment with a given ERC-20, the wallet must approve the canonical Permit2 contract (one-time, off-band):
```
PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3 // same on every EVM chain
IERC20(token).approve(PERMIT2_ADDRESS, <amount>)
```
If not yet approved, `payment pay` fails with `Permit2 allowance insufficient on token 0x... for chain ...`. OKX ships a helper binary `mpplab/permit2-approve-calldata` that generates the approve calldata. **Present the choices verbatim — do NOT default to MAX:**
> Permit2 allowance 不足,需要先授权一次:
> - **MAX**(uint256::MAX,一次到位;Permit2 官方合约审计过,业界默认)
> - **数字**(atomic units,本次至少 `<required>`;缓冲多笔可 ≈1000000 = $1;填 0 = 撤销已有授权)
Validation: 数字 < required → reject;数字 > 1e15 → 提示是否手滑想给 MAX;0 → 二次确认是撤销。`feedback_x402_no_confirm` 不覆盖 approve 类持续授权,此处仍需询问。After approve, all future Permit2 payments for that token are off-chain signatures only — retry `onchainos payment pay --payload '<raw>'`.
## Local-key fallback (`pay-local` — `exact + EIP-3009` / `exact + Permit2` / `upto`)
```bash
onchainos payment pay-local --payload '<base64 ...>'
```
Reads `EVM_PRIVATE_KEY` (env var or `~/.onchainos/.env`), derives the payer, generates the nonce, computes the time window from `maxTimeoutSeconds`, and signs locally — no TEE, no JWT. Auto-selects the scheme by the same rules as `payment pay` (`accepts[].scheme` + `accepts[].extra.assetTransferMethod`) and returns the same `{authorization_header, ...}` shape (v2). Output is a standard secp256k1 EIP-712 / EIP-3009 signature — identical wire shape to the TEE path, with **no `sessionCert`** for `upto`. Supports `exact + EIP-3009`, `exact + Permit2`, and `upto`; **rejects `aggr_deferred`** (TEE-resident session key required). Prerequisites: the payer holds enough of the `asset` token on the target chain; for `exact + EIP-3009` the token supports `transferWithAuthorization` and `accepts[].extra.name` (EIP-712 domain name) is present (`version` optional, defaults `"2"`); for `Permit2` / `upto` the one-time Permit2 approve is done (see above), and `upto` additionally requires `accepts[].extra.facilitatorAddress`. ⚠️ Signs with your local key (NOT TEE-protected) — `chmod 600 ~/.onchainos/.env`; the recommended path is always TEE `payment pay`.
## Legacy: x402 v1 (`X-PAYMENT`)
For a v1 payload (body `x402Version: 1`, no `resource` object), `payment pay` returns the **raw proof** `{signature, authorization}` instead of `authorization_header`. Assemble the `X-PAYMENT` header yourself, then replay:
```
paymentPayload = { x402Version: 1, scheme: "<exact|aggr_deferred|upto>", network: <accepts entry network>, payload: { signature, authorization } }
X-PAYMENT: btoa(JSON.stringify(paymentPayload))
```
## CLI Reference
```bash
onchainos payment pay --payload '<base64 of the decoded 402 payload / raw PAYMENT-REQUIRED>' [--selected-index <n>]
onchainos payment pay-local --payload '<base64 ...>' # exact+EIP-3009 / exact+Permit2 / upto (not aggr_deferred)
```
| Param | Required | Description |
|---|---|---|
| `--payload` | Yes | base64 (or base64url) of `{x402Version, resource, accepts}` — the raw `PAYMENT-REQUIRED` header value. CLI decodes, signs, and returns the assembled header (v2). |
| `--selected-index` | No | 0-based index into `accepts[]` pinning the scheme the user chose in a multi-scheme prompt. Omit → CLI auto-selects (`exact` > `aggr_deferred` > first). |
Signs from the currently selected wallet account.
## Edge cases
- **`Permit2 allowance insufficient`** — see one-time approve above, then retry.
- **`upto scheme requires extra.facilitatorAddress`** — the seller's 402 is missing `facilitatorAddress` in `accepts[].extra`; seller-side misconfig — don't retry, tell the user and stop.
- **Replay returns 402 again** — typically a stale signature; re-fetch a fresh 402 → re-sign. Never reuse a stale signature.
- **Wrong proxy in signature (upto)** — facilitator rejects with an `invalid_permit2_spender`-class `invalidReason`; this is a CLI / SDK bug, not user error — surface the message and stop.
- **Network error on replay** — retry once, then prompt the user.
- **TEE signing failure / session expired** — re-login or fall back to `pay-local` (`exact + EIP-3009` / `exact + Permit2` / `upto`, not `aggr_deferred`); ask the user, don't silently cancel.
- **`insufficient_allowance`** (facilitator error code) — same as `Permit2 allowance insufficient`: surface the one-time Permit2 approve prompt and retry.
- **`invalid_eoa_signature`** (facilitator error code) — the `signature` field is not `0x`-prefixed or is not 65 bytes; this is a CLI / SDK bug, not user error — surface the message and stop.
- **`upto_signature_route_conflict`** (facilitator error code) — the request carried both a `sessionCert` and an EOA secp256k1 signature route, an invalid combination; CLI / SDK bug — surface and stop.
- **Unsupported / non-EVM network** — EVM only (CAIP-2 `eip155:<chainId>`); a non-EVM `network` → stop and tell the user the resource is unsupported.
- **No wallet for chain** — the logged-in account needs an address on the target chain; if missing, add it via `okx-agentic-wallet`.
## Security
- TEE path: the secp256k1 key never leaves the enclave; the signature is bound to its fields (`exact`: `(from, to, value, nonce)`; `upto`: also `witness.facilitator`, so a leaked signature is only usable by the named facilitator) — it can't be retargeted or replayed past `deadline`. `sessionCert` (`aggr_deferred`) proves the session key's authority; the CLI embeds it for you.
- Local-key fallback signs entirely on the host — treat `EVM_PRIVATE_KEY` as a credential (`chmod 600`).
- This reference only **signs** — settlement happens on-chain when the recipient / facilitator redeems the authorization.
references/charge.md
# `charge` intent (one-shot)
> **CLI down-sink:** receipt-decode is now `onchainos payment
> decode-receipt (--header <b64> | --receipt <json>)`. Transaction-vs-hash mode
> routing stays with the agent.
> Loaded from `../SKILL.md` when the dispatcher decoded a `WWW-Authenticate: Payment` 402 challenge with `intent="charge"`. Decode + display + wallet-status check have already happened upstream — start here at "Decide mode".
One-shot payment. CLI TEE-signs an EIP-3009 authorization (or wraps a client-broadcast tx hash) and returns a ready `authorization_header`. Optional `methodDetails.splits[]` (max 10 entries) splits the amount across multiple recipients in a single signed authorization.
**TEE-only** — local private key signing is NOT supported on this path. If the wallet session is unavailable and the user can't log in, stop and surface the limitation.
## Decide mode
`methodDetails.feePayer` from the decoded challenge:
- **`true` → transaction mode** (default, server pays gas) → [Sign via TEE](#transaction-mode-sign-via-tee)
- **`false` → hash mode** (user broadcasts the on-chain tx first) → [Hash mode](#hash-mode-broadcast-then-wrap)
## Transaction mode (sign via TEE)
```bash
onchainos payment charge \
--challenge '<full WWW-Authenticate header value>' \
[--from '<0xPayer>']
```
The CLI auto-detects `methodDetails.splits[]` — no extra flag needed. Output:
```json
{ "ok": true, "data": { "authorization_header": "...", "wallet": "0x...", "mode": "transaction", "..." } }
```
Save `data.authorization_header` and proceed to [Replay](#replay).
## Hash mode (broadcast then wrap)
When `feePayer=false`, the user must broadcast `transferWithAuthorization` themselves before the CLI can wrap the credential. Ask:
> The seller isn't paying gas, so you need to send the payment transaction on-chain yourself first, then give me the tx hash. How would you like to send it?
> 1. **Help me send it** — switch to `okx-agentic-wallet` (recommended)
> 2. **I'll send it manually** — paste the tx hash when ready
Option 1: hand off to `okx-agentic-wallet`, return here with the resulting `0x...` hash. Option 2: wait for the user to paste a 66-char `0x...` hash.
Then:
```bash
onchainos payment charge \
--challenge '<full WWW-Authenticate header value>' \
--tx-hash '0x<64-char hex>' \
[--from '<0xPayer>']
```
Output is the same shape as transaction mode, but `mode: "hash"`. Save `authorization_header`.
## Replay
Send `Authorization: <authorization_header>` to the original URL — the value already includes the `Payment ` prefix, do **NOT** add another (`Payment Payment …` is rejected). Expect `HTTP 200` + a `Payment-Receipt` header; decode it locally (`echo '<value>' | base64 -d | jq .`). 关键字段:`status` / `transaction`(on-chain tx hash)/ `chainId`。Charge complete. If a fresh `HTTP 402` returns (stale challenge), re-run the original request to fetch a new `WWW-Authenticate`, then sign again from the top.
## CLI Reference
`onchainos payment charge` — sign or wrap a one-shot charge.
| Param | Required | Default | Description |
|---|---|---|---|
| `--challenge` | Yes | - | Full `WWW-Authenticate: Payment ...` header value from the 402 response |
| `--tx-hash` | Hash mode only | - | 66-char `0x...` tx hash of the user-broadcast `transferWithAuthorization` |
| `--from` | No | selected account | Payer address |
| `--base-url` | No | production | Override backend URL (must be `https://`; `http://` triggers a 301 POST→GET redirect that drops the body and surfaces as `30001 incorrect params`) |
## Reading seller errors
Use **`../SKILL.md` → "Reading seller errors"** (priority order + `❌ Seller rejected: <reason> (code <code>, HTTP <status>)` format).
## Edge cases
| Symptom | Cause | Fix |
|---|---|---|
| `30001 incorrect params` | Wrong base URL or `http://` redirect | Verify `MPP_SA_URL` is `https://...` |
| `--tx-hash` rejected: must be `0x` + 64 hex | Malformed hash | Copy full 66-char hash |
| `chain not found` | Unsupported chainId | `onchainos wallet chains` |
| Challenge expired (`expires` in the past) | Stale challenge | Re-send original request to fetch fresh 402 |
| `feePayer=false` but user has no wallet to broadcast | Hash mode prerequisite missing | Either log in to OKX wallet via `okx-agentic-wallet` or use `okx-agentic-wallet` to broadcast |
references/multi-scheme.md
# Multi-scheme recommendation (SKILL.md Step A3.5)
> **CLI down-sink:** balance-fetch, full-amount sufficiency filtering, tie-break
> scoring, and recommendation-card math now live in the CLI. `onchainos payment
> quote` returns `candidates` / `alternatives` already ranked — present
> `recommended:true`, confirm the selection, then `payment pay --payment-id`.
> This file is retained for the legacy manual A3.5 path only.
Loaded from `SKILL.md` **Step A3.5** when the combined candidate pool contains **2 or more** of `{exact, aggr_deferred, charge}`. Single-candidate flows skip this file and go straight to Step A4. This file owns the full recommend-and-confirm flow and hands the **selected candidate** back to Step A4 / Step A6.
> **🔇 Silence rule for A3.5 internals.** Substeps A3.5.1–A3.5.4 (candidate enumeration, wallet-status check, balance fetch, address/chain-mapping normalization, balance filtering, tie-breaker application) are **internal** — produce **no user-facing narration** during them. The only A3.5 output the user sees is (a) the login prompt in A3.5.2 *if* the wallet isn't logged in, and (b) the recommendation card / alternatives list in A3.5.5. Do **not** announce "I'm checking your balance", "Let me verify the chain mapping", "After filtering, X candidates remain", "Per Rule 2 carve-out…", or any other progress chatter between Step A3 finishing and the recommendation card appearing. Just go silent and emit the card.
>
> **🚫 Exactly one user gate per payment, mandatory.** Per payment, the user sees exactly one confirmation surface: A3.5's recommendation card (when 2+ candidates and the user accepts with `yes`), OR A4's per-payment confirmation card (when there's only 1 candidate, OR when the user picked an alternative from A3.5's expanded list). Do not skip the applicable gate on your own initiative — no "past preference", "streamlining", or "they confirmed once before" shortcuts; those preferences do not exist. Equally, do not duplicate gates: after a `yes` on A3.5.5, do NOT also render A4 with the same info.
## A3.5.1: Build the candidate pool
- Each entry in `accepts[]` → one candidate. Scheme = `accepts[i].scheme` (`exact` or `aggr_deferred`).
- A `WWW-Authenticate: Payment` 402 with `intent="charge"` → one candidate. Scheme = `charge`.
- `WWW-Authenticate: Payment` with `intent="session"` is **never** part of this pool — it's handled by the session-vs-one-shot branch in Step A2.
Each candidate carries `{scheme, chainId, tokenAddress, tokenSymbol, amount (atomic), amountHuman, isMainnet}`. Determine `isMainnet` from the chain registry (`onchainos wallet chains` lists chain metadata).
## A3.5.2: Get wallet balance
- If a recent wallet-balance snapshot already exists in conversation context (from an earlier `onchainos wallet balance` call this session), **reuse it** — do not re-query.
- Otherwise, check login first via `onchainos wallet status`:
- **Not logged in** → ask the user to log in (the recommendation depends on knowing their balance). Don't fall back silently.
- **Logged in** → query balance:
```bash
onchainos wallet balance
```
## A3.5.3: Filter by sufficient balance
Keep only candidates whose `balanceStatus == "sufficient"` for the matching
`(chainId, tokenAddress)`. A positive balance smaller than the required amount
is `insufficient`, not payable/recommendable.
**Edge case — zero candidates pass the filter**: list **all original candidates**
to the user (no recommendation badge, no tie-breakers applied). Each row shows
only `sufficient`, `insufficient — shortfall <shortfall>`, or `unavailable` at
this pre-selection stage. Do not display the deposit address and do not generate
a QR until the user picks one. Carry the selected candidate to Step A4.
## A3.5.4: Tie-breakers (apply in order; stop when one wins)
If more than one candidate remains after A3.5.3:
1. **Smallest required payment amount — same-symbol only.** Group remaining candidates by `tokenSymbol`. If they all share a single symbol, the one with the smallest `amountHuman` wins. If the remaining set spans multiple symbols, skip this rule.
2. **Mainnet over testnet.** Drop testnet candidates if any mainnet candidate remains. Different mainnets are equal — no preference between e.g. Ethereum, Base, X Layer.
3. **Scheme priority:** `aggr_deferred` > `exact` > `charge`.
The survivor is the **recommended candidate**. The rest are **alternatives**.
## A3.5.5: Display the recommendation
**Carve-out scoping** — the recommendation card itself does **NOT** contain a `Scheme:` line, and the "N other methods" summary line does **NOT** preview their schemes / amounts / tokens. Scheme literals appear **only** inside the expanded alternatives list, and only when the user explicitly asks for it. Render the card with `N = number_of_alternatives`:
> We recommend paying via the **OKX Agent Payments Protocol**:
>
> - **Network**: `<chain name>` (`eip155:<chainId>`)
> - **Token**: `<symbol>` (`<token address>`)
> - **Amount**: `<human> (<atomic>)`
> - **Pay to**: `<recipient>`
> - **Balance**: `sufficient` (the CLI only recommends a sufficient candidate)
>
> `<N == 0 ? "No other methods available." : "There are <N> other supported method(s) you could use instead.">` Use the recommended method? (yes / show others)
**⚠️ Do NOT inline alternatives in the summary line.** Forbidden: ❌ "There are 2 other methods (exact 0.001 USD₮0, charge 0.0005 USD₮0)". Required: ✅ "There are 2 other supported methods you could use instead." Detail only appears after the user picks "show others".
- **yes** (or `N == 0`) → the recommended candidate becomes the **selected candidate**; continue at Step A4.
- **show others** → only now expand the alternatives list, each row as `<index>. scheme=<exact | aggr_deferred | charge>, network=<…>, token=<…>, amount=<…>, balance=<sufficient | insufficient — shortfall … | unavailable>`. Do not show `depositAddress` or generate a QR in this list. User picks one by index → that becomes the selected candidate; continue at Step A4. If the selected candidate is insufficient, Step A4 adds the runtime-appropriate QR funding guidance (PNG for `image-notify`, Unicode for `terminal-unicode`) to that same single confirmation card.
## A3.5.6: Carry the selection forward
- **`accepts`-based selection** (`exact` or `aggr_deferred` from `accepts[]`) → remember the **index of the selected accept within `decoded.accepts`**. In Step A6 you pass it as `--selected-index <index>` so the CLI signs exactly that entry and cannot deviate from the user's choice.
- **`charge` selection** (from WWW-Authenticate) → in Step A6, take the WWW-Authenticate / `references/charge.md` path; ignore the accepts-based candidates entirely.
Step A4 (back in `SKILL.md`) now describes the **selected candidate**. Step A5's wallet-status check is already satisfied if A3.5.2 ran the login flow — skip the re-check; just continue to A6.
references/session.md
# `session` intent (channel: open / voucher / topUp / close)
> **CLI down-sink:** voucher reuse-vs-sign, cumulative math, the
> top-up inequality, resign-on-drift, and refund-on-close are now decided by the
> CLI. Run `onchainos payment session <open|voucher|topup|close>` and relay
> `data.{strategy, needsTopUp, cumulative_amount, refund, recovery, reason_text}`
> — don't recompute them. NL→command routing for each op stays here.
> Loaded from `../SKILL.md` when the dispatcher decoded a `WWW-Authenticate: Payment` 402 challenge with `intent="session"`. Decode + display + wallet-status check have already happened upstream — start here at "Phase S1: Open Channel".
>
> **Also enter this reference for any mid-session operation** (close / topUp / settle / voucher / refund) when the user mentions an existing `channel_id`, even without a fresh 402. Jump directly to the matching phase below.
State machine: **open → N vouchers → close**, optional **topUp** between vouchers. The seller drives transitions via fresh 402 challenges (or the user issues a close).
**TEE-only** — local private key signing is NOT supported on this path. If the wallet session is unavailable and the user can't log in, stop.
> **🔑 Action-first, URL-stays-the-same** — When a user asks for ANY
> mid-session operation ("open / 开通道", "buy a translation", "top up /
> 充值", "close / 关闭"), the action lives in the credential
> `payload.action`, NOT in the URL path. The URL is **always the
> original business URL** — the same one the user asked to access.
>
> | User intent (any language) | `payload.action` | CLI command |
> |---|---|---|
> | open / 开通道 / start session | `open` | `payment session open` |
> | buy / call / 调用 / use service | `voucher` | `payment session voucher` |
> | top up / 充值 / add deposit | `topUp` | `payment session topup` |
> | close / 关闭 / end session / settle | `close` | `payment session close` |
>
> All four flows share ONE URL and ONE pattern:
> 1. Re-issue the **original business URL** with no `Authorization` →
> seller responds `402 + WWW-Authenticate: Payment ... intent="session"`.
> 2. Pick the right CLI command above and pass the WWW-Authenticate as
> `--challenge`. The CLI sets `payload.action` for you.
> 3. Resend to the **same original business URL** with
> `Authorization: <authorization_header>`.
>
> **`<authorization_header>` already includes the `Payment ` scheme
> prefix** — paste the CLI's `data.authorization_header` value verbatim
> into the `Authorization` header. **Do NOT** prepend another `Payment `
> yourself; that would produce `Payment Payment <b64>` and the seller
> will reject it.
>
> **Never probe** for `/open`, `/voucher`, `/topup`, `/close`,
> `/<resource>/topup`, etc. — they don't exist. If you can't think of
> a URL, the answer is always "the original business URL the user
> asked about".
## Talk to users in plain language
Match the user's language. Use action-verb phrasing — "issue a voucher / 签发凭证", "top up your balance / 补充余额", "close the channel / 关闭通道", "your prepaid balance / 通道余额" — don't dump bare jargon (`voucher`, `topUp`, `close`, `escrow`, `cumulativeAmount`) on the user. Field names are fine in **state echo** since the user copy-pastes those across sessions.
## Session state to track
Save the moment `payment session open` returns and maintain across phases:
| Field | Source |
|---|---|
| `channel_id` | `payment session open` output |
| `escrow` | open challenge `methodDetails.escrowContract` |
| `chain_id` | open challenge `methodDetails.chainId` |
| `currency` | open challenge `currency` |
| `payer_addr` | open output `wallet` |
| `current_cum` | highest signed cum so far (open `--initial-cum` or last issued voucher's cum) |
| `current_sig` | last voucher signature (`signature` field of open / voucher / close output) |
| `estimated_spent` | sum of `unit_amount` across served business requests since the last fresh sign |
| `unit_amount` | latest voucher challenge `amount` (seller is authoritative) |
| `deposit` | open output `deposit` + topup `--additional-deposit` |
Track in conversation context. Across conversations, ask the user to re-supply `channel_id` / `escrow` / `current_cum` / `current_sig` to continue.
**Mandatory state echo** — after `payment session open`, after each voucher (sign or reuse), after topup, and immediately before close, end your message with one line:
> 📋 Channel `<channel_id>` · chain `<chain_id>` · escrow `<escrow>` · deposit `<human(deposit)>` (`<deposit>`) · cum `<human(current_cum)>` (`<current_cum>`) · spent~`<human(estimated_spent)>` (`<estimated_spent>`) · sig `<current_sig prefix...>`
**All user-facing amounts in BOTH human and atomic form** — `<human> (<atomic>)`; see `../_shared/amount-display.md` for the decimals table + fallback.
---
## Phase S1: Open Channel
First step of any session. Decide the **deposit** with the user:
> A session payment needs you to lock a prepaid balance up front (held in escrow). How much would you like to prepay?
> Suggested: `<human(suggestedDeposit)> (<suggestedDeposit>)` (or `unit_amount × 100` if no suggestion — enough for ~100 requests).
> Each request draws from this balance. You can add more later, or close the channel anytime to refund whatever's unused.
Wait for the user's amount.
### Optional initial-voucher prepay
Opening a channel signs a baseline voucher with `cumulativeAmount=0` by default. To override:
- `--initial-cum N` — explicit baseline (atomic units).
- `--prepay-first` — use the unit price from `challenge.amount` (silently falls back to 0 if missing/`"0"`).
Pick from user intent: no preference → no flag; "pay first request immediately" → `--prepay-first`; "pre-authorize N" → `--initial-cum N`. Constraint: `initial_cum ≤ deposit` (SDK rejects with `70012`).
### Mode branch
Branch by `methodDetails.feePayer`.
**Transaction mode (`feePayer=true`)**:
```bash
onchainos payment session open \
--challenge '<full WWW-Authenticate header value>' \
--deposit '<atomic units>' \
[--initial-cum '<atomic>' | --prepay-first] \
[--from '<0xPayer>']
```
CLI TEE-signs EIP-3009 `receiveWithAuthorization` (deposit into escrow) + EIP-712 baseline Voucher (channelId, cum=initial_cum). Output: `data.{authorization_header, channel_id, escrow, chain_id, deposit, wallet}` — save all to session state. Initial `current_cum` = the initial-cum value (default `"0"`).
**Hash mode (`feePayer=false`)** — user must send the on-chain "open channel" tx themselves first (delegate to `okx-agentic-wallet` or manual). Then:
```bash
onchainos payment session open \
--challenge '<full WWW-Authenticate header value>' \
--deposit '<atomic units>' \
--tx-hash '0x<64-char hex>' \
--salt '0x<64-char hex>' \
[--initial-cum '<atomic>' | --prepay-first] \
[--from '<0xPayer>']
```
`--salt` MUST be the same bytes32 the user passed to the on-chain `escrow.open(...)` call. The CLI recomputes `channelId = keccak256(abi.encode(payer, payee, token, salt, authorizedSigner, escrow, chainId))` and the seller compares it to what the on-chain event emitted — supply a fresh random salt and the open is rejected with a channelId mismatch. If the user broadcast through `okx-agentic-wallet`, the salt is the bytes32 they (or you) passed into the gateway's contract-call arguments.
CLI still TEE-signs the initial voucher; only the deposit tx is replaced by the supplied hash.
### Send open to seller
```
<original method> <original url>
Authorization: <authorization_header>
```
Outcomes:
- **HTTP 200** — channel open, response carries the first business result. Echo state. Subsequent requests to the same resource: send without `Authorization` first; seller responds with a voucher 402 → Phase S2.
- **HTTP 402 (fresh `WWW-Authenticate: Payment`)** — channel opened but seller wants the first voucher signed. Go straight to Phase S2.
---
## Phase S2: Business Request (Voucher Loop)
Run for **each** business request during the session.
**Enter triggers** when `channel_id` is active: user says "next request" / "again" / "another one" / "再调一次" / "再发一个" / "继续" / "voucher" / "凭证" / "签一个授权"; or user requests the resource again and gets a fresh 402.
### How vouchers actually work
A voucher is a **cumulative authorization**, not a single-request payment. Once signed, the seller keeps deducting until `spent` reaches the signed `cumulativeAmount`. So one voucher with `cum=50` funds 50× `unit_amount=1` requests **without re-signing** — provided the seller supports reuse (mppx / OKX TS Session / OKX Rust SDK ≥ this version). Legacy OKX Rust SDK treats byte-replay as idempotent retry and skips the deduct; force re-sign every request if you suspect this.
Per-request job: pick **reuse** vs **sign** based on remaining balance.
### S2.1: Send the request
If you don't have a fresh challenge yet, send the business request. Seller responds with HTTP 402 + fresh `WWW-Authenticate: Payment` — this is a **voucher challenge** for the new request. Decode `request` to extract `amount` (the seller-quoted unit price).
### S2.2: Decide reuse vs sign
```
unit_amount = <amount from this voucher challenge> // seller is authoritative
remaining = current_cum - estimated_spent // headroom under existing voucher
if current_sig is set AND remaining >= unit_amount:
strategy = REUSE # spend remaining headroom under existing voucher
cum_for_this_call = current_cum # unchanged
else:
strategy = SIGN # need a higher cum
cum_for_this_call = current_cum + unit_amount
# Hard guards (apply regardless of strategy)
if cum_for_this_call > deposit:
→ Phase S2b (TopUp) first, then re-evaluate
if methodDetails.minVoucherDelta is set AND strategy == SIGN:
ensure (cum_for_this_call - current_cum) >= minVoucherDelta
```
`unit_amount` always comes from the **current** voucher challenge, never a cached value — the seller can adjust pricing between requests and the latest 402 wins.
### S2.3a: Reuse path (no TEE)
```bash
onchainos payment session voucher \
--challenge '<fresh WWW-Authenticate from this 402>' \
--channel-id '<saved channel_id>' \
--cumulative-amount '<current_cum>' \
--reuse-signature '<saved current_sig>' \
[--from '<saved payer_addr>']
```
Don't pass `--escrow` / `--chain-id` here — the existing signature already binds them. CLI skips TEE and wraps the existing signature bytes verbatim. `mode = "reuse"`.
### S2.3b: Sign path (TEE)
```bash
onchainos payment session voucher \
--challenge '<fresh WWW-Authenticate from this 402>' \
--channel-id '<saved channel_id>' \
--cumulative-amount '<cum_for_this_call>' \
--escrow '<saved escrow>' \
--chain-id '<saved chain_id>' \
[--from '<saved payer_addr>']
```
CLI signs an EIP-712 Voucher(channelId, cum_for_this_call) via TEE. `mode = "sign"`. Both paths return `data.{authorization_header, channel_id, cumulative_amount, signature, mode}`.
### S2.4: Replay the business request
```
<original method> <original url>
Authorization: <authorization_header>
```
**Non-empty Step A3-Params plan?** Also attach its params on their carriers (query / body / header / path), using the plan's `input.method` if it differs from the original. The `Authorization` voucher header rides alongside. (Channel ops — open / topup / close — carry their action in the credential payload, not business params, so they're unaffected.)
Expected: `HTTP 200`. **Update state**: `current_cum = cum_for_this_call`, `current_sig = <signature>`, `estimated_spent += unit_amount`. (Reuse path: `current_cum` / `current_sig` unchanged; only `estimated_spent` advances.)
### S2.5: Insufficient-balance fallback
When the seller rejects a voucher with `reason: "insufficient balance"`, `detail: "voucher exhausted"`, or OKX Rust SDK private code `70015`, `estimated_spent` drifted. Recover:
1. Surface the seller's reason: `❌ Seller rejected: insufficient balance — your current authorization is fully used. Signing a new one to continue.`
2. Set `estimated_spent = current_cum` (treat existing voucher as exhausted).
3. Re-enter S2.2 — `remaining = 0`, **SIGN** is picked.
4. Sign a new voucher with `cum = current_cum + unit_amount` and retry.
**Do NOT loop reuse-on-insufficient-balance** — always escalate to SIGN.
Other rejections: `amount_exceeds_deposit` → topup (S2b); `delta_too_small` → raise cum; `invalid_signature` → check seller logs. Always surface the seller's reason text first, code in parens second.
### S2.6: Loop
Repeat S2.1–S2.4 for each request. Same voucher funds many calls while `remaining ≥ unit_amount`; re-sign only when balance runs out.
> Voucher rejections come from **seller-SDK local validation**, not a backend round-trip (the `700xx` codes are in Troubleshooting below).
---
## Phase S2b (Optional): TopUp Mid-Session
Triggered when `current_cum + unit_amount > deposit` (seller refuses with `70012` or pre-emptively sends a topUp challenge).
Ask the user:
> Your prepaid balance is running low. How much would you like to add (atomic units)?
> Current balance: `<human(deposit)> (<deposit>)` · Used so far: `<human(current_cum)> (<current_cum>)`
Branch by `methodDetails.feePayer` from the topUp challenge.
**Transaction mode**:
```bash
onchainos payment session topup \
--challenge '<WWW-Authenticate for topUp>' \
--channel-id '<saved channel_id>' \
--additional-deposit '<atomic units>' \
--escrow '<saved escrow>' \
--chain-id '<saved chain_id>' \
--currency '<saved currency>' \
[--from '<saved payer_addr>']
```
CLI TEE-signs `receiveWithAuthorization`. EIP-3009 nonce is `keccak256(abi.encode(channelId, additionalDeposit, from, topUpSalt))` — must match the on-chain contract.
**Hash mode** (user broadcasts top-up tx first, then):
```bash
onchainos payment session topup \
--challenge '<WWW-Authenticate for topUp>' \
--channel-id '<saved channel_id>' \
--additional-deposit '<atomic units>' \
--escrow '<saved escrow>' \
--chain-id '<saved chain_id>' \
--tx-hash '0x<64-char hex>' \
[--from '<saved payer_addr>']
```
`--currency` is optional in hash mode (CLI doesn't sign EIP-3009; the on-chain tx already covers it).
**After TopUp**: `deposit = deposit + additional_deposit`. Resume Phase S2.
---
## Phase S3: Close Channel
When the user is done — says "close the channel / 关闭通道 / end the session", or after the final request. **Always close** when done; otherwise the prepaid balance stays escrowed until the seller's timeout (typically 12–24h).
### S3.1: Decide final cumulativeAmount
`final_cum = current_cum` — the highest voucher cum sent in this session. **Don't add `unit_amount`** — close reuses the last voucher's cum (no new service is delivered).
### S3.2: Sign close voucher
```bash
onchainos payment session close \
--challenge '<WWW-Authenticate for close, or fresh 402 if seller issues one>' \
--channel-id '<saved channel_id>' \
--cumulative-amount '<final_cum>' \
--escrow '<saved escrow>' \
--chain-id '<saved chain_id>' \
[--from '<saved payer_addr>']
```
CLI signs an EIP-712 Voucher(channelId, final_cum) via TEE — same signing path as a regular voucher, used at close time. Output: `data.{authorization_header, channel_id, cumulative_amount}`.
### S3.3: Send close to seller
```
<original method> <original url> # typically a dedicated close endpoint, e.g. /session/manage
Authorization: <authorization_header>
```
Seller settles on-chain (transfers `final_cum` to merchant, refunds the rest to payer) and returns a `Payment-Receipt` header. Decode it locally (`echo '<value>' | base64 -d | jq .`) — 关键字段:`status` / `transaction`(on-chain tx hash,S3.4 报给用户用)/ `chainId`。
**Clear session state** — channel is closed.
### S3.4: Confirm to user
> ✅ Channel closed. Charged `<human(final_cum)> (<final_cum>)` of your `<human(deposit)> (<deposit>)` prepaid balance. Refund of `<human(deposit - final_cum)> (<deposit - final_cum>)` returned to your wallet.
> On-chain tx: `<reference from response>`
---
## Reading seller errors
Use **`../SKILL.md` → "Reading seller errors"** (priority order + `❌ Seller rejected: <reason> (code <code>, HTTP <status>)` format).
## Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| `not logged in` / `session expired` | Wallet session missing or expired | `onchainos wallet login` |
| Voucher rejected: `70012 amount_exceeds_deposit` | cum > channel deposit | Phase S2b TopUp first |
| Voucher rejected: `70000 invalid_params` (cum not strictly increasing) | new_cum ≤ current_cum | Increase strictly; ensure you're tracking current_cum |
| Voucher rejected: `70013 voucher_delta_too_small` | Delta below `minVoucherDelta` | Raise cum by at least the minimum |
| Voucher rejected: `InsufficientBalance` (HTTP 402; OKX Rust SDK `70015`) | seller's spent + new_amount > highest voucher | S2.5 fallback |
| Open fails: `chain not found` | Unsupported chainId or chain entry missing | `onchainos wallet chains` to list supported chains |
| `--tx-hash` rejected: must be `0x` + 64 hex chars | Malformed hash | Copy full 66-char hash (with `0x` prefix) |
| Session 402 keeps repeating after voucher sent | channel_id / escrow / chain_id mismatch | Re-check saved session state; all three must match the open |
| `30001 incorrect params` | Wrong base URL / `http://` redirect | Verify backend URL is `https://...` |
| `70004 invalid signature` | EIP-3009 typename mismatch / wrong domain | Check seller logs; usually means CLI is older than spec |
| `70008 channel finalized` | Channel was already closed on-chain | Session is done; do not retry close |
| `70010 channel not found` | Wrong channel_id, or seller has no record | Verify channel_id against open response |
| Seller returns ETIMEOUT or hangs | SA backend down or slow | Wait + retry; SDK has 30s timeout |
references/subscription.md
# x402 `period` subscription (a.k.a. `permit2_subscription`) — buyer side
> Loaded from `../SKILL.md` **only when** a 402 `accepts[]` entry has `scheme == "period"`, or the user asks to manage an existing subscription (access / change / cancel). Skip on one-shot `exact` / `aggr_deferred` / `upto` offers.
The `period` scheme is recurring (subscription) billing. The buyer **subscribes once** — signing a Permit2 `PermitSingle` (delegating a bounded allowance to the subscription contract) plus a `SubscriptionTerms` EIP-712 authorization — and thereafter serves the protected resource by attaching a lightweight `APP-Access` proof header **without re-paying**. The buyer self-manages the lifecycle: upgrade/downgrade (`change`), teardown (`cancel`), revoke a scheduled downgrade (`cancel-pending`), and inspect state (`my-subscriptions`, `allowance-status`). **Every command signs or reads only — none broadcasts a transaction or moves funds** (on-chain execution is seller/facilitator-side, bounded by the Permit2 `permit.amount` / `permit.expiration` the buyer signs). All commands print the standard `{ "ok": …, "data"/"error": … }` JSON envelope; exit `0` (success) / `1` (error) only — there is no `confirming` gate.
## Decide operation
| Situation | Command |
|---|---|
| New `period` 402 offer, no active sub for this host | `payment subscription subscribe --accepts '<json>' --url <url>` |
| Resource already has an active sub (check `my-subscriptions` first) | `payment subscription access --url <url>` — **never re-subscribe** |
| Change-offer 402 (`extra.changeFrom`), upgrade/downgrade | `payment subscription access` (proof) → proof-carrying probe → `payment subscription change --accepts '<json>' --sub-id <cur>` (see `change` — never probe naked) |
| Cancel an active sub | `payment subscription cancel --sub-id <s> --contract <c>` |
| Revoke a scheduled (not-yet-effective) downgrade | `payment subscription cancel-pending --sub-id <s> --new-sub-id <n> --contract <c>` |
| Inspect state / reconcile cache | `payment subscription my-subscriptions` · `payment subscription allowance-status --token <t>` |
## Pre-flight: allowance check (before subscribe / change)
`subscribe` and `change` read `allowance-status` **immediately before signing** (to obtain a fresh `nonce` / `reservedAmount` / `permit2Allowance`; the CLI does this for you). The signed Permit2 must satisfy `permit.amount ≥ reservedAmount + this-subscription-total-commitment`, and `permit.expiration` must cover the whole service window (`fixed_seconds`: `startAt + maxPeriods × periodSec`; `calendar_month`: `addMonths(effective_start, maxPeriods)`). If `allowance-status` shows an insufficient `permit2Allowance` (first payment for that token, or a grown window), the command errors with `allowance_expired` guidance — run the **one-time `ERC20 → Permit2 approve`** via the existing approve flow (NOT a subscription verb), then retry.
## Interpreting the result
| Command | How to read the result |
|---|---|
| subscribe / change | replay `.data.paymentHeaderValue` under header `.data.paymentHeaderName` (`PAYMENT-SIGNATURE`); persist `.data.subId` (the CLI also caches host→subId) |
| subscribe / change | on `… contract mismatch` in `.error`, treat as a hard security abort (exit 1) — the seller-declared contract did not match the authoritative `allowance-status`; do not retry or force |
| access | replay `.data.accessHeaderValue` under `APP-Access`; `.data.source` (`cache` \| `override`) tells you whether the subId came from the local cache or `--sub-id` |
| cancel / cancel-pending | relay the `.data.cancelAuth` / `.data.pendingChangeCancelAuth` object to the seller; the sub stays active/billable until the contract executes — the local cache is NOT flipped to canceled |
| my-subscriptions | `.data.subscriptions[]` — each item's `state` is `0` pending / `1` active / `2` completed / `3` canceled / `4` changed / `99` failed; the local cache is reconciled from this authoritative state |
| allowance-status | all 10 fields (`approvedAmount`, `reservedAmount`, `permit2Allowance`, `subscriptionContract`, …) always present |
## CLI Reference
```bash
onchainos payment subscription subscribe --accepts '<json>' [--from <addr>] [--url <url>]
onchainos payment subscription access --url <url> [--sub-id <id>] [--from <addr>] [--chain <name|index>]
onchainos payment subscription change --accepts '<json>' --sub-id <id> [--from <addr>] [--url <url>]
onchainos payment subscription cancel --sub-id <id> [--contract <addr>] [--token <addr>] [--chain <name|index>] [--from <addr>]
onchainos payment subscription cancel-pending --sub-id <id> --new-sub-id <id> [--contract <addr>] [--token <addr>] [--chain <name|index>] [--from <addr>]
onchainos payment subscription my-subscriptions [--chain <name|index>] [--from <addr>] [--limit <n>] [--offset <n>]
onchainos payment subscription allowance-status --token <addr> [--chain <name|index>] [--from <addr>]
```
### `subscribe`
| Param | Required | Description |
|---|---|---|
| `--accepts` | yes | the 402 `accepts` array or single object (JSON); must contain a `period` entry |
| `--from` | no | payer address; default = selected account |
| `--url` | no | protected-resource URL; used as `resource.url` and the cache-key host |
### `access`
| Param | Required | Description |
|---|---|---|
| `--url` | yes | protected-resource URL; its host is the cache key |
| `--sub-id` | no | override the active subId (skips the cache); `source` becomes `override` |
| `--from` | no | payer address; default = selected account |
| `--chain` | no | chain name or index (default `xlayer` / `196`); resolved via `chains::resolve_chain` |
### `change`
| Param | Required | Description |
|---|---|---|
| `--accepts` | yes | change-offer accepts (JSON) carrying `extra.changeFrom` |
| `--sub-id` | yes | the subId being changed (from `my-subscriptions`, matched by host); overrides `extra.changeFrom.fromSubId` |
| `--from` | no | payer address; default = selected account |
| `--url` | no | cache key for the resulting subscription |
**Pre-flight probe (upgrade/downgrade) — carry an `APP-Access` proof, do NOT probe naked.** The change endpoint only returns a full change-offer with `extra.changeFrom` (the `direction` + `fromSubId` of the current subscription) when the probing request proves ownership of the current subscription. A **naked probe** (no proof) returns an offer **missing** `extra.changeFrom`, which fails `change` signing (`change` requires `--accepts` to carry `extra.changeFrom`) and forces a wasteful re-probe with the proof — one extra LLM round-trip + one extra CLI call. Always probe with the proof attached from the start:
1. `payment subscription access --url <change endpoint>` — generates the current subscription's `APP-Access` proof (replay `.data.accessHeaderValue` under the `APP-Access` header).
2. Probe the change endpoint **with that `APP-Access` header attached** → the 402 returns the change-offer carrying `extra.changeFrom` (matching `direction` / `fromSubId`) in a single probe.
3. `payment subscription change --accepts '<that offer>' --sub-id <cur>` → sign.
Required sequence: `access` (proof) → one proof-carrying probe of the change endpoint → `change`. Never do `naked probe → missing changeFrom → re-probe`.
### `cancel`
| Param | Required | Description |
|---|---|---|
| `--sub-id` | yes | the subscription to cancel |
| `--contract` | no | subscription-contract EIP-712 verifying-domain address |
| `--token` | no | used to look up `--contract` via `allowance-status` when `--contract` omitted |
| `--chain` | no | chain name or index (default `xlayer` / `196`) |
| `--from` | no | payer address; default = selected account |
### `cancel-pending`
| Param | Required | Description |
|---|---|---|
| `--sub-id` | yes | the active subscription |
| `--new-sub-id` | yes | the PENDING downgrade's `newSubId` (from `my-subscriptions` → `pendingPlanChange.newSubId`); signed into the auth and must equal the on-chain pending value |
| `--contract` | no | verifying contract (as in `cancel`) |
| `--token` | no | used to look up `--contract` when omitted |
| `--chain` | no | chain name or index (default `xlayer` / `196`) |
| `--from` | no | payer address; default = selected account |
### `my-subscriptions`
| Param | Required | Description |
|---|---|---|
| `--chain` | no | chain name or index (default `xlayer` / `196`) |
| `--from` | no | buyer address; default = selected account |
| `--limit` | no | page size (default `50`) |
| `--offset` | no | page offset (default `0`) |
### `allowance-status`
| Param | Required | Description |
|---|---|---|
| `--token` | yes | token contract address |
| `--chain` | no | chain name or index (default `xlayer` / `196`) |
| `--from` | no | buyer address; default = selected account |
## Edge cases
- `access` with no cached sub + no `--sub-id` → the error names the host; run `my-subscriptions` to reconcile the cache, or pass `--sub-id`.
- `permit2Allowance` insufficient / `allowance_expired` → do the one-time `ERC20 → Permit2 approve` via the existing approve flow, then retry.
- `subscription contract mismatch` / `permit2 contract mismatch` → **fail-closed security stop, not a transient error.** Before signing, `subscribe`/`change` cross-check the seller's `extra.contracts.subscription` / `extra.contracts.permit2` against the authoritative `allowance-status` values; on any mismatch — or a missing authoritative value — the command emits `{"ok": false, "error": "… contract mismatch: …"}` and exits `1` **before** any signature or approve. This is intentional (a tampered contract address). Do **NOT** retry, re-probe, or attempt to force it — abort and surface the mismatch to the user. There is no `--force` bypass (it is never a `confirming` gate).
- `fixed_seconds` needs `periodSec > 0`; `calendar_month` needs `periodSec == 0` — an inconsistency errors out.
- `cancel` does NOT stop billing locally — the sub stays active until the contract executes; `my-subscriptions` reconcile corrects the local cache later.
- `cancel-pending` requires `--new-sub-id`, which must equal the on-chain pending `newSubId`.
- `period` is EVM-only (Permit2 / EIP-712 / EIP-191); Solana (`501`) and other non-EVM chains are out of scope for this scheme.
## Security
- **TEE-only signing** — signatures are always produced by the logged-in wallet's TEE path; no plaintext key/mnemonic ever appears in code, logs, or output. The CLI never accepts a private key or a hand-crafted signature.
- **Contract addresses verified against a trusted root** — before signing, `subscribe`/`change` cross-check the seller's `extra.contracts.subscription` / `extra.contracts.permit2` against the authoritative buyer-direct `allowance-status` (`subscriptionContract` / `permit2Contract`). Comparison is EVM checksum-insensitive; an empty/absent authoritative value fails closed (reject). On a match, the **authoritative** values are used as the Permit2 `spender`, the EIP-712 `verifyingContract`s, and the Layer-1 `approve` target — so a signed subscription is always anchored to the authoritative source, never an unverified seller declaration. (`cancel`/`cancel-pending` resolve the subscription contract from `allowance-status` when called with `--token`, or use the caller-supplied `--contract` verbatim (no cross-check) — acceptable because a `CancelAuth` / `PendingChangeCancelAuth` carries no transfer authority.)
- **Bounded commitment** — the financial exposure is capped by the signed Permit2 `permit.amount` / `permit.expiration`; the pre-sign allowance check enforces the bound.
- **Never re-subscribe** an already-active resource — `access` or `change` it instead.
SKILL.md
---
name: okx-agent-payments-protocol
description: "For agent payments and paid endpoints via x402, MPP, payment links, a2a-pay, and HTTP-payment recurring or metered billing. Use it for HTTP 402/payment-required; paid Agent or A2MCP endpoints; x402/Permit2; MPP channels, vouchers, or sessions; HTTP-payment subscriptions; or paymentId/link operations or status. Trigger phrases: x402/x402Version, X-PAYMENT, PAYMENT-REQUIRED, PAYMENT-SIGNATURE, WWW-Authenticate: Payment, x402 exact/exact+Permit2/upto/aggr_deferred, MPP charge/session, channelId/channel_id, payment-channel voucher/topup/settle/refund, metered billing, paymentId, a2a_, payment link, A2MCP, paid endpoint, and HTTP 402 period/permit2_subscription."
license: MIT
metadata:
author: okx
version: "4.5.3"
homepage: "https://web3.okx.com"
---
# OKX Agent Payments Protocol (Dispatcher)
> **⚠️ READ FIRST — ZERO-TEXT-ON-TRIGGER + NEVER-SKIP-USER-GATES.**
>
> Between detecting a 402 (or any trigger word) and emitting the first user-facing card — the Step A3.5 recommendation card, or the Step A4 confirmation card — output **ZERO** user-visible text. No "received 402", no "triggered OKX Agent Payments Protocol", no "detected N schemes", no enumeration of schemes / networks / tokens / amounts, no "loading skill" — in any language (the same prohibition applies to the equivalent phrases in any other language). The skill-load tool call may run but emits no surrounding prose.
>
> Exactly **one** confirmation card runs per payment: A3.5's recommendation card (2+ candidates and user picks `yes`) OR A4's confirmation card (single candidate, OR user picked an alternative from A3.5's expanded list). Do NOT skip the applicable card under the pretext of "past user preference" / "streamlining" / "already confirmed once" — those preferences do not exist. Do NOT render both cards back-to-back with the same info — after `yes` on A3.5.5, go straight to Step A5. The next user-visible text after detection MUST be one of the two cards.
Three payment paths, distinguished by HTTP signature: **`accepts`-based 402** (challenge in body for v1 or `PAYMENT-REQUIRED` header for v2), **`WWW-Authenticate: Payment` 402** (channel-capable, `intent="charge"` or `"session"`), and **a2a-pay** (paymentId-based, no 402). Shared steps below (detect → decode → confirm → wallet check), then dispatch to a reference.
> **User-facing terminology — IMPORTANT**
>
> **Rule 1 — Always call it "OKX Agent Payments Protocol", and always render it bolded.** Use the exact English term **OKX Agent Payments Protocol** in user-visible messages regardless of the user's language, and always wrap it in markdown bold (`**OKX Agent Payments Protocol**`) so the user sees it emphasized. Keep it as a fixed English noun phrase even inside otherwise-Chinese sentences. Reserve protocol literals and internal identifiers for CLI invocations, HTTP headers, JSON payloads, and code — never speak them to the user.
>
> **Rule 2 — Do not narrate internal protocol detection.** The dispatch logic (which header was detected, which reference is being loaded, which scheme/intent was selected, TEE vs local-key path) is internal — keep it internal. The user only needs to see: (a) what is being paid, (b) what they need to confirm, (c) the result.
>
> **Rule 2 carve-out — narrow, alternatives list only.** Inside Step A3.5, the literals `exact` / `aggr_deferred` / `charge` may be exposed to the user **only** in the expanded **alternatives list** (the list rendered after the user picks "show others"), because at that point the user is explicitly choosing between schemes. They MUST NOT appear in: the default recommendation card, the "N other methods" summary line, status narration, error displays, post-payment summaries, or anywhere else. The recommendation card shows network / token / amount / recipient only — never the scheme name.
>
> **Rule 3 — Externally-defined protocol literals stay byte-for-byte exact.** The JSON field `x402Version`, the HTTP headers `X-PAYMENT` / `PAYMENT-SIGNATURE` / `PAYMENT-REQUIRED` / `WWW-Authenticate: Payment`, and the reference URL `https://x402.org` MUST appear verbatim wherever the protocol/server requires them — these are externally defined and changing them breaks interop. CLI subcommand names (`onchainos payment pay` / `pay-local` / `charge` / `session ...` / `a2a-pay ...`) are this CLI's own surface and may evolve; refer to them by their current name in CLI invocations and code, but never speak them to the user (Rule 2).
>
> **Example**
>
> (EN) `Preparing a payment via the **OKX Agent Payments Protocol**. Here are the charge details — please confirm before I proceed…`
> When narrating in another language, translate this lead line but keep **OKX Agent Payments Protocol** as a bolded English noun phrase.
> **Progress narration counts as user-visible — Rules 1-3 still apply.**
>
> Long-running flows (decode → confirm → wallet check → sign → replay) tempt status updates. Every progress line ("I'm now…", or its Chinese equivalent) is user-facing; Step labels and reference/scheme names are internal — do NOT echo them. The anchors:
>
> | ❌ Don't say | ✅ Say |
> |---|---|
> | "Detected HTTP 402, triggering OKX Agent Payments Protocol" / "Detected `PAYMENT-REQUIRED`, loading `exact`" | _(silent — detection / routing is internal)_ |
> | "CLI selected `exact`, assembling the `PAYMENT-SIGNATURE` header" / "taking the TEE path" | "Signing done, replaying the request" |
> | "Detected 2 schemes: exact (USD₮0), aggr_deferred (USDG)" / "checking balance to filter candidates" | _(silent — enumeration + balance check are internal; only the recommendation card is user-visible)_ |
> | "Entering session / charge mode" | "Channel opened" — describe the user-visible effect, not the internal mode |
> | "Per past preference, paying without re-confirming" | _(forbidden — no such preference; the gate is mandatory every time)_ |
>
> The same rules apply when narrating in any other language — match the intent of these ❌/✅ phrasings, not just the English wording.
>
> **These rules are authoritative and always in force** — when unsure whether a status line leaks internals, match it against the rows above and default to silence.
## Triggers (full list)
- **EN**: `402`, payment required, `x402`, `x402Version`, `X-PAYMENT`, `PAYMENT-REQUIRED`, `PAYMENT-SIGNATURE`, `WWW-Authenticate: Payment`, `permit2`, `upto`, metered billing, open / close / topup / settle channel, voucher, session payment, `channelId`, `channel_id`, `paymentId`, `a2a_`, create payment link, payment link, payment status
- subscribe / subscription / recurring payment / recurring charge / "pay every month" / cancel subscription / upgrade plan / downgrade plan → `period` scheme (see `references/subscription.md`)
- ⚠️ **EXCEPT** when the message contains jobId / subId / ASP / provider / trial / renew / deliver / periodCount / subscription task — those are Agent Commerce subscription tasks (monthly service agreements), route to `okx-ai` instead.
- The same trigger vocabulary applies to its equivalents in any other language (e.g. Chinese subscription / recurring-billing terms route to the `period` scheme the same way).
- Carve-out: AI-service/ASP subscriptions from the agent marketplace (context: ASP / Agent#N / 任务 / 试用期 / 服务方; NO 402 offer / resource URL / paymentId) belong to okx-ai (onchainos agent my-subscriptions / subscribe-detail), NOT the period scheme. For a bare "my subscriptions / 我的订阅" with neither signal, ask the user once instead of assuming period.
Any close / topup / settle / voucher / refund near a `channel_id` or session context = MPP mid-session op → `references/session.md`.
## Pre-flight Checks
At the start of each thread, complete the checks in `../okx-agentic-wallet/_shared/preflight.md`. If missing, read `_shared/preflight.md`.
## Command Routing & Reference map
Each 402 signal (or paymentId) → CLI command → reference. Detailed gating + decode/confirm steps are in Path A / Path B below.
| Signal | Command | Reference |
|---|---|---|
| 402 + `PAYMENT-REQUIRED` (v2) / body `x402Version` (v1) — one **or many** `accepts[]` schemes (`exact` / `exact`+Permit2 / `upto` / `aggr_deferred`) | **Primary — Path A:** `payment quote <url>` → confirm → `payment pay --payment-id --yes`. Single-scheme and multi-scheme take the **same** quote flow (the CLI decodes, converts, balance-checks, signs, **replays**, and returns the receipt). Even if you already curled the raw 402, re-enter via `payment quote <url>` — never assemble a header by hand and never jump straight to sign-only. **Compat only:** `payment pay --payload [--selected-index]` (sign-only + manual replay) when `quote` is unavailable. | Success path loads **no** reference. `references/accepts-schemes.md` only for: post-pay scheme-specific receipt reading, `Permit2 allowance insufficient` one-time approve, `pay-local`, the `pay --payload` compat path, or legacy x402 v1 (the CLI-output field tells you which scheme — `permit2Authorization` = `upto` / `exact`+Permit2, `sessionCert` = `aggr_deferred`, `authorization` = `exact`) |
| 402 offer with an `accepts[]` entry whose `scheme == "period"` (a.k.a. `permit2_subscription`) — recurring/subscription billing | `payment subscription subscribe/access/change/cancel/cancel-pending/my-subscriptions/allowance-status` | `references/subscription.md` |
| 402 + `WWW-Authenticate: Payment`, `intent="charge"` | `payment charge --challenge` | `references/charge.md` |
| 402 + `WWW-Authenticate: Payment`, `intent="session"` (or mid-session `channel_id`) | `payment session open/voucher/topup/close` | `references/session.md` |
| paymentId / `a2a_…` link / create-or-check payment link | `payment a2a-pay create/pay/status` | `references/a2a_charge.md` |
| A2MCP / 402 endpoint URL, "pay this endpoint", entry A/B payment node | `payment quote <url> [--param k=v ...] [--method GET \| POST \| ...]` | (inline — Path A) |
| A2MCP **MCP-transport** endpoint (URL ends `/mcp` or `/sse`, returns `text/event-stream` / JSON-RPC, or you have a tool name) | `payment quote <url>` (discovery → `mcpTools[]`) → `payment quote <url> --tool <name> --param k=v` (trigger 402) → `payment pay --payment-id <id> --yes` | `references/a2mcp-mcp.md` |
| User confirmed the quoted payment (currency/amount/scheme chosen) | `payment pay --payment-id <id> [--selected-index <n>] --yes` | (inline — Path A) |
| Need to decode a `PAYMENT-RESPONSE` header or a charge receipt | `payment decode-receipt (--header <b64> \| --receipt <json>)` | (inline — read-only) |
> **Don't load a reference on the success path.** On the primary Path A flow, `onchainos payment pay --payment-id --yes` signs, replays, and returns the settled receipt directly — skip `references/accepts-schemes.md` entirely (this holds for a single `accepts[]` scheme exactly as for multi-scheme). On the compat `pay --payload` path the CLI returns an `authorization_header` you replay yourself — same rule, no reference on success. Load `references/accepts-schemes.md` only on a **failure / legacy** path: `Permit2 allowance insufficient` → `references/accepts-schemes.md` (one-time approve), or a legacy x402 v1 raw proof → its "Legacy: x402 v1" section. `charge` / `session` / `a2a_charge` are always loaded — those are multi-phase flows.
> **Channel mid-session ops** (close / topup / settle / voucher / refund mentioned with an active `channel_id`, regardless of fresh 402) → stay here, jump straight into `references/session.md` at the matching phase. **Do NOT** search for a separate `close-channel` / `topup-channel` / `settle-channel` tool — they're all `onchainos payment session ...` subcommands.
---
# Path A: HTTP 402
## Path A (accepts-based): quote → confirm → pay — PREFERRED 2-round flow
**For an `accepts`-based 402 / A2MCP endpoint, the CLI does all mechanical work.
You do exactly two reasoning rounds.** (For `WWW-Authenticate: Payment` charge /
session challenges, skip this and use the protocol-detection steps below.)
### Step A1 — Extract params (round 1)
From the user prompt (Entry A) or the task payment node (Entry B), extract the
endpoint `url` and any known business params. Do NOT curl, decode, or convert
anything yourself.
### Step A2 — Quote
Run: `onchainos payment quote <url> [--param key=value ...] [--method GET|POST|...]`
The CLI probes the endpoint, parses the 402, checks your wallet balance, ranks
candidates, and writes a `paymentId`.
> **Probe method** — the CLI probes with `GET` by default. When the service
> declaration or the user's intent says the endpoint's initial call is **not GET**
> (e.g. the Bazaar `outputSchema.method` / business mind-map declares `"POST"`, or
> the user says "POST this endpoint"), pass `--method POST` (or the correct verb).
> Known business params then ride in the JSON **body** instead of the query string.
> Probing a POST-only A2MCP endpoint with the default GET can return 405 / a non-402
> response → `endpoint_unreachable` instead of the payment challenge. (The paid replay
> still uses `outputSchema.method` regardless — this flag only fixes the initial probe.)
> **MCP-transport A2MCP (`tools/call`-gated).** If `payment quote` returns `data.mcpTools[]`
> (the endpoint is MCP-type: URL ends `/mcp`|`/sse`, or replied `text/event-stream` / JSON-RPC),
> the paywall is at the tool-invocation layer, not the bare URL. **Read `references/a2mcp-mcp.md`**
> and follow it: pick a tool from `mcpTools[]` per the user's intent (use `AskUserQuestion` if
> ambiguous), assemble `--param key=value` from the tool's `inputSchema`, and re-run
> `payment quote <url> --tool <name> --param …` to trigger the 402 and land a `paymentId`. Then
> resume the normal Step A3 confirm → Step A4 `payment pay --payment-id <id> --yes`. Do NOT
> hand-write JSON-RPC or parse SSE — the CLI does the `initialize → tools/list → tools/call`
> handshake and SSE parsing internally.
Read `data`:
- `summary` — the human one-liner. `needsConfirm` is always true here.
- `candidates[]` (with `recommended:true`) and `alternatives[]` — the ranked schemes. Each carries `acceptsIndex` — its position in `accepts[]` (the ranked order differs from `accepts[]`, so never treat a candidate's list position as the index).
- Every candidate also carries `balanceStatus` (`sufficient` / `insufficient` / `unavailable`), `availableAmount`, `requiredAmount`, and `shortfall`. Before the user selects a method, display only the status and (when insufficient) the shortfall alongside each method — **do not display `depositAddress`, generate a QR, or send a funding notice yet**.
- `missingParams[]` + `merchantBody` — params the CLI could not fill; find the rest in `merchantBody`.
- `walletError` — if `login_required`, tell the user to log in, then re-quote.
- `recommended:null` on every candidate ⇒ no balance anywhere; present the list and ask.
### Step A3 — Confirm (round 2) ⚠ MANDATORY — never skip
Confirm the **full** payment terms — the same set Step A4 shows, so the buyer
always sees where the money goes before signing. Use `AskUserQuestion` for a
sufficient candidate; the insufficient-candidate funding card described below
replaces it as the one confirmation surface:
- **Network**: `chainName` (`chainId`) of the chosen candidate
- **Token / amount**: `amountHuman` `tokenSymbol` (for the `upto` scheme this is an
authorization cap — render it as "up to `amountHuman`", not a fixed charge)
- **Scheme**: the chosen candidate's `scheme`
- **Pay to**: the challenge `recipient` (the `payTo` address)
- any `missingParams`
If the selected candidate is `insufficient`, use one funding-first card:
1. Run `onchainos agent funding-notice --chain <chainName> --currency <tokenSymbol> --available <availableAmount> --required <requiredAmount> --shortfall <shortfall> --deposit-address <depositAddress> --deposit-chain <chainName> --reason payment-402 --format json`.
2. Localize `contentCanonical`, preserving balance, address, four funding
options, and one gas line only: X Layer = **platform-paid gas; no OKB or other
native token required**; other chains = generic gas warning.
3. Follow `displayMode`: `image-notify` → call `user-notify --image-path` once;
`terminal-unicode` → include `terminalQr`. QR failure → full text + address.
Offer `funded`, `cancel`, and—only if available—`choose another payment method`;
never advertise `pay anyway` or ask generic `yes/no`. Route: `funded` → re-quote;
alternative → show other sufficient entries from `candidates[] + alternatives[]`
(re-quote first if expired), then confirm once using `acceptsIndex`; `cancel` → stop. An unsolicited, unambiguous
request to pay despite the shortfall is that single authorization; ambiguous
`yes` is not. Do not run `funding-check` or add a hard balance gate.
Pass the chosen candidate's **`acceptsIndex`** as `--selected-index`
(NOT its position in `candidates[]`/`alternatives[]`) so the CLI signs exactly the
entry the user approved. **You MUST stop and confirm before paying — do not auto-pay.**
### Step A4 — Pay
Run: `onchainos payment pay --payment-id <id> --selected-index <n> --yes [--param key=value ...]`
`--yes` is required (the fund-moving confirming gate). `pay` signs the quoted payload,
replays, and returns the receipt — it never re-fetches the 402. Read `data.status`:
- `success` → report `txHash`; (Entry B) the task system marks the node paid.
- `failed` → surface `data.error`; offer retry.
- `pending` → poll / await terminal, then continue.
> To decode a returned `PAYMENT-RESPONSE` header or a charge receipt at any time,
> run `onchainos payment decode-receipt (--header <b64> | --receipt <json>)`.
---
## Step A1: Start from the original response (legacy / WWW-Authenticate detail)
> **⚠️ `accepts`-based 402 → go back to Path A `payment quote`.** The steps below are the **legacy manual path** (decode → assemble → replay yourself) plus the shared decode detail for `WWW-Authenticate: Payment` charge / session challenges. If the 402 you hold is **`accepts`-based** (`PAYMENT-REQUIRED` header v2 / `x402Version` body v1 — `exact` / `exact`+Permit2 / `upto` / `aggr_deferred`, whether a **single** scheme or many), do **not** continue here: discard your raw 402 and re-enter at **Path A** with `payment quote <url>`. The quote flow runs the same mandatory confirm gate and returns the same receipt schema for single- and multi-scheme alike — a single scheme is not a shortcut for skipping `quote`. Continue below **only** for the `WWW-Authenticate: Payment` charge / session detail, or when `payment quote` is genuinely unavailable and you must fall back to the explicit `pay --payload` sign-only compat path.
You already have the original HTTP response. If it is **not 402**, return the body directly. Otherwise → Step A2.
**Capture any request parameters the user's prompt supplies** (e.g. "weather in San Francisco" → `city=San Francisco`, `token=0x…`; "translate to Chinese" → `lang=zh`). Record each as `name → value` for the Step A3-Params plan — values given here are **never re-asked**, just shown in the confirmation card. Keep them even if the first request didn't need them; the seller may require them on the paid replay.
## Step A2: Detect the protocol
```
Priority 1: response.headers['WWW-Authenticate']
starts with "Payment " → continue at Step A3-WWW-Authenticate
Priority 2: response.headers['PAYMENT-REQUIRED']
base64-encoded JSON → continue at Step A3-Accepts (v2)
Priority 3: response body JSON has "x402Version"
→ continue at Step A3-Accepts (v1)
Otherwise → not a supported payment protocol, stop
```
**Both indicators present** — branch on the WWW-Authenticate intent:
- `intent="session"` offered alongside `accepts`-based options → STOP and ask the user:
> The server offers two payment styles via the **OKX Agent Payments Protocol**:
> 1. **Session (multi-request)** — open a channel and issue vouchers per request
> 2. **One-shot purchase**
>
> Which would you like to use?
Option 1 → continue at Step A3-WWW-Authenticate (session path). Option 2 → drop the session intent and continue at Step A3-Accepts with the accepts options.
- `intent="charge"` offered alongside `accepts`-based options → all options are one-shot; **do not** show the session-vs-one-shot prompt. Decode both protocol families (Step A3-Accepts AND Step A3-WWW-Authenticate), merge the candidates, and let Step A3.5 handle the recommendation.
## Step A3-Accepts: Decode
Decode the 402 payload **yourself** for **display + recommendation only** — no CLI round-trip:
```
raw_402 = response.headers['PAYMENT-REQUIRED'] // v2 (base64-encoded JSON)
or response.body // v1 (already plain JSON)
decoded = JSON.parse(atob(raw_402)) // v2; for v1 it's already JSON: JSON.parse(response.body)
```
Extract for display:
```
accepts = decoded.accepts
option = decoded.accepts[0] // for display only
```
**Keep `raw_402` verbatim** — Step A6 passes it straight to `onchainos payment pay --payload` (the CLI re-decodes and signs). The local decode is display-only; never re-encode or assemble anything.
## Step A3-WWW-Authenticate: Decode
Parse the WWW-Authenticate header:
```
Payment id="...", realm="...", method="evm", intent="...", request="<base64url>", expires="..."
```
base64url-decode `request` to get the JSON body. Save:
```
intent charge | session
amount base units string (e.g. "1000000")
currency ERC-20 contract address
recipient merchant payee address
methodDetails:
chainId EVM chain ID (e.g. 196 for X Layer)
escrowContract REQUIRED for session, ABSENT for charge
feePayer true (transaction mode) | false (hash mode)
splits optional, charge only, max 10 entries
minVoucherDelta optional, session only
channelId optional, session topUp/voucher only — pre-existing channel
suggestedDeposit optional, session only — suggested initial deposit
unitType optional — "request" | "second" | "byte" etc.
```
**Method check** — only `method="evm"` is supported here. If `method` is `"tempo"`, `"svm"`, `"stripe"`, etc. → stop and tell the user this dispatcher cannot handle it.
**Challenge expiry** — if `expires=...` (ISO-8601) is in the past, the challenge is dead: re-send the original request to get a fresh 402 before signing. Stale challenges fail with `30001 incorrect params`.
Convert `amount` from base units to human-readable (see `_shared/amount-display.md`).
## Step A3-Params: Build the request-parameter plan
> **Runs after Step A3 decode, before any confirmation card.** Beyond payment terms, the seller may declare which parameters the **paid replay** must carry and how. Build a **param plan** so the user confirms params alongside payment and the replay attaches them correctly.
A param plan is a list of `{ name, value, carrier, required, source }`, `carrier ∈ {query, body, header, path}`. No seller-declared params and none named by the user → **empty plan**; replay unchanged.
### Source 1 — Bazaar `outputSchema.input` (preferred)
If the decoded 402 (or any `accepts[i]`) carries `outputSchema.input`, parse it:
| Field | Use |
|---|---|
| `input.type` | `"http"` → handle here. `"mcp"` → out of scope, skip param assembly. |
| `input.method` | Method to replay with (may differ from the original). `GET`/`HEAD`/`DELETE` → params go in **query**; `POST`/`PUT`/`PATCH` → in **body** (`input.bodyType`: `json`/`form-data`/`text`). |
| `input.queryParams` / `input.body` / `input.pathParams` / `input.headers` | Params for that carrier (query / body / path / header). |
The JSON Schema `properties` + `required` give each param's type and whether it's mandatory. One plan entry per declared param.
### Source 2 — non-Bazaar (conservative)
No `outputSchema.input` → add a param **only** on an explicit seller signal; **never invent one**:
- response **body** lists requirements (`required` / `params` / `parameters` / `fields` / `inputSchema`), OR
- an **error message** names a missing param (e.g. `missing required query param "city"`), OR
- a documented response **header** asks for one.
Ambiguous → add nothing, replay unchanged.
### Fill values
Per entry, resolve `value`: (1) user's prompt (Step A1) → `source=prompt`, don't re-ask; (2) conversation context → `source=context`; (3) still missing **and required** → ask the user, one grouped question for all of them (a legitimate gate, not narration — ZERO-TEXT-ON-TRIGGER doesn't forbid it). Optional + unresolved → drop.
## Step A3.5: Multi-scheme recommendation (when applicable)
**Applies only when** the combined candidate pool contains **2 or more** of `{exact, aggr_deferred, charge, period}`. Otherwise skip straight to Step A4 with the single available candidate.
> When the 402 `accepts[]` contains 2 or more of `{exact, aggr_deferred, charge, period}`, load `references/multi-scheme.md`. Treat `period` as the recurring-billing option: recommend it only when the user intent is an ongoing subscription, not a single call.
When it applies → **load `references/multi-scheme.md`** and follow it end to end. It returns the **selected candidate** and tells you where to resume: Step A4 (user picked an alternative) or straight to Step A6 (user accepted with `yes` — A5's wallet check already satisfied).
## Step A4: Display payment details and STOP
**🟢 Skip this step entirely if** the user accepted the recommendation in A3.5.5 with `yes` (the card already showed network / token / amount / recipient). Go straight to Step A5 (a no-op if A3.5.2 already handled login) → A6.
**🔴 Run this step normally if** either:
- Step A3.5 did not run (single-candidate path), OR
- The user picked an alternative from A3.5's expanded list (the picked candidate still needs full-detail confirmation).
**⚠️ MANDATORY (when run): Display details and STOP to wait for explicit user confirmation. Do NOT call `onchainos wallet status` or any other tool until the user confirms.**
For a quote-flow candidate, also show its `balanceStatus`; when insufficient,
show `availableAmount`, `requiredAmount`, and `shortfall`, then follow Step A3's
single funding-first card and action table.
For **`accepts`-based 402** (`PAYMENT-REQUIRED` header v2 / `x402Version` body v1):
> This resource requires payment via the **OKX Agent Payments Protocol**:
> - **Network**: `<chain name>` (`<option.network>`)
> - **Token**: `<token symbol>` (`<option.asset>`)
> - **Amount**: `<human-readable amount>` (from `option.amount` for v2, or `option.maxAmountRequired` for v1; convert from minimal units using token decimals). For the `upto` scheme this amount is an authorization **cap**, not a fixed charge — render it as "up to `<amount>`" / "最多 `<amount>`".
> - **Pay to**: `<option.payTo>`
> - **Request parameters** (omit this line entirely if the Step A3-Params plan is empty): one row per param as `<name> = <value>` → `<carrier: query | body | header | path>`
>
> Proceed with payment? (yes / no)
For **`WWW-Authenticate: Payment` 402**:
> This resource requires payment via the **OKX Agent Payments Protocol**:
> - **Payment type**: `<one-shot payment | session (multiple requests)>` (render as "one-shot payment" / "session (multiple requests)" — never "single purchase"; keep the same distinction when translating to another language)
> - **Network**: `<chain name>` (`eip155:<chainId>`)
> - **Token**: `<symbol>` (`<currency address>`)
> - **Amount per request**: `<human-readable>` (atomic: `<amount>`)
> - **Pay to**: `<recipient>`
> - **Who pays gas**: `<server (transaction mode) | you broadcast it yourself (hash mode)>`
> - **Split recipients** (one-shot only, if present): `<N other parties also receive a share>`
> - **Suggested prepaid balance** (session only, if present): `<human-readable>`
> - **Request parameters** (omit this line entirely if the Step A3-Params plan is empty): one row per param as `<name> = <value>` → `<carrier: query | body | header | path>`
>
> Proceed with payment? (yes / no)
- **User confirms** → Step A5.
- **User declines** → stop. No payment, no wallet check.
## Step A5: Check wallet status (only after the user explicitly confirms)
```bash
onchainos wallet status
```
- **Logged in** → Step A6.
- **Not logged in (`accepts`-based path)** → ask the user to choose between (1) wallet login (TEE signing) or (2) local private key (`onchainos payment pay-local`, supports `exact + EIP-3009`, `exact + Permit2`, and `upto` — `aggr_deferred` not supported, requires TEE session key). Don't read files or check env vars until the user picks.
- **Not logged in (`WWW-Authenticate: Payment` path)** → ask the user to log in via `onchainos wallet login`. **TEE-only — no local-key fallback for this path** (only the `accepts`-based path has one).
## Step A6: Hand off to the scheme/intent reference
| Path | Action |
|---|---|
| **`accepts`-based** (`PAYMENT-REQUIRED` header v2 / `x402Version` body v1) | **Primary — Path A:** you should already be on the `payment quote <url>` → confirm → `payment pay --payment-id --yes` flow (top of Path A); it signs, replays, and returns the receipt — no hand-assembly, and no reference load on success. This is identical for a single `accepts[]` scheme and for multi-scheme.<br>**Compat / fallback only** (`quote` unavailable, or an explicit legacy request): run `onchainos payment pay --payload '<raw_402 from Step A3>'`. If Step A3.5 ran and the user picked an accepts-based candidate, add `--selected-index <index in decoded.accepts>` so the CLI signs exactly that entry; omit it for a single candidate (CLI auto-selects). The CLI decodes, signs from the selected account, and returns `{authorization_header, header_name, scheme, wallet}` — **no hand-assembly**; then go to Replay below.<br>If the user picked the local-key fallback, run `onchainos payment pay-local --payload '<raw_402>'` instead (same success rule; supports `exact + EIP-3009`, `exact + Permit2`, and `upto` — `aggr_deferred` is TEE-only).<br>**`Permit2 allowance insufficient` error** (`upto` / `exact`+permit2, first payment for that token) → load **`references/accepts-schemes.md`** for the one-time approve, then retry the pay.<br>**Legacy v1** — CLI returns a raw proof (`signature`+`authorization`, no `authorization_header`) → load **`references/accepts-schemes.md`** and follow its "Legacy: x402 v1" section to assemble the `X-PAYMENT` header. |
| `period` (subscription / `permit2_subscription`) | Load **`references/subscription.md`** at "Decide operation" (subscribe vs access vs change vs cancel). First-time offer → `payment subscription subscribe`; already-active resource → `payment subscription access` (never re-subscribe); upgrade/downgrade → `change`; teardown → `cancel` / `cancel-pending`. |
| **`WWW-Authenticate: Payment`, `intent="charge"`** | Load **`references/charge.md`** at "Decide mode". |
| **`WWW-Authenticate: Payment`, `intent="session"`** | Load **`references/session.md`** at "Phase S1: Open Channel" (or jump to S2 / S2b / S3 if the user is mid-session with an active `channel_id`). |
**Replay (success path — no reference needed):** resend the original request with the returned header (`<header_name>: <authorization_header>`, or the `X-PAYMENT` you assembled for legacy v1), expect `HTTP 200`, and decode any `PAYMENT-RESPONSE` header locally (`echo '<value>' | base64 -d | jq .`) to read `status` / `transaction` / `amount` / `payer`. Surface the settlement details to the user; suggest follow-ups conversationally — never expose internal field names or skill IDs.
---
# Path B: a2a-pay (paymentId-based, no 402)
The user invokes this path explicitly — by mentioning a `paymentId` / `a2a_...` link, asking to "create a payment link", or asking to check a2a payment status.
## Step B1: Identify the role
| User says… | Load | Role |
|---|---|---|
| "create payment link" / "generate payment" / `--amount`/`--recipient` | `references/a2a_charge.md` → "Seller — Create" | Seller |
| Provides a `paymentId` / `a2a_...` to pay | `references/a2a_charge.md` → "Buyer — Pay" | Buyer |
| Provides a `paymentId` and asks for status | `references/a2a_charge.md` → "Status — Query" | Either |
If the user says only "I want to pay" without a paymentId — STOP and ask the user to provide the seller-issued paymentId. Do not attempt anything else.
## Step B2: Wallet status
Both `create` and `pay` require a live wallet session. Run `onchainos wallet status`:
- **Logged in** → proceed (load the reference and follow it).
- **Not logged in** → ask the user to log in via `onchainos wallet login`. **Do NOT sign without a live session.**
## Step B3: Hand off to `references/a2a_charge.md`
The reference has the full create/pay/status flow (incl. auto-poll and the trust-delegation note). Buyer-side trust is delegated upstream — the buyer signs whatever the on-server challenge declares.
---
# Cross-cutting
## Reading seller errors (`WWW-Authenticate: Payment` / a2a-pay)
When the seller rejects, do NOT show raw JSON or just the numeric code. Extract the human-readable explanation in priority order, use the first non-empty match:
1. `body.reason` (mppx, OKX TS Session)
2. `body.detail` (RFC 9457 ProblemDetails)
3. `body.message`
4. `body.msg` (OKX SA API)
5. `body.error`
6. `body.title` (RFC 9457 short title — fallback only)
7. fallthrough — format the whole body and add the HTTP status
Format:
> ❌ Seller rejected: `<reason text>` (code `<code if present>`, HTTP `<status>`)
## Amount display
All user-facing amounts in BOTH human and atomic form: `<human> (<atomic>)`, e.g. `0.0004 USDC (400)`. Decimals table + unknown-symbol fallback → `_shared/amount-display.md`.
## Suggest next steps
After a successful payment + response, suggest conversationally:
| Just completed | Suggest |
|---|---|
| `payment quote` returned `needsConfirm:true` | Confirm once: use `AskUserQuestion` when sufficient, or the adaptive QR-enriched confirmation (`image-notify` PNG / `terminal-unicode`) when insufficient; then `payment pay --payment-id <id> --selected-index <n> --yes` |
| `payment quote` returned `data.mcpTools[]` (MCP-transport, no `paymentId`) | pick a tool per the user's intent, then `payment quote <url> --tool <name> --param k=v …` to trigger the 402 (see `references/a2mcp-mcp.md`) |
| `payment pay` returned `status:"success"` | Report `txHash`; if a `PAYMENT-RESPONSE` header is present, `payment decode-receipt --header <b64>` |
| `payment pay` returned `status:"pending"` | `payment a2a-pay status --payment-id <id> --wait` (a2a) or await the facilitator callback |
| Successful HTTP 402 replay | Check balance impact via `okx-agentic-wallet`; or make another request to the same resource |
| Successful a2a payment | Verify post-payment balance via `okx-agentic-wallet` |
| 402 on replay (expired) | Retry with a fresh signature |
| Channel session in progress | Issue another voucher when the next request arrives; close the channel when done |