moltspay 1.6.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.env.example +23 -0
  2. package/CHANGELOG.md +551 -0
  3. package/README.md +466 -7
  4. package/dist/cdp/index.js.map +1 -1
  5. package/dist/cdp/index.mjs.map +1 -1
  6. package/dist/{cdp-DeohBe1o.d.ts → cdp-B5PhDUTC.d.ts} +1 -1
  7. package/dist/{cdp-p_eHuQpb.d.mts → cdp-D-5Hg3y_.d.mts} +1 -1
  8. package/dist/chains/index.d.mts +102 -1
  9. package/dist/chains/index.d.ts +102 -1
  10. package/dist/chains/index.js +66 -0
  11. package/dist/chains/index.js.map +1 -1
  12. package/dist/chains/index.mjs +57 -0
  13. package/dist/chains/index.mjs.map +1 -1
  14. package/dist/cli/index.js +6850 -2127
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/cli/index.mjs +6857 -2128
  17. package/dist/cli/index.mjs.map +1 -1
  18. package/dist/client/index.d.mts +348 -1
  19. package/dist/client/index.d.ts +348 -1
  20. package/dist/client/index.js +1462 -43
  21. package/dist/client/index.js.map +1 -1
  22. package/dist/client/index.mjs +1469 -51
  23. package/dist/client/index.mjs.map +1 -1
  24. package/dist/client/web/index.d.mts +17 -1
  25. package/dist/client/web/index.mjs +11 -0
  26. package/dist/client/web/index.mjs.map +1 -1
  27. package/dist/facilitators/index.d.mts +663 -4
  28. package/dist/facilitators/index.d.ts +663 -4
  29. package/dist/facilitators/index.js +1292 -10
  30. package/dist/facilitators/index.js.map +1 -1
  31. package/dist/facilitators/index.mjs +1268 -9
  32. package/dist/facilitators/index.mjs.map +1 -1
  33. package/dist/index.d.mts +78 -3
  34. package/dist/index.d.ts +78 -3
  35. package/dist/index.js +4229 -556
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +4214 -548
  38. package/dist/index.mjs.map +1 -1
  39. package/dist/mcp/index.js +1462 -45
  40. package/dist/mcp/index.js.map +1 -1
  41. package/dist/mcp/index.mjs +1473 -56
  42. package/dist/mcp/index.mjs.map +1 -1
  43. package/dist/{registry-OsEO2dOu.d.mts → registry-DIo0WNoO.d.mts} +8 -1
  44. package/dist/{registry-OsEO2dOu.d.ts → registry-DIo0WNoO.d.ts} +8 -1
  45. package/dist/server/index.d.mts +336 -2
  46. package/dist/server/index.d.ts +336 -2
  47. package/dist/server/index.js +2516 -188
  48. package/dist/server/index.js.map +1 -1
  49. package/dist/server/index.mjs +2516 -188
  50. package/dist/server/index.mjs.map +1 -1
  51. package/dist/verify/index.js.map +1 -1
  52. package/dist/verify/index.mjs.map +1 -1
  53. package/dist/wallet/index.js.map +1 -1
  54. package/dist/wallet/index.mjs.map +1 -1
  55. package/package.json +20 -2
  56. package/schemas/moltspay.services.schema.json +211 -6
  57. package/scripts/postinstall.js +91 -0
