walletcap 0.2.1__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 (70) hide show
  1. {walletcap-0.2.1 → walletcap-0.2.2}/.circleci/config.yml +5 -0
  2. {walletcap-0.2.1 → walletcap-0.2.2}/.gitignore +1 -0
  3. walletcap-0.2.2/CHANGELOG.md +28 -0
  4. {walletcap-0.2.1 → walletcap-0.2.2}/PKG-INFO +34 -8
  5. {walletcap-0.2.1 → walletcap-0.2.2}/README.md +33 -7
  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.1 → walletcap-0.2.2}/js/README.md +4 -0
  13. walletcap-0.2.2/js/_keccak.mjs +87 -0
  14. {walletcap-0.2.1 → walletcap-0.2.2}/js/ledger.mjs +6 -0
  15. {walletcap-0.2.1 → walletcap-0.2.2}/js/money.mjs +28 -3
  16. {walletcap-0.2.1 → walletcap-0.2.2}/js/package.json +2 -1
  17. {walletcap-0.2.1 → walletcap-0.2.2}/js/tests/budget.test.mjs +8 -1
  18. {walletcap-0.2.1 → walletcap-0.2.2}/js/tests/cli.test.mjs +1 -1
  19. walletcap-0.2.2/js/tests/keccak.test.mjs +19 -0
  20. {walletcap-0.2.1 → walletcap-0.2.2}/js/tests/money.test.mjs +10 -1
  21. {walletcap-0.2.1 → walletcap-0.2.2}/pyproject.toml +1 -1
  22. {walletcap-0.2.1 → walletcap-0.2.2}/scripts/ci.sh +4 -0
  23. walletcap-0.2.2/solidity/README.md +79 -0
  24. {walletcap-0.2.1 → walletcap-0.2.2}/solidity/evm/package.json +1 -1
  25. {walletcap-0.2.1 → walletcap-0.2.2}/solidity/src/AccountCapSpender.sol +1 -0
  26. {walletcap-0.2.1 → walletcap-0.2.2}/src/walletcap/__init__.py +1 -1
  27. walletcap-0.2.2/src/walletcap/_keccak.py +71 -0
  28. {walletcap-0.2.1 → walletcap-0.2.2}/src/walletcap/ledger.py +9 -0
  29. {walletcap-0.2.1 → walletcap-0.2.2}/src/walletcap/money.py +35 -4
  30. {walletcap-0.2.1 → walletcap-0.2.2}/tests/test_budget.py +12 -1
  31. walletcap-0.2.2/tests/test_keccak.py +10 -0
  32. walletcap-0.2.2/tests/test_money.py +36 -0
  33. walletcap-0.2.1/CHANGELOG.md +0 -24
  34. {walletcap-0.2.1 → walletcap-0.2.2}/.github/workflows/ci.yml +0 -0
  35. {walletcap-0.2.1 → walletcap-0.2.2}/.github/workflows/publish.yml +0 -0
  36. {walletcap-0.2.1 → walletcap-0.2.2}/LICENSE +0 -0
  37. {walletcap-0.2.1 → walletcap-0.2.2}/NOTICE +0 -0
  38. {walletcap-0.2.1 → walletcap-0.2.2}/js/LICENSE +0 -0
  39. {walletcap-0.2.1 → walletcap-0.2.2}/js/NOTICE +0 -0
  40. {walletcap-0.2.1 → walletcap-0.2.2}/js/agentic.mjs +0 -0
  41. {walletcap-0.2.1 → walletcap-0.2.2}/js/cdp.mjs +0 -0
  42. {walletcap-0.2.1 → walletcap-0.2.2}/js/cli.mjs +0 -0
  43. {walletcap-0.2.1 → walletcap-0.2.2}/js/errors.mjs +0 -0
  44. {walletcap-0.2.1 → walletcap-0.2.2}/js/index.d.ts +0 -0
  45. {walletcap-0.2.1 → walletcap-0.2.2}/js/index.mjs +0 -0
  46. {walletcap-0.2.1 → walletcap-0.2.2}/js/tests/agentic.test.mjs +0 -0
  47. {walletcap-0.2.1 → walletcap-0.2.2}/js/tests/cdp-sdk.test.mjs +0 -0
  48. {walletcap-0.2.1 → walletcap-0.2.2}/js/tests/cdp.test.mjs +0 -0
  49. {walletcap-0.2.1 → walletcap-0.2.2}/js/tests/processes.test.mjs +0 -0
  50. {walletcap-0.2.1 → walletcap-0.2.2}/js/tests/support/fake-awal/dist/index.js +0 -0
  51. {walletcap-0.2.1 → walletcap-0.2.2}/js/tests/support/fake-awal/dist/ipcClient.js +0 -0
  52. {walletcap-0.2.1 → walletcap-0.2.2}/js/tests/support/fake-awal/package.json +0 -0
  53. {walletcap-0.2.1 → walletcap-0.2.2}/js/tests/support/fake-payments-mcp.mjs +0 -0
  54. {walletcap-0.2.1 → walletcap-0.2.2}/js/tests/support/payer.mjs +0 -0
  55. {walletcap-0.2.1 → walletcap-0.2.2}/js/tests/support/race.mjs +0 -0
  56. {walletcap-0.2.1 → walletcap-0.2.2}/js/tests/support/x402-kit.mjs +0 -0
  57. {walletcap-0.2.1 → walletcap-0.2.2}/js/tests/support/x402-payer.mjs +0 -0
  58. {walletcap-0.2.1 → walletcap-0.2.2}/js/tests/x402-fetch.test.mjs +0 -0
  59. {walletcap-0.2.1 → walletcap-0.2.2}/js/tests/x402.test.mjs +0 -0
  60. {walletcap-0.2.1 → walletcap-0.2.2}/js/x402.mjs +0 -0
  61. {walletcap-0.2.1 → walletcap-0.2.2}/pytest.ini +0 -0
  62. {walletcap-0.2.1 → walletcap-0.2.2}/solidity/evm/check.mjs +0 -0
  63. {walletcap-0.2.1 → walletcap-0.2.2}/solidity/foundry.toml +0 -0
  64. {walletcap-0.2.1 → walletcap-0.2.2}/solidity/test/AccountCapSpender.t.sol +0 -0
  65. {walletcap-0.2.1 → walletcap-0.2.2}/solidity/test/Mocks.sol +0 -0
  66. {walletcap-0.2.1 → walletcap-0.2.2}/src/walletcap/__main__.py +0 -0
  67. {walletcap-0.2.1 → walletcap-0.2.2}/src/walletcap/budget.py +0 -0
  68. {walletcap-0.2.1 → walletcap-0.2.2}/src/walletcap/errors.py +0 -0
  69. {walletcap-0.2.1 → walletcap-0.2.2}/src/walletcap/py.typed +0 -0
  70. {walletcap-0.2.1 → 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.1
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
@@ -60,6 +60,21 @@ with budget.pay("$12.50"):
60
60
  `npm install walletcap` (Node 22.13+) · `pip install walletcap` (Python 3.11+) ·
61
61
  no dependencies · Apache-2.0
62
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
+
63
78
  ## Why a per-wallet count
64
79
 
65
80
  Most agent spend controls keep "spent so far" next to something temporary or repeatable: a
@@ -280,8 +295,9 @@ only for a demo or a first-run setup.
280
295
  go around it: call the wallet directly, or delete the file. To bound an agent like that, keep
281
296
  the budget out of its reach (another OS user, another service) or enforce on chain, as
282
297
  `AccountCapSpender` does for Spend Permissions.
283
- - **One file is one host, or a shared volume.** A Postgres backend for replicas on separate
284
- 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.
285
301
  - **It can't see a revoke that lands later in the same block,** and for x402 schemes that settle
286
302
  later (batch vouchers), a payment counts from when the server confirms it.
287
303
  - **Some x402 payments can't be confirmed.** `@x402/fetch` doesn't report v1 settlements
@@ -300,8 +316,8 @@ only for a demo or a first-run setup.
300
316
  | `tests/` | the Python suite, including the tests that drive both languages against one budget file |
301
317
 
302
318
  ```console
303
- $ cd js && npm install && node --test # 58 tests
304
- $ uv run --with pytest pytest -q # 54 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
305
321
  $ cd solidity/evm && npm install && node check.mjs .. # the contract on an EVM, no Foundry needed
306
322
  $ cd solidity && forge test # the contract's canonical tests, if you have Foundry
307
323
  ```
@@ -312,9 +328,19 @@ MCP proxy against stand-ins for Coinbase's wallet and MCP server. The Python sui
312
328
  the CLI and one budget shared between both languages. `solidity/evm/check.mjs` compiles the
313
329
  contract with solc-js and drives it on an in-process EVM, covering what the Foundry tests cover.
314
330
 
315
- One test is **not** here: the integration test that drives Coinbase's CDP spend controls through
316
- the shared store. It runs against a vendored build of Coinbase's SDK and lives in ShadowPath's
317
- 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.
318
344
 
319
345
  ## Prove it holds in your deployment
320
346
 
@@ -37,6 +37,21 @@ with budget.pay("$12.50"):
37
37
  `npm install walletcap` (Node 22.13+) · `pip install walletcap` (Python 3.11+) ·
38
38
  no dependencies · Apache-2.0
39
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
+
40
55
  ## Why a per-wallet count
41
56
 
42
57
  Most agent spend controls keep "spent so far" next to something temporary or repeatable: a
@@ -257,8 +272,9 @@ only for a demo or a first-run setup.
257
272
  go around it: call the wallet directly, or delete the file. To bound an agent like that, keep
258
273
  the budget out of its reach (another OS user, another service) or enforce on chain, as
259
274
  `AccountCapSpender` does for Spend Permissions.
260
- - **One file is one host, or a shared volume.** A Postgres backend for replicas on separate
261
- 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.
262
278
  - **It can't see a revoke that lands later in the same block,** and for x402 schemes that settle
263
279
  later (batch vouchers), a payment counts from when the server confirms it.
264
280
  - **Some x402 payments can't be confirmed.** `@x402/fetch` doesn't report v1 settlements
@@ -277,8 +293,8 @@ only for a demo or a first-run setup.
277
293
  | `tests/` | the Python suite, including the tests that drive both languages against one budget file |
278
294
 
279
295
  ```console
280
- $ cd js && npm install && node --test # 58 tests
281
- $ uv run --with pytest pytest -q # 54 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
282
298
  $ cd solidity/evm && npm install && node check.mjs .. # the contract on an EVM, no Foundry needed
283
299
  $ cd solidity && forge test # the contract's canonical tests, if you have Foundry
284
300
  ```
@@ -289,9 +305,19 @@ MCP proxy against stand-ins for Coinbase's wallet and MCP server. The Python sui
289
305
  the CLI and one budget shared between both languages. `solidity/evm/check.mjs` compiles the
290
306
  contract with solc-js and drives it on an in-process EVM, covering what the Foundry tests cover.
291
307
 
292
- One test is **not** here: the integration test that drives Coinbase's CDP spend controls through
293
- the shared store. It runs against a vendored build of Coinbase's SDK and lives in ShadowPath's
294
- 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.
295
321
 
296
322
  ## Prove it holds in your deployment
297
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();
@@ -0,0 +1,70 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """A ten-second demo with a fake payee: no wallet, chain, key or network.
3
+
4
+ Production code opens the budget with create=False; the owner creates it once
5
+ with walletcap set.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ import warnings
12
+ from pathlib import Path
13
+
14
+ sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "src"))
15
+
16
+ from walletcap import BudgetRefused, open_budget
17
+
18
+ PATH = Path(__file__).with_name("demo.sqlite")
19
+ WALLET = "0xd797ead28c38575fdf4fc523866287372340ca02"
20
+ TX = "0x" + "ab" * 32
21
+
22
+ PATH.unlink(missing_ok=True)
23
+
24
+ budget = open_budget(
25
+ WALLET,
26
+ path=PATH,
27
+ per_payment="$0.05",
28
+ per_day="$0.15",
29
+ create=True, # demo only
30
+ )
31
+
32
+
33
+ def attempt(target: object, label: str, amount: str) -> None:
34
+ try:
35
+ with target.pay(amount) as payment: # type: ignore[attr-defined]
36
+ payment.tx = TX
37
+ print(f"paid {label:<28} {amount}")
38
+ except BudgetRefused as error:
39
+ print(f"REFUSED {label:<28} {amount} ({error.reason})")
40
+
41
+
42
+ print(f"Budget: {budget.status().summary}\n")
43
+ for amount in ("$0.05", "$0.05", "$0.05"):
44
+ attempt(budget, "three small payments", amount)
45
+ attempt(budget, "past the per-payment cap", "$1.00")
46
+ attempt(budget, "past the rolling cap", "$0.05")
47
+
48
+ before_restart = budget.status().spent
49
+ budget.close()
50
+
51
+ messages: list[str] = []
52
+ with warnings.catch_warnings(record=True) as caught:
53
+ warnings.simplefilter("always")
54
+ restarted = open_budget(
55
+ WALLET,
56
+ path=PATH,
57
+ per_payment="$2.50",
58
+ per_day="$5",
59
+ )
60
+ messages = [str(item.message) for item in caught]
61
+ print(f"\nRestart asks for looser limits; stored limits stay: {messages[0] if messages else 'no warning'}")
62
+ print(f"Count before restart: {before_restart}; after restart: {restarted.status().spent}\n")
63
+ attempt(restarted, "the same $0.05 after restart", "$0.05")
64
+
65
+ final = open_budget(WALLET, path=PATH)
66
+ print(f"\nFinal: {final.status().summary}")
67
+ print(f"Result: {final.status().spent} of {final.status().cap} spent, {final.status().remaining} left.")
68
+ print(f"History: {len(final.history())} payments; the first is {final.history()[0].status}.")
69
+ final.close()
70
+ restarted.close()
@@ -37,6 +37,10 @@ $ npx walletcap set --wallet 0xYourAgentWallet --per-payment 0.05 --per-day 0.15
37
37
  $ npx walletcap freeze
38
38
  ```
39
39
 
40
+ Security: [policy](https://github.com/coriolislabs/WalletCap/blob/main/SECURITY.md) ·
41
+ [threat model](https://github.com/coriolislabs/WalletCap/blob/main/docs/threat-model.md).
42
+ `AccountCapSpender` is unaudited; testnet only until an independent audit.
43
+
40
44
  [Source, Python package and full documentation](https://github.com/coriolislabs/WalletCap)
41
45
 
42
46
  Apache-2.0. Copyright 2026 Coriolis Labs Inc.
@@ -0,0 +1,87 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Keccak-256, for EIP-55 address checksums. No dependencies: this is
3
+ // Keccak-f[1600] with the original Keccak padding, which is what Ethereum
4
+ // uses. (node:crypto's sha3-256 is NIST SHA3, a different padding.)
5
+
6
+ const MASK = (1n << 64n) - 1n;
7
+
8
+ const RC = [
9
+ 0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n,
10
+ 0x000000000000808bn, 0x0000000080000001n, 0x8000000080008081n, 0x8000000000008009n,
11
+ 0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an,
12
+ 0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n,
13
+ 0x8000000000008002n, 0x8000000000000080n, 0x000000000000800an, 0x800000008000000an,
14
+ 0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n,
15
+ ];
16
+
17
+ const ROT = [
18
+ [0, 36, 3, 41, 18],
19
+ [1, 44, 10, 45, 2],
20
+ [62, 6, 43, 15, 61],
21
+ [28, 55, 25, 21, 56],
22
+ [27, 20, 39, 8, 14],
23
+ ];
24
+
25
+ const rotl = (value, amount) => amount === 0
26
+ ? value & MASK
27
+ : ((value << BigInt(amount)) | (value >> BigInt(64 - amount))) & MASK;
28
+
29
+ function permute(state) {
30
+ for (const rc of RC) {
31
+ const c = [];
32
+ for (let x = 0; x < 5; x += 1) {
33
+ c[x] = state[x] ^ state[x + 5] ^ state[x + 10] ^ state[x + 15] ^ state[x + 20];
34
+ }
35
+ const d = [];
36
+ for (let x = 0; x < 5; x += 1) {
37
+ d[x] = c[(x + 4) % 5] ^ rotl(c[(x + 1) % 5], 1);
38
+ }
39
+ for (let x = 0; x < 5; x += 1) {
40
+ for (let y = 0; y < 5; y += 1) state[x + 5 * y] ^= d[x];
41
+ }
42
+ const b = new Array(25).fill(0n);
43
+ for (let x = 0; x < 5; x += 1) {
44
+ for (let y = 0; y < 5; y += 1) {
45
+ b[y + 5 * ((2 * x + 3 * y) % 5)] = rotl(state[x + 5 * y], ROT[x][y]);
46
+ }
47
+ }
48
+ for (let x = 0; x < 5; x += 1) {
49
+ for (let y = 0; y < 5; y += 1) {
50
+ state[x + 5 * y] = (b[x + 5 * y] ^ ((~b[(x + 1) % 5 + 5 * y] & MASK) & b[(x + 2) % 5 + 5 * y])) & MASK;
51
+ }
52
+ }
53
+ state[0] ^= rc;
54
+ }
55
+ }
56
+
57
+ export function keccak256(data) {
58
+ const rate = 136;
59
+ const bytes = Array.from(data);
60
+ bytes.push(0x01);
61
+ while (bytes.length % rate !== rate - 1) bytes.push(0x00);
62
+ bytes.push(0x80);
63
+
64
+ const state = new Array(25).fill(0n);
65
+ for (let offset = 0; offset < bytes.length; offset += rate) {
66
+ for (let lane = 0; lane < rate / 8; lane += 1) {
67
+ let value = 0n;
68
+ for (let j = 0; j < 8; j += 1) {
69
+ value |= BigInt(bytes[offset + lane * 8 + j]) << BigInt(8 * j);
70
+ }
71
+ state[lane] ^= value;
72
+ }
73
+ permute(state);
74
+ }
75
+
76
+ const out = new Uint8Array(32);
77
+ for (let i = 0; i < 4; i += 1) {
78
+ let value = state[i];
79
+ for (let j = 0; j < 8; j += 1) {
80
+ out[i * 8 + j] = Number(value & 0xffn);
81
+ value >>= 8n;
82
+ }
83
+ }
84
+ return out;
85
+ }
86
+
87
+ export const keccak256Hex = text => Buffer.from(keccak256(new TextEncoder().encode(text))).toString('hex');
@@ -291,6 +291,12 @@ export class Ledger {
291
291
  return this.#row(instrument);
292
292
  }
293
293
  if (!unitsMatch(row.unit, unit)) throw new BudgetError(`${shortWallet(instrument)}'s budget counts in ${row.unit}, not ${unit}`);
294
+ if (limits.windowMs !== undefined && limits.windowMs !== row.window_ms) {
295
+ const used = this.statement('SELECT 1 FROM spend WHERE instrument = ? LIMIT 1').get(instrument);
296
+ if (used) {
297
+ throw new BudgetError(`${shortWallet(instrument)} has spend in this budget; the window cannot change. Create a new budget instead.`);
298
+ }
299
+ }
294
300
  this.statement('UPDATE budgets SET cap = ?, window_ms = ?, per_payment = ?, max_uses = ? WHERE instrument = ?').run(
295
301
  limits.cap ?? row.cap,
296
302
  limits.windowMs ?? row.window_ms,