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.
- {walletcap-0.2.0 → walletcap-0.2.2}/.circleci/config.yml +5 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/.gitignore +1 -0
- walletcap-0.2.2/CHANGELOG.md +28 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/PKG-INFO +68 -28
- {walletcap-0.2.0 → walletcap-0.2.2}/README.md +67 -27
- walletcap-0.2.2/SECURITY.md +64 -0
- walletcap-0.2.2/docs/threat-model.md +54 -0
- walletcap-0.2.2/docs/vendor-disclosure.md +48 -0
- walletcap-0.2.2/examples/quickstart/README.md +24 -0
- walletcap-0.2.2/examples/quickstart/quickstart.mjs +59 -0
- walletcap-0.2.2/examples/quickstart/quickstart.py +70 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/js/README.md +11 -1
- walletcap-0.2.2/js/_keccak.mjs +87 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/js/cdp.mjs +28 -6
- {walletcap-0.2.0 → walletcap-0.2.2}/js/index.mjs +8 -3
- {walletcap-0.2.0 → walletcap-0.2.2}/js/ledger.mjs +6 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/js/money.mjs +28 -3
- {walletcap-0.2.0 → walletcap-0.2.2}/js/package.json +5 -1
- {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/agentic.test.mjs +2 -2
- {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/budget.test.mjs +18 -3
- walletcap-0.2.2/js/tests/cdp-sdk.test.mjs +87 -0
- walletcap-0.2.2/js/tests/cdp.test.mjs +117 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/cli.test.mjs +4 -4
- walletcap-0.2.2/js/tests/keccak.test.mjs +19 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/money.test.mjs +10 -1
- {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/processes.test.mjs +3 -2
- {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/x402-fetch.test.mjs +1 -1
- {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/x402.test.mjs +2 -2
- {walletcap-0.2.0 → walletcap-0.2.2}/js/x402.mjs +11 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/pyproject.toml +1 -1
- {walletcap-0.2.0 → walletcap-0.2.2}/scripts/ci.sh +4 -0
- walletcap-0.2.2/solidity/README.md +79 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/solidity/evm/package.json +1 -1
- {walletcap-0.2.0 → walletcap-0.2.2}/solidity/src/AccountCapSpender.sol +1 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/src/walletcap/__init__.py +2 -1
- walletcap-0.2.2/src/walletcap/_keccak.py +71 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/src/walletcap/budget.py +8 -5
- {walletcap-0.2.0 → walletcap-0.2.2}/src/walletcap/ledger.py +9 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/src/walletcap/money.py +35 -4
- {walletcap-0.2.0 → walletcap-0.2.2}/tests/test_budget.py +31 -11
- walletcap-0.2.2/tests/test_keccak.py +10 -0
- walletcap-0.2.2/tests/test_money.py +36 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/.github/workflows/ci.yml +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/.github/workflows/publish.yml +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/LICENSE +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/NOTICE +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/js/LICENSE +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/js/NOTICE +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/js/agentic.mjs +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/js/cli.mjs +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/js/errors.mjs +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/js/index.d.ts +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/support/fake-awal/dist/index.js +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/support/fake-awal/dist/ipcClient.js +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/support/fake-awal/package.json +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/support/fake-payments-mcp.mjs +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/support/payer.mjs +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/support/race.mjs +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/support/x402-kit.mjs +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/js/tests/support/x402-payer.mjs +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/pytest.ini +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/solidity/evm/check.mjs +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/solidity/foundry.toml +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/solidity/test/AccountCapSpender.t.sol +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/solidity/test/Mocks.sol +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/src/walletcap/__main__.py +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/src/walletcap/errors.py +0 -0
- {walletcap-0.2.0 → walletcap-0.2.2}/src/walletcap/py.typed +0 -0
- {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: |
|
|
@@ -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.
|
|
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
|
|
80
|
-
unknown keeps counting, because a payment that failed
|
|
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
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
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.
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|
270
|
-
|
|
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 #
|
|
290
|
-
$ uv run --with pytest pytest -q #
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
57
|
-
unknown keeps counting, because a payment that failed
|
|
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
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
130
|
-
|
|
131
|
-
|
|
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.
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
247
|
-
|
|
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 #
|
|
267
|
-
$ uv run --with pytest pytest -q #
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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();
|