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.
- package/.env.example +23 -0
- package/CHANGELOG.md +551 -0
- package/README.md +466 -7
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/index.mjs.map +1 -1
- package/dist/{cdp-DeohBe1o.d.ts → cdp-B5PhDUTC.d.ts} +1 -1
- package/dist/{cdp-p_eHuQpb.d.mts → cdp-D-5Hg3y_.d.mts} +1 -1
- package/dist/chains/index.d.mts +102 -1
- package/dist/chains/index.d.ts +102 -1
- package/dist/chains/index.js +66 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +57 -0
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +6850 -2127
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +6857 -2128
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +348 -1
- package/dist/client/index.d.ts +348 -1
- package/dist/client/index.js +1462 -43
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +1469 -51
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/web/index.d.mts +17 -1
- package/dist/client/web/index.mjs +11 -0
- package/dist/client/web/index.mjs.map +1 -1
- package/dist/facilitators/index.d.mts +663 -4
- package/dist/facilitators/index.d.ts +663 -4
- package/dist/facilitators/index.js +1292 -10
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +1268 -9
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/index.d.mts +78 -3
- package/dist/index.d.ts +78 -3
- package/dist/index.js +4229 -556
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4214 -548
- package/dist/index.mjs.map +1 -1
- package/dist/mcp/index.js +1462 -45
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/index.mjs +1473 -56
- package/dist/mcp/index.mjs.map +1 -1
- package/dist/{registry-OsEO2dOu.d.mts → registry-DIo0WNoO.d.mts} +8 -1
- package/dist/{registry-OsEO2dOu.d.ts → registry-DIo0WNoO.d.ts} +8 -1
- package/dist/server/index.d.mts +336 -2
- package/dist/server/index.d.ts +336 -2
- package/dist/server/index.js +2516 -188
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +2516 -188
- package/dist/server/index.mjs.map +1 -1
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/index.mjs.map +1 -1
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +20 -2
- package/schemas/moltspay.services.schema.json +211 -6
- 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
|
|
781
|
-
2.
|
|
782
|
-
3. Server
|
|
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
|
-
#
|
|
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` - $
|
|
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
|
|