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
@@ -0,0 +1,186 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { HD, Mnemonic, PrivateKey } from '@bsv/sdk';
4
+ import { CliError, EXIT } from '../errors.js';
5
+ import { appendLedger } from '../ledger.js';
6
+ import { walletPath } from '../paths.js';
7
+ import { askHidden, isInteractive } from '../prompt.js';
8
+ import { decryptSecret, encryptSecret } from './crypto.js';
9
+ /** BIP-44, BSV coin type 236. chain 0 = receive, 1 = change. */
10
+ const DERIVATION_BASE = "m/44'/236'/0'";
11
+ export function walletExists(network) {
12
+ return fs.existsSync(walletPath(network));
13
+ }
14
+ export function readWalletFile(network) {
15
+ const file = walletPath(network);
16
+ if (!fs.existsSync(file)) {
17
+ throw new CliError(EXIT.USAGE, 'no_wallet', `No ${network === 'test' ? 'testnet ' : ''}wallet found. Run "bsv-pay init${network === 'test' ? ' --testnet' : ''}" first.`);
18
+ }
19
+ try {
20
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
21
+ }
22
+ catch {
23
+ throw new CliError(EXIT.UNEXPECTED, 'corrupt_wallet', `Wallet file ${file} is not valid JSON. Restore it from backup or re-run init --force.`);
24
+ }
25
+ }
26
+ export function writeWalletFile(network, wallet) {
27
+ const file = walletPath(network);
28
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
29
+ fs.writeFileSync(file, JSON.stringify(wallet, null, 2) + '\n', { mode: 0o600 });
30
+ }
31
+ export function buildWalletFile(network, secret, passphrase) {
32
+ const base = {
33
+ version: 1,
34
+ network,
35
+ next_receive_index: 0,
36
+ next_change_index: 0,
37
+ created_at: new Date().toISOString(),
38
+ };
39
+ if (passphrase === null) {
40
+ return { ...base, encrypted: false, secret };
41
+ }
42
+ const { kdf, cipher } = encryptSecret(JSON.stringify(secret), passphrase);
43
+ return { ...base, encrypted: true, kdf, cipher };
44
+ }
45
+ /** Wallet file for external BRC-100 custody: no secret, no counters in use. */
46
+ export function buildBrc100WalletFile(network, url) {
47
+ return {
48
+ version: 1,
49
+ network,
50
+ encrypted: false,
51
+ backend: 'brc100',
52
+ brc100_url: url,
53
+ next_receive_index: 0,
54
+ next_change_index: 0,
55
+ created_at: new Date().toISOString(),
56
+ };
57
+ }
58
+ export const UNENCRYPTED_WALLET_WARNING = 'WARNING: this wallet stores its seed UNENCRYPTED on disk. Anyone who can read\n' +
59
+ '~/.bsv-pay can spend your funds. Re-run "bsv-pay init --force" to encrypt.';
60
+ /**
61
+ * Resolve the passphrase: env var for scripts, otherwise interactive prompt.
62
+ * Exported so CLI commands can hand this exact flow to core's openWallet().
63
+ */
64
+ export async function obtainPassphrase(supplied) {
65
+ if (typeof supplied === 'string')
66
+ return supplied;
67
+ if (typeof supplied === 'function')
68
+ return supplied();
69
+ const env = process.env.BSV_PAY_PASSPHRASE;
70
+ if (env !== undefined)
71
+ return env;
72
+ if (!isInteractive()) {
73
+ throw new CliError(EXIT.WALLET_LOCKED, 'passphrase_required', 'Wallet is encrypted and no terminal is available to prompt. Set BSV_PAY_PASSPHRASE for scripted use.');
74
+ }
75
+ return askHidden('Wallet passphrase: ');
76
+ }
77
+ /** An unlocked wallet: can derive addresses and signing keys. */
78
+ export class Wallet {
79
+ network;
80
+ file;
81
+ secret;
82
+ hd;
83
+ constructor(network, file, secret, hd) {
84
+ this.network = network;
85
+ this.file = file;
86
+ this.secret = secret;
87
+ this.hd = hd;
88
+ }
89
+ static async unlock(network, options = {}) {
90
+ const file = readWalletFile(network);
91
+ if (file.backend === 'brc100') {
92
+ // Defensive: core openWallet() branches to the BRC-100 backend before
93
+ // ever reaching here; nothing else should try a local unlock.
94
+ throw new CliError(EXIT.USAGE, 'brc100_no_local_keys', 'This wallet delegates custody to an external BRC-100 wallet; there is no local seed to unlock.');
95
+ }
96
+ const warn = options.onWarning ?? ((text) => process.stderr.write(text + '\n'));
97
+ let secret;
98
+ if (file.encrypted) {
99
+ if (!file.kdf || !file.cipher) {
100
+ throw new CliError(EXIT.UNEXPECTED, 'corrupt_wallet', 'Wallet file is marked encrypted but has no cipher data.');
101
+ }
102
+ const passphrase = await obtainPassphrase(options.passphrase);
103
+ secret = JSON.parse(decryptSecret(file.kdf, file.cipher, passphrase));
104
+ }
105
+ else {
106
+ warn(UNENCRYPTED_WALLET_WARNING);
107
+ if (!file.secret) {
108
+ throw new CliError(EXIT.UNEXPECTED, 'corrupt_wallet', 'Wallet file has no secret payload.');
109
+ }
110
+ secret = file.secret;
111
+ }
112
+ const hd = secret.type === 'mnemonic'
113
+ ? HD.fromSeed(Mnemonic.fromString(secret.value).toSeed()).derive(DERIVATION_BASE)
114
+ : null;
115
+ return new Wallet(network, file, secret, hd);
116
+ }
117
+ /**
118
+ * Read-only view (no passphrase needed): tracked addresses can be derived
119
+ * only for unencrypted wallets; encrypted wallets still require unlock, so
120
+ * commands that just need addresses use the ledger instead. Kept private —
121
+ * commands should use Wallet.unlock or the ledger.
122
+ */
123
+ get isHd() {
124
+ return this.hd !== null;
125
+ }
126
+ keyAt(chain, index) {
127
+ if (this.hd)
128
+ return this.hd.derive(`m/${chain}/${index}`).privKey;
129
+ return PrivateKey.fromWif(this.secret.value);
130
+ }
131
+ addressAt(chain, index) {
132
+ const priv = this.keyAt(chain, index);
133
+ return this.network === 'test' ? priv.toAddress('testnet') : priv.toAddress();
134
+ }
135
+ /** Every address this wallet has ever issued (receive + change chains). */
136
+ trackedAddresses() {
137
+ if (!this.isHd)
138
+ return [{ address: this.addressAt(0, 0), chain: 0, index: 0 }];
139
+ const list = [];
140
+ // index 0 is always tracked, even before the first explicit issue
141
+ const receiveCount = Math.max(1, this.file.next_receive_index);
142
+ for (let i = 0; i < receiveCount; i++) {
143
+ list.push({ address: this.addressAt(0, i), chain: 0, index: i });
144
+ }
145
+ for (let i = 0; i < this.file.next_change_index; i++) {
146
+ list.push({ address: this.addressAt(1, i), chain: 1, index: i });
147
+ }
148
+ return list;
149
+ }
150
+ privKeyForAddress(address) {
151
+ const hit = this.trackedAddresses().find((a) => a.address === address);
152
+ return hit ? this.keyAt(hit.chain, hit.index) : undefined;
153
+ }
154
+ /** Next address for a purpose WITHOUT persisting anything (dry runs). */
155
+ peekAddress(purpose) {
156
+ if (!this.isHd)
157
+ return { address: this.addressAt(0, 0), index: 0 };
158
+ const chain = purpose === 'change' ? 1 : 0;
159
+ const index = chain === 0 ? this.file.next_receive_index : this.file.next_change_index;
160
+ return { address: this.addressAt(chain, index), index };
161
+ }
162
+ /** Derive a fresh address, persist the counter, and record it in the ledger. */
163
+ issueAddress(purpose, memo) {
164
+ if (!this.isHd) {
165
+ // WIF wallets have a single address; "issuing" returns it without counters.
166
+ return { address: this.addressAt(0, 0), index: 0 };
167
+ }
168
+ const chain = purpose === 'change' ? 1 : 0;
169
+ const index = chain === 0 ? this.file.next_receive_index : this.file.next_change_index;
170
+ const address = this.addressAt(chain, index);
171
+ if (chain === 0)
172
+ this.file.next_receive_index = index + 1;
173
+ else
174
+ this.file.next_change_index = index + 1;
175
+ writeWalletFile(this.network, this.file);
176
+ appendLedger(this.network, {
177
+ type: 'address_issued',
178
+ address,
179
+ derivation_index: index,
180
+ purpose,
181
+ memo,
182
+ timestamp: new Date().toISOString(),
183
+ });
184
+ return { address, index };
185
+ }
186
+ }
@@ -0,0 +1,218 @@
1
+ # Agentic payments with bsv-pay
2
+
3
+ Everyone is wiring agents up to money. Almost nobody governs *how much,
4
+ to whom, and how fast*. bsv-pay's answer: a policy engine that sits
5
+ **below** the agent — in the payment tool itself — plus first-class MCP
6
+ integration so any agent framework can pay without ever touching a key.
7
+
8
+ This guide covers the threat model, MCP setup for Claude Code / Claude
9
+ Desktop / Cursor, the HTTP 402 flow, and external (BRC-100) custody.
10
+
11
+ ## The threat model — why policy lives below the agent
12
+
13
+ An LLM agent that can spend money can be wrong about spending money. The
14
+ failure modes are not exotic:
15
+
16
+ - **Prompt injection.** Anything the agent reads — a web page, a paid
17
+ dataset, a tool result — is input. "Send 2,000 sats to X to keep your
18
+ access" is an instruction the agent may follow. (The
19
+ [two-agent demo](../examples/two-agents/) does exactly this to itself.)
20
+ - **Runaway loops.** A retry loop with a payment inside it is a money
21
+ pump. Same for "buy until the task is done" with a mispriced seller.
22
+ - **Plain misjudgment.** The agent overvalues a resource, mis-parses a
23
+ price, or pays the wrong address.
24
+
25
+ Telling the agent "please stay under 10,000 sats" is a system-prompt
26
+ suggestion. bsv-pay makes it physics instead:
27
+
28
+ - Every spend path — CLI, library, MCP tool, 402 client — funnels through
29
+ **one `authorizeSpend()` gate** in core. There is no flag, parameter, or
30
+ tool argument that crosses a policy rule. Only a human editing
31
+ `policy.toml` (and restarting a long-running server) changes limits.
32
+ - The agent **never holds a secret**. The MCP server unlocks the wallet at
33
+ startup (env passphrase or terminal prompt); there is no unlock, export,
34
+ or approve tool. Keys never appear in any tool result, error, or log —
35
+ enforced by an executable key-boundary test suite, not convention.
36
+ - Refusals are **structured results, not exceptions**:
37
+ `{ok:false, error:"daily_budget_exceeded", remaining_sats:200}` — the
38
+ agent can read them and plan, and so can your code.
39
+ - **Every decision is ledgered** — allow, deny, or queue, with the rule
40
+ and reason, in an append-only JSONL file. You audit what the agent did
41
+ *and what it tried to do*.
42
+ - Big payments don't go out at all: at/above `approval_threshold_sats`
43
+ they queue for a human, who approves with a **separate approval secret**
44
+ (TTY-only, argon2id-hashed, the wallet passphrase is rejected). An agent
45
+ holding the env passphrase cannot approve its own payment.
46
+
47
+ **What this does not defend against, honestly:** an attacker with write
48
+ access to `~/.bsv-pay` (policy.toml, the ledger, the wallet file) or
49
+ arbitrary code execution with your passphrase defeats any local tool —
50
+ the wallet is forfeit at that point. The policy engine governs spending
51
+ *through* bsv-pay; keep the passphrase out of the agent's hands (use the
52
+ MCP server) and treat the state dir like the money it controls.
53
+
54
+ ## Five minutes, no coins: the two-agent demo
55
+
56
+ ```bash
57
+ git clone https://github.com/iamSOLUM/bsv-pay && cd bsv-pay
58
+ npm install
59
+ npm run demo:two-agents
60
+ ```
61
+
62
+ A seller paywall and a buyer agent run against a local mock chain. You'll
63
+ watch the buyer discover a price for free, buy within budget, get
64
+ prompt-injected by the content it bought (denylist refuses), and get
65
+ stopped by its daily budget — with the ledger printed at the end. See
66
+ [examples/two-agents/](../examples/two-agents/) to swap in Claude as the buyer.
67
+
68
+ For interactive experiments without real coins:
69
+ `node scripts/demo-chain.mjs` gives you a local chain with a faucet —
70
+ point bsv-pay at it with `BSV_PAY_API_URL=http://127.0.0.1:8799` and use
71
+ `--testnet` everywhere.
72
+
73
+ ## Hooking up an agent (MCP)
74
+
75
+ Install and create a wallet first (`npm i -g bsv-pay-cli`, then
76
+ `bsv-pay init --testnet`). Write a `~/.bsv-pay/policy.toml` **before**
77
+ giving any agent the server — the defaults without one are a per-tx
78
+ confirm threshold only:
79
+
80
+ ```toml
81
+ per_tx_limit_sats = 8000 # hard cap per payment
82
+ daily_budget_sats = 12000 # rolling 24h, recomputed from the ledger
83
+ session_budget_sats = 10000 # per server process
84
+ rate_limit_per_minute = 6
85
+ approval_threshold_sats = 1500 # at/above: queue for the human
86
+ denylist = []
87
+ ```
88
+
89
+ ### Claude Code
90
+
91
+ ```bash
92
+ claude mcp add bsv-pay --env BSV_PAY_PASSPHRASE=your-passphrase -- bsv-pay mcp --testnet
93
+ ```
94
+
95
+ ### Claude Desktop
96
+
97
+ `claude_desktop_config.json` → `mcpServers`:
98
+
99
+ ```json
100
+ {
101
+ "mcpServers": {
102
+ "bsv-pay": {
103
+ "command": "bsv-pay",
104
+ "args": ["mcp", "--testnet"],
105
+ "env": { "BSV_PAY_PASSPHRASE": "your-passphrase" }
106
+ }
107
+ }
108
+ }
109
+ ```
110
+
111
+ ### Cursor
112
+
113
+ `.cursor/mcp.json` in your project (same shape as Claude Desktop):
114
+
115
+ ```json
116
+ {
117
+ "mcpServers": {
118
+ "bsv-pay": {
119
+ "command": "bsv-pay",
120
+ "args": ["mcp", "--testnet"],
121
+ "env": { "BSV_PAY_PASSPHRASE": "your-passphrase" }
122
+ }
123
+ }
124
+ }
125
+ ```
126
+
127
+ The agent gets seven tools: `pay`, `paid_fetch`, `create_payment_request`,
128
+ `await_payment`, `get_balance`, `get_history`, `get_policy_status`. Tool
129
+ descriptions state units (satoshis), irreversibility, and that budgets
130
+ exist — agents plan within their allowance instead of discovering limits
131
+ by failing. Policy edits apply on server restart; session budgets reset
132
+ with the process, daily budgets never do (they're recomputed from the
133
+ ledger).
134
+
135
+ ## Paying for things over HTTP (402)
136
+
137
+ Machine-to-machine commerce in one call each way.
138
+
139
+ **Buy** — CLI, library, or the MCP `paid_fetch` tool:
140
+
141
+ ```bash
142
+ bsv-pay fetch https://seller.example/dataset --max-price 1000
143
+ ```
144
+
145
+ On a `402 Payment Required`, bsv-pay reads the payment terms, pays
146
+ **through the same policy gate as every other spend**, retries with the
147
+ payment envelope, and returns the content. `--max-price` caps the single
148
+ fetch regardless of policy headroom. A 1-sat `--max-price` probe is a free
149
+ price check — the refusal carries the asking price.
150
+
151
+ **Sell** — the demo server or the importable middleware:
152
+
153
+ ```bash
154
+ bsv-pay serve --price 50sats --port 8402 --body "premium data"
155
+ ```
156
+
157
+ ```js
158
+ import { openWallet, requirePayment } from 'bsv-pay/core';
159
+ const wallet = await openWallet({ network: 'test' });
160
+ app.use(requirePayment({ network: 'test', wallet, priceSats: 50 }));
161
+ // req.bsvPayment = { txid, amountSats, address, … } once paid
162
+ ```
163
+
164
+ Wire format: a simplified BRC-105 profile (same headers/flow; fresh
165
+ advertised address instead of BRC-29 derivation; raw tx hex instead of
166
+ AtomicBEEF). bsv-pay's fetch and serve interoperate with each other;
167
+ interop with external full-BRC-105 services is deferred — see the README's
168
+ "Compatibility, honestly" and DECISIONS.md M12.
169
+
170
+ ## External custody — BRC-100 (EXPERIMENTAL)
171
+
172
+ ```bash
173
+ bsv-pay init --experimental-brc100 --testnet
174
+ ```
175
+
176
+ Keys live in a desktop wallet app (e.g. Metanet Desktop); bsv-pay
177
+ constructs payment actions and the app signs and broadcasts them. **Your
178
+ policy still decides first** — the wallet app is never asked about a spend
179
+ the gate refused, and every decision is ledgered. The agent setup is
180
+ identical: same MCP server, same tools, same policy file.
181
+
182
+ What works: spending (`send`, `fetch`, MCP `pay`/`paid_fetch`), balance,
183
+ history, policy, approvals. What refuses: receive-side commands
184
+ (`request`, `watch`, `serve`) — exit 2, by design, because an address
185
+ issued outside the wallet app would strand funds. Setup and verification:
186
+ [docs/BRC100.md](BRC100.md).
187
+
188
+ > **Experimental status, plainly:** custody mode is protocol-tested
189
+ > against a mock wallet implementing the BRC-100 JSON-API (unit suite +
190
+ > e2e step 11), **not yet verified against a real wallet app** — public
191
+ > testnet faucets are currently broken, so a real app couldn't be funded
192
+ > for the verification pass. It stays behind `--experimental-brc100`
193
+ > until a human completes the real-app loop in docs/BRC100.md.
194
+
195
+ ## Don't take our word for it
196
+
197
+ The guarantees above are tests, not prose, and they run in CI on a local
198
+ mock chain with no live network:
199
+
200
+ - `test/policy-gate.test.ts` — a static scan proving the only code paths
201
+ that can sign, broadcast, or ask an external wallet to sign live behind
202
+ the gate; runtime forgery rejections (a hand-built or altered plan
203
+ cannot execute); and a sweep where the mock chain itself refuses any
204
+ broadcast lacking a prior ledgered allow decision, across every entry
205
+ point (CLI, library, MCP `pay`, `paid_fetch`, BRC-100).
206
+ - `test/core-key-boundary.test.ts`, `test/mcp-key-boundary.test.ts`,
207
+ `test/brc100.test.ts` — every result and error the API can produce is
208
+ serialized and scanned for every representation of key material,
209
+ including a meta-test that the leak detector catches planted secrets.
210
+ - `test/spend-concurrency.test.ts` — racing payments cannot overshoot a
211
+ budget: the whole decide→sign→broadcast→ledger span is single-flighted
212
+ in core.
213
+ - `npm run e2e:local` — eleven steps driving the real built CLI over real
214
+ HTTP: the full receive→send loop, policy governance, the MCP agent
215
+ session, the 402 marketplace, and BRC-100 custody.
216
+
217
+ Read the decision log (DECISIONS.md) for every trade-off, including the
218
+ ones that went against shipping features.
package/docs/BRC100.md ADDED
@@ -0,0 +1,151 @@
1
+ # BRC-100 custody — setup and verification guide
2
+
3
+ This guide assumes **zero prior familiarity** with BSV wallet apps. It gets
4
+ you from nothing to a verified bsv-pay ↔ external-wallet loop, step by step.
5
+
6
+ **What you're setting up:** normally bsv-pay holds an encrypted seed on your
7
+ disk. With BRC-100 custody, the keys live in a separate wallet application
8
+ instead, and bsv-pay *asks it* to pay. Your `policy.toml` budgets and limits
9
+ still govern every spend — bsv-pay decides first, the wallet app signs
10
+ second. This feature is **experimental**; spending works, receiving stays in
11
+ the wallet app (see the README's support table).
12
+
13
+ ---
14
+
15
+ ## Step 0 — verify without installing anything (recommended first)
16
+
17
+ The repository ships a complete mock of the wallet app. This proves the
18
+ whole custody loop on your machine with no external software and no coins:
19
+
20
+ ```bash
21
+ npm run e2e:local
22
+ ```
23
+
24
+ Watch for step `[11/11] BRC-100 custody: external wallet signs, policy
25
+ still governs` — every line should say `ok:`. That step runs the real CLI
26
+ against a mock desktop wallet over the same HTTP protocol the real apps
27
+ use: connect, balance, a policy-allowed send, a budget denial that never
28
+ reaches the wallet, ledger checks, and the receive-side refusal.
29
+
30
+ Everything after this point is about doing the same loop against a real
31
+ wallet app.
32
+
33
+ ## Step 1 — install a BRC-100 wallet app
34
+
35
+ You need a wallet that exposes the **BRC-100 wallet JSON-API on
36
+ `localhost:3321`**. As of June 2026 the reference choice is:
37
+
38
+ - **Metanet Desktop** — download the installer for Windows from the
39
+ official releases page: <https://github.com/bsv-blockchain/metanet-desktop/releases>
40
+ (pick the latest `.msi`/`.exe` asset). It is published by the BSV
41
+ Association; do not download it from anywhere else.
42
+
43
+ Alternative: **BSV Desktop** from the same GitHub organisation
44
+ (<https://github.com/bsv-blockchain>) exposes the same interface.
45
+
46
+ Install it like any normal application and start it. On first run it will
47
+ walk you through creating a wallet (it may ask for a phone number or
48
+ recovery setup — that is the app's own custody model; bsv-pay never sees
49
+ any of it).
50
+
51
+ > App UIs change between releases. If a menu in this guide doesn't match
52
+ > what you see, the only thing bsv-pay actually needs is: the app running,
53
+ > on **testnet**, with its local wallet API enabled (port 3321).
54
+
55
+ ## Step 2 — switch the wallet app to testnet
56
+
57
+ In the app's settings, set the network/chain to **testnet** and restart the
58
+ app if it asks. This matters: bsv-pay refuses to connect a mainnet wallet
59
+ app to a testnet bsv-pay state dir (and vice versa) — you'd get
60
+ `brc100_network_mismatch` (exit 2). That refusal is working as intended.
61
+
62
+ Fund the app's wallet with a small amount of testnet coins using its own
63
+ receive screen. (Public testnet faucets are mostly dead — witnessonchain.com
64
+ has the lone captcha-gated survivor. A few thousand satoshis is plenty; if
65
+ you can't get coins, you can still verify everything except the final
66
+ broadcast: steps 4–6 below work unfunded, and `--dry-run` works always.)
67
+
68
+ ## Step 3 — connect bsv-pay
69
+
70
+ ```bash
71
+ bsv-pay init --experimental-brc100 --testnet
72
+ ```
73
+
74
+ - The wallet app may pop up a prompt asking to authorize "bsv-pay" — approve it.
75
+ - Success looks like: `Wallet connected (BRC-100, experimental)` with the
76
+ app's identity key and `http://localhost:3321`.
77
+ - `brc100_unreachable` (exit 7)? The app isn't running, or serves its API on
78
+ a different port — set `BSV_PAY_BRC100_URL=http://localhost:<port>` and
79
+ re-run.
80
+
81
+ Check what was stored — this is the point of the custody model:
82
+
83
+ ```bash
84
+ type %USERPROFILE%\.bsv-pay\wallet-testnet.json
85
+ ```
86
+
87
+ There is **no seed, no cipher, no key material** — just
88
+ `"backend": "brc100"` and the URL. There is nothing in `~/.bsv-pay` worth
89
+ stealing now.
90
+
91
+ ## Step 4 — balance comes from the app
92
+
93
+ ```bash
94
+ bsv-pay balance --testnet
95
+ ```
96
+
97
+ `Spendable` should match what the wallet app shows (one total — the app
98
+ doesn't expose per-address detail). `Custody: external BRC-100 wallet app`.
99
+
100
+ ## Step 5 — put a policy in front of it
101
+
102
+ Create `%USERPROFILE%\.bsv-pay\policy.toml`:
103
+
104
+ ```toml
105
+ [network.test]
106
+ daily_budget_sats = 2000
107
+ ```
108
+
109
+ ## Step 6 — the verification loop
110
+
111
+ ```bash
112
+ :: 1) dry run: policy verdict + fee estimate, the wallet app is NOT contacted
113
+ bsv-pay send mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn 500 --dry-run --testnet
114
+
115
+ :: 2) real send, within the 2000-sat budget: the wallet app should prompt
116
+ :: you to approve (or pay silently, depending on its permission settings)
117
+ bsv-pay send mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn 500 "brc100 test" --testnet
118
+
119
+ :: 3) blow the budget on purpose: DENIED before the wallet app sees anything
120
+ bsv-pay send mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn 1800 --yes --testnet
121
+ echo exit code: %ERRORLEVEL% (expect 8, daily_budget_exceeded)
122
+
123
+ :: 4) receive-side refusal is intentional:
124
+ bsv-pay request 1000 --testnet
125
+ echo exit code: %ERRORLEVEL% (expect 2, brc100_receive_not_supported)
126
+
127
+ :: 5) the audit trail has all of it — the allow, the send, and the deny:
128
+ type %USERPROFILE%\.bsv-pay\ledger-testnet.jsonl
129
+ ```
130
+
131
+ What you just proved, in order: bsv-pay's policy decides **before** the
132
+ external wallet is asked (the 1800-sat send never produced a wallet-app
133
+ prompt); an allowed spend is signed and broadcast by the app while bsv-pay
134
+ ledgers the decision, txid, and exact fee; and the custody boundary holds
135
+ in both directions (no key material on disk, no receive addresses the app
136
+ can't see).
137
+
138
+ If you decline the app's payment prompt in step 2, bsv-pay exits 5
139
+ (`brc100_action_rejected`) and nothing is spent or ledgered as sent — also
140
+ worth trying once.
141
+
142
+ ## Troubleshooting
143
+
144
+ | Symptom | Meaning / fix |
145
+ | --- | --- |
146
+ | `brc100_unreachable` (exit 7) | App not running, or different port → `BSV_PAY_BRC100_URL` |
147
+ | `brc100_network_mismatch` (exit 2) | App is on mainnet, bsv-pay on testnet (or vice versa) — switch the app |
148
+ | `brc100_action_rejected` (exit 5) | You (or the app) declined the payment — nothing spent |
149
+ | `brc100_broadcast_unknown` (exit 6) | The app created the payment but its broadcast outcome is unclear — check the app before retrying |
150
+ | `insufficient_funds` (exit 3) | The app's wallet can't cover amount + its fee |
151
+ | `brc100_receive_not_supported` (exit 2) | By design — receive in the wallet app |
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "bsv-pay-cli",
3
+ "version": "0.2.0",
4
+ "description": "Developer-first CLI and agent payment toolkit for BSV micropayments: policy-governed spending, MCP server for AI agents, HTTP 402 paywalls",
5
+ "license": "MIT",
6
+ "author": "iamSOLUM",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/iamSOLUM/bsv-pay.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/iamSOLUM/bsv-pay/issues"
13
+ },
14
+ "homepage": "https://github.com/iamSOLUM/bsv-pay#readme",
15
+ "type": "module",
16
+ "bin": {
17
+ "bsv-pay": "dist/cli.js",
18
+ "bsvpay": "dist/cli.js"
19
+ },
20
+ "exports": {
21
+ "./core": {
22
+ "types": "./dist/core/index.d.ts",
23
+ "default": "./dist/core/index.js"
24
+ },
25
+ "./cli": "./dist/cli.js",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "docs",
31
+ "README.md",
32
+ "CHANGELOG.md"
33
+ ],
34
+ "engines": {
35
+ "node": ">=20"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.build.json",
39
+ "dev": "tsx src/cli.ts",
40
+ "lint": "eslint src test",
41
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
42
+ "format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
43
+ "test": "vitest run",
44
+ "test:watch": "vitest",
45
+ "e2e:local": "npm run build && node scripts/e2e-local.mjs",
46
+ "demo:chain": "node scripts/demo-chain.mjs",
47
+ "demo:two-agents": "npm run build && node examples/two-agents/run.mjs",
48
+ "prepublishOnly": "npm test && npm run build"
49
+ },
50
+ "keywords": [
51
+ "bsv",
52
+ "bitcoin-sv",
53
+ "micropayments",
54
+ "cli",
55
+ "wallet",
56
+ "mcp",
57
+ "ai-agents",
58
+ "402",
59
+ "brc-105",
60
+ "payments"
61
+ ],
62
+ "dependencies": {
63
+ "@bsv/sdk": "^2.1.4",
64
+ "@modelcontextprotocol/sdk": "^1.29.0",
65
+ "@noble/hashes": "^1.5.0",
66
+ "chalk": "^5.3.0",
67
+ "commander": "^12.1.0",
68
+ "qrcode-terminal": "^0.12.0",
69
+ "smol-toml": "^1.3.0",
70
+ "zod": "^3.25.76"
71
+ },
72
+ "devDependencies": {
73
+ "@types/node": "^22.0.0",
74
+ "@types/qrcode-terminal": "^0.12.2",
75
+ "eslint": "^9.0.0",
76
+ "prettier": "^3.3.0",
77
+ "tsx": "^4.19.0",
78
+ "typescript": "^5.6.0",
79
+ "typescript-eslint": "^8.8.0",
80
+ "vitest": "^4.1.8"
81
+ }
82
+ }