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.
- package/CHANGELOG.md +79 -0
- package/LICENSE +21 -0
- package/README.md +435 -0
- package/dist/address.d.ts +6 -0
- package/dist/address.js +35 -0
- package/dist/chain/provider.d.ts +35 -0
- package/dist/chain/provider.js +1 -0
- package/dist/chain/whatsonchain.d.ts +23 -0
- package/dist/chain/whatsonchain.js +98 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +169 -0
- package/dist/commands/approvals.d.ts +19 -0
- package/dist/commands/approvals.js +112 -0
- package/dist/commands/balance.d.ts +3 -0
- package/dist/commands/balance.js +28 -0
- package/dist/commands/donate.d.ts +8 -0
- package/dist/commands/donate.js +16 -0
- package/dist/commands/fetch.d.ts +13 -0
- package/dist/commands/fetch.js +49 -0
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.js +188 -0
- package/dist/commands/mcp.d.ts +13 -0
- package/dist/commands/mcp.js +32 -0
- package/dist/commands/policy.d.ts +8 -0
- package/dist/commands/policy.js +101 -0
- package/dist/commands/request.d.ts +9 -0
- package/dist/commands/request.js +85 -0
- package/dist/commands/send.d.ts +11 -0
- package/dist/commands/send.js +125 -0
- package/dist/commands/serve.d.ts +16 -0
- package/dist/commands/serve.js +59 -0
- package/dist/commands/watch.d.ts +10 -0
- package/dist/commands/watch.js +163 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +51 -0
- package/dist/context.d.ts +13 -0
- package/dist/context.js +12 -0
- package/dist/core/balance.d.ts +24 -0
- package/dist/core/balance.js +34 -0
- package/dist/core/context.d.ts +27 -0
- package/dist/core/context.js +9 -0
- package/dist/core/history.d.ts +18 -0
- package/dist/core/history.js +15 -0
- package/dist/core/index.d.ts +22 -0
- package/dist/core/index.js +17 -0
- package/dist/core/internal.d.ts +22 -0
- package/dist/core/internal.js +19 -0
- package/dist/core/policy-status.d.ts +55 -0
- package/dist/core/policy-status.js +49 -0
- package/dist/core/request.d.ts +43 -0
- package/dist/core/request.js +77 -0
- package/dist/core/send.d.ts +108 -0
- package/dist/core/send.js +277 -0
- package/dist/core/spend-lock.d.ts +2 -0
- package/dist/core/spend-lock.js +25 -0
- package/dist/core/wallet.d.ts +53 -0
- package/dist/core/wallet.js +77 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +39 -0
- package/dist/http402/client.d.ts +32 -0
- package/dist/http402/client.js +85 -0
- package/dist/http402/middleware.d.ts +37 -0
- package/dist/http402/middleware.js +96 -0
- package/dist/http402/protocol.d.ts +50 -0
- package/dist/http402/protocol.js +114 -0
- package/dist/ledger.d.ts +51 -0
- package/dist/ledger.js +27 -0
- package/dist/mcp/server.d.ts +32 -0
- package/dist/mcp/server.js +484 -0
- package/dist/output.d.ts +17 -0
- package/dist/output.js +44 -0
- package/dist/paths.d.ts +11 -0
- package/dist/paths.js +24 -0
- package/dist/policy/approvals.d.ts +24 -0
- package/dist/policy/approvals.js +89 -0
- package/dist/policy/budget.d.ts +16 -0
- package/dist/policy/budget.js +47 -0
- package/dist/policy/engine.d.ts +76 -0
- package/dist/policy/engine.js +199 -0
- package/dist/policy/policy.d.ts +30 -0
- package/dist/policy/policy.js +126 -0
- package/dist/prompt.d.ts +11 -0
- package/dist/prompt.js +57 -0
- package/dist/tx.d.ts +29 -0
- package/dist/tx.js +68 -0
- package/dist/units.d.ts +13 -0
- package/dist/units.js +53 -0
- package/dist/wallet/brc100.d.ts +105 -0
- package/dist/wallet/brc100.js +217 -0
- package/dist/wallet/crypto.d.ts +25 -0
- package/dist/wallet/crypto.js +46 -0
- package/dist/wallet/wallet.d.ts +86 -0
- package/dist/wallet/wallet.js +186 -0
- package/docs/AGENTIC-PAYMENTS.md +218 -0
- package/docs/BRC100.md +151 -0
- 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.
|
package/dist/address.js
ADDED
|
@@ -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
|
+
}
|