bsv-pay-cli 0.2.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 (96) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/LICENSE +21 -0
  3. package/README.md +435 -0
  4. package/dist/address.d.ts +6 -0
  5. package/dist/address.js +35 -0
  6. package/dist/chain/provider.d.ts +35 -0
  7. package/dist/chain/provider.js +1 -0
  8. package/dist/chain/whatsonchain.d.ts +23 -0
  9. package/dist/chain/whatsonchain.js +98 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.js +169 -0
  12. package/dist/commands/approvals.d.ts +19 -0
  13. package/dist/commands/approvals.js +112 -0
  14. package/dist/commands/balance.d.ts +3 -0
  15. package/dist/commands/balance.js +28 -0
  16. package/dist/commands/donate.d.ts +8 -0
  17. package/dist/commands/donate.js +16 -0
  18. package/dist/commands/fetch.d.ts +13 -0
  19. package/dist/commands/fetch.js +49 -0
  20. package/dist/commands/init.d.ts +11 -0
  21. package/dist/commands/init.js +188 -0
  22. package/dist/commands/mcp.d.ts +13 -0
  23. package/dist/commands/mcp.js +32 -0
  24. package/dist/commands/policy.d.ts +8 -0
  25. package/dist/commands/policy.js +101 -0
  26. package/dist/commands/request.d.ts +9 -0
  27. package/dist/commands/request.js +85 -0
  28. package/dist/commands/send.d.ts +11 -0
  29. package/dist/commands/send.js +125 -0
  30. package/dist/commands/serve.d.ts +16 -0
  31. package/dist/commands/serve.js +59 -0
  32. package/dist/commands/watch.d.ts +10 -0
  33. package/dist/commands/watch.js +163 -0
  34. package/dist/config.d.ts +16 -0
  35. package/dist/config.js +51 -0
  36. package/dist/context.d.ts +13 -0
  37. package/dist/context.js +12 -0
  38. package/dist/core/balance.d.ts +24 -0
  39. package/dist/core/balance.js +34 -0
  40. package/dist/core/context.d.ts +27 -0
  41. package/dist/core/context.js +9 -0
  42. package/dist/core/history.d.ts +18 -0
  43. package/dist/core/history.js +15 -0
  44. package/dist/core/index.d.ts +22 -0
  45. package/dist/core/index.js +17 -0
  46. package/dist/core/internal.d.ts +22 -0
  47. package/dist/core/internal.js +19 -0
  48. package/dist/core/policy-status.d.ts +55 -0
  49. package/dist/core/policy-status.js +49 -0
  50. package/dist/core/request.d.ts +43 -0
  51. package/dist/core/request.js +77 -0
  52. package/dist/core/send.d.ts +108 -0
  53. package/dist/core/send.js +277 -0
  54. package/dist/core/spend-lock.d.ts +2 -0
  55. package/dist/core/spend-lock.js +25 -0
  56. package/dist/core/wallet.d.ts +53 -0
  57. package/dist/core/wallet.js +77 -0
  58. package/dist/errors.d.ts +30 -0
  59. package/dist/errors.js +39 -0
  60. package/dist/http402/client.d.ts +32 -0
  61. package/dist/http402/client.js +85 -0
  62. package/dist/http402/middleware.d.ts +37 -0
  63. package/dist/http402/middleware.js +96 -0
  64. package/dist/http402/protocol.d.ts +50 -0
  65. package/dist/http402/protocol.js +114 -0
  66. package/dist/ledger.d.ts +51 -0
  67. package/dist/ledger.js +27 -0
  68. package/dist/mcp/server.d.ts +32 -0
  69. package/dist/mcp/server.js +484 -0
  70. package/dist/output.d.ts +17 -0
  71. package/dist/output.js +44 -0
  72. package/dist/paths.d.ts +11 -0
  73. package/dist/paths.js +24 -0
  74. package/dist/policy/approvals.d.ts +24 -0
  75. package/dist/policy/approvals.js +89 -0
  76. package/dist/policy/budget.d.ts +16 -0
  77. package/dist/policy/budget.js +47 -0
  78. package/dist/policy/engine.d.ts +76 -0
  79. package/dist/policy/engine.js +199 -0
  80. package/dist/policy/policy.d.ts +30 -0
  81. package/dist/policy/policy.js +126 -0
  82. package/dist/prompt.d.ts +11 -0
  83. package/dist/prompt.js +57 -0
  84. package/dist/tx.d.ts +29 -0
  85. package/dist/tx.js +68 -0
  86. package/dist/units.d.ts +13 -0
  87. package/dist/units.js +53 -0
  88. package/dist/wallet/brc100.d.ts +105 -0
  89. package/dist/wallet/brc100.js +217 -0
  90. package/dist/wallet/crypto.d.ts +25 -0
  91. package/dist/wallet/crypto.js +46 -0
  92. package/dist/wallet/wallet.d.ts +86 -0
  93. package/dist/wallet/wallet.js +186 -0
  94. package/docs/AGENTIC-PAYMENTS.md +218 -0
  95. package/docs/BRC100.md +151 -0
  96. package/package.json +82 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,79 @@