package/README.md CHANGED
@@ -25,6 +25,10 @@ MoltsPay enables agent-to-agent commerce using the [x402 protocol](https://www.x
25
25
  - **Payment Verification** - Automatic on-chain verification
26
26
  - **Secure Wallet** - Spending limits, whitelist, and audit logging
27
27
  - **Multi-chain** - Base, Polygon, Solana, BNB, Tempo (mainnet & testnet)
28
+ - **Fiat Rail — Alipay (`2.0.0`)** - Accept CNY via Alipay AI Pay from China mainland merchants. CLI-only (Node), browser unsupported. See [`docs/ALIPAY-RAIL.md`](https://github.com/Yaqing2023/moltspay/blob/v2.4.0/docs/ALIPAY-RAIL.md)
29
+ - **Fiat Rail — WeChat Pay (`2.1.0`)** - Accept CNY via WeChat Pay v3 Native (scan-to-pay). SDK-managed recoverable sessions persist QR/order context, poll in the background, and fulfill idempotently. See [`docs/WECHAT-RAIL-DESIGN.md`](https://github.com/Yaqing2023/moltspay/blob/v2.4.0/docs/WECHAT-RAIL-DESIGN.md)
30
+ - **Balance Rail — Password-Free (`2.2.0`)** - Top up once, then pay with no signature or QR per transaction. Server-custodied SQLite ledger with atomic deducts, hard spending limits, and idempotent retries. See [`docs/WECHAT-BALANCE-PASSWORDLESS-DESIGN.md`](https://github.com/Yaqing2023/moltspay/blob/v2.4.0/docs/WECHAT-BALANCE-PASSWORDLESS-DESIGN.md)
31
+ - **Balance Identity & Authentication (`2.4.0`)** - The balance rail gets a real user: accounts are anchored to the WeChat payer's `openid` at top-up, and each deduction is authorized by a per-request signature (`auth_mode: off` | `shadow` | `enforce`). Knowing a `buyer_id` is no longer enough to spend — closes the bearer hole. See [Balance Authentication](#balance-authentication-240) below
28
32
  - **Agent-to-Agent** - Complete A2A payment flow support
29
33
  - **Multi-VM** - EVM chains + Solana (SVM) with unified API
30
34
  - **MCP Server** - Expose wallet + payments to Claude Desktop, Cursor, and other MCP hosts
@@ -274,7 +278,7 @@ Servers advertising for the web need `cors` enabled; CLI callers are unaffected
274
278
 
275
279
  **Security posture.** No private key ever enters browser memory. No filesystem access, no `~/.moltspay/` — the wallet is always external. The `signer` object the client receives only has permission to sign typed data and (for BNB) submit one `approve` transaction, which the user explicitly confirms in the wallet UI.
276
280
 
277
- **Reference demo.** `examples/web/` is a runnable React + Vite app that exercises every path above. `cd examples/web && npm install && npm run dev` — it connects to `https://moltspay.com/a/zen7` by default. See [`examples/web/README.md`](examples/web/README.md) for the full matrix of tested wallets + chains.
281
+ **Reference demo.** `examples/web/` is a runnable React + Vite app that exercises every path above. `cd examples/web && npm install && npm run dev` — it connects to `https://moltspay.com/a/zen7` by default. See [`examples/web/README.md`](https://github.com/Yaqing2023/moltspay/blob/v2.4.0/examples/web/README.md) for the full matrix of tested wallets + chains.
278
282
 
279
283
  ## MCP Server (For AI Assistants)
280
284
 
@@ -318,7 +322,7 @@ Each host has its own config file for registering stdio MCP servers — check yo
318
322
  2. **Dry-run mode** — launch with `--dry-run` and payments return a preview instead of signing.
319
323
  3. **Confirmation gate** — set `MOLTSPAY_MCP_REQUIRE_CONFIRM=1` to require a second tool call (`confirmed: true`) for any payment exceeding `maxPerTx / 10`.
320
324
 
321
- Private keys and mnemonics are never exposed over MCP — wallet creation stays on the CLI (`npx moltspay init`) by design. See [`docs/MCP-USAGE.md`](docs/MCP-USAGE.md) for full tool arguments and troubleshooting.
325
+ Private keys and mnemonics are never exposed over MCP — wallet creation stays on the CLI (`npx moltspay init`) by design. See [`docs/MCP-USAGE.md`](https://github.com/Yaqing2023/moltspay/blob/v2.4.0/docs/MCP-USAGE.md) for full tool arguments and troubleshooting.
322
326
 
323
327
  ## Payment Protocols
324
328
 
@@ -330,6 +334,8 @@ MoltsPay supports multiple payment protocols, each optimized for different chain
330
334
  | x402 + SOL | Solana | Gasless (server pays) | HTTP 402 + SPL transfer |
331
335
  | x402 + BNB | BNB | Gasless (server pays) | HTTP 402 + EIP-712 intent signing |
332
336
  | MPP | Tempo | Gas-free native | HTTP 402 + WWW-Authenticate |
337
+ | x402 + Alipay | Alipay | No blockchain gas | HTTP 402 + Alipay AI Pay proof |
338
+ | x402 + WeChat Pay | WeChat Pay Native | No blockchain gas | HTTP 402 + scan-to-pay QR, poll-based verify |
333
339
 
334
340
  ### How x402 Protocol Works
335
341
 
@@ -568,10 +574,19 @@ npx moltspay faucet --chain solana_devnet # Get Solana devnet USDC
568
574
  npx moltspay faucet --chain bnb_testnet # Get BNB testnet USDC + tBNB
569
575
  npx moltspay faucet --chain tempo_moderato # Get Tempo testnet tokens
570
576
  npx moltspay status # Check balance (all chains)
577
+ npx moltspay transfer <to> <amount> # Transfer USDC/USDT out to any address (e.g. an exchange)
571
578
  npx moltspay config # Update limits
572
579
  npx moltspay services <url> # List provider's services
573
580
  npx moltspay pay <url> <service> # Pay and execute service
574
581
 
582
+ # === Transfer / Withdraw (2.4.0) ===
583
+ # Move USDC/USDT out of your wallet to any address (e.g. an exchange deposit address).
584
+ npx moltspay transfer <to> <amount> # 5 USDC on Base (default), interactive confirm
585
+ npx moltspay transfer <to> 10 --token USDT --chain bnb # USDT on BNB Chain
586
+ npx moltspay transfer <to> 5 --chain polygon --yes --json # non-interactive (agents/scripts)
587
+ # EVM chains only (base/polygon/bnb/...); a normal on-chain transfer, so the wallet needs a
588
+ # little native gas (ETH/BNB/POL). The transfer network must match the receiver's deposit network.
589
+
575
590
  # === Service Discovery ===
576
591
  npx moltspay services # List all from registry
577
592
  npx moltspay services https://provider.com # List from specific provider
@@ -590,6 +605,38 @@ npx moltspay pay <url> <service> --chain solana_devnet # Pay on Solana devnet
590
605
  npx moltspay pay <url> <service> --chain bnb # Pay on BNB
591
606
  npx moltspay pay <url> <service> --chain bnb_testnet # Pay on BNB testnet
592
607
  npx moltspay pay <url> <service> --chain tempo_moderato # Pay on Tempo
608
+ npx moltspay pay <url> <service> --rail alipay # Pay with Alipay AI Pay
609
+ npx moltspay pay <url> <service> --rail wechat # Pay with WeChat Pay Native QR
610
+ npx moltspay pay <url> <service> --rail balance # Pay password-free from prepaid balance
611
+
612
+ # === BNB One-Time Approve ===
613
+ # BNB payments need a one-time spender approval first (the spender comes from the 402 response).
614
+ npx moltspay approve --spender <address> --chain bnb # Approve, then `pay --chain bnb` is gasless
615
+ npx moltspay approve --spender <address> # --chain defaults to bnb_testnet
616
+
617
+ # === WeChat Recoverable Sessions ===
618
+ npx moltspay wechat start <url> <service> --prompt "..." # Start order, return QR metadata immediately
619
+ npx moltspay wechat status <session-or-out_trade_no> # Query/recover a pending session
620
+ npx moltspay wechat fulfill <session-or-out_trade_no> # Idempotently fulfill if paid
621
+ npx moltspay wechat cancel <session-or-out_trade_no> # Mark local session cancelled
622
+ npx moltspay wechat list # List persisted WeChat sessions
623
+
624
+ # === Custodial Balance (Password-Free) ===
625
+ npx moltspay balance set-buyer <id> # Persist the buyer id (account label)
626
+ npx moltspay balance query <url> # Balance, limits, today's spend, bound signer/openid
627
+ npx moltspay balance transactions <url> # Ledger history, newest first
628
+ npx moltspay balance topup <url> <amount> --rail crypto --tx-hash 0x... # Credit from a settled payment (crypto/wechat/alipay)
629
+
630
+ # --- Identity & auth (2.4.0) ---
631
+ npx moltspay balance whoami [url] # Show local signer address (and, vs a server, the account's bound signer/openid)
632
+ npx moltspay balance bind <url> # Bind the local signer to an account (runs a minimal top-up to establish identity)
633
+
634
+ # --- Recoverable WeChat-funded top-up (for chat agents; non-blocking) ---
635
+ npx moltspay balance topup-order <url> --pack 2.00 # Mint a WeChat pack order, emit the QR, exit immediately
636
+ npx moltspay balance topup-confirm <out_trade_no> # Confirm + credit a pending order later (idempotent on out_trade_no)
637
+ npx moltspay balance topup-status <out_trade_no> # Query one pending order
638
+ npx moltspay balance topup-list # List pending top-up orders
639
+ npx moltspay balance topup-pack <url> # Blocking variant: mint pack, wait for the scan, credit inline
593
640
 
594
641
  # === Server Commands ===
595
642
  npx moltspay start <skill-dir> # Start server
@@ -599,6 +646,7 @@ npx moltspay validate <path> # Validate manifest
599
646
  # === Options ===
600
647
  --port <port> # Server port (default 3000)
601
648
  --chain <chain> # Chain: base, polygon, solana, bnb, tempo_moderato, + testnets
649
+ --rail <rail> # Rail: alipay, wechat, or balance (password-free)
602
650
  --token <token> # Token: USDC, USDT
603
651
  --max-per-tx <amount> # Spending limit per transaction
604
652
  --max-per-day <amount> # Daily spending limit
@@ -777,15 +825,21 @@ npx moltspay pay https://server.com service-id --chain solana --prompt "test"
777
825
 
778
826
  BNB uses the **BNBFacilitator** with a pre-approval flow. Since CDP doesn't support BNB, we use a different approach:
779
827
 
780
- 1. Client signs an EIP-712 payment intent (no gas needed)
781
- 2. Server validates and executes the transfer
782
- 3. Server sponsors gas (~$0.0001 per tx)
828
+ 1. Client approves a spender **once** (a one-time on-chain `approve`, costs ~0.001 BNB gas)
829
+ 2. Client signs an EIP-712 payment intent (no gas needed)
830
+ 3. Server validates and executes the transfer
831
+ 4. Server sponsors gas (~$0.0001 per tx)
783
832
 
784
833
  ```bash
785
834
  # Get free testnet USDC + tBNB for gas
786
835
  npx moltspay faucet --chain bnb_testnet
787
836
 
788
- # Pay on BNB (client pays no gas!)
837
+ # One-time approve (required before the first BNB payment).
838
+ # The spender address comes from the server's 402 response; a first `pay` also
839
+ # prints it if you haven't approved yet.
840
+ npx moltspay approve --spender 0xSPENDER --chain bnb_testnet
841
+
842
+ # Pay on BNB (client pays no gas after the approve!)
789
843
  npx moltspay pay https://server.com service-id --chain bnb_testnet --prompt "test"
790
844
 
791
845
  # Check BNB balance
@@ -821,13 +875,418 @@ npx moltspay status
821
875
 
822
876
  **Explorer:** https://explore.testnet.tempo.xyz
823
877
 
878
+ ## Fiat Rail (Alipay / CNY)
879
+
880
+ Since **`2.0.0`**, MoltsPay supports a **fiat payment rail via Alipay AI Pay**, settling in **CNY** alongside the existing USDC crypto rails. It uses the same HTTP 402 flow, so a service can price in USDC, CNY, or both — a purely additive change that leaves existing crypto-only services untouched.
881
+
882
+ Unlike the crypto rails, `alipay` is a **fiat rail, not a blockchain**: there is no gas and no on-chain settlement — the buyer pays in the Alipay app and the merchant settles in CNY. The scheme is `alipay-aipay`, signed with RSA2.
883
+
884
+ > ⚠️ **Node CLI only.** The Alipay client wraps the `alipay-bot` CLI and requires Node ≥ 22. The browser build (`moltspay/web`) throws `UnsupportedChainError` for `alipay`. Provider/server config is 100% native TypeScript.
885
+
886
+ ### Provider Setup (Selling in CNY)
887
+
888
+ Add `"alipay"` to `chains`, configure `provider.alipay` with your merchant keys, and add a per-service `alipay` block with the CNY price. Crypto and fiat can coexist:
889
+
890
+ ```json
891
+ {
892
+ "provider": {
893
+ "name": "My Service",
894
+ "wallet": "0x...",
895
+ "chains": ["base", "alipay"],
896
+ "alipay": {
897
+ "seller_id": "2088xxxxxxxxxxxx",
898
+ "app_id": "2021xxxxxxxxxxxx",
899
+ "seller_name": "My Shop",
900
+ "service_id_default": "API_XXXXXXXXXXXXXXXX",
901
+ "private_key_path": "./cert/app_private_key.pem",
902
+ "alipay_public_key_path": "./cert/alipay_public_key.pem"
903
+ }
904
+ },
905
+ "services": [{
906
+ "id": "my-service",
907
+ "function": "handleRequest",
908
+ "price": 0.50,
909
+ "currency": "USDC",
910
+ "alipay": { "price_cny": "7.00", "goods_name": "My Service" }
911
+ }]
912
+ }
913
+ ```
914
+
915
+ **`provider.alipay`** (required when `chains` includes `alipay`):
916
+
917
+ | Field | Req | Description |
918
+ |-------|-----|-------------|
919
+ | `seller_id` | ✅ | Merchant Alipay ID (16 digits) |
920
+ | `app_id` | ✅ | Application ID from the Alipay Open Platform (16 digits) |
921
+ | `seller_name` | ✅ | Merchant display name (shown in the 402 challenge) |
922
+ | `service_id_default` | ✅ | Default `service_id` for this provider (e.g. `API_XXXXXXXXXXXXXXXX`) |
923
+ | `private_key_path` | ✅ | Path to the RSA2 merchant private key PEM (relative to the manifest) |
924
+ | `alipay_public_key_path` | ✅ | Path to the Alipay platform public key PEM |
925
+ | `gateway_url` | — | Open API gateway (default production; use the sandbox URL for testing) |
926
+ | `sign_type` | — | Signature algorithm — only `RSA2` is supported |
927
+
928
+ **`services[].alipay`** (set on each service that accepts CNY):
929
+
930
+ | Field | Req | Description |
931
+ |-------|-----|-------------|
932
+ | `price_cny` | ✅ | CNY price as a decimal string in **yuan**, e.g. `"7.00"` = 7 CNY (**NOT cents**) |
933
+ | `goods_name` | ✅ | Goods name shown to the buyer in the Alipay app |
934
+ | `service_id` | — | Per-service override (defaults to `provider.alipay.service_id_default`) |
935
+
936
+ > ⚠️ `price` (USDC) and `price_cny` (CNY) are **two independent prices** — MoltsPay does no FX conversion. You decide whether USDC `0.50` is equivalent to CNY `7.00`. The merchant-backend unit price must match `price_cny` or the challenge is rejected.
937
+
938
+ ### Client Usage (Paying in CNY)
939
+
940
+ The Alipay client relies on the `alipay-bot` CLI, which is **auto-provisioned on install** from the official Alipay CDN (postinstall, best-effort). Set `MOLTSPAY_SKIP_CLI_INSTALL=1` to skip it, or install it manually later:
941
+
942
+ ```bash
943
+ # Manual / CI install of the bundled CLI
944
+ npx -y @alipay/agent-payment install-cli
945
+ alipay-bot --version # expect >= 0.3.15
946
+
947
+ # One-time: open & authorize the Alipay wallet (scan the returned QR in the app)
948
+ npx moltspay alipay apply
949
+ npx moltspay alipay bind -c "<auth-code>"
950
+ npx moltspay alipay check
951
+
952
+ # Pay a service in CNY
953
+ npx moltspay pay https://server.com my-service --rail alipay --prompt "..."
954
+ ```
955
+
956
+ > 🔐 **Keep keys safe.** The RSA2 private key authorizes collection on your merchant account. Store the PEM files outside version control and reference them by path in the manifest.
957
+
958
+ See [`docs/ALIPAY-RAIL.md`](https://github.com/Yaqing2023/moltspay/blob/v2.4.0/docs/ALIPAY-RAIL.md) for the full reference (402 flow, error codes, end-to-end example, and known issues).
959
+
960
+ ## Fiat Rail (WeChat Pay / CNY)
961
+
962
+ Since **`2.1.0`**, MoltsPay supports a second **fiat payment rail via WeChat Pay v3 Native (scan-to-pay)**, settling in **CNY** alongside USDC and Alipay. It uses the same HTTP 402 flow, so a service can price in USDC, Alipay-CNY, WeChat-CNY, or any combination — purely additive.
963
+
964
+ Like `alipay`, `wechat` is a **fiat rail, not a blockchain**. The scheme is `wechatpay-native`, requests are signed with **SHA256-RSA** (`WECHATPAY2-SHA256-RSA2048`), and amounts are sent to WeChat in **fen** (the manifest still uses **yuan** decimal strings for consistency).
965
+
966
+ > ⚠️ **Not a fully autonomous payer.** WeChat Pay has no agent-payment product equivalent to Alipay's `alipay-bot`. The flow is: the server issues a **payer-agnostic `code_url`** (Native, no `openid` — anyone can scan), the SDK client persists the session and polls, a human scans and pays, and the server verifies the order (`trade_state === SUCCESS`). It is **one code, one payment** — issue a new code to collect again. See [`docs/WECHAT-RAIL-DESIGN.md`](https://github.com/Yaqing2023/moltspay/blob/v2.4.0/docs/WECHAT-RAIL-DESIGN.md).
967
+
968
+ ### Provider Setup (Selling in CNY)
969
+
970
+ Add `"wechat"` to `chains`, configure `provider.wechat` with your merchant credentials, and add a per-service `wechat` block with the CNY price:
971
+
972
+ ```json
973
+ {
974
+ "provider": {
975
+ "name": "My Service",
976
+ "wallet": "0x...",
977
+ "chains": ["base", "wechat"],
978
+ "wechat": {
979
+ "mchid": "1900000001",
980
+ "appid": "wx8888888888888888",
981
+ "serial_no": "5157F09EFDC096DE15EBE81A47057A72...",
982
+ "private_key_path": "./cert/wechat_apiclient_key.pem",
983
+ "platform_public_key_path": "./cert/wechat_platform_cert.pem",
984
+ "apiv3_key": "your32byteapiv3keyhere0123456789",
985
+ "notify_url": "https://your.host/wechat/notify"
986
+ }
987
+ },
988
+ "services": [{
989
+ "id": "my-service",
990
+ "function": "handleRequest",
991
+ "price": 0.50,
992
+ "currency": "USDC",
993
+ "wechat": { "price_cny": "7.00", "description": "My Service" }
994
+ }]
995
+ }
996
+ ```
997
+
998
+ **`provider.wechat`** (required when `chains` includes `wechat`):
999
+
1000
+ | Field | Req | Description |
1001
+ |-------|-----|-------------|
1002
+ | `mchid` | ✅ | Merchant id |
1003
+ | `appid` | ✅ | App id (official account / mini-program / app) |
1004
+ | `serial_no` | ✅ | Merchant API certificate serial number |
1005
+ | `private_key_path` | ✅ | Path to the merchant RSA private key PEM (relative to the manifest) |
1006
+ | `notify_url` | ✅ | Async result notify URL (required by Native order create even when polling) |
1007
+ | `platform_public_key_path` | — | WeChat platform certificate / public key PEM — enables response signature verification |
1008
+ | `apiv3_key` | — | APIv3 key (32 bytes) — only needed for callback decryption (Phase 2) |
1009
+ | `api_base` | — | Open API base URL (default `https://api.mch.weixin.qq.com`) |
1010
+
1011
+ **`services[].wechat`** (set on each service that accepts CNY):
1012
+
1013
+ | Field | Req | Description |
1014
+ |-------|-----|-------------|
1015
+ | `price_cny` | ✅ | CNY price as a decimal string in **yuan**, e.g. `"7.00"` = 7 CNY (**NOT fen**) |
1016
+ | `description` | ✅ | Order description shown to the payer in the WeChat app |
1017
+
1018
+ > ⚠️ `price` (USDC) and `price_cny` (CNY) are **independent prices** — MoltsPay does no FX conversion.
1019
+
1020
+ ### How It Works
1021
+
1022
+ 1. An unpaid request to `/execute` returns **402** with a `wechatpay-native` entry in `accepts[]`, carrying `extra.code_url` (`weixin://wxpay/bizpayurl?pr=...`) and `extra.out_trade_no`.
1023
+ 2. The SDK client persists a payment session before surfacing the QR. A channel runtime can show the returned PNG/`MEDIA` image and continue independently of the original tool call.
1024
+ 3. A human scans and pays. The Native `code_url` is a QR payload, not a normal HTTPS checkout link.
1025
+ 4. The SDK client polls by re-requesting `/execute` with an `X-Payment` payload echoing `out_trade_no`; the server verifies via order query and, on `SUCCESS`, runs the skill and confirms settlement.
1026
+ 5. If the process is interrupted, the session can be resumed by `payment_session_id` or `out_trade_no`; `fulfill` is idempotent and returns the stored result once completed.
1027
+
1028
+ ### Buyer SDK Usage
1029
+
1030
+ Blocking terminal-style payment:
1031
+
1032
+ ```ts
1033
+ import { MoltsPayClient } from 'moltspay/client';
1034
+
1035
+ const client = new MoltsPayClient();
1036
+ const result = await client.pay(
1037
+ 'https://server.com',
1038
+ 'my-service',
1039
+ { prompt: 'hello' },
1040
+ {
1041
+ rail: 'wechat',
1042
+ onPaymentPending: ({ paymentUrl, tradeNo }) => {
1043
+ console.log('Render this as a QR:', paymentUrl, tradeNo);
1044
+ },
1045
+ },
1046
+ );
1047
+ ```
1048
+
1049
+ Recoverable channel/session payment:
1050
+
1051
+ ```ts
1052
+ const session = await client.startWechatPayment(
1053
+ 'https://server.com',
1054
+ 'my-service',
1055
+ { prompt: 'hello' },
1056
+ {
1057
+ rail: 'wechat',
1058
+ autoPoll: true,
1059
+ onPaymentPending: ({ paymentUrl, tradeNo }) => {
1060
+ // Render paymentUrl as QR and send it to the current channel.
1061
+ },
1062
+ onWechatPaymentCompleted: async (completed) => {
1063
+ // Send completed.resultBody, parsed as needed, back to the channel.
1064
+ },
1065
+ onWechatPaymentFailed: async (failed) => {
1066
+ // Notify timeout/cancel/failure.
1067
+ },
1068
+ },
1069
+ );
1070
+
1071
+ console.log(session.paymentSessionId, session.outTradeNo);
1072
+ ```
1073
+
1074
+ CLI recoverable flow:
1075
+
1076
+ ```bash
1077
+ npx moltspay wechat start https://server.com my-service --prompt "hello" --json
1078
+ npx moltspay wechat status mpay_sess_...
1079
+ npx moltspay wechat fulfill mpay_sess_...
1080
+ ```
1081
+
1082
+ See [`examples/wechat-native-pay.ts`](https://github.com/Yaqing2023/moltspay/blob/v2.4.0/examples/wechat-native-pay.ts) for a runnable scenario-A demo (mock by default; `WECHAT_REAL=1` hits the live gateway).
1083
+
1084
+ > 🔐 **Keep keys safe.** The merchant private key and APIv3 key authorize collection on your merchant account. Store them outside version control and reference them by path.
1085
+
1086
+ ## Balance Rail (Password-Free Payments)
1087
+
1088
+ Since **`2.2.0`**, MoltsPay supports a third payment mode: a **custodial balance rail (password-free)**. A buyer tops up once — via any existing rail — and subsequent purchases are deducted from a server-custodied balance: **no signature, no QR scan, no password per transaction**. It uses the same HTTP 402 flow (scheme/network `balance`), so a service can offer crypto, Alipay-CNY, WeChat-CNY, and balance simultaneously — purely additive.
1089
+
1090
+ Unlike the other rails, nothing settles externally at purchase time: the ledger lives in SQLite on the provider server (Node's built-in `node:sqlite`, zero new dependencies). Enabling the rail requires **Node.js >= 22.5** on the server; servers that don't enable it keep the package's `node >= 18` floor.
1091
+
1092
+ > ⚠️ **Custodial trust model.** Buyer funds are held by the provider. Since **`2.4.0`**, spending is authorized by a **per-request signature**, not a bare `buyer_id` — see [Balance Authentication](#balance-authentication-240) below. With `auth_mode: enforce`, knowing a `buyer_id` is not enough to spend. (`auth_mode` defaults to `off` for backward compatibility, where the id retains bearer semantics.) See [`docs/WECHAT-BALANCE-PASSWORDLESS-DESIGN.md`](https://github.com/Yaqing2023/moltspay/blob/v2.4.0/docs/WECHAT-BALANCE-PASSWORDLESS-DESIGN.md).
1093
+
1094
+ ### Balance Authentication (`2.4.0`)
1095
+
1096
+ Before 2.4.0 the balance rail identified a buyer by a single plaintext `buyer_id`: anyone who knew it could spend that balance (bearer semantics), and the same person writing it two ways split into two accounts. 2.4.0 gives the rail a real identity, in two complementary halves:
1097
+
1098
+ - **Identity anchor = WeChat `openid`.** On a WeChat-funded top-up, the server extracts `payer.openid` from the (signature-verified) order query and records it on the account. Accounts are anchored to the person who actually paid, not to a self-reported string — this removes the account-splitting class of bug at the source. Binding is observational and never overwrites a conflicting openid.
1099
+ - **Spend credential = a per-request signature.** Each deduction carries an EIP-191 signature over a canonical message (domain `moltspay-balance-auth:v1` / `balance-deduct` / `buyer_id` / `request_id` / `service` / `timestamp`). The server recovers the signer address, TOFU-binds it to the account on first use, and verifies it thereafter. The amount is deliberately *not* signed (the service id fixes the price server-side); a `request_id` + a ±5-minute `timestamp` window bound replay.
1100
+
1101
+ **`provider.balance.auth_mode`** — staged rollout, not on by default:
1102
+
1103
+ | Mode | Behavior |
1104
+ |------|----------|
1105
+ | `off` (default) | Signature is not checked. Backward-compatible; `buyer_id` keeps bearer semantics. |
1106
+ | `shadow` | Verify and log what it *would* reject, without blocking. Use this to confirm every client is signing before tightening. |
1107
+ | `enforce` | Reject unsigned / invalid / wrong-signer requests with **401**. |
1108
+
1109
+ The client signs with its EVM wallet, or — for balance-only clients with no crypto wallet — a per-configDir identity key auto-created at `<configDir>/balance-identity.key` (0600). Inspect and bind identity with `moltspay balance whoami` / `moltspay balance bind`.
1110
+
1111
+ > **Foundation — WeChat response verification.** The `openid` anchor is only trustworthy if the order-query response is verified against the WeChat platform certificate. Configure `provider.wechat.platform_public_key_path` (present ⇒ every response is verified, and a verification failure throws); without it, the identity anchor is not trustworthy.
1112
+
1113
+ > **Trust model — one agent key spends for all its users.** The shipped model is *agent-custodial*: a single agent key is bound to every account it tops up (`signer_address` is one-to-many). Accounts stay separated by openid, but **whoever holds the agent's key can spend every bound user's balance** — protect `balance-identity.key` accordingly. There is currently no revocation/freeze API (operator-only, via the ledger DB); per-user key isolation is a future upgrade.
1114
+
1115
+ ### WeChat-Funded Password-Free (Scan Once, Then Password-Free)
1116
+
1117
+ Since **`2.3.0`**, the WeChat rail can **fund** the balance instead of charging per transaction. The buyer scans **once** to load a top-up pack (e.g. ¥20); every subsequent purchase deducts from the balance with **no scan, no password**, until it runs low and one more scan tops it up. WeChat has no autonomous payer, so "password-free" lives on the balance side — the first purchase against an empty balance still needs one scan, but it buys a pack, not a single item.
1118
+
1119
+ Set the ledger currency to `CNY` (WeChat `payer_total` credits 1:1 as fen — no FX), define top-up packs, and the client handles the rest:
1120
+
1121
+ ```json
1122
+ {
1123
+ "provider": {
1124
+ "name": "My Service",
1125
+ "wallet": "0x...",
1126
+ "chains": ["balance", "wechat"],
1127
+ "balance": {
1128
+ "db_path": "./data/balance-cny.sqlite",
1129
+ "currency": "CNY",
1130
+ "single_limit": "50.00",
1131
+ "daily_limit": "200.00",
1132
+ "topup_packs": ["20.00", "50.00"],
1133
+ "default_pack": "20.00",
1134
+ "auto_topup_max": "50.00"
1135
+ },
1136
+ "wechat": {
1137
+ "mchid": "1900000001",
1138
+ "appid": "wx8888888888888888",
1139
+ "serial_no": "...",
1140
+ "private_key_path": "/abs/apiclient_key.pem",
1141
+ "notify_url": "https://your-host/wechat/notify",
1142
+ "platform_public_key_path": "/abs/wechat_platform_cert.pem",
1143
+ "apiv3_key": "<32-byte apiv3 key>"
1144
+ }
1145
+ },
1146
+ "services": [{
1147
+ "id": "my-service",
1148
+ "function": "handleRequest",
1149
+ "price": 3.99,
1150
+ "currency": "CNY",
1151
+ "balance": { "price": "3.99" }
1152
+ }]
1153
+ }
1154
+ ```
1155
+
1156
+ **New `provider.balance` fields (2.3.0):**
1157
+
1158
+ | Field | Req | Description |
1159
+ |-------|-----|-------------|
1160
+ | `currency` | — | Set to `"CNY"` to fund via WeChat 1:1 (fen). A ledger remembers its currency and refuses to start on a mismatch |
1161
+ | `topup_packs` | — | Offered recharge amounts (yuan strings), e.g. `["20.00","50.00"]` |
1162
+ | `default_pack` | — | Pack the client auto-selects when funds are short (must be in `topup_packs`) |
1163
+ | `auto_topup_max` | — | Ceiling on client auto-top-up without explicit pack selection |
1164
+
1165
+ Top-ups are confirmed by **polling the WeChat order query** (`trade_state === SUCCESS`) and credited idempotently on `out_trade_no`. The async callback webhook (`POST /wechat/notify`) is **not implemented yet** — polling is the only confirmation path today, which is safe but adds latency. Configure `platform_public_key_path` regardless: it makes the server verify WeChat's response signatures, which is what makes the `payer.openid` behind balance identity trustworthy. Usage — a single `pay` command:
1166
+
1167
+ ```bash
1168
+ npx moltspay balance set-buyer my-buyer-id
1169
+ # First purchase: prints a pack QR, waits for the scan, credits, completes.
1170
+ # Later purchases: password-free, no QR — deducted from the balance.
1171
+ npx moltspay pay https://server.com my-service '{"prompt":"hello"}'
1172
+ ```
1173
+
1174
+ ### Money-Safety Design
1175
+
1176
+ - **Integer-cent accounting** — no floating-point drift.
1177
+ - **Atomic check-and-deduct** — a single SQLite transaction (`UPDATE ... WHERE balance_sat >= ?`); a balance can never go negative under concurrency.
1178
+ - **Triple idempotency**, each enforced by a unique index: deducts replay on the client's `request_id`, top-ups replay on the external settlement reference, refunds replay per deduct. No retry can double-charge, double-credit, or double-refund.
1179
+ - **Server-side hard limits** per buyer: per-transaction (default `5.00`) and daily (default `10.00`).
1180
+ - **Automatic refund** — the deduct lands *before* the skill runs; if the skill fails, the server refunds it in the same request.
1181
+ - **Pure 402 challenges** — unlike the QR rails, a 402 response mints nothing, so unpaid challenges have no side effects.
1182
+ - **Full audit trail** — every top-up, deduct, and refund is a permanent ledger row, queryable per buyer.
1183
+
1184
+ ### Provider Setup
1185
+
1186
+ Add `"balance"` to `chains`, configure `provider.balance`, and add a per-service `balance` block (its `price` defaults to the service's top-level price):
1187
+
1188
+ ```json
1189
+ {
1190
+ "provider": {
1191
+ "name": "My Service",
1192
+ "wallet": "0x...",
1193
+ "chains": ["base", "balance"],
1194
+ "balance": {
1195
+ "db_path": "./data/balance.sqlite",
1196
+ "currency": "USD",
1197
+ "single_limit": "5.00",
1198
+ "daily_limit": "10.00"
1199
+ }
1200
+ },
1201
+ "services": [{
1202
+ "id": "my-service",
1203
+ "function": "handleRequest",
1204
+ "price": 3.99,
1205
+ "currency": "USDC",
1206
+ "balance": { "price": "3.99" }
1207
+ }]
1208
+ }
1209
+ ```
1210
+
1211
+ **`provider.balance`** (required when `chains` includes `balance`):
1212
+
1213
+ | Field | Req | Description |
1214
+ |-------|-----|-------------|
1215
+ | `db_path` | ✅ | SQLite ledger file path (relative to the manifest); created on first start |
1216
+ | `currency` | — | Ledger quote currency (default `USD`) |
1217
+ | `single_limit` | — | Default per-transaction limit for new buyers (default `"5.00"`) |
1218
+ | `daily_limit` | — | Default daily limit for new buyers (default `"10.00"`) |
1219
+
1220
+ **`services[].balance`**:
1221
+
1222
+ | Field | Req | Description |
1223
+ |-------|-----|-------------|
1224
+ | `price` | — | Price in the ledger currency as a decimal string; defaults to the service's `price` |
1225
+
1226
+ The server also exposes balance-management endpoints:
1227
+
1228
+ | Endpoint | Description |
1229
+ |----------|-------------|
1230
+ | `GET /balance?buyer_id=` | Balance, limits, and today's spend |
1231
+ | `POST /balance/topup` | Verify an externally settled payment and credit the ledger |
1232
+ | `POST /balance/refund` | Reverse a deduct (operator/agent use) |
1233
+ | `GET /balance/transactions?buyer_id=` | Ledger history, newest first |
1234
+
1235
+ > ⚠️ **Protect these endpoints** (reverse-proxy auth / IP allowlist). In particular, the `alipay` top-up path is operator-trusted in this MVP: Alipay AI-Pay verification requires the buyer-side `payment_proof`, so the server credits a reported `trade_no` without gateway verification. The `crypto` path verifies the `tx_hash` on-chain (receipt + transfer to the provider wallet + amount) and the `wechat` path queries the order for `SUCCESS`; all three are idempotent on the external reference.
1236
+
1237
+ ### How It Works
1238
+
1239
+ 1. An unpaid request to `/execute` returns **402** with a `balance` entry in `accepts[]` (pure — nothing is minted).
1240
+ 2. The client re-requests with an `X-Payment` payload carrying `{ buyer_id, request_id }`, where `request_id` is a fresh client-generated UUID.
1241
+ 3. The server **deducts atomically first** (idempotent on `request_id`), then runs the skill, and refunds automatically if the skill fails. This is the inverse of the QR rails (verify paid → run → confirm).
1242
+ 4. The response carries the ledger transaction id in `X-Payment-Response`.
1243
+
1244
+ ### Buyer Usage
1245
+
1246
+ ```bash
1247
+ # One-time: persist your buyer id (account label). Under auth_mode=enforce, spending also
1248
+ # requires the bound signing key — see "Balance Authentication" above.
1249
+ npx moltspay balance set-buyer my-buyer-id
1250
+
1251
+ # Top up once, via any settled rail
1252
+ npx moltspay balance topup https://server.com 10.00 --rail crypto --tx-hash 0xabc... --chain base
1253
+ npx moltspay balance topup https://server.com 10.00 --rail wechat --out-trade-no MP1719...
1254
+ npx moltspay balance topup https://server.com 10.00 --rail alipay --trade-no 2026...
1255
+
1256
+ # Then pay password-free — no wallet, no QR
1257
+ npx moltspay pay https://server.com my-service '{"prompt":"hello"}' --rail balance
1258
+
1259
+ # Check balance and history
1260
+ npx moltspay balance query https://server.com
1261
+ npx moltspay balance transactions https://server.com
1262
+ ```
1263
+
1264
+ SDK equivalent:
1265
+
1266
+ ```ts
1267
+ import { MoltsPayClient } from 'moltspay/client';
1268
+
1269
+ const client = new MoltsPayClient();
1270
+ client.setBuyerId('my-buyer-id'); // persisted in config.json
1271
+
1272
+ await client.topupBalance('https://server.com', {
1273
+ rail: 'crypto', amount: '10.00', txHash: '0xabc...', chain: 'base',
1274
+ });
1275
+
1276
+ const result = await client.pay('https://server.com', 'my-service',
1277
+ { prompt: 'hello' }, { rail: 'balance' });
1278
+
1279
+ const info = await client.getBuyerBalance('https://server.com');
1280
+ console.log(info.balance, info.today_spent);
1281
+ ```
1282
+
824
1283
  ## Live Example: Zen7 Video Generation
825
1284
 
826
1285
  Live service at `https://moltspay.com/a/zen7`
827
1286
 
828
1287
  **Services:**
829
1288
  - `text-to-video` - $0.01 USDC - Generate video from text prompt
830
- - `image-to-video` - $0.01 USDC - Animate a static image
1289
+ - `image-to-video` - $1.49 USDC - Animate a static image
831
1290
 
832
1291
  **Supported Chains:** Base, Polygon, Solana, BNB, Tempo (mainnet & testnet)
833
1292