walletcap 0.2.0__tar.gz → 0.2.2__tar.gz

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 (69) hide show
  1. {walletcap-0.2.0 → walletcap-0.2.2}/.circleci/config.yml +5 -0
  2. {walletcap-0.2.0 → walletcap-0.2.2}/.gitignore +1 -0
  3. walletcap-0.2.2/CHANGELOG.md +28 -0
  4. {walletcap-0.2.0 → walletcap-0.2.2}/PKG-INFO +68 -28
  5. {walletcap-0.2.0 → walletcap-0.2.2}/README.md +67 -27
  6. walletcap-0.2.2/SECURITY.md +64 -0
  7. walletcap-0.2.2/docs/threat-model.md +54 -0
  8. walletcap-0.2.2/docs/vendor-disclosure.md +48 -0
  9. walletcap-0.2.2/examples/quickstart/README.md +24 -0
  10. walletcap-0.2.2/examples/quickstart/quickstart.mjs +59 -0
  11. walletcap-0.2.2/examples/quickstart/quickstart.py +70 -0
  12. {walletcap-0.2.0 → walletcap-0.2.2}/js/README.md +11 -1
  13. walletcap-0.2.2/js/_keccak.mjs +87 -0
  14. {walletcap-0.2.0 → walletcap-0.2.2}/js/cdp.mjs +28 -6
  15. {walletcap-0.2.0 → walletcap-0.2.2}/js/index.mjs +8 -3
  16. {walletcap-0.2.0 → walletcap-0.2.2}/js/ledger.mjs +6 -0
  17. {walletcap-0.2.0 → walletcap-0.2.2}/js/money.mjs +28 -3
  18. {walletcap-0.2.0 → walletcap-0.2.2}/js/package.json +5 -1
  19. {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/agentic.test.mjs +2 -2
  20. {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/budget.test.mjs +18 -3
  21. walletcap-0.2.2/js/tests/cdp-sdk.test.mjs +87 -0
  22. walletcap-0.2.2/js/tests/cdp.test.mjs +117 -0
  23. {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/cli.test.mjs +4 -4
  24. walletcap-0.2.2/js/tests/keccak.test.mjs +19 -0
  25. {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/money.test.mjs +10 -1
  26. {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/processes.test.mjs +3 -2
  27. {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/x402-fetch.test.mjs +1 -1
  28. {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/x402.test.mjs +2 -2
  29. {walletcap-0.2.0 → walletcap-0.2.2}/js/x402.mjs +11 -0
  30. {walletcap-0.2.0 → walletcap-0.2.2}/pyproject.toml +1 -1
  31. {walletcap-0.2.0 → walletcap-0.2.2}/scripts/ci.sh +4 -0
  32. walletcap-0.2.2/solidity/README.md +79 -0
  33. {walletcap-0.2.0 → walletcap-0.2.2}/solidity/evm/package.json +1 -1
  34. {walletcap-0.2.0 → walletcap-0.2.2}/solidity/src/AccountCapSpender.sol +1 -0
  35. {walletcap-0.2.0 → walletcap-0.2.2}/src/walletcap/__init__.py +2 -1
  36. walletcap-0.2.2/src/walletcap/_keccak.py +71 -0
  37. {walletcap-0.2.0 → walletcap-0.2.2}/src/walletcap/budget.py +8 -5
  38. {walletcap-0.2.0 → walletcap-0.2.2}/src/walletcap/ledger.py +9 -0
  39. {walletcap-0.2.0 → walletcap-0.2.2}/src/walletcap/money.py +35 -4
  40. {walletcap-0.2.0 → walletcap-0.2.2}/tests/test_budget.py +31 -11
  41. walletcap-0.2.2/tests/test_keccak.py +10 -0
  42. walletcap-0.2.2/tests/test_money.py +36 -0
  43. {walletcap-0.2.0 → walletcap-0.2.2}/.github/workflows/ci.yml +0 -0
  44. {walletcap-0.2.0 → walletcap-0.2.2}/.github/workflows/publish.yml +0 -0
  45. {walletcap-0.2.0 → walletcap-0.2.2}/LICENSE +0 -0
  46. {walletcap-0.2.0 → walletcap-0.2.2}/NOTICE +0 -0
  47. {walletcap-0.2.0 → walletcap-0.2.2}/js/LICENSE +0 -0
  48. {walletcap-0.2.0 → walletcap-0.2.2}/js/NOTICE +0 -0
  49. {walletcap-0.2.0 → walletcap-0.2.2}/js/agentic.mjs +0 -0
  50. {walletcap-0.2.0 → walletcap-0.2.2}/js/cli.mjs +0 -0
  51. {walletcap-0.2.0 → walletcap-0.2.2}/js/errors.mjs +0 -0
  52. {walletcap-0.2.0 → walletcap-0.2.2}/js/index.d.ts +0 -0
  53. {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/support/fake-awal/dist/index.js +0 -0
  54. {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/support/fake-awal/dist/ipcClient.js +0 -0
  55. {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/support/fake-awal/package.json +0 -0
  56. {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/support/fake-payments-mcp.mjs +0 -0
  57. {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/support/payer.mjs +0 -0
  58. {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/support/race.mjs +0 -0
  59. {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/support/x402-kit.mjs +0 -0
  60. {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/support/x402-payer.mjs +0 -0
  61. {walletcap-0.2.0 → walletcap-0.2.2}/pytest.ini +0 -0
  62. {walletcap-0.2.0 → walletcap-0.2.2}/solidity/evm/check.mjs +0 -0
  63. {walletcap-0.2.0 → walletcap-0.2.2}/solidity/foundry.toml +0 -0
  64. {walletcap-0.2.0 → walletcap-0.2.2}/solidity/test/AccountCapSpender.t.sol +0 -0
  65. {walletcap-0.2.0 → walletcap-0.2.2}/solidity/test/Mocks.sol +0 -0
  66. {walletcap-0.2.0 → walletcap-0.2.2}/src/walletcap/__main__.py +0 -0
  67. {walletcap-0.2.0 → walletcap-0.2.2}/src/walletcap/errors.py +0 -0
  68. {walletcap-0.2.0 → walletcap-0.2.2}/src/walletcap/py.typed +0 -0
  69. {walletcap-0.2.0 → walletcap-0.2.2}/tests/test_ledger.py +0 -0
@@ -22,6 +22,11 @@ jobs:
22
22
  python3 -m venv /tmp/venv
23
23
  /tmp/venv/bin/pip install --quiet pytest
24
24
  /tmp/venv/bin/python -m pytest -q
25
+ - run:
26
+ name: Examples
27
+ command: |
28
+ node examples/quickstart/quickstart.mjs
29
+ python3 examples/quickstart/quickstart.py
25
30
  - run:
26
31
  name: Solidity
27
32
  command: |
@@ -10,6 +10,7 @@ __pycache__/
10
10
  out/
11
11
  cache/
12
12
  work/
13
+ examples/quickstart/demo.sqlite
13
14
 
14
15
  # The fake awal stand-in in the tests is committed on purpose: the CLI test
15
16
  # needs its dist/ layout, and the dist/ rule above would hide it.
@@ -0,0 +1,28 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ # Changelog
3
+
4
+ ## 0.2.2 — 2026-11-05
5
+
6
+ ### Changed
7
+
8
+ - A mixed-case EVM address is checked against EIP-55 before it is accepted. A bad
9
+ checksum is refused; all-lowercase and all-uppercase hex are still accepted.
10
+ - A Solana CAIP-10 address is reduced to its address, so it and the bare address
11
+ share one budget.
12
+ - Numeric `window` values are milliseconds in Python too, matching JavaScript.
13
+
14
+ ### Fixed
15
+
16
+ - The rolling window cannot be shortened once the budget has spend. An edit can
17
+ change the cap and the per-payment limit, but it cannot reset the count by
18
+ shrinking the window.
19
+
20
+ ### Added
21
+
22
+ - `SECURITY.md`, `docs/threat-model.md`, `docs/vendor-disclosure.md`, and the
23
+ Solidity audit status in `solidity/README.md`.
24
+ - `examples/quickstart/` in JavaScript and Python, run in CI. No wallet, chain,
25
+ keys or network.
26
+ - EIP-55, Solana CAIP-10, and window-freeze tests in both languages.
27
+
28
+ ## 0.2.1 — 2026-11-05
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: walletcap
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: One spend budget per wallet for AI agents: counted across every process, replica, key, permission and payment rail.
5
5
  Project-URL: Homepage, https://github.com/coriolislabs/WalletCap
6
6
  Project-URL: Repository, https://github.com/coriolislabs/WalletCap
@@ -33,6 +33,13 @@ period. So a restart, a second replica, one more key, or the next period hands t
33
33
  fresh budget. walletcap counts **per wallet**: one budget that every process,
34
34
  replica, key, permission and payment rail spends from.
35
35
 
36
+ The owner creates the budget once. Opening it never creates one; pass `create: true` only
37
+ for a demo or first-run setup:
38
+
39
+ ```console
40
+ $ npx walletcap set --wallet 0xYourAgentWallet --per-payment 50 --per-day 150
41
+ ```
42
+
36
43
  ```js
37
44
  import { openBudget } from 'walletcap';
38
45
 
@@ -53,6 +60,21 @@ with budget.pay("$12.50"):
53
60
  `npm install walletcap` (Node 22.13+) · `pip install walletcap` (Python 3.11+) ·
54
61
  no dependencies · Apache-2.0
55
62
 
63
+ ## Try it without a wallet
64
+
65
+ Two runnable demos use a fake payee and a temporary file: no wallet, chain, keys or
66
+ network.
67
+
68
+ ```console
69
+ $ node examples/quickstart/quickstart.mjs
70
+ $ python3 examples/quickstart/quickstart.py
71
+ ```
72
+
73
+ They show three `$0.05` payments against a `$0.15` rolling cap, a `$1.00` refusal, a
74
+ fourth `$0.05` refusal, and a restart that cannot raise the limits. The demo uses
75
+ `create: true`; a production agent opens with the default `create: false` after the
76
+ owner runs `walletcap set`.
77
+
56
78
  ## Why a per-wallet count
57
79
 
58
80
  Most agent spend controls keep "spent so far" next to something temporary or repeatable: a
@@ -76,8 +98,9 @@ own code. Findings go to the vendors first; this package is the fix, not the fin
76
98
  leaves $50, not $200. A restart can lower a limit but never raise one: if the code comes
77
99
  back asking for looser limits (a kit's defaults, say), the stored limits stay and you get a
78
100
  warning with the command that raises them on purpose.
79
- 4. **Fail closed.** If the count can't be read, nothing pays. A payment whose outcome is
80
- unknown keeps counting, because a payment that failed halfway may still have moved money.
101
+ 4. **Fail closed.** If the count can't be read, nothing pays; a missing file is not a new
102
+ budget. A payment whose outcome is unknown keeps counting, because a payment that failed
103
+ halfway may still have moved money.
81
104
 
82
105
  The count lives in a SQLite file that every process on the machine shares, and each payment is
83
106
  reserved inside one write transaction, so replicas racing for the last dollar can't both win.
@@ -113,7 +136,7 @@ gives you a reservation to `commit()` or `release()` yourself.
113
136
  ### x402
114
137
 
115
138
  `guard()` puts the budget in front of any x402 v2 client from `@x402/core`, so it works under
116
- `@x402/fetch` and alongside Coinbase's CDP spend controls.
139
+ `@x402/fetch`. Use it as the one counter when the client has no CDP spend store.
117
140
 
118
141
  ```js
119
142
  import { x402Client } from '@x402/core/client';
@@ -138,20 +161,24 @@ const fetchPaid = wrapFetchWithPayment(fetch, client);
138
161
 
139
162
  ### Coinbase CDP spend controls
140
163
 
141
- `applySpendControls` builds on an x402 client, so `budget.guard(client)` works alongside it, and
142
- it's the one to use:
164
+ There are two ways to count a CDP payment in the budget; use **one** per client.
165
+
166
+ - `budget.guard(client)` — the budget prices every asset, refuses anything it cannot price,
167
+ and reserves in one write transaction. Use it when the budget is the counter.
168
+ - `applySpendControls(client, { ..., store: budget.cdpStore({ asset: USDC }) })` — keep
169
+ CDP's controls, and make its count durable and shared. The store re-checks the budget inside
170
+ the write transaction, so replicas sharing the file cannot both take the last dollar.
171
+
172
+ `budget.cdpStore()` is walletcap's SQLite ledger behind CDP's `SpendStore` interface — **not**
173
+ CDP's in-memory default. Replacing that default is the fix, not a second control.
143
174
 
144
- - **It counts every token.** `guard()` prices the USD stablecoins it knows and refuses
145
- anything else, whatever assets the client would otherwise accept.
146
- - **It reserves in one transaction.** Each payment is checked and reserved inside one write
147
- transaction on the budget file, so processes and replicas sharing the file can't both take
148
- the last dollar.
149
- - **It keeps counting what the payee calls failed** until the authorization expires, then frees
150
- it on its own.
175
+ Do **not** call both on one client: the CDP store and the guard each record the payment, so
176
+ the budget would count it twice. `budget.guard()` refuses a client that already has CDP
177
+ controls, and a client guarded first refuses CDP controls before signing.
151
178
 
152
- To keep CDP's own count durable and shared as well, swap its in-memory store for walletcap's,
153
- set `maxCumulativeSpend` (CDP only records through its store when a cumulative cap is set), and
154
- set `allowedAssets`: CDP never consults its store for a token outside the cap.
179
+ To use the store, swap CDP's in-memory store for walletcap's, set `maxCumulativeSpend` (CDP
180
+ only records through its store when a cumulative cap is set), and set `allowedAssets`: CDP
181
+ never consults its store for a token outside the cap.
155
182
 
156
183
  ```js
157
184
  applySpendControls(client, {
@@ -161,13 +188,15 @@ applySpendControls(client, {
161
188
  allowedAssets: [USDC],
162
189
  store: budget.cdpStore({ asset: USDC }),
163
190
  });
164
- budget.guard(client);
165
191
  ```
166
192
 
193
+ `guard()` is the other choice, and the one that prices every token: it refuses a USD stablecoin
194
+ it cannot value instead of letting CDP count only the asset named in the cap.
195
+
167
196
  walletcap's store re-checks the budget when CDP records a payment, so it holds across replicas
168
197
  on its own. When CDP removes a payment from its store, the store can't tell why, so the payment
169
198
  stays counted as in-flight until you check the chain and run `npx walletcap release <id>`. Use
170
- CDP 1.56.0 or later.
199
+ CDP 1.56.0 or later; the store is tested against the published 1.60.0.
171
200
 
172
201
  ### Coinbase Agentic Wallet (awal CLI and MCP server)
173
202
 
@@ -256,9 +285,9 @@ The default file is per user: `~/.local/state/walletcap/budget.sqlite` on Linux,
256
285
  compute the same default.
257
286
 
258
287
  The budget is as durable as that file. In a container, put it on a volume that outlives the
259
- container, or a redeploy starts a new file. For agents that can touch the filesystem, open the
260
- budget with `create: false` and create it once, as the owner, with `npx walletcap set`:
261
- then a missing file refuses every payment instead of starting a new budget.
288
+ container, or a redeploy starts a new file. Opening a budget does not create one: a missing file
289
+ refuses every payment. The owner creates it once with `npx walletcap set`; pass `create: true`
290
+ only for a demo or a first-run setup.
262
291
 
263
292
  ## What it doesn't do
264
293
 
@@ -266,8 +295,9 @@ then a missing file refuses every payment instead of starting a new budget.
266
295
  go around it: call the wallet directly, or delete the file. To bound an agent like that, keep
267
296
  the budget out of its reach (another OS user, another service) or enforce on chain, as
268
297
  `AccountCapSpender` does for Spend Permissions.
269
- - **One file is one host, or a shared volume.** A Postgres backend for replicas on separate
270
- hosts is next.
298
+ - **One file is one host.** A shared volume can work for one host's containers, but SQLite
299
+ over NFS is not a multi-host database. A Postgres backend for replicas on separate hosts is
300
+ next.
271
301
  - **It can't see a revoke that lands later in the same block,** and for x402 schemes that settle
272
302
  later (batch vouchers), a payment counts from when the server confirms it.
273
303
  - **Some x402 payments can't be confirmed.** `@x402/fetch` doesn't report v1 settlements
@@ -286,8 +316,8 @@ then a missing file refuses every payment instead of starting a new budget.
286
316
  | `tests/` | the Python suite, including the tests that drive both languages against one budget file |
287
317
 
288
318
  ```console
289
- $ cd js && npm install && node --test # 50 tests
290
- $ uv run --with pytest pytest -q # 53 tests, from the repository root
319
+ $ cd js && npm install && node --test # 61 tests
320
+ $ uv run --with pytest pytest -q # 59 tests, from the repository root
291
321
  $ cd solidity/evm && npm install && node check.mjs .. # the contract on an EVM, no Foundry needed
292
322
  $ cd solidity && forge test # the contract's canonical tests, if you have Foundry
293
323
  ```
@@ -298,9 +328,19 @@ MCP proxy against stand-ins for Coinbase's wallet and MCP server. The Python sui
298
328
  the CLI and one budget shared between both languages. `solidity/evm/check.mjs` compiles the
299
329
  contract with solc-js and drives it on an in-process EVM, covering what the Foundry tests cover.
300
330
 
301
- One test is **not** here: the integration test that drives Coinbase's CDP spend controls through
302
- the shared store. It runs against a vendored build of Coinbase's SDK and lives in ShadowPath's
303
- own repository. Porting it to the published `@coinbase/cdp-sdk` is open work.
331
+ The JavaScript suite also drives the published `@coinbase/cdp-sdk@1.60.0` through the
332
+ shared store, so the CDP integration is covered against the package users install.
333
+
334
+ ## Security and audit
335
+
336
+ - [Security policy](SECURITY.md): how to report a vulnerability, supported versions,
337
+ and the 90-day coordinated-disclosure window for walletcap itself.
338
+ - [Threat model](docs/threat-model.md): what the guard stops, what it does not, and the
339
+ deployment patterns that hold.
340
+ - [Vendor benchmark disclosure](docs/vendor-disclosure.md): the separate 7-day
341
+ pre-publication notice for vendor findings in the Agent Payment Benchmark.
342
+ - `AccountCapSpender` is **unaudited**. Read [solidity/README.md](solidity/README.md)
343
+ before deploying it; testnet only until an independent audit.
304
344
 
305
345
  ## Prove it holds in your deployment
306
346
 
@@ -10,6 +10,13 @@ period. So a restart, a second replica, one more key, or the next period hands t
10
10
  fresh budget. walletcap counts **per wallet**: one budget that every process,
11
11
  replica, key, permission and payment rail spends from.
12
12
 
13
+ The owner creates the budget once. Opening it never creates one; pass `create: true` only
14
+ for a demo or first-run setup:
15
+
16
+ ```console
17
+ $ npx walletcap set --wallet 0xYourAgentWallet --per-payment 50 --per-day 150
18
+ ```
19
+
13
20
  ```js
14
21
  import { openBudget } from 'walletcap';
15
22
 
@@ -30,6 +37,21 @@ with budget.pay("$12.50"):
30
37
  `npm install walletcap` (Node 22.13+) · `pip install walletcap` (Python 3.11+) ·
31
38
  no dependencies · Apache-2.0
32
39
 
40
+ ## Try it without a wallet
41
+
42
+ Two runnable demos use a fake payee and a temporary file: no wallet, chain, keys or
43
+ network.
44
+
45
+ ```console
46
+ $ node examples/quickstart/quickstart.mjs
47
+ $ python3 examples/quickstart/quickstart.py
48
+ ```
49
+
50
+ They show three `$0.05` payments against a `$0.15` rolling cap, a `$1.00` refusal, a
51
+ fourth `$0.05` refusal, and a restart that cannot raise the limits. The demo uses
52
+ `create: true`; a production agent opens with the default `create: false` after the
53
+ owner runs `walletcap set`.
54
+
33
55
  ## Why a per-wallet count
34
56
 
35
57
  Most agent spend controls keep "spent so far" next to something temporary or repeatable: a
@@ -53,8 +75,9 @@ own code. Findings go to the vendors first; this package is the fix, not the fin
53
75
  leaves $50, not $200. A restart can lower a limit but never raise one: if the code comes
54
76
  back asking for looser limits (a kit's defaults, say), the stored limits stay and you get a
55
77
  warning with the command that raises them on purpose.
56
- 4. **Fail closed.** If the count can't be read, nothing pays. A payment whose outcome is
57
- unknown keeps counting, because a payment that failed halfway may still have moved money.
78
+ 4. **Fail closed.** If the count can't be read, nothing pays; a missing file is not a new
79
+ budget. A payment whose outcome is unknown keeps counting, because a payment that failed
80
+ halfway may still have moved money.
58
81
 
59
82
  The count lives in a SQLite file that every process on the machine shares, and each payment is
60
83
  reserved inside one write transaction, so replicas racing for the last dollar can't both win.
@@ -90,7 +113,7 @@ gives you a reservation to `commit()` or `release()` yourself.
90
113
  ### x402
91
114
 
92
115
  `guard()` puts the budget in front of any x402 v2 client from `@x402/core`, so it works under
93
- `@x402/fetch` and alongside Coinbase's CDP spend controls.
116
+ `@x402/fetch`. Use it as the one counter when the client has no CDP spend store.
94
117
 
95
118
  ```js
96
119
  import { x402Client } from '@x402/core/client';
@@ -115,20 +138,24 @@ const fetchPaid = wrapFetchWithPayment(fetch, client);
115
138
 
116
139
  ### Coinbase CDP spend controls
117
140
 
118
- `applySpendControls` builds on an x402 client, so `budget.guard(client)` works alongside it, and
119
- it's the one to use:
141
+ There are two ways to count a CDP payment in the budget; use **one** per client.
142
+
143
+ - `budget.guard(client)` — the budget prices every asset, refuses anything it cannot price,
144
+ and reserves in one write transaction. Use it when the budget is the counter.
145
+ - `applySpendControls(client, { ..., store: budget.cdpStore({ asset: USDC }) })` — keep
146
+ CDP's controls, and make its count durable and shared. The store re-checks the budget inside
147
+ the write transaction, so replicas sharing the file cannot both take the last dollar.
148
+
149
+ `budget.cdpStore()` is walletcap's SQLite ledger behind CDP's `SpendStore` interface — **not**
150
+ CDP's in-memory default. Replacing that default is the fix, not a second control.
120
151
 
121
- - **It counts every token.** `guard()` prices the USD stablecoins it knows and refuses
122
- anything else, whatever assets the client would otherwise accept.
123
- - **It reserves in one transaction.** Each payment is checked and reserved inside one write
124
- transaction on the budget file, so processes and replicas sharing the file can't both take
125
- the last dollar.
126
- - **It keeps counting what the payee calls failed** until the authorization expires, then frees
127
- it on its own.
152
+ Do **not** call both on one client: the CDP store and the guard each record the payment, so
153
+ the budget would count it twice. `budget.guard()` refuses a client that already has CDP
154
+ controls, and a client guarded first refuses CDP controls before signing.
128
155
 
129
- To keep CDP's own count durable and shared as well, swap its in-memory store for walletcap's,
130
- set `maxCumulativeSpend` (CDP only records through its store when a cumulative cap is set), and
131
- set `allowedAssets`: CDP never consults its store for a token outside the cap.
156
+ To use the store, swap CDP's in-memory store for walletcap's, set `maxCumulativeSpend` (CDP
157
+ only records through its store when a cumulative cap is set), and set `allowedAssets`: CDP
158
+ never consults its store for a token outside the cap.
132
159
 
133
160
  ```js
134
161
  applySpendControls(client, {
@@ -138,13 +165,15 @@ applySpendControls(client, {
138
165
  allowedAssets: [USDC],
139
166
  store: budget.cdpStore({ asset: USDC }),
140
167
  });
141
- budget.guard(client);
142
168
  ```
143
169
 
170
+ `guard()` is the other choice, and the one that prices every token: it refuses a USD stablecoin
171
+ it cannot value instead of letting CDP count only the asset named in the cap.
172
+
144
173
  walletcap's store re-checks the budget when CDP records a payment, so it holds across replicas
145
174
  on its own. When CDP removes a payment from its store, the store can't tell why, so the payment
146
175
  stays counted as in-flight until you check the chain and run `npx walletcap release <id>`. Use
147
- CDP 1.56.0 or later.
176
+ CDP 1.56.0 or later; the store is tested against the published 1.60.0.
148
177
 
149
178
  ### Coinbase Agentic Wallet (awal CLI and MCP server)
150
179
 
@@ -233,9 +262,9 @@ The default file is per user: `~/.local/state/walletcap/budget.sqlite` on Linux,
233
262
  compute the same default.
234
263
 
235
264
  The budget is as durable as that file. In a container, put it on a volume that outlives the
236
- container, or a redeploy starts a new file. For agents that can touch the filesystem, open the
237
- budget with `create: false` and create it once, as the owner, with `npx walletcap set`:
238
- then a missing file refuses every payment instead of starting a new budget.
265
+ container, or a redeploy starts a new file. Opening a budget does not create one: a missing file
266
+ refuses every payment. The owner creates it once with `npx walletcap set`; pass `create: true`
267
+ only for a demo or a first-run setup.
239
268
 
240
269
  ## What it doesn't do
241
270
 
@@ -243,8 +272,9 @@ then a missing file refuses every payment instead of starting a new budget.
243
272
  go around it: call the wallet directly, or delete the file. To bound an agent like that, keep
244
273
  the budget out of its reach (another OS user, another service) or enforce on chain, as
245
274
  `AccountCapSpender` does for Spend Permissions.
246
- - **One file is one host, or a shared volume.** A Postgres backend for replicas on separate
247
- hosts is next.
275
+ - **One file is one host.** A shared volume can work for one host's containers, but SQLite
276
+ over NFS is not a multi-host database. A Postgres backend for replicas on separate hosts is
277
+ next.
248
278
  - **It can't see a revoke that lands later in the same block,** and for x402 schemes that settle
249
279
  later (batch vouchers), a payment counts from when the server confirms it.
250
280
  - **Some x402 payments can't be confirmed.** `@x402/fetch` doesn't report v1 settlements
@@ -263,8 +293,8 @@ then a missing file refuses every payment instead of starting a new budget.
263
293
  | `tests/` | the Python suite, including the tests that drive both languages against one budget file |
264
294
 
265
295
  ```console
266
- $ cd js && npm install && node --test # 50 tests
267
- $ uv run --with pytest pytest -q # 53 tests, from the repository root
296
+ $ cd js && npm install && node --test # 61 tests
297
+ $ uv run --with pytest pytest -q # 59 tests, from the repository root
268
298
  $ cd solidity/evm && npm install && node check.mjs .. # the contract on an EVM, no Foundry needed
269
299
  $ cd solidity && forge test # the contract's canonical tests, if you have Foundry
270
300
  ```
@@ -275,9 +305,19 @@ MCP proxy against stand-ins for Coinbase's wallet and MCP server. The Python sui
275
305
  the CLI and one budget shared between both languages. `solidity/evm/check.mjs` compiles the
276
306
  contract with solc-js and drives it on an in-process EVM, covering what the Foundry tests cover.
277
307
 
278
- One test is **not** here: the integration test that drives Coinbase's CDP spend controls through
279
- the shared store. It runs against a vendored build of Coinbase's SDK and lives in ShadowPath's
280
- own repository. Porting it to the published `@coinbase/cdp-sdk` is open work.
308
+ The JavaScript suite also drives the published `@coinbase/cdp-sdk@1.60.0` through the
309
+ shared store, so the CDP integration is covered against the package users install.
310
+
311
+ ## Security and audit
312
+
313
+ - [Security policy](SECURITY.md): how to report a vulnerability, supported versions,
314
+ and the 90-day coordinated-disclosure window for walletcap itself.
315
+ - [Threat model](docs/threat-model.md): what the guard stops, what it does not, and the
316
+ deployment patterns that hold.
317
+ - [Vendor benchmark disclosure](docs/vendor-disclosure.md): the separate 7-day
318
+ pre-publication notice for vendor findings in the Agent Payment Benchmark.
319
+ - `AccountCapSpender` is **unaudited**. Read [solidity/README.md](solidity/README.md)
320
+ before deploying it; testnet only until an independent audit.
281
321
 
282
322
  ## Prove it holds in your deployment
283
323
 
@@ -0,0 +1,64 @@
1
+ # Security policy
2
+
3
+ walletcap is a spend guard that sits on the payment path. If you find a way
4
+ around its count, its reservations, or its failure modes, we want to know.
5
+
6
+ ## Supported versions
7
+
8
+ The latest **0.2.x** release is supported. The 0.2.1 release fixed the
9
+ fail-open default and the CDP store/guard collision; earlier 0.2.0 installs
10
+ should upgrade before relying on the guard.
11
+
12
+ ## Reporting a vulnerability
13
+
14
+ Email **paul@coriolislabs.ca** with `walletcap security` in the subject. If you
15
+ want to encrypt, ask for a PGP key in the first message; do not send the details
16
+ in a public GitHub issue.
17
+
18
+ Please include:
19
+
20
+ - the version, language (JavaScript or Python), and platform;
21
+ - the configuration (single process or replicas, one file or a shared volume);
22
+ - a minimal reproduction, or the sequence of calls;
23
+ - what you expected and what happened.
24
+
25
+ ## What to expect
26
+
27
+ - We aim to acknowledge a report within **5 business days**.
28
+ - For walletcap itself we use a **90-day** coordinated disclosure window, and
29
+ we will credit you unless you ask us not to.
30
+ - If a fix ships earlier, we will publish the advisory as soon as users can
31
+ upgrade.
32
+
33
+ ## In scope
34
+
35
+ - the SQLite ledger (`ledger.mjs`, `src/walletcap/ledger.py`) and its
36
+ transaction/counting rules;
37
+ - the JavaScript and Python budget APIs and the CLI;
38
+ - the x402 guard and the CDP `SpendStore`;
39
+ - the Agentic Wallet CLI/MCP wrapper;
40
+ - `AccountCapSpender` and its transfer/allowance handling.
41
+
42
+ ## Out of scope
43
+
44
+ - an agent with shell access on the same OS user: it can call the wallet
45
+ directly or delete the file. The guard is not an OS sandbox.
46
+ - editing the SQLite file by someone who can write it;
47
+ - private-key custody and the wallet or chain itself;
48
+ - vulnerabilities in Coinbase, Safe, MetaMask, Tempo, Google or other vendor
49
+ products. Those are handled under the separate benchmark disclosure policy
50
+ in [docs/vendor-disclosure.md](docs/vendor-disclosure.md).
51
+
52
+ ## Known limitations
53
+
54
+ See [docs/threat-model.md](docs/threat-model.md) for the full model. The short
55
+ version:
56
+
57
+ - the guard binds only the calls that pass through it;
58
+ - the budget file is single-host; a shared volume is not a substitute for a
59
+ database that speaks a network protocol;
60
+ - an x402 payment whose settlement cannot be confirmed keeps counting until its
61
+ authorization expires;
62
+ - `AccountCapSpender` is **unaudited**; see
63
+ [solidity/README.md](solidity/README.md). Do not deploy it with real funds
64
+ until it has an independent audit.
@@ -0,0 +1,54 @@
1
+ # Threat model
2
+
3
+ This is the model a security reviewer should test against. It is deliberately
4
+ narrow: walletcap is a spend *counter*, not a sandbox, a key store, or a chain
5
+ observer.
6
+
7
+ ## Assets
8
+
9
+ - the funds in the wallet;
10
+ - the budget file (limits, reservations, payment history);
11
+ - the agent's ability to keep paying after a restart, a replica, or a key
12
+ rotation.
13
+
14
+ ## Adversaries and controls
15
+
16
+ | Adversary | What they can do | Control | Residual risk |
17
+ |---|---|---|---|
18
+ | A buggy agent | Pay more than intended | Per-payment cap, rolling cap, `max_uses`, one reservation per payment | A bug that bypasses the guard entirely |
19
+ | An agent with the same OS user | Delete the budget file, edit it, call the wallet directly | A missing file refuses by default (`create: false`); owner-only `walletcap set`; the file is the source of truth | It can still call the wallet or edit the file. Use a separate OS user, a separate service, or the on-chain cap |
20
+ | A second process or replica on the same host | Race for the last dollar | `BEGIN IMMEDIATE` + a re-check inside the write transaction | A process that writes the file outside the library |
21
+ | A payee | Ask for a different asset, a longer validity, or a "failed" settlement it keeps | The guard prices known USD stablecoins and refuses the rest; the CDP store refuses a different asset; an unknown outcome keeps counting | An asset the guard cannot price is refused, not valued |
22
+ | A payee with a 402 it can replay | Hold authorizations and settle later | A reservation counts until it is committed/released, or its expiry plus one window | A confirmed settlement still needs the chain to be read back |
23
+ | Another local user | Read the budget file | The parent directory is created `0700`; use a `0600` file if you pass a custom `path` | Files copied out of that directory keep their mode |
24
+ | A host failure | Lose the file | The database is one file; back it up like any other state | A lost file is a lost count; the default refuses to recreate it |
25
+ | A later on-chain revoke | Land in the same block after a payment | The payment is already ordered ahead of the revoke | The owner's revocation is not retroactive |
26
+
27
+ ## Deployment patterns
28
+
29
+ 1. **One agent, one OS user.** The owner runs `walletcap set` and keeps the
30
+ budget file out of the agent's reach. This is the strong pattern.
31
+ 2. **A service the agent calls.** The service owns the file and the wallet; the
32
+ agent gets an API, not the key.
33
+ 3. **On chain.** `AccountCapSpender` makes the cap part of the payment path, so
34
+ the agent's own code cannot raise it. It is unaudited; testnet only until an
35
+ audit.
36
+ 4. **One host, one volume.** SQLite locking is a local-filesystem contract. A
37
+ shared volume is not a multi-host database; replicas on separate hosts need
38
+ the Postgres backend (next), not a network mount.
39
+
40
+ ## What the guard does not do
41
+
42
+ - It does not decide whether a payment *should* happen; it only bounds it.
43
+ - It does not confirm settlement. A payment the payee calls failed keeps
44
+ counting until its authorization expires, and a payment with no expiry keeps
45
+ counting until an owner releases it.
46
+ - It does not protect the wallet key or the chain.
47
+ - It does not make an unaudited contract safe.
48
+
49
+ ## CDP store vs guard
50
+
51
+ Use **one** per client. `budget.guard(client)` is walletcap's own counter and
52
+ prices every asset. `applySpendControls(..., { store: budget.cdpStore() })`
53
+ keeps CDP's policy layer but replaces its store with walletcap's ledger. Both
54
+ write the same budget, so both on one client double-count the payment.
@@ -0,0 +1,48 @@
1
+ # Vendor benchmark disclosure policy
2
+
3
+ This policy covers findings about **vendor products** in the Agent Payment
4
+ Benchmark. It is separate from walletcap's own 90-day policy in
5
+ [SECURITY.md](../SECURITY.md).
6
+
7
+ ## The notice
8
+
9
+ Before publishing a named benchmark result, we send a **7-day pre-publication
10
+ notice** to the vendor's security contact, the product owner, and the relevant
11
+ executive. The notice includes:
12
+
13
+ - the affected product and version;
14
+ - the configuration tested;
15
+ - the measured impact;
16
+ - a private reproduction package;
17
+ - the exact publication date and time;
18
+ - an offer of a technical call within 48 hours.
19
+
20
+ We publish on the stated date whether or not the vendor replies. If the vendor
21
+ replies, their statement is included verbatim in the benchmark's response
22
+ column.
23
+
24
+ ## Status labels
25
+
26
+ - **Unpatched** — no fixed public release as of the publication timestamp.
27
+ - **Acknowledged** — the vendor replied; a fix is in progress.
28
+ - **Fix in review** — a public PR or release candidate exists.
29
+ - **Fixed** — re-tested on the fixed release; the date and version are shown.
30
+ - **Disputed** — the vendor disagrees; their response and our evidence are both
31
+ published.
32
+ - **No response** — the notice was delivered; no reply by the publication date.
33
+
34
+ ## What we do not do
35
+
36
+ - We do not change a status because a vendor buys an evaluation, a re-test, or a
37
+ CI gate. The benchmark publishes regardless.
38
+ - We do not publish a vendor's private data or a customer's identity.
39
+ - We do not treat an in-review report as a public finding before the notice
40
+ window. Until publication, the public material uses counts and classes, not
41
+ vendor names or routes.
42
+
43
+ ## Re-tests
44
+
45
+ Every benchmark entry has a publication timestamp and the exact version tested.
46
+ When a vendor ships a fix, we re-test and update the entry. The paid evaluation
47
+ and annual CI gate are the productized form of that re-test, not a condition of
48
+ the label.
@@ -0,0 +1,24 @@
1
+ # Quickstart
2
+
3
+ No wallet, no chain, no keys, no network. The demo uses a fake payee and a
4
+ temporary SQLite file.
5
+
6
+ ```sh
7
+ node examples/quickstart/quickstart.mjs
8
+ python3 examples/quickstart/quickstart.py
9
+ ```
10
+
11
+ Both print the same story: three `$0.05` payments against a `$0.15` rolling
12
+ cap; a `$1.00` payment refused by the per-payment cap; a fourth `$0.05`
13
+ refused by the rolling cap; a restart that asks for looser limits and is
14
+ refused because the stored count and limits stay.
15
+
16
+ The demo opens with `create: true` because it starts from an empty file. A
17
+ production agent opens with the default `create: false`, after the owner has
18
+ created the budget once:
19
+
20
+ ```sh
21
+ npx walletcap set --wallet 0xYourAgentWallet --per-payment 50 --per-day 150
22
+ ```
23
+
24
+ The demo file is `examples/quickstart/demo.sqlite` and is gitignored.
@@ -0,0 +1,59 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // A ten-second demo with a fake payee: no wallet, chain, key or network.
3
+ // Production code opens the budget with create: false; the owner creates it
4
+ // once with `npx walletcap set`.
5
+ import { rmSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { BudgetRefused, openBudget } from '../../js/index.mjs';
9
+
10
+ const PATH = join(dirname(fileURLToPath(import.meta.url)), 'demo.sqlite');
11
+ const WALLET = '0xd797ead28c38575fdf4fc523866287372340ca02';
12
+ const TX = `0x${'ab'.repeat(32)}`;
13
+
14
+ rmSync(PATH, { force: true });
15
+
16
+ const budget = openBudget({
17
+ wallet: WALLET,
18
+ path: PATH,
19
+ perPayment: '$0.05',
20
+ perDay: '$0.15',
21
+ create: true, // demo only
22
+ });
23
+
24
+ const attempt = async (target, label, amount) => {
25
+ try {
26
+ await target.pay(amount, async () => TX);
27
+ console.log(`paid ${label.padEnd(28)} ${amount}`);
28
+ } catch (error) {
29
+ if (!(error instanceof BudgetRefused)) throw error;
30
+ console.log(`REFUSED ${label.padEnd(28)} ${amount} (${error.reason})`);
31
+ }
32
+ };
33
+
34
+ console.log(`Budget: ${budget.status().summary}\n`);
35
+ for (const amount of ['$0.05', '$0.05', '$0.05']) await attempt(budget, 'three small payments', amount);
36
+ await attempt(budget, 'past the per-payment cap', '$1.00');
37
+ await attempt(budget, 'past the rolling cap', '$0.05');
38
+
39
+ const beforeRestart = budget.status().spent;
40
+ budget.close();
41
+
42
+ const warnings = [];
43
+ const restarted = openBudget({
44
+ wallet: WALLET,
45
+ path: PATH,
46
+ perPayment: '$2.50',
47
+ perDay: '$5',
48
+ onWarning: message => warnings.push(message),
49
+ });
50
+ console.log(`\nRestart asks for looser limits; stored limits stay: ${warnings[0] ?? 'no warning'}`);
51
+ console.log(`Count before restart: ${beforeRestart}; after restart: ${restarted.status().spent}\n`);
52
+ await attempt(restarted, 'the same $0.05 after restart', '$0.05');
53
+
54
+ const final = openBudget({ wallet: WALLET, path: PATH });
55
+ console.log(`\nFinal: ${final.status().summary}`);
56
+ console.log(`Result: ${final.status().spent} of ${final.status().cap} spent, ${final.status().remaining} left.`);
57
+ console.log(`History: ${final.history().length} payments; the first is ${final.history()[0].status}.`);
58
+ final.close();
59
+ restarted.close();