1
+ # Changelog
2
+
3
+ All notable changes to bsv-pay. The project follows semver; exit codes,
4
+ `--json` shapes, and MCP tool schemas are stable contracts (additive-only).
5
+
6
+ ## 0.2.0 — 2026-06-12 (Phase 2: agentic payments)
7
+
8
+ bsv-pay grows from a human-facing CLI into an agent payment toolkit: a
9
+ policy engine below every spend path, an MCP server so agents pay without
10
+ holding keys, HTTP 402 buying and selling, and experimental external
11
+ custody. Nothing from 0.1.0 regressed: existing commands, flags, exit
12
+ codes, and JSON shapes are byte-compatible.
13
+
14
+ ### Added
15
+
16
+ - **`bsv-pay/core`** — the engine as an importable library: `openWallet`,
17
+ `getBalance`, `send`/`planSend`/`executeSend`, `createRequest`,
18
+ `awaitPayment`, `getHistory`, `getPolicyStatus`, `paidFetch`,
19
+ `requirePayment`. Typed results, typed errors (`BsvPayError` carries the
20
+ CLI's stable code numbers), no prompts, no console output, and never any
21
+ key material in a return value.
22
+ - **Policy engine** (`~/.bsv-pay/policy.toml`): hard `per_tx_limit_sats`,
23
+ `daily_budget_sats` (rolling 24h, recomputed from the ledger),
24
+ `session_budget_sats`, rate limits per minute/hour, allow/denylists, and
25
+ `approval_threshold_sats` queueing big payments for a human. One
26
+ `authorizeSpend()` gate decides every spend from every surface; every
27
+ decision (allow/deny/queue, rule + reason) is appended to the ledger.
28
+ New commands: `bsv-pay policy show`, `bsv-pay policy test`.
29
+ - **Approvals**: `bsv-pay approvals list|approve|reject|set-secret` with a
30
+ dedicated approval secret (argon2id-hashed, TTY-only, separate from and
31
+ rejecting the wallet passphrase) — an agent cannot approve its own
32
+ queued payment.
33
+ - **MCP server** (`bsv-pay mcp`): seven tools over stdio — `pay`,
34
+ `paid_fetch`, `create_payment_request`, `await_payment`, `get_balance`,
35
+ `get_history`, `get_policy_status`. The wallet unlocks once at startup;
36
+ there is no unlock/approve/key tool. Policy refusals are structured
37
+ `{ok:false, error, ...data}` results agents can read and adapt to.
38
+ Concurrent payments are single-flighted in core so racing spends cannot
39
+ overshoot a budget.
40
+ - **HTTP 402 (simplified BRC-105 profile)**: `bsv-pay fetch <url>
41
+ [--max-price]` pays paywalls within policy; `bsv-pay serve --price` and
42
+ the zero-dependency Express-compatible `requirePayment()` middleware
43
+ sell behind them. New exit code 10 `payment_not_redeemed` (paid but the
44
+ server refused the content; txid in the error).
45
+ - **BRC-100 external custody (EXPERIMENTAL)**: `bsv-pay init
46
+ --experimental-brc100` delegates signing to a desktop wallet app
47
+ (Metanet/BSV Desktop JSON-API). The policy gate decides before the
48
+ wallet app is ever asked. Spending, balance, history, policy, and
49
+ approvals work; receive-side commands refuse by design
50
+ (`brc100_receive_not_supported`). **Status, plainly: protocol-tested
51
+ against a mock wallet (unit + e2e), not yet verified against a real
52
+ wallet app** — it stays behind the experimental flag until that pass
53
+ happens (docs/BRC100.md).
54
+ - **Two-agent demo** (`npm run demo:two-agents`): a seller paywall and an
55
+ MCP-only buyer agent on a local mock chain — price discovery, governed
56
+ purchases, a prompt-injection payment blocked by the denylist, a budget
57
+ stop, and the printed audit trail.
58
+ - **Docs**: [Agentic payments with bsv-pay](docs/AGENTIC-PAYMENTS.md) (the
59
+ guide), [docs/BRC100.md](docs/BRC100.md) (custody setup + verification).
60
+ - New exit codes 9 (`pending_approval`) and 10 (`payment_not_redeemed`);
61
+ additive `--json` fields (`backend`, `fee_estimated`, policy detail
62
+ fields on refusals).
63
+
64
+ ### Notes
65
+
66
+ - Full BRC-105 interop with external services (the SDK's AuthFetch,
67
+ BRC-103/104 mutual auth) is deferred with recorded reasoning — see
68
+ DECISIONS.md M12 and the README's "Compatibility, honestly".
69
+ - Session budgets are per-process by definition; daily budgets are
70
+ recomputed from the append-only ledger and survive restarts.
71
+
72
+ ## 0.1.0 — 2026-06-10 (Phase 1: the CLI MVP)
73
+
74
+ Initial release: `init` (create/import, argon2id + AES-256-GCM encrypted
75
+ seed, BIP-44 derivation), `balance`, `send` (UTXO selection, fee
76
+ estimation, dry runs, spend-limit confirmation), `request` (fresh address,
77
+ BIP-21 URI, QR, `--wait`), `watch`, `donate`; WhatsOnChain provider with
78
+ testnet/mainnet state separation; append-only JSONL ledger; stable exit
79
+ codes 0–8; `--json` everywhere; local mock-chain e2e harness.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 iamSOLUM
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,435 @@
1
+ # bsv-pay
2
+
3
+ A developer-first CLI — and an agent payment toolkit — for sending and
4
+ receiving micropayments on Bitcoin SV. Script-friendly by design: every
5
+ command supports `--json`, exit codes are stable and documented, and
6
+ nothing on stdout ever needs human parsing. Agent-safe by design: a policy
7
+ engine sits below every spend path, agents connect over MCP without ever
8
+ touching a key, and every decision lands in an append-only ledger.
9
+
10
+ > **Hot wallet.** bsv-pay keeps an encrypted seed on your disk and talks to a
11
+ > public API. Treat it like cash in your pocket: keep small amounts only.
12
+
13
+ **Giving an AI agent a wallet?** Start with
14
+ [Agentic payments with bsv-pay](docs/AGENTIC-PAYMENTS.md) — the threat
15
+ model, MCP setup for Claude Code / Claude Desktop / Cursor, the 402 flow,
16
+ and external custody. Or watch the whole thesis run in one command, no
17
+ coins needed:
18
+
19
+ ```bash
20
+ npm run demo:two-agents # a seller, a buyer agent, and the policy that governs it
21
+ ```
22
+
23
+ ## 60-second quickstart
24
+
25
+ ```bash
26
+ npm install -g bsv-pay-cli # or: npm install -g bsvpay (same thing)
27
+
28
+ # 1. Create a wallet (you'll write down a 12-word seed and pick a passphrase)
29
+ bsv-pay init --testnet
30
+
31
+ # 2. Fund it from a testnet faucet (e.g. https://witnessonchain.com/faucet/tbsv)
32
+ bsv-pay request 10000sats "faucet top-up" --testnet # prints address + QR
33
+
34
+ # 3. Watch it arrive (0-conf shows within seconds)
35
+ bsv-pay watch --testnet
36
+
37
+ # 4. Check and spend
38
+ bsv-pay balance --testnet
39
+ bsv-pay send <address> 5000sats "thanks" --testnet
40
+ ```
41
+
42
+ Drop `--testnet` for real money. Set `network = "test"` in
43
+ `~/.bsv-pay/config.toml` to make testnet the default.
44
+
45
+ ## Commands
46
+
47
+ ### `bsv-pay init`
48
+
49
+ Create or import a wallet. Refuses to overwrite an existing wallet without
50
+ `--force`.
51
+
52
+ | Flag | Meaning |
53
+ | --- | --- |
54
+ | `--import-seed` | Import a BIP-39 seed phrase (checksum validated) |
55
+ | `--import-wif` | Import a raw WIF key (single address, risk warning shown) |
56
+ | `--force` | Overwrite an existing wallet |
57
+ | `--no-encrypt` | Explicit opt-in to store the seed unencrypted (warned on every run) |
58
+ | `--experimental-brc100` | EXPERIMENTAL: delegate custody to a BRC-100 wallet app — see below |
59
+ | `--brc100` | Reserved; points you at `--experimental-brc100` |
60
+
61
+ ### `bsv-pay balance`
62
+
63
+ Confirmed and unconfirmed balance across every address the wallet has issued.
64
+ JSON shape: `{ok, confirmed_sats, unconfirmed_sats, addresses: [...]}`.
65
+ No passphrase needed (addresses come from the local ledger).
66
+
67
+ ### `bsv-pay send <address> <amount> ["memo"]`
68
+
69
+ Builds, confirms, and broadcasts a payment. Always shows recipient, amount,
70
+ fee, and resulting balance before broadcasting.
71
+
72
+ | Flag | Meaning |
73
+ | --- | --- |
74
+ | `-y, --yes` | Skip the confirmation prompt (spend limit still enforced) |
75
+ | `--allow-large` | With `--yes`, permit sends at/above the spend limit |
76
+ | `--dry-run` | Build and sign but never broadcast; persists nothing |
77
+ | `--confirmed-only` | Don't spend unconfirmed UTXOs (spent by default) |
78
+
79
+ Memos are stored only in your local ledger — never on-chain.
80
+
81
+ ### `bsv-pay request <amount> ["memo"]`
82
+
83
+ Derives a fresh receiving address, prints a BIP-21 URI
84
+ (`bitcoin:<addr>?sv&amount=<bsv>&label=<memo>`) and a terminal QR code
85
+ (suppressed when piped or with `--json`).
86
+
87
+ | Flag | Meaning |
88
+ | --- | --- |
89
+ | `--wait` | Poll until the payment is seen at 0-conf, then exit 0 with the txid |
90
+ | `--timeout <sec>` | With `--wait`, give up after this many seconds (default 600, exit 4) |
91
+
92
+ With `--json --wait` the output is NDJSON: first a `request_created` object
93
+ (so your script has the address), then a `payment_received` object.
94
+
95
+ ### `bsv-pay watch`
96
+
97
+ Polls all tracked addresses (default every 10s, `--interval <sec>`, floor 5s)
98
+ and reports incoming payments at 0-conf as `pending`, then `confirmed`. Shows
99
+ the memo when the payment matches a request address, plus a session running
100
+ total. `--json` emits one NDJSON object per event. Rate limits back off
101
+ gracefully; Ctrl-C exits cleanly with a session summary.
102
+
103
+ ### `bsv-pay donate [amount]`
104
+
105
+ Sends a donation (default 10,000 sats) to the project donation address
106
+ (`131CswxfV8Swi8zUSc3XfH9tEJLxzxmpa4`). On testnet the address is still a
107
+ placeholder — the command warns; use `--dry-run` there.
108
+
109
+ ### `bsv-pay policy show` / `bsv-pay policy test <address> <amount>`
110
+
111
+ `show` prints the active policy, live budget usage, and pending approvals.
112
+ `test` dry-runs a decision without sending or recording anything:
113
+ exit 0 = would allow, 8 = would deny, 9 = would queue for approval.
114
+
115
+ ### `bsv-pay approvals list|approve <id>|reject <id>|set-secret`
116
+
117
+ Reviews and resolves payments queued by `approval_threshold_sats`.
118
+ `approve`, `reject`, and `set-secret` are **interactive only**: they require
119
+ a real terminal and the approval secret. There is deliberately no flag or
120
+ environment variable for the secret — see the policy section below.
121
+
122
+ ### `bsv-pay fetch <url>`
123
+
124
+ Fetch a URL, automatically paying a BRC-105 `402 Payment Required` response
125
+ within policy. `--max-price <amount>` refuses to pay more than that for this
126
+ fetch, regardless of remaining budget. The body is the machine output (raw
127
+ on stdout, or in `--json`); payment details go to stderr. Free resources
128
+ cost nothing. See "Machine-to-machine payments" below.
129
+
130
+ ### `bsv-pay serve --price <amount>`
131
+
132
+ A demo BRC-105 paywall: every request pays `--price` into this wallet before
133
+ it gets the content (`--port`, default 8402; `--host`, default localhost-only;
134
+ `--body` for the content). The real product is the importable
135
+ `requirePayment()` middleware this wraps — see below.
136
+
137
+ ### `bsv-pay mcp`
138
+
139
+ Serves MCP tools over stdio for AI agents (`pay`, `paid_fetch`,
140
+ `create_payment_request`, `await_payment`, `get_balance`, `get_history`,
141
+ `get_policy_status`). The
142
+ wallet unlocks once at startup — `BSV_PAY_PASSPHRASE` or a terminal prompt —
143
+ and there is deliberately no unlock, approve, or key tool, so the connected
144
+ agent never holds a secret. Every `pay` goes through the same policy gate as
145
+ the CLI. See "Using bsv-pay with Claude Code" below.
146
+
147
+ ## Amounts
148
+
149
+ Bare numbers are **satoshis**. Suffixes `sats` and `bsv` are accepted:
150
+ `5000`, `5000sats`, `0.0001bsv`. Anything ambiguous (`5,000`, `1e3`,
151
+ fractional sats) is an error — bsv-pay never guesses.
152
+
153
+ ## Policy engine — `~/.bsv-pay/policy.toml`
154
+
155
+ Budgets and rules that sit **below** every spend path — CLI, library, and
156
+ MCP tools. No flag, parameter, or tool argument can cross a
157
+ policy.toml rule; only editing the file (and restarting any long-running
158
+ process) changes limits. Without a policy.toml nothing changes: only the
159
+ legacy `spend_limit_sats` confirm threshold from config.toml applies.
160
+
161
+ ```toml
162
+ per_tx_limit_sats = 50000 # HARD cap per transaction (no --allow-large escape)
163
+ daily_budget_sats = 200000 # rolling 24h total, recomputed from the ledger
164
+ session_budget_sats = 100000 # per long-running process (e.g. an MCP server)
165
+ rate_limit_per_minute = 6 # max payments per minute
166
+ rate_limit_per_hour = 60 # and per hour
167
+ approval_threshold_sats = 25000 # at/above this, queue for human approval (exit 9)
168
+ allowlist = [] # when non-empty, ONLY these recipients
169
+ denylist = [] # always wins
170
+
171
+ [network.test] # optional per-network overrides
172
+ daily_budget_sats = 1000000
173
+ ```
174
+
175
+ - Every decision — allow, deny, or queue — is appended to the ledger with
176
+ the rule and reason. Denials exit 8 with a machine-readable `error`
177
+ (`daily_budget_exceeded`, `recipient_denied`, …) and useful numbers
178
+ (`remaining_sats`) so scripts and agents can adapt instead of retrying.
179
+ - Daily budgets and rate limits are recomputed from the append-only ledger
180
+ at every decision — restarting a process never resets them. Unknown-status
181
+ broadcasts count as spent. Typos in policy.toml are hard errors, never
182
+ silently ignored.
183
+ - **Approvals**: a queued payment is sent only after a human runs
184
+ `bsv-pay approvals approve <id>` and types the **approval secret** — a
185
+ second secret, separate from the wallet passphrase, stored only as an
186
+ argon2id hash. An agent holding `BSV_PAY_PASSPHRASE` cannot approve its
187
+ own payment: the wallet passphrase is not accepted, and there is no
188
+ non-interactive path. Approval re-checks every rule against today's
189
+ ledger — it satisfies the threshold, never the budgets.
190
+ - **Threat model, honestly**: the policy engine governs anything that spends
191
+ *through* bsv-pay. An actor with write access to `~/.bsv-pay` (or
192
+ arbitrary code plus your passphrase) can bypass any local tool — so don't
193
+ give agents the passphrase. The recommended agent setup is `bsv-pay mcp`:
194
+ the server holds the unlocked wallet while the agent gets only budgeted
195
+ tools.
196
+
197
+ ## Using bsv-pay with Claude Code
198
+
199
+ ```bash
200
+ claude mcp add bsv-pay --env BSV_PAY_PASSPHRASE=your-passphrase -- bsv-pay mcp --testnet
201
+ ```
202
+
203
+ The server process holds the passphrase; Claude gets seven tools and nothing
204
+ else — no unlock, no approvals, no keys, no way to raise its own limits.
205
+ Policy edits apply on server restart (session budgets reset with the
206
+ process; daily budgets never reset — they are recomputed from the ledger).
207
+
208
+ A budget-governed session against this `~/.bsv-pay/policy.toml`:
209
+
210
+ ```toml
211
+ per_tx_limit_sats = 8000
212
+ daily_budget_sats = 12000
213
+ approval_threshold_sats = 1500
214
+ ```
215
+
216
+ The agent plans within its allowance instead of discovering limits by
217
+ failing — and when it crosses one anyway, the refusal is a structured
218
+ result it can read, not an opaque error:
219
+
220
+ ```
221
+ get_policy_status → { ok: true, daily_remaining_sats: 2300, pending_approvals: [], … }
222
+ pay (800 sats) → { ok: true, txid: "d6d818f6…", fee_sats: 12, … }
223
+ pay (1600 sats) → { ok: false, error: "daily_budget_exceeded", remaining_sats: 1500, … }
224
+ pay (1500 sats) → { ok: false, error: "pending_approval", approval_id: "8b1f42…", … }
225
+ ```
226
+
227
+ That last payment was **not sent** — it is queued for you:
228
+
229
+ ```bash
230
+ bsv-pay approvals list # review what the agent wants to pay
231
+ bsv-pay approvals approve 8b1f42 # type the approval secret to release it
232
+ ```
233
+
234
+ Every decision — allowed, denied, or queued — lands in the append-only
235
+ ledger with its rule and reason, so you can audit exactly what the agent
236
+ did and what it tried to do. The same loop runs end-to-end in CI against a
237
+ local mock chain (`npm run e2e:local`, step 9), so none of the above
238
+ depends on live coins to verify.
239
+
240
+ ## Machine-to-machine payments — HTTP 402 (BRC-105)
241
+
242
+ Buy:
243
+
244
+ ```bash
245
+ bsv-pay fetch https://seller.example/dataset --max-price 1000
246
+ ```
247
+
248
+ On a `402 Payment Required`, fetch reads the BRC-105 headers, pays within
249
+ policy (the same gate, budgets, and ledger as `send`), retries with the
250
+ `x-bsv-payment` envelope, and prints the content. Agents get the same flow
251
+ as the MCP `paid_fetch` tool.
252
+
253
+ Sell — either the demo server:
254
+
255
+ ```bash
256
+ bsv-pay serve --price 50sats --port 8402 --body "premium data"
257
+ ```
258
+
259
+ or the importable middleware (Express-compatible, zero dependencies):
260
+
261
+ ```js
262
+ import { openWallet, requirePayment } from 'bsv-pay/core';
263
+
264
+ const wallet = await openWallet({ network: 'test' });
265
+ const gate = requirePayment({ network: 'test', wallet, priceSats: 50 });
266
+ app.use(gate); // req.bsvPayment = { txid, amountSats, address, … } once paid
267
+ ```
268
+
269
+ Each 402 quotes a **fresh wallet address** with a single-use nonce prefix
270
+ (10-minute TTL); the seller confirms the payment on its own chain view
271
+ before serving and ledgers the receive. The buyer broadcasts through the
272
+ policy gate, so a 402 spend can be denied (exit 8), capped (`--max-price`,
273
+ exit 8 before any spend), or queued for approval (exit 9) exactly like any
274
+ other payment. Exit 10 means you paid but the server refused the content —
275
+ the txid is in the error, take it up with the seller.
276
+
277
+ **Compatibility, honestly**: this is a simplified BRC-105 profile — same
278
+ headers, flow, and version, but the payment destination is an advertised
279
+ fresh address rather than BRC-29 derived keys, and the envelope carries raw
280
+ tx hex rather than AtomicBEEF. bsv-pay's fetch and serve interoperate with
281
+ each other today (including under BRC-100 custody, where the external
282
+ wallet signs the 402 payment). Interop with external full-BRC-105 services
283
+ needs the SDK's AuthFetch (BRC-103/104 mutual auth); that integration is
284
+ deferred — there is no server-side implementation in our dependency set to
285
+ test a handshake against, and we won't ship untestable code in the spend
286
+ path. Details and the full reasoning in DECISIONS.md (M12).
287
+
288
+ ## External wallet custody — BRC-100 (EXPERIMENTAL)
289
+
290
+ ```bash
291
+ bsv-pay init --experimental-brc100 --testnet
292
+ ```
293
+
294
+ Instead of a local seed, bsv-pay connects to a BRC-100 wallet app running
295
+ on your machine (e.g. Metanet Desktop, which serves the wallet JSON-API on
296
+ `localhost:3321`; override with `BSV_PAY_BRC100_URL`). Keys live in the
297
+ wallet app and **never** touch bsv-pay; bsv-pay constructs payment actions
298
+ and the app funds, signs, and broadcasts them — asking for your approval in
299
+ its own UI as it sees fit.
300
+
301
+ **The policy engine stays in front.** Every spend still passes the same
302
+ `authorizeSpend()` gate — budgets, rate limits, allow/denylists, approval
303
+ queue — *before* the wallet app is ever asked, and every decision is
304
+ ledgered. The wallet app is a second pair of hands, not a way around your
305
+ policy. That layering is the point: the app protects the keys, bsv-pay
306
+ governs the spending.
307
+
308
+ What works under BRC-100 custody today, and what doesn't:
309
+
310
+ | Surface | Status |
311
+ | --- | --- |
312
+ | `send`, `donate`, `fetch` (402 buyer), MCP `pay` + `paid_fetch` | ✅ governed by policy, ledgered, exact fee reported |
313
+ | `balance` (one spendable total from the app), `history`, `policy`, `approvals` | ✅ |
314
+ | `request`, `watch`, `serve` / `requirePayment()`, MCP request tools | ❌ exit 2 `brc100_receive_not_supported` — receive in the wallet app itself |
315
+
316
+ Receiving refuses by design rather than half-working: an address issued by
317
+ bsv-pay would be invisible to the wallet app, so funds sent there could not
318
+ be seen or spent from it. Use the app's own receive screen, or a local-seed
319
+ wallet for the selling side.
320
+
321
+ Setup and a step-by-step verification walkthrough: [docs/BRC100.md](docs/BRC100.md).
322
+
323
+ ## Exit codes (stable)
324
+
325
+ | Code | Meaning |
326
+ | --- | --- |
327
+ | 0 | Success |
328
+ | 1 | Unexpected error |
329
+ | 2 | Invalid usage, address, amount/unit, or config |
330
+ | 3 | Insufficient funds |
331
+ | 4 | Network/API error (after one automatic retry) — also `--wait` timeout |
332
+ | 5 | Broadcast rejected by the network |
333
+ | 6 | Broadcast sent but status unknown (txid is still printed — check before retrying) |
334
+ | 7 | Wallet locked / bad passphrase (also: wrong approval secret) |
335
+ | 8 | Spend limit exceeded / denied by policy (`error` says which rule) |
336
+ | 9 | Queued for human approval (`approval_id` in `--json`; see `bsv-pay approvals`) |
337
+ | 10 | 402 payment broadcast but the server refused the content (`txid` in the error) |
338
+
339
+ ## Scripting
340
+
341
+ Every command takes `--json`: a single JSON object on stdout (NDJSON for
342
+ `watch` and `request --wait`), errors as
343
+ `{"ok": false, "code": <int>, "error": "<snake_case>", "message": "..."}`.
344
+ All prompts and human text go to stderr.
345
+
346
+ ```bash
347
+ export BSV_PAY_PASSPHRASE=... # unlock without a prompt
348
+ ADDR=$(bsv-pay request 5000 --json | jq -r .address)
349
+ bsv-pay send "$DEST" 5000sats --yes --json | jq -r .txid
350
+ ```
351
+
352
+ The whole receive→send loop is scriptable with `--json` + exit codes alone.
353
+
354
+ ## Library usage — `bsv-pay/core`
355
+
356
+ The CLI is a thin layer over an importable engine. `bsv-pay/core` exposes the
357
+ same operations with typed results and typed errors — no prompts, no console
358
+ output, no `process.exit`, and never any key material in a return value:
359
+
360
+ ```ts
361
+ import { openWallet, getBalance, send, createRequest, awaitPayment, BsvPayError } from 'bsv-pay/core';
362
+
363
+ const opts = { network: 'test' } as const;
364
+ const wallet = await openWallet({ ...opts, passphrase: process.env.WALLET_PASS });
365
+
366
+ const { confirmedSats, unconfirmedSats } = await getBalance(opts);
367
+
368
+ try {
369
+ const result = await send(wallet, opts, { to: address, amountSats: 5000, memo: 'thanks' });
370
+ console.log(result.txid, result.feeSats);
371
+ } catch (e) {
372
+ if (e instanceof BsvPayError) console.error(e.errorCode, e.exitCode); // e.g. insufficient_funds, 3
373
+ }
374
+
375
+ const invoice = createRequest(wallet, { amountSats: 10_000, memo: 'invoice #7' });
376
+ const paid = await awaitPayment(opts, { address: invoice.address, timeoutMs: 600_000 });
377
+ ```
378
+
379
+ `BsvPayError.exitCode` carries the same stable numbers as the CLI exit codes
380
+ below; `errorCode` is the same snake_case string `--json` emits. The config
381
+ spend limit applies to library sends too (`allowAboveLimit` mirrors
382
+ `--allow-large`); ledger entries are written exactly as the CLI writes them.
383
+ `planSend()`/`executeSend()` split the flow when you need to show fees before
384
+ committing, and `getHistory()` reads the local ledger. Wallet *creation* is
385
+ CLI-only for now — run `bsv-pay init` first.
386
+
387
+ ## Configuration — `~/.bsv-pay/config.toml`
388
+
389
+ ```toml
390
+ network = "main" # or "test"
391
+ fee_rate_sats_per_kb = 50 # miner fee rate
392
+ poll_interval_secs = 10 # watch/request --wait cadence (floor 5)
393
+ spend_limit_sats = 100000 # per-transaction confirm threshold
394
+ fiat_display = false
395
+ ```
396
+
397
+ All keys are optional; the values above are the defaults.
398
+
399
+ ## Security notes
400
+
401
+ - **Seed encryption at rest**: argon2id-derived key + AES-256-GCM; wallet and
402
+ ledger files are written `0600` under `~/.bsv-pay/`.
403
+ - **Passphrase**: interactive prompt, or `BSV_PAY_PASSPHRASE` for scripts.
404
+ `--no-encrypt` exists but warns on every run.
405
+ - **Keys never leave the machine** and never appear in logs, errors, the
406
+ ledger, or `--json` output. Transactions are signed locally; only raw signed
407
+ hex is sent to the API.
408
+ - **Spend limit**: sends at/above `spend_limit_sats` (default 100k) require an
409
+ explicit interactive confirmation or `--yes --allow-large`.
410
+ - **Address checksums** (and network prefix) are validated before any network
411
+ call — a mainnet/testnet mix-up is exit 2, not lost coins.
412
+ - **Hot-wallet framing**: this is for micropayments. Do not store more than
413
+ you would carry in cash. Testnet state lives in separate files from mainnet.
414
+ - The chain API is WhatsOnChain behind a `ChainProvider` interface; swap it by
415
+ implementing five methods.
416
+
417
+ ## Development
418
+
419
+ ```bash
420
+ npm install
421
+ npm test # vitest unit suite (mock provider, no live network)
422
+ npm run lint
423
+ npm run build
424
+ npm run e2e:local # full loop through the real CLI against a local mock API
425
+ node scripts/e2e-testnet.mjs # live testnet loop, needs BSV_PAY_E2E=1 + faucet coins
426
+ ```
427
+
428
+ `e2e:local` runs the whole definition-of-done loop (init → request → payment →
429
+ watch detects → send back → balance reconciles) by spawning the actual binary
430
+ against a local WhatsOnChain-compatible server — no coins or captchas needed —
431
+ then re-runs the loop through the built `bsv-pay/core` library against the
432
+ same mock. Point the CLI at any WoC-compatible API with `BSV_PAY_API_URL`.
433
+
434
+ The local ledger (`~/.bsv-pay/ledger.jsonl`) is append-only JSONL recording
435
+ every send, receive, and issued address.
@@ -0,0 +1,6 @@
1
+ import type { Network } from './paths.js';
2
+ /**
3
+ * Validate a P2PKH address checksum and network prefix BEFORE any network
4
+ * call (invariant 4). Throws exit 2 on failure.
5
+ */
6
+ export declare function validateAddress(address: string, network: Network): void;
@@ -0,0 +1,35 @@
1
+ import { Utils } from '@bsv/sdk';
2
+ import { usageError } from './errors.js';
3
+ const MAINNET_P2PKH_PREFIX = 0x00;
4
+ const TESTNET_P2PKH_PREFIX = 0x6f;
5
+ /**
6
+ * Validate a P2PKH address checksum and network prefix BEFORE any network
7
+ * call (invariant 4). Throws exit 2 on failure.
8
+ */
9
+ export function validateAddress(address, network) {
10
+ let prefix;
11
+ let data;
12
+ try {
13
+ const decoded = Utils.fromBase58Check(address);
14
+ prefix = decoded.prefix;
15
+ data = decoded.data;
16
+ }
17
+ catch {
18
+ throw usageError('invalid_address', `"${address}" is not a valid BSV address (base58check checksum failed). Check for typos.`);
19
+ }
20
+ if (data.length !== 20 || prefix.length !== 1) {
21
+ throw usageError('invalid_address', `"${address}" is not a P2PKH address. Only standard P2PKH addresses are supported.`);
22
+ }
23
+ const expected = network === 'test' ? TESTNET_P2PKH_PREFIX : MAINNET_P2PKH_PREFIX;
24
+ if (prefix[0] !== expected) {
25
+ const wrongNet = prefix[0] === MAINNET_P2PKH_PREFIX
26
+ ? 'mainnet'
27
+ : prefix[0] === TESTNET_P2PKH_PREFIX
28
+ ? 'testnet'
29
+ : 'an unknown network';
30
+ throw usageError('wrong_network_address', `"${address}" is ${wrongNet === 'an unknown network' ? 'for' : 'a'} ${wrongNet} address but you are on ${network === 'test' ? 'testnet' : 'mainnet'}. ` +
31
+ (network === 'test'
32
+ ? 'Drop --testnet or use a testnet address.'
33
+ : 'Pass --testnet or use a mainnet address.'));
34
+ }
35
+ }
@@ -0,0 +1,35 @@
1
+ import type { Network } from '../paths.js';
2
+ export interface Utxo {
3
+ txid: string;
4
+ vout: number;
5
+ satoshis: number;
6
+ /** Block height; 0 or undefined means unconfirmed (mempool). */
7
+ height?: number;
8
+ }
9
+ export interface AddressBalance {
10
+ confirmed: number;
11
+ unconfirmed: number;
12
+ }
13
+ export interface HistoryItem {
14
+ txid: string;
15
+ /** 0 or -1 means unconfirmed. */
16
+ height: number;
17
+ }
18
+ export interface BroadcastResult {
19
+ ok: boolean;
20
+ txid?: string;
21
+ /** Miner/API rejection message when ok is false. */
22
+ error?: string;
23
+ }
24
+ /**
25
+ * Chain access abstraction. WhatsOnChain is the default implementation;
26
+ * keep this interface provider-agnostic so it can be swapped later.
27
+ */
28
+ export interface ChainProvider {
29
+ readonly network: Network;
30
+ getBalance(address: string): Promise<AddressBalance>;
31
+ getUtxos(address: string): Promise<Utxo[]>;
32
+ getHistory(address: string): Promise<HistoryItem[]>;
33
+ getRawTx(txid: string): Promise<string>;
34
+ broadcast(rawTxHex: string): Promise<BroadcastResult>;
35
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import type { Network } from '../paths.js';
2
+ import type { AddressBalance, BroadcastResult, ChainProvider, HistoryItem, Utxo } from './provider.js';
3
+ export declare class WhatsOnChainProvider implements ChainProvider {
4
+ readonly network: Network;
5
+ readonly baseUrl: string;
6
+ constructor(network: Network);
7
+ /**
8
+ * GET with one automatic retry (invariant 5). 429s wait longer before the
9
+ * retry; persistent failure is exit 4.
10
+ */
11
+ private get;
12
+ private getJson;
13
+ getBalance(address: string): Promise<AddressBalance>;
14
+ getUtxos(address: string): Promise<Utxo[]>;
15
+ getHistory(address: string): Promise<HistoryItem[]>;
16
+ getRawTx(txid: string): Promise<string>;
17
+ /**
18
+ * Broadcast is NOT silently retried: a retry after an ambiguous failure
19
+ * could double-report. A definitive API rejection returns ok=false; an
20
+ * ambiguous network failure throws (caller maps to exit 6, status unknown).
21
+ */
22
+ broadcast(rawTxHex: string): Promise<BroadcastResult>;
23
+ }