
Okx Agent Payments Protocol
- 8k installs
- 315 repo stars
- Updated July 27, 2026
- okx/onchainos-skills
okx-agent-payments-protocol guides agents through OKX Agent Payments Protocol for HTTP 402, x402, session channels, and a2a-pay via onchainos CLI.
About
The OKX Agent Payments Protocol skill dispatches agent payments when APIs return HTTP 402 or when users mention x402, PAYMENT-REQUIRED, WWW-Authenticate Payment, channelId, paymentId, or a2a links. Three paths cover accepts-based 402, WWW-Authenticate charge or session intents, and a2a-pay without 402. Between trigger detection and the first confirmation card, agents emit zero user-visible text about internal routing. User-facing copy always bolds OKX Agent Payments Protocol and hides scheme names except in expanded alternative lists. Shared steps decode payloads, show charge details, check wallet status with onchainos wallet status, sign via onchainos payment pay or session subcommands, and replay requests with returned authorization headers. Mandatory confirmation gates run every payment with no streamlining exceptions. References load for charge, session, accepts-schemes failures, multi-scheme choice, and a2a create or pay flows. Amounts display in human and atomic form.
- Zero user-visible text between 402 detection and the mandatory confirmation card.
- Dispatches x402 accepts, WWW-Authenticate charge or session, and a2a-pay paymentId paths.
- Always bold OKX Agent Payments Protocol in user messages; hide internal scheme names elsewhere.
- onchainos payment pay signs and returns authorization_header for direct replay on success.
- Mandatory user confirmation every payment; no skipping gates for past preferences.
Okx Agent Payments Protocol by the numbers
- 7,951 all-time installs (skills.sh)
- +1,566 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #5 of 483 Web3 & Blockchain skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
okx-agent-payments-protocol capabilities & compatibility
- Capabilities
- 402 protocol detection and payload decode · multi scheme recommendation and user confirmatio · onchainos wallet status and payment signing · session channel open topup close and voucher ops · a2a pay create pay and status flows
- Use cases
- orchestration · api development
npx skills add https://github.com/okx/onchainos-skills --skill okx-agent-payments-protocolAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8k |
|---|---|
| repo stars | ★ 315 |
| Security audit | 1 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | okx/onchainos-skills ↗ |
How do I pay for an HTTP 402 resource or handle x402, session channels, or a2a paymentId without leaking internal protocol details?
Handle HTTP 402 payment-required, x402, MPP session channels, and a2a-pay paymentId flows via onchainos CLI with mandatory user confirmation gates.
Who is it for?
Developers using onchainos to automate agent micropayments, session channels, and a2a-pay flows.
Skip if: Skip when you need manual wallet UI only without HTTP 402 or onchainos CLI automation.
When should I use this skill?
User hits HTTP 402, mentions x402, PAYMENT-REQUIRED, channelId, paymentId, a2a link, or metered billing.
What you get
User-confirmed payment with signed authorization replayed on the original request and settlement details surfaced.
- Signed payment authorization header
- Replayed HTTP 200 response
- User-visible settlement summary
By the numbers
- 3 payment paths: accepts 402 WWW-Authenticate a2a-pay
- Mandatory confirmation card every payment
- Zero internal narration before confirmation
Files
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", no "正在处理 / 触发 / 检测到 / 收到". 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 picksyes) 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 — afteryeson 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 literalsexact/aggr_deferred/chargemay 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 fieldx402Version, the HTTP headersX-PAYMENT/PAYMENT-SIGNATURE/PAYMENT-REQUIRED/WWW-Authenticate: Payment, and the reference URLhttps://x402.orgMUST 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
>
(中) 准备通过 **OKX Agent Payments Protocol** 完成本次支付,下面是扣款明细,请确认……(EN) Preparing a payment via the **OKX Agent Payments Protocol**. Here are the charge details — please confirm before I proceed…Progress narration counts as user-visible — Rules 1-3 still apply.
>
Long-running flows (decode → confirm → wallet check → sign → replay) tempt status updates. Every"正在…"/"I'm now…"line is user-facing; Step labels and reference/scheme names are internal — do NOT echo them. The anchors:
>
| ❌ Don't say | ✅ Say |
|---|---|
| "收到 HTTP 402,触发 OKX Agent Payments Protocol" / "DetectedPAYMENT-REQUIRED, loadingexact" | _(silent — detection / routing is internal)_ |
| "CLI 选了exact,组装PAYMENT-SIGNATURE头" / "走 TEE 路径" | "签名完成,正在重放请求" / "Signing done, replaying" |
| "检测到 2 个 scheme:exact (USD₮0)、aggr_deferred (USDG)" / "正在查余额筛选候选" | _(silent — enumeration + balance check are internal; only the recommendation card is user-visible)_ |
| "进入 session / charge 模式" / "Entering session intent" | "支付通道已开" / "Channel opened" — describe the user-visible effect, not the internal mode |
| "按之前的偏好,直接付不再确认" / "Per past preference, skipping confirmation" | _(forbidden — no such preference; the gate is mandatory every time)_ |
>
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 + 中文)
- 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 - 中文:按量计费、支付上限、支付通道、关闭/充值/续费/结算通道、关闭会话、结算会话、凭证、会话支付、付款链接、创建支付、支付状态
Any close / topup / settle / voucher / refund near a channel_id or session context = MPP mid-session op → references/session.md.
Pre-flight Checks
Read ../okx-agentic-wallet/_shared/preflight.md (fallback: _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) | payment pay --payload [--selected-index] | Success (v2): none — replay the returned authorization_header directly (Step A6). On error / legacy v1, load references/accepts-schemes.md (covers exact / aggr_deferred / upto + Permit2; the CLI-output field tells you which scheme — permit2Authorization = upto / exact+Permit2, sessionCert = aggr_deferred, authorization = exact) |
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 |
Don't load a reference on the success path. Whenonchainos payment payreturns anauthorization_header(x402 v2 — the normalexact/aggr_deferred/uptooutcome), replay directly per Step A6 and skipreferences/accepts-schemes.mdentirely. Load it 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_chargeare always loaded — those are multi-phase flows.
Channel mid-session ops (close / topup / settle / voucher / refund mentioned with an activechannel_id, regardless of fresh 402) → stay here, jump straight intoreferences/session.mdat the matching phase. Do NOT search for a separateclose-channel/topup-channel/settle-channeltool — they're allonchainos payment session ...subcommands.
---
Path A: HTTP 402
Step A1: Start from the original response
You already have the original HTTP response. If it is not 402, return the body directly. Otherwise → Step A2.
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, stopBoth indicators present — branch on the WWW-Authenticate intent:
intent="session"offered alongsideaccepts-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 alongsideaccepts-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 onlyKeep `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.5: Multi-scheme recommendation (when applicable)
Applies only when the combined candidate pool contains 2 or more of {exact, aggr_deferred, charge}. Otherwise skip straight to Step A4 with the single available candidate.
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 `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>(fromoption.amountfor v2, oroption.maxAmountRequiredfor v1; convert from minimal units using token decimals)
- Pay to: <option.payTo>>
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 in Chinese as单次支付/会话支付(多请求)— NEVER单次购买)
- 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>>
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)
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,exactscheme only). 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 email OTP or AK. 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) | 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.<br>Success (normal path) — authorization_header present → go straight to Replay below; do NOT load any scheme reference.<br>If the user picked the local-key fallback, run onchainos payment pay-local --payload '<raw_402>' instead (same success rule; exact 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. |
| `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 loginoronchainos wallet login <email>. 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 |
|---|---|
| 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 |
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-token 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 Pre-flight Checks
This file is shared across all onchainos skills. Follow these steps, in order, before the first onchainos command each session. Do not echo routine command output to the user; give a brief status update only when installing, updating, or handling a failure.1. Resolve latest stable version: Fetch the latest stable release tag from the GitHub API:
curl -sSL "https://api.github.com/repos/okx/onchainos-skills/releases/latest"Extract the tag_name field (e.g., v1.0.5) into LATEST_TAG. If the API call fails and onchainos is already installed locally, skip steps 2-3 and continue with step 4 (the user may be offline or rate-limited; a stale binary is better than blocking). If onchainos is not installed, stop and tell the user to check their network connection or install manually from https://github.com/okx/onchainos-skills.
2. Install or update:
- Already installed → run
onchainos upgrade --throttle --skill-version "<this skill's frontmatter version>" (handles the 12h throttle, channel selection, beta→stable graduation, checkout switching, and its own cache refresh). If the JSON output has an action field, perform it and re-read SKILL.md. On failure, keep the current binary. Skip the installer bullets below.
- Not installed → download the installer and its checksum file from the latest release tag:
- macOS/Linux:
curl -sSL "https://raw.githubusercontent.com/okx/onchainos-skills/${LATEST_TAG}/install.sh" -o /tmp/onchainos-install.sh curl -sSL "https://github.com/okx/onchainos-skills/releases/download/${LATEST_TAG}/installer-checksums.txt" -o /tmp/installer-checksums.txt
- Windows:
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/okx/onchainos-skills/${LATEST_TAG}/install.ps1" -OutFile "$env:TEMP\onchainos-install.ps1" Invoke-WebRequest -Uri "https://github.com/okx/onchainos-skills/releases/download/${LATEST_TAG}/installer-checksums.txt" -OutFile "$env:TEMP\installer-checksums.txt"
- Verify the installer's SHA256 against
installer-checksums.txt. On mismatch, stop and warn — the installer may have been tampered with. - Execute:
sh /tmp/onchainos-install.sh(or& "$env:TEMP\onchainos-install.ps1"on Windows).
If the active skill's frontmatter version contains -beta, append --beta so beta skills install the matching beta CLI. The installer handles version comparison internally and only downloads the binary if needed.
- On other failures, point to https://github.com/okx/onchainos-skills.
3. Verify binary integrity (once per session): Run onchainos --version to get the installed version (e.g., 1.0.5 or 2.0.0-beta.0). Construct the installed tag as v<version>. Download checksums.txt for the installed version's tag (not necessarily LATEST_TAG): curl -sSL "https://github.com/okx/onchainos-skills/releases/download/v<version>/checksums.txt" -o /tmp/onchainos-checksums.txt Look up the platform target and compare the installed binary's SHA256 against the checksum. On mismatch, reinstall (step 2) and re-verify. If still mismatched, stop and warn.
- Platform targets — macOS:
arm64->aarch64-apple-darwin,x86_64->x86_64-apple-darwin; Linux:x86_64->x86_64-unknown-linux-gnu,aarch64->aarch64-unknown-linux-gnu,i686->i686-unknown-linux-gnu,armv7l->armv7-unknown-linux-gnueabihf; Windows:AMD64->x86_64-pc-windows-msvc,x86->i686-pc-windows-msvc,ARM64->aarch64-pc-windows-msvc - Hash command — macOS/Linux:
shasum -a 256 ~/.local/bin/onchainos; Windows:(Get-FileHash "$env:USERPROFILE\.local\bin\onchainos.exe" -Algorithm SHA256).Hash.ToLower()
4. Version drift check — REQUIRED, run even if steps 1-3 were skipped.
- Run
onchainos --version→ CLI version (e.g.,2.2.9) - Read
versionfield from the active skill's YAML frontmatter (e.g.,version: "2.0.0"at the top of SKILL.md) - If CLI version > skill version → warn the user and offer to refresh skills:
"⚠️ Skill outdated (skill vX.Y.Z < CLI vA.B.C). Run `onchainos upgrade` to refresh both the CLI and any locally cloned skill checkouts (`~/.codex/onchainos-skills`, `~/.openclaw/onchainos-skills`, `~/.cursor/onchainos-skills`, `~/.opencode/onchainos-skills`). For skills installed via a plugin manager (Claude Code `/plugin`, npm, Cursor marketplace), update through that manager instead — `onchainos upgrade` will skip those paths and print guidance."
- After the user re-runs the skill (or confirms they've updated), re-read SKILL.md so subsequent steps use the fresh content.
- Continue to the user's command.
5. Do NOT auto-reinstall on command failures. Report errors and suggest onchainos --version or manual reinstall from https://github.com/okx/onchainos-skills. 6. Rate limit errors. If a command hits rate limits, the shared API key may be throttled. Suggest creating a personal key at the OKX Developer Portal. If the user creates a .env file, remind them to add .env to .gitignore.
a2a_charge — agent-to-agent payment links (onchainos payment a2a-pay)
Loaded from../SKILL.mdwhen the user mentions a paymentId, ana2a_...link, "create payment link", or asks to check a2a payment status. Unlike the HTTP 402 paths (accepts-based andWWW-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 (AK) or onchainos wallet login <email> (OTP). 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:
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) orpaymentId=<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'spaymentId/deliveries.urlagainst 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:
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:
{
"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:
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
statusplus the paymentId, and tell the user: "Status is still<status>after 60s; you can runstatusagain 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.
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-runstatus.completed→ recommendokx-agentic-walletto verify post-payment balance delta.failed→ recommend checking buyer balance viaokx-agentic-wallet, and iftx_hashis present, inspect it viaokx-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-token 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
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
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
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
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-terminalaccepts-based schemes — exact / aggr_deferred / upto (+ Permit2)
Loaded from../SKILL.mdonly on a failure or legacy path. On the success pathonchainos payment payreturns a readyauthorization_header— you replay it directly (SKILL.md Step A6) and do not load this file. Load it when:payreturnsPermit2 allowance insufficient(one-time approve), a legacy x402 v1 raw proof, or you need to interpret a scheme-specific settlement result.
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 / upto without clobbering name / version), and returns {authorization_header, header_name, scheme, wallet}. You never assemble or merge anything. The local-key fallback pay-local is `exact` only — no aggr_deferred / Permit2.
Interpreting the settlement result (after Replay)
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 .). Read 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 only)
onchainos payment pay-local --payload '<base64 ...>'Reads EVM_PRIVATE_KEY (env var or ~/.onchainos/.env), derives the payer, generates the nonce, computes validBefore = now + maxTimeoutSeconds, and signs exact locally — no TEE, no JWT. Returns the same {authorization_header, ...} shape (v2). Rejects aggr_deferred / Permit2. Prerequisites: the payer holds enough of the asset token on the target chain; the token supports EIP-3009 transferWithAuthorization; accepts[].extra.name (EIP-712 domain name) is present (version optional, defaults "2"). ⚠️ 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
onchainos payment pay --payload '<base64 of the decoded 402 payload / raw PAYMENT-REQUIRED>' [--selected-index <n>]
onchainos payment pay-local --payload '<base64 ...>' # exact only| 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
facilitatorAddressinaccepts[].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-classinvalidReason; 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(exactonly); ask the user, don't silently cancel. - Unsupported / non-EVM network — EVM only (CAIP-2
eip155:<chainId>); a non-EVMnetwork→ 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: alsowitness.facilitator, so a leaked signature is only usable by the named facilitator) — it can't be retargeted or replayed pastdeadline.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_KEYas a credential (chmod 600). - This reference only signs — settlement happens on-chain when the recipient / facilitator redeems the authorization.
charge intent (one-shot)
Loaded from../SKILL.mdwhen the dispatcher decoded aWWW-Authenticate: Payment402 challenge withintent="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
- `false` → hash mode (user broadcasts the on-chain tx first) → Hash mode
Transaction mode (sign via TEE)
onchainos payment charge \
--challenge '<full WWW-Authenticate header value>' \
[--from '<0xPayer>']The CLI auto-detects methodDetails.splits[] — no extra flag needed. Output:
{ "ok": true, "data": { "authorization_header": "...", "wallet": "0x...", "mode": "transaction", "..." } }Save data.authorization_header and proceed to 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-onchain-gateway (recommended)2. I'll send it manually — paste the tx hash when ready
Option 1: hand off to okx-onchain-gateway, return here with the resulting 0x... hash. Option 2: wait for the user to paste a 66-char 0x... hash.
Then:
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-onchain-gateway to broadcast |
Multi-scheme recommendation (SKILL.md Step A3.5)
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 withyes), 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 ayeson 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(exactoraggr_deferred). - A
WWW-Authenticate: Payment402 withintent="charge"→ one candidate. Scheme =charge. WWW-Authenticate: Paymentwithintent="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 balancecall 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:
onchainos wallet balanceA3.5.3: Filter by has-balance
Keep only candidates where the wallet has a non-zero balance for the matching (chainId, tokenAddress).
Edge case — zero candidates pass the filter: list all original candidates to the user (no recommendation badge, no tie-breakers applied). User picks one; carry it 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>>
<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=<…>. User picks one by index → that becomes the selected candidate; continue at Step A4.
A3.5.6: Carry the selection forward
- `accepts`-based selection (
exactoraggr_deferredfromaccepts[]) → 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.mdpath; 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.
session intent (channel: open / voucher / topUp / close)
Loaded from../SKILL.mdwhen the dispatcher decoded aWWW-Authenticate: Payment402 challenge withintent="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 theoriginal 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 setspayload.actionfor you.
3. Resend to the same original business URL with
Authorization: <authorization_header>.>
**<authorization_header>already includes thePaymentscheme
prefix** — paste the CLI's data.authorization_header value verbatiminto theAuthorizationheader. Do NOT prepend anotherPayment
yourself; that would produce Payment Payment <b64> and the sellerwill reject it.
>
Never probe for/open,/voucher,/topup,/close,
/<resource>/topup, etc. — they don't exist. If you can't think ofa 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>)(orunit_amount × 100if 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 fromchallenge.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`):
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-onchain-gateway or manual). Then:
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-onchain-gateway, 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
Authorizationfirst; 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) >= minVoucherDeltaunit_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)
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)
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>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:
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):
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
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 or onchainos wallet login <email> |
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 |
Related skills
How it compares
okx-agent-payments-protocol automates OKX agent payments for 402 and a2a flows, not generic crypto wallet tutorials.
FAQ
Who is okx-agent-payments-protocol for?
Developers automating OKX agent payments through onchainos when APIs return 402 or payment triggers appear.
When should I use okx-agent-payments-protocol?
When paying HTTP 402 resources, opening payment channels, topping up sessions, or creating or paying a2a links.
Is okx-agent-payments-protocol safe to install?
Review the Security Audits panel; every payment requires explicit user confirmation before signing.