@wafermapxyz/cli 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,42 +1,53 @@
1
- # Changelog
2
-
3
- ## 0.4.1 (2 Nov 2026)
4
-
5
- On npm as **`@wafermapxyz/cli`**: `npm i -g @wafermapxyz/cli` (SDK: `import { Wafermap } from "@wafermapxyz/cli"`). The command stays `wafermap`, the wallet stays in `~/.wafermap`. The registry refuses the bare name `wafermap` for everyone, so the package lives under our own scope. If you installed from the tarball, run `npm rm -g wafermap` before installing the scoped package. No code changes since 0.4.0.
6
-
7
- ## 0.4.0 (2 Nov 2026)
8
-
9
- From the second public review by @whoiskn0why, thank you.
10
-
11
- - **Shorter payment authorizations.** EVM payments are signed for at most 90 seconds (was up to 300), or less when the server asks for less. That is the window in which "a failed call costs $0" rests on the server's word; `wafermap history --verify` still checks every one on-chain afterwards.
12
- - **Time limits.** Requests to paid resources and services, Wafermap's API, the swap API and Solana RPC give up after 60 seconds (`timeoutMs` in the SDK, `DEFAULT_TIMEOUT_MS`); EVM RPC keeps viem's time-outs. Before, a server that never answered held a call forever. A paid call that times out after signing says the payment stays counted (and, for EVM payments, until when it can be executed); one that times out before signing says nothing was paid.
13
- - **Swaps are checked before anything is signed.** The build now comes first and the approval after it. The quote and the build must name KyberSwap's router (`KYBER_ROUTER`, `0x6131B5fae19EA4f9D964eAc0408E4408b66337b5`). The swap calldata is decoded (`decodeSwap`) and must trade the quoted tokens, spend exactly the quoted amount, pay the output to this wallet, take no fee, and set an on-chain minimum output no lower than the built output less your slippage; the built output must be within your slippage of the quote, and ETH is sent only when selling ETH. Otherwise nothing is approved or sent (`unexpected_router`, `unexpected_calldata`, `build_mismatch`, `price_moved`). `amountOutMin` is now the minimum encoded in the transaction.
14
- - **The MCP password can leave the config file.** `wafermap keychain on` stores it with Windows DPAPI, the macOS Keychain or the Linux Secret Service; `wafermap mcp` reads it from there, and warns when `WAFERMAP_PASSWORD` is still set in plain text.
15
- - An empty `WAFERMAP_PASSWORD` counts as not set. `wafermap keychain off` checks that the password is really gone.
16
- - Tests for all of the above (`test/hardening.test.mjs`: real DPAPI on Windows; the macOS and Linux stores are exercised against a stub). CI also runs on Windows and macOS.
17
- - The SDK exports `quoteSwap`, `executeSwap`, `decodeSwap`, `KYBER_ROUTER`, `MAX_VALIDITY_SECONDS` and `DEFAULT_TIMEOUT_MS`.
18
-
19
- npm: the name `wafermap` is refused by the registry for everyone (too similar to an existing package). From 0.4.1 the package is `@wafermapxyz/cli`.
20
-
21
- ## 0.3.3 (1 Nov 2026)
22
-
23
- **Fix: the daily cap no longer trusts the server.** Reported publicly in a code review by @whoiskn0why, thank you.
24
-
25
- Before: the spend record was written after the call, from the server's receipt (`call()`), or as $0 when the server answered with an error (`pay()`). A server that reported $0, sent no receipt, answered with an error or dropped the connection could take signed payments that never counted towards the daily cap. With a $0.10 cap, $0.15 in signed payments could go through.
26
-
27
- Now:
28
- - Every payment is recorded at its full signed amount the moment it is signed, before it is sent, and stays counted whatever the server answers. The outcome (ok / failed / refused) is written as a second line that adds nothing to the total.
29
- - Check, sign and record run one payment at a time within a process, so concurrent calls cannot both pass the cap on the same remaining budget (found while writing the tests). Across processes sharing one wallet (two MCP clients, CLI + MCP), each signed payment is checked again against the shared log before it is sent and dropped, never sent, if another process used the budget meanwhile (found in our own adversarial review).
30
- - `wafermap history --verify` (SDK: `checkPayment`) reads `authorizationState` on the token contract to show which signed EVM payments were executed and which expired unused, without trusting any server.
31
- - `wafermap history` shows each payment's status and notes that log lines from before 0.3.3 hold what the server reported.
32
- - Tests (`npm test`): the caps against a local mock server that lies, errors, refuses or hangs up; CI on Node 20 and 22.
33
-
34
- Older `spend.jsonl` lines are still read; `mergeLog` joins the new two-line records.
35
-
36
- ## 0.3.2
37
-
38
- Opt-in per-payment confirmation for the MCP server (`wafermap limits --mcp-confirm on`).
39
-
40
- ## 0.3.1
41
-
42
- First public source release: CLI, SDK and MCP server.
1
+ # Changelog
2
+
3
+ ## 0.4.3 (2 Nov 2026)
4
+
5
+ The two open points from the third public review by @whoiskn0why (8/10), thank you.
6
+
7
+ - **`wafermap resources` has a time limit.** It was the one command that still used a bare `fetch` and could wait forever on a server that never answered. It now gives up after 60 seconds with `timeout`, like every other request, and reports `network_error` instead of printing nothing when the catalog cannot be read. The SDK exports the same function as `listResources(baseUrl?, timeoutMs?)`; `Wafermap.resources()` uses it. `WAFERMAP_TIMEOUT_MS` sets the limit for the CLI and the MCP server.
8
+ - **npm provenance.** From this version the package is published by GitHub Actions (`.github/workflows/publish.yml`) through npm trusted publishing, with a signed provenance statement that ties the tarball on npm to the commit and workflow run that built it. Check it on the npm page ("Provenance") or with `npm audit signatures`. CI holds no npm token: npm trusts this one workflow file in this repo, and nothing else.
9
+
10
+ ## 0.4.2 (2 Nov 2026)
11
+
12
+ **$WAFERMAP holder discount.** `call()` (CLI `wafermap call`, MCP `wafermap_call`) now names the paying wallet to Wafermap in an `X-Wafermap-Payer` header, so a wallet holding at least 100,000 $WAFERMAP on Robinhood Chain is quoted 50% less for Wafermap's own resources, on EVM networks. Wafermap checks the balance again against the signer of the payment, so the header only ever lowers the price for the wallet that actually holds. The header goes to Wafermap only: `pay()` sends nothing extra to third-party services. Marketplace listings keep their seller's price.
13
+
14
+ ## 0.4.1 (2 Nov 2026)
15
+
16
+ On npm as **`@wafermapxyz/cli`**: `npm i -g @wafermapxyz/cli` (SDK: `import { Wafermap } from "@wafermapxyz/cli"`). The command stays `wafermap`, the wallet stays in `~/.wafermap`. The registry refuses the bare name `wafermap` for everyone, so the package lives under our own scope. If you installed from the tarball, run `npm rm -g wafermap` before installing the scoped package. No code changes since 0.4.0.
17
+
18
+ ## 0.4.0 (2 Nov 2026)
19
+
20
+ From the second public review by @whoiskn0why, thank you.
21
+
22
+ - **Shorter payment authorizations.** EVM payments are signed for at most 90 seconds (was up to 300), or less when the server asks for less. That is the window in which "a failed call costs $0" rests on the server's word; `wafermap history --verify` still checks every one on-chain afterwards.
23
+ - **Time limits.** Requests to paid resources and services, Wafermap's API, the swap API and Solana RPC give up after 60 seconds (`timeoutMs` in the SDK, `DEFAULT_TIMEOUT_MS`); EVM RPC keeps viem's time-outs. Before, a server that never answered held a call forever. A paid call that times out after signing says the payment stays counted (and, for EVM payments, until when it can be executed); one that times out before signing says nothing was paid.
24
+ - **Swaps are checked before anything is signed.** The build now comes first and the approval after it. The quote and the build must name KyberSwap's router (`KYBER_ROUTER`, `0x6131B5fae19EA4f9D964eAc0408E4408b66337b5`). The swap calldata is decoded (`decodeSwap`) and must trade the quoted tokens, spend exactly the quoted amount, pay the output to this wallet, take no fee, and set an on-chain minimum output no lower than the built output less your slippage; the built output must be within your slippage of the quote, and ETH is sent only when selling ETH. Otherwise nothing is approved or sent (`unexpected_router`, `unexpected_calldata`, `build_mismatch`, `price_moved`). `amountOutMin` is now the minimum encoded in the transaction.
25
+ - **The MCP password can leave the config file.** `wafermap keychain on` stores it with Windows DPAPI, the macOS Keychain or the Linux Secret Service; `wafermap mcp` reads it from there, and warns when `WAFERMAP_PASSWORD` is still set in plain text.
26
+ - An empty `WAFERMAP_PASSWORD` counts as not set. `wafermap keychain off` checks that the password is really gone.
27
+ - Tests for all of the above (`test/hardening.test.mjs`: real DPAPI on Windows; the macOS and Linux stores are exercised against a stub). CI also runs on Windows and macOS.
28
+ - The SDK exports `quoteSwap`, `executeSwap`, `decodeSwap`, `KYBER_ROUTER`, `MAX_VALIDITY_SECONDS` and `DEFAULT_TIMEOUT_MS`.
29
+
30
+ npm: the name `wafermap` is refused by the registry for everyone (too similar to an existing package). From 0.4.1 the package is `@wafermapxyz/cli`.
31
+
32
+ ## 0.3.3 (1 Nov 2026)
33
+
34
+ **Fix: the daily cap no longer trusts the server.** Reported publicly in a code review by @whoiskn0why, thank you.
35
+
36
+ Before: the spend record was written after the call, from the server's receipt (`call()`), or as $0 when the server answered with an error (`pay()`). A server that reported $0, sent no receipt, answered with an error or dropped the connection could take signed payments that never counted towards the daily cap. With a $0.10 cap, $0.15 in signed payments could go through.
37
+
38
+ Now:
39
+ - Every payment is recorded at its full signed amount the moment it is signed, before it is sent, and stays counted whatever the server answers. The outcome (ok / failed / refused) is written as a second line that adds nothing to the total.
40
+ - Check, sign and record run one payment at a time within a process, so concurrent calls cannot both pass the cap on the same remaining budget (found while writing the tests). Across processes sharing one wallet (two MCP clients, CLI + MCP), each signed payment is checked again against the shared log before it is sent and dropped, never sent, if another process used the budget meanwhile (found in our own adversarial review).
41
+ - `wafermap history --verify` (SDK: `checkPayment`) reads `authorizationState` on the token contract to show which signed EVM payments were executed and which expired unused, without trusting any server.
42
+ - `wafermap history` shows each payment's status and notes that log lines from before 0.3.3 hold what the server reported.
43
+ - Tests (`npm test`): the caps against a local mock server that lies, errors, refuses or hangs up; CI on Node 20 and 22.
44
+
45
+ Older `spend.jsonl` lines are still read; `mergeLog` joins the new two-line records.
46
+
47
+ ## 0.3.2
48
+
49
+ Opt-in per-payment confirmation for the MCP server (`wafermap limits --mcp-confirm on`).
50
+
51
+ ## 0.3.1
52
+
53
+ First public source release: CLI, SDK and MCP server.
package/README.md CHANGED
@@ -1,114 +1,118 @@
1
- # wafermap
2
-
3
- The CLI, SDK and MCP server behind [Wafermap](https://wafermap.xyz): an x402 wallet for AI agents. One local, encrypted key that can:
4
-
5
- - **call** Wafermap's pay-per-call resources (LLMs, image and audio models, web reading, on-chain data, tokenized stocks, prediction markets)
6
- - **discover** third-party x402 services in the open x402 index and **pay** them
7
- - **swap** tokens through the KyberSwap aggregator
8
-
9
- Every payment is checked against a per-payment cap and a daily cap before anything is signed. Swaps have their own per-swap cap. Payments settle in USDG on Robinhood Chain or USDC on Solana, Base, Arbitrum or Polygon and are gasless; swaps need a little ETH for gas.
10
-
11
- ## Install
12
-
13
- ```bash
14
- npm i -g @wafermapxyz/cli
15
- wafermap init
16
- ```
17
-
18
- The command is `wafermap`. The package is **`@wafermapxyz/cli`** on npm (the `wafermapxyz` scope, after the wafermap.xyz domain), the same build as https://wafermap.xyz/cli/wafermap.tgz and this repository. The bare name `wafermap` on npm is **not** this package: the registry refuses it for everyone. Coming from the tarball install? Run `npm rm -g wafermap` first; your wallet in `~/.wafermap` stays.
19
-
20
- `init` creates a key and encrypts it with your password (scrypt + AES-256-GCM) in `~/.wafermap/keystore.json`. Back it up with `wafermap export-key`. Then send a few dollars of USDC on Solana to the Solana address it prints, or USDG (Robinhood Chain) or USDC (Base, Arbitrum, Polygon) to its EVM address.
21
-
22
- The Solana address comes from the same key (derived with HMAC-SHA256), so one backup covers both. `wafermap export-key --solana` prints the Solana secret key in the form Phantom and Solflare import. `wafermap address` shows both addresses.
23
-
24
- ## CLI
25
-
26
- ```bash
27
- wafermap balance
28
- wafermap limits --per-payment 0.1 --per-day 1 --per-swap 25 --network robinhood
29
- wafermap resources stock
30
- wafermap call prediction-markets query="fed rates"
31
- wafermap call flux-2-dev prompt="a glossy silicon wafer" --out wafer.png
32
- wafermap discover weather
33
- wafermap pay https://x402.agentindex.world/weather city=Jakarta
34
- wafermap swap 0.01 ETH USDG
35
- wafermap history
36
- ```
37
-
38
- Add `--yes` to skip the confirmation prompt (caps still apply), `--network base`, `arbitrum`, `polygon` or `solana` to pay in USDC on that network (swaps run on `robinhood` or `base`). Set `WAFERMAP_PASSWORD` to avoid the password prompt in scripts.
39
-
40
- ## MCP server
41
-
42
- Give Claude Desktop, Cursor or any MCP client the same wallet. First put the wallet password in your operating system's keychain, once, in a terminal:
43
-
44
- ```bash
45
- wafermap keychain on
46
- ```
47
-
48
- Then the client config holds no secret:
49
-
50
- ```json
51
- {
52
- "mcpServers": {
53
- "wafermap": {
54
- "command": "wafermap",
55
- "args": ["mcp"]
56
- }
57
- }
58
- }
59
- ```
60
-
61
- `wafermap keychain on` stores the password with Windows DPAPI (a file only your Windows user can decrypt), in the macOS Keychain, or in the Secret Service on Linux (GNOME Keyring, KWallet, through `secret-tool`). `wafermap keychain` shows whether it is set and still unlocks the wallet; `wafermap keychain off` removes it. This keeps the password out of config files, backups and screenshots. It does not stop a program already running as you, which could also ask the OS for it. Setting `WAFERMAP_PASSWORD` in the client's `env` still works, but the password then sits in plain text in that config file, and the server says so when it starts.
62
-
63
- Tools: `wafermap_wallet`, `wafermap_resources`, `wafermap_call`, `wafermap_discover`, `wafermap_pay`, `wafermap_swap_quote`, `wafermap_swap`, `wafermap_history`. Swaps from MCP are off until you run `wafermap limits --mcp-swaps on`.
64
-
65
- To approve every payment yourself, run `wafermap limits --mcp-confirm on` and restart the MCP server. Each payment and swap then waits for you in the client, showing the amount, asset, recipient, network and resource, before the wallet signs. It uses MCP elicitation; a client that cannot show the prompt gets a refusal and nothing is paid. The caps still apply first.
66
-
67
- ## SDK
68
-
69
- ```ts
70
- import { Wafermap } from "@wafermapxyz/cli";
71
-
72
- const wm = new Wafermap({ privateKey: process.env.AGENT_KEY as `0x${string}`, limits: { perPayment: 0.05, perDay: 2 } });
73
-
74
- const { result, receipt } = await wm.call("stock-signal", { symbol: "TSLA" });
75
- const services = await wm.discover("token price");
76
- const paid = await wm.pay(services[0].url, { method: "POST", input: { symbol: "ETH" }, network: "base" });
77
- const quote = await wm.swapQuote("ETH", "USDG", "0.01");
78
- await wm.swap(quote);
79
- ```
80
-
81
- Pass `approve: (a) => boolean` to confirm each payment or swap yourself, and `ledger` to persist spending for the daily cap. Lower level: `x402fetch(url, init, { signer })` pays any x402 v1 or v2 endpoint.
82
-
83
- ## Where the limits live
84
-
85
- The caps sit with the key, on your machine. Before the wallet signs anything it reads the 402 quote and refuses when the price is above the per-payment cap or would take today's spend past the daily cap, so nothing is signed and nothing can be charged. With `--mcp-confirm on`, you also approve each MCP payment yourself before it is signed. A budget written in a prompt is an instruction, not a limit: set it with `wafermap limits`. More: https://wafermap.xyz/docs#limits
86
-
87
- **The daily cap counts what the wallet signed, not what a server says it charged.** Every payment is written to `spend.jsonl` at its full signed amount the moment it is signed, before it is sent, and stays counted whatever the server answers: an error, a receipt saying $0, no receipt, or no answer at all. Check, sign and record run one payment at a time within a process, so calls started together cannot share the same remaining budget. Across processes on the same wallet (two MCP clients, or the CLI next to the MCP server), the wallet re-reads the shared log right after recording a signed payment and, if the cap is now passed, drops that payment before it is sent. (Before 0.3.3 the cap trusted the server's receipt; see [CHANGELOG.md](CHANGELOG.md).)
88
-
89
- **What the wallet cannot enforce.** An x402 `exact` payment is a signed authorization (EIP-3009 on EVM chains, a partially signed transaction on Solana). Whoever receives it can execute it once until it expires. The wallet signs EVM authorizations for at most 90 seconds, or less when the server asks for less (300 before 0.4.0). "A failed call costs $0" is therefore a promise of the server you pay: Wafermap's server settles only after the resource worked and otherwise lets the authorization expire unused. You do not have to take that on trust: `wafermap history --verify` asks each token contract (`authorizationState`) whether every signed payment was executed, and shows the ones that expired unused (`error` means the RPC did not answer). Solana payments are not checked yet. The caps bound the worst case either way.
90
-
91
- **Time limits.** Requests to paid resources and services, to Wafermap's API, to the swap API and to Solana RPC give up after 60 seconds (`timeoutMs` in the SDK); EVM RPC calls keep viem's own time-outs. A paid call that times out after the payment was signed says so: the payment stays counted, and for EVM payments the message says until when it can be executed.
92
-
93
- **Swaps.** The wallet builds the swap, checks it, and only then approves and sends. The swap must go to KyberSwap's router (`0x6131B5fae19EA4f9D964eAc0408E4408b66337b5` on Robinhood Chain and Base). The calldata itself is decoded and must trade the quoted tokens, spend exactly the quoted amount, send the output to this wallet, take no fee, and carry an on-chain minimum output of the built output less your slippage, where the built output must be within your slippage of the quote. ETH is sent only when selling ETH, and an ERC-20 approval is for exactly the amount, to that router, and only when the allowance is lower. The router and the route it executes are KyberSwap's: the wallet checks what the call asks for, not what the router contract does.
94
-
95
- The checks are in [`src/core.ts`](src/core.ts) (`gate`, `paidFetch`, `checkPayment`), the keystore in [`src/store.ts`](src/store.ts), payment signing in [`shared/x402client.ts`](shared/x402client.ts) and [`shared/solana.ts`](shared/solana.ts). The tests in [`test/caps.test.mjs`](test/caps.test.mjs) run the caps against a local server that lies; [`test/hardening.test.mjs`](test/hardening.test.mjs) covers expiry, time limits, swap checks and the keychain (real DPAPI on Windows; on macOS and Linux the `security` and `secret-tool` calls run against a stub, not a real keychain). CI runs them on Linux, Windows and macOS.
96
-
97
- ## Build from source
98
-
99
- ```bash
100
- git clone https://github.com/wafermap/wafermap-cli.git && cd wafermap-cli
101
- npm ci
102
- npm run build # dist/cli.js, dist/index.js and type declarations (strict tsc)
103
- npm test # the caps against a mock server that misbehaves
104
- node dist/cli.js --help
105
- npm i -g . # optional: install this build as `wafermap`
106
- ```
107
-
108
- Layout: `src/` is the CLI, SDK and MCP server; `shared/` is the x402 client, the Solana payment builder, the swap helpers and the resource catalog, the same files the Wafermap site uses.
109
-
110
- ## Files
111
-
112
- `~/.wafermap/keystore.json` (encrypted key), `config.json` (caps, network), `spend.jsonl` (history). Override the folder with `WAFERMAP_HOME`, the RPCs with `WAFERMAP_RPC_ROBINHOOD` / `WAFERMAP_RPC_BASE`.
113
-
114
- Docs: https://wafermap.xyz/docs#cli · Security: [SECURITY.md](SECURITY.md) · License: [MIT](LICENSE)
1
+ # wafermap
2
+
3
+ The CLI, SDK and MCP server behind [Wafermap](https://wafermap.xyz): an x402 wallet for AI agents. One local, encrypted key that can:
4
+
5
+ - **call** Wafermap's pay-per-call resources (LLMs, image and audio models, web reading, on-chain data, tokenized stocks, prediction markets)
6
+ - **discover** third-party x402 services in the open x402 index and **pay** them
7
+ - **swap** tokens through the KyberSwap aggregator
8
+
9
+ Every payment is checked against a per-payment cap and a daily cap before anything is signed. Swaps have their own per-swap cap. Payments settle in USDG on Robinhood Chain or USDC on Solana, Base, Arbitrum or Polygon and are gasless; swaps need a little ETH for gas.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm i -g @wafermapxyz/cli
15
+ wafermap init
16
+ ```
17
+
18
+ The command is `wafermap`. The package is **`@wafermapxyz/cli`** on npm (the `wafermapxyz` scope, after the wafermap.xyz domain), the same build as https://wafermap.xyz/cli/wafermap.tgz and this repository. The bare name `wafermap` on npm is **not** this package: the registry refuses it for everyone. Coming from the tarball install? Run `npm rm -g wafermap` first; your wallet in `~/.wafermap` stays.
19
+
20
+ `init` creates a key and encrypts it with your password (scrypt + AES-256-GCM) in `~/.wafermap/keystore.json`. Back it up with `wafermap export-key`. Then send a few dollars of USDC on Solana to the Solana address it prints, or USDG (Robinhood Chain) or USDC (Base, Arbitrum, Polygon) to its EVM address.
21
+
22
+ The Solana address comes from the same key (derived with HMAC-SHA256), so one backup covers both. `wafermap export-key --solana` prints the Solana secret key in the form Phantom and Solflare import. `wafermap address` shows both addresses.
23
+
24
+ ## CLI
25
+
26
+ ```bash
27
+ wafermap balance
28
+ wafermap limits --per-payment 0.1 --per-day 1 --per-swap 25 --network robinhood
29
+ wafermap resources stock
30
+ wafermap call prediction-markets query="fed rates"
31
+ wafermap call flux-2-dev prompt="a glossy silicon wafer" --out wafer.png
32
+ wafermap discover weather
33
+ wafermap pay https://x402.agentindex.world/weather city=Jakarta
34
+ wafermap swap 0.01 ETH USDG
35
+ wafermap history
36
+ ```
37
+
38
+ Add `--yes` to skip the confirmation prompt (caps still apply), `--network base`, `arbitrum`, `polygon` or `solana` to pay in USDC on that network (swaps run on `robinhood` or `base`). Set `WAFERMAP_PASSWORD` to avoid the password prompt in scripts.
39
+
40
+ ## MCP server
41
+
42
+ Give Claude Desktop, Cursor or any MCP client the same wallet. First put the wallet password in your operating system's keychain, once, in a terminal:
43
+
44
+ ```bash
45
+ wafermap keychain on
46
+ ```
47
+
48
+ Then the client config holds no secret:
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "wafermap": {
54
+ "command": "wafermap",
55
+ "args": ["mcp"]
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ `wafermap keychain on` stores the password with Windows DPAPI (a file only your Windows user can decrypt), in the macOS Keychain, or in the Secret Service on Linux (GNOME Keyring, KWallet, through `secret-tool`). `wafermap keychain` shows whether it is set and still unlocks the wallet; `wafermap keychain off` removes it. This keeps the password out of config files, backups and screenshots. It does not stop a program already running as you, which could also ask the OS for it. Setting `WAFERMAP_PASSWORD` in the client's `env` still works, but the password then sits in plain text in that config file, and the server says so when it starts.
62
+
63
+ Tools: `wafermap_wallet`, `wafermap_resources`, `wafermap_call`, `wafermap_discover`, `wafermap_pay`, `wafermap_swap_quote`, `wafermap_swap`, `wafermap_history`. Swaps from MCP are off until you run `wafermap limits --mcp-swaps on`.
64
+
65
+ To approve every payment yourself, run `wafermap limits --mcp-confirm on` and restart the MCP server. Each payment and swap then waits for you in the client, showing the amount, asset, recipient, network and resource, before the wallet signs. It uses MCP elicitation; a client that cannot show the prompt gets a refusal and nothing is paid. The caps still apply first.
66
+
67
+ ## SDK
68
+
69
+ ```ts
70
+ import { Wafermap } from "@wafermapxyz/cli";
71
+
72
+ const wm = new Wafermap({ privateKey: process.env.AGENT_KEY as `0x${string}`, limits: { perPayment: 0.05, perDay: 2 } });
73
+
74
+ const { result, receipt } = await wm.call("stock-signal", { symbol: "TSLA" });
75
+ const services = await wm.discover("token price");
76
+ const paid = await wm.pay(services[0].url, { method: "POST", input: { symbol: "ETH" }, network: "base" });
77
+ const quote = await wm.swapQuote("ETH", "USDG", "0.01");
78
+ await wm.swap(quote);
79
+ ```
80
+
81
+ Pass `approve: (a) => boolean` to confirm each payment or swap yourself, and `ledger` to persist spending for the daily cap. Lower level: `x402fetch(url, init, { signer })` pays any x402 v1 or v2 endpoint.
82
+
83
+ ## $WAFERMAP holder discount
84
+
85
+ A wallet that holds at least 100,000 $WAFERMAP on Robinhood Chain pays 50% less for Wafermap's own resources when it pays on an EVM network (Robinhood Chain, Base, Arbitrum, Polygon). Nothing to set up: `wafermap call` names the wallet to Wafermap and the quote comes back discounted; the server re-checks the balance of the wallet that signs. Marketplace listings and Solana payments keep the regular price. Check a wallet: https://wafermap.xyz/api/holder?address=0x…
86
+
87
+ ## Where the limits live
88
+
89
+ The caps sit with the key, on your machine. Before the wallet signs anything it reads the 402 quote and refuses when the price is above the per-payment cap or would take today's spend past the daily cap, so nothing is signed and nothing can be charged. With `--mcp-confirm on`, you also approve each MCP payment yourself before it is signed. A budget written in a prompt is an instruction, not a limit: set it with `wafermap limits`. More: https://wafermap.xyz/docs#limits
90
+
91
+ **The daily cap counts what the wallet signed, not what a server says it charged.** Every payment is written to `spend.jsonl` at its full signed amount the moment it is signed, before it is sent, and stays counted whatever the server answers: an error, a receipt saying $0, no receipt, or no answer at all. Check, sign and record run one payment at a time within a process, so calls started together cannot share the same remaining budget. Across processes on the same wallet (two MCP clients, or the CLI next to the MCP server), the wallet re-reads the shared log right after recording a signed payment and, if the cap is now passed, drops that payment before it is sent. (Before 0.3.3 the cap trusted the server's receipt; see [CHANGELOG.md](CHANGELOG.md).)
92
+
93
+ **What the wallet cannot enforce.** An x402 `exact` payment is a signed authorization (EIP-3009 on EVM chains, a partially signed transaction on Solana). Whoever receives it can execute it once until it expires. The wallet signs EVM authorizations for at most 90 seconds, or less when the server asks for less (300 before 0.4.0). "A failed call costs $0" is therefore a promise of the server you pay: Wafermap's server settles only after the resource worked and otherwise lets the authorization expire unused. You do not have to take that on trust: `wafermap history --verify` asks each token contract (`authorizationState`) whether every signed payment was executed, and shows the ones that expired unused (`error` means the RPC did not answer). Solana payments are not checked yet. The caps bound the worst case either way.
94
+
95
+ **Time limits.** Requests to paid resources and services, to Wafermap's API, to the swap API and to Solana RPC give up after 60 seconds (`timeoutMs` in the SDK); EVM RPC calls keep viem's own time-outs. `wafermap resources` uses the same limit. Set `WAFERMAP_TIMEOUT_MS` to change it for the CLI and the MCP server. A paid call that times out after the payment was signed says so: the payment stays counted, and for EVM payments the message says until when it can be executed.
96
+
97
+ **Swaps.** The wallet builds the swap, checks it, and only then approves and sends. The swap must go to KyberSwap's router (`0x6131B5fae19EA4f9D964eAc0408E4408b66337b5` on Robinhood Chain and Base). The calldata itself is decoded and must trade the quoted tokens, spend exactly the quoted amount, send the output to this wallet, take no fee, and carry an on-chain minimum output of the built output less your slippage, where the built output must be within your slippage of the quote. ETH is sent only when selling ETH, and an ERC-20 approval is for exactly the amount, to that router, and only when the allowance is lower. The router and the route it executes are KyberSwap's: the wallet checks what the call asks for, not what the router contract does.
98
+
99
+ The checks are in [`src/core.ts`](src/core.ts) (`gate`, `paidFetch`, `checkPayment`), the keystore in [`src/store.ts`](src/store.ts), payment signing in [`shared/x402client.ts`](shared/x402client.ts) and [`shared/solana.ts`](shared/solana.ts). The tests in [`test/caps.test.mjs`](test/caps.test.mjs) run the caps against a local server that lies; [`test/hardening.test.mjs`](test/hardening.test.mjs) covers expiry, time limits, swap checks and the keychain (real DPAPI on Windows; on macOS and Linux the `security` and `secret-tool` calls run against a stub, not a real keychain). CI runs them on Linux, Windows and macOS.
100
+
101
+ ## Build from source
102
+
103
+ ```bash
104
+ git clone https://github.com/wafermap/wafermap-cli.git && cd wafermap-cli
105
+ npm ci
106
+ npm run build # dist/cli.js, dist/index.js and type declarations (strict tsc)
107
+ npm test # the caps against a mock server that misbehaves
108
+ node dist/cli.js --help
109
+ npm i -g . # optional: install this build as `wafermap`
110
+ ```
111
+
112
+ Layout: `src/` is the CLI, SDK and MCP server; `shared/` is the x402 client, the Solana payment builder, the swap helpers and the resource catalog, the same files the Wafermap site uses.
113
+
114
+ ## Files
115
+
116
+ `~/.wafermap/keystore.json` (encrypted key), `config.json` (caps, network), `spend.jsonl` (history). Override the folder with `WAFERMAP_HOME`, the RPCs with `WAFERMAP_RPC_ROBINHOOD` / `WAFERMAP_RPC_BASE`.
117
+
118
+ Docs: https://wafermap.xyz/docs#cli · Security: [SECURITY.md](SECURITY.md) · License: [MIT](LICENSE)
package/dist/cli.js CHANGED
@@ -447,6 +447,7 @@ __export(core_exports, {
447
447
  WafermapError: () => WafermapError,
448
448
  checkPayment: () => checkPayment,
449
449
  isNet: () => isNet,
450
+ listResources: () => listResources,
450
451
  mergeLog: () => mergeLog
451
452
  });
452
453
  import { createPublicClient, createWalletClient, erc20Abi as erc20Abi2, formatUnits as formatUnits2, http } from "viem";
@@ -472,6 +473,20 @@ async function checkPayment(e, rpc) {
472
473
  return "error";
473
474
  }
474
475
  }
476
+ async function listResources(baseUrl = "https://wafermap.xyz", timeoutMs = DEFAULT_TIMEOUT_MS) {
477
+ const host = new URL(baseUrl).host;
478
+ const gaveUp = () => new WafermapError("timeout", `${host} did not answer within ${Math.round(timeoutMs / 1e3)} s. Nothing was paid.`);
479
+ const signal = AbortSignal.timeout(timeoutMs);
480
+ const r = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/resources`, { signal }).catch((e) => {
481
+ throw timedOut(e) ? gaveUp() : new WafermapError("network_error", `${host}: ${e?.message ?? "request failed"}`);
482
+ });
483
+ const j = await r.json().catch((e) => {
484
+ if (timedOut(e)) throw gaveUp();
485
+ return null;
486
+ });
487
+ if (!r.ok || !Array.isArray(j?.data)) throw new WafermapError("network_error", `${host} returned no catalog (HTTP ${r.status}).`);
488
+ return j.data;
489
+ }
475
490
  var NETS, isNet, isSwapNet, WafermapError, CHAINS, AUTH_STATE_ABI, DEFAULT_LIMITS, DEFAULT_TIMEOUT_MS, timedOut, Wafermap;
476
491
  var init_core = __esm({
477
492
  "src/core.ts"() {
@@ -632,15 +647,10 @@ var init_core = __esm({
632
647
  }
633
648
  // ---- Wafermap resources ----
634
649
  async resources() {
635
- const host = new URL(this.baseUrl).host;
636
- const r = await this.fetch(`${this.baseUrl}/v1/resources`).catch((e) => {
637
- throw timedOut(e) ? this.timeoutError(host) : new WafermapError("network_error", `${host}: ${e?.message ?? "request failed"}`);
638
- });
639
- const j = await this.body(r, () => r.json(), null, host);
640
- return j?.data ?? [];
650
+ return listResources(this.baseUrl, this.timeoutMs);
641
651
  }
642
652
  async call(id, input = {}, o = {}) {
643
- const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(input) }, o.network);
653
+ const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address }, body: JSON.stringify(input) }, o.network);
644
654
  const settle = (r2, out2) => {
645
655
  if (!r2.paid || !r2.id) return;
646
656
  const status = r2.res.status === 402 ? "refused" : r2.res.ok ? "ok" : "failed";
@@ -927,6 +937,7 @@ function openWallet(password2, approve) {
927
937
  const wm = new Wafermap({
928
938
  privateKey: loadKey(password2),
929
939
  baseUrl: process.env.WAFERMAP_URL,
940
+ timeoutMs: Number(process.env.WAFERMAP_TIMEOUT_MS) || void 0,
930
941
  network: c.network,
931
942
  limits: c.limits,
932
943
  ledger: { entries: spendLog, record: appendSpend },
@@ -949,7 +960,7 @@ var VERSION;
949
960
  var init_version = __esm({
950
961
  "src/version.ts"() {
951
962
  "use strict";
952
- VERSION = "0.4.1";
963
+ VERSION = "0.4.3";
953
964
  }
954
965
  });
955
966
 
@@ -1680,8 +1691,8 @@ Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment
1680
1691
  }
1681
1692
  case "resources": {
1682
1693
  const q = a._.join(" ").toLowerCase();
1683
- const r = await fetch(`${process.env.WAFERMAP_URL ?? "https://wafermap.xyz"}/v1/resources`).then((x) => x.json());
1684
- for (const x of (r.data ?? []).filter((x2) => !q || `${x2.id} ${x2.name} ${x2.summary} ${x2.category}`.toLowerCase().includes(q))) console.log(`${x.id.padEnd(22)} $${String(x.price).padEnd(7)} ${x.summary}`);
1694
+ const list = await listResources(process.env.WAFERMAP_URL, Number(process.env.WAFERMAP_TIMEOUT_MS) || void 0);
1695
+ for (const x of list.filter((x2) => !q || `${x2.id} ${x2.name} ${x2.summary} ${x2.category}`.toLowerCase().includes(q))) console.log(`${x.id.padEnd(22)} $${String(x.price).padEnd(7)} ${x.summary}`);
1685
1696
  return;
1686
1697
  }
1687
1698
  case "discover": {
package/dist/index.js CHANGED
@@ -442,6 +442,20 @@ async function checkPayment(e, rpc) {
442
442
  var DEFAULT_LIMITS = { perPayment: 0.1, perDay: 1, perSwap: 25 };
443
443
  var DEFAULT_TIMEOUT_MS = 6e4;
444
444
  var timedOut = (e) => e?.name === "TimeoutError" || e?.cause?.name === "TimeoutError";
445
+ async function listResources(baseUrl = "https://wafermap.xyz", timeoutMs = DEFAULT_TIMEOUT_MS) {
446
+ const host = new URL(baseUrl).host;
447
+ const gaveUp = () => new WafermapError("timeout", `${host} did not answer within ${Math.round(timeoutMs / 1e3)} s. Nothing was paid.`);
448
+ const signal = AbortSignal.timeout(timeoutMs);
449
+ const r = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/resources`, { signal }).catch((e) => {
450
+ throw timedOut(e) ? gaveUp() : new WafermapError("network_error", `${host}: ${e?.message ?? "request failed"}`);
451
+ });
452
+ const j = await r.json().catch((e) => {
453
+ if (timedOut(e)) throw gaveUp();
454
+ return null;
455
+ });
456
+ if (!r.ok || !Array.isArray(j?.data)) throw new WafermapError("network_error", `${host} returned no catalog (HTTP ${r.status}).`);
457
+ return j.data;
458
+ }
445
459
  var Wafermap = class {
446
460
  account;
447
461
  baseUrl;
@@ -580,15 +594,10 @@ var Wafermap = class {
580
594
  }
581
595
  // ---- Wafermap resources ----
582
596
  async resources() {
583
- const host = new URL(this.baseUrl).host;
584
- const r = await this.fetch(`${this.baseUrl}/v1/resources`).catch((e) => {
585
- throw timedOut(e) ? this.timeoutError(host) : new WafermapError("network_error", `${host}: ${e?.message ?? "request failed"}`);
586
- });
587
- const j = await this.body(r, () => r.json(), null, host);
588
- return j?.data ?? [];
597
+ return listResources(this.baseUrl, this.timeoutMs);
589
598
  }
590
599
  async call(id, input = {}, o = {}) {
591
- const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(input) }, o.network);
600
+ const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address }, body: JSON.stringify(input) }, o.network);
592
601
  const settle = (r2, out2) => {
593
602
  if (!r2.paid || !r2.id) return;
594
603
  const status = r2.res.status === 402 ? "refused" : r2.res.ok ? "ok" : "failed";
@@ -711,6 +720,7 @@ export {
711
720
  choose,
712
721
  decodeSwap,
713
722
  executeSwap,
723
+ listResources,
714
724
  mergeLog,
715
725
  quoteSwap,
716
726
  readQuote,
@@ -87,6 +87,8 @@ export declare function mergeLog(entries: SpendEntry[]): SpendEntry[];
87
87
  export declare function checkPayment(e: Pick<SpendEntry, "network" | "payer" | "nonce" | "validBefore">, rpc?: string): Promise<"used" | "unused" | "pending" | "unknown" | "error">;
88
88
  export declare const DEFAULT_LIMITS: Limits;
89
89
  export declare const DEFAULT_TIMEOUT_MS = 60000;
90
+ /** Wafermap's resource catalog. Needs no wallet; gives up after `timeoutMs` like every other request. */
91
+ export declare function listResources(baseUrl?: string, timeoutMs?: number): Promise<any[]>;
90
92
  export declare class Wafermap {
91
93
  readonly account: LocalAccount;
92
94
  readonly baseUrl: string;
@@ -1,4 +1,4 @@
1
- export { Wafermap, WafermapError, DEFAULT_LIMITS, DEFAULT_TIMEOUT_MS, checkPayment, mergeLog } from "./core.js";
1
+ export { Wafermap, WafermapError, DEFAULT_LIMITS, DEFAULT_TIMEOUT_MS, checkPayment, listResources, mergeLog } from "./core.js";
2
2
  export type { Approval, Limits, Net, PaymentStatus, SpendEntry, SwapQuote, Token, WafermapOptions } from "./core.js";
3
3
  export { x402fetch, readQuote, choose, signPayment, PAY_CHAINS, MAX_VALIDITY_SECONDS, X402Error } from "../shared/x402client";
4
4
  export { quoteSwap, executeSwap, decodeSwap, KYBER_ROUTER, SwapError } from "../shared/swap";
package/package.json CHANGED
@@ -1,68 +1,69 @@
1
- {
2
- "name": "@wafermapxyz/cli",
3
- "version": "0.4.1",
4
- "description": "Wafermap for agents: an encrypted local wallet that pays x402 resources, discovers and pays third-party x402 services (USDG on Robinhood Chain; USDC on Solana, Base, Arbitrum, Polygon), and swaps on Robinhood Chain and Base. CLI, SDK and MCP server.",
5
- "type": "module",
6
- "bin": {
7
- "wafermap": "dist/cli.js"
8
- },
9
- "main": "dist/index.js",
10
- "types": "dist/types/src/index.d.ts",
11
- "exports": {
12
- ".": {
13
- "types": "./dist/types/src/index.d.ts",
14
- "default": "./dist/index.js"
15
- }
16
- },
17
- "files": [
18
- "dist",
19
- "README.md",
20
- "CHANGELOG.md",
21
- "LICENSE"
22
- ],
23
- "engines": {
24
- "node": ">=20"
25
- },
26
- "scripts": {
27
- "build": "node build.mjs",
28
- "test": "node --test"
29
- },
30
- "keywords": [
31
- "x402",
32
- "agents",
33
- "mcp",
34
- "wallet",
35
- "robinhood-chain",
36
- "base",
37
- "arbitrum",
38
- "polygon",
39
- "solana",
40
- "usdg",
41
- "usdc"
42
- ],
43
- "homepage": "https://wafermap.xyz/docs#cli",
44
- "repository": {
45
- "type": "git",
46
- "url": "git+https://github.com/wafermap/wafermap-cli.git"
47
- },
48
- "bugs": {
49
- "url": "https://github.com/wafermap/wafermap-cli/issues"
50
- },
51
- "license": "MIT",
52
- "publishConfig": {
53
- "access": "public"
54
- },
55
- "dependencies": {
56
- "@modelcontextprotocol/sdk": "1.32.1",
57
- "@noble/curves": "^1.9.1",
58
- "@noble/hashes": "^1.8.0",
59
- "@scure/base": "^1.2.6",
60
- "viem": "^2.56.8",
61
- "zod": "4.6.7"
62
- },
63
- "devDependencies": {
64
- "@types/node": "^22.10.0",
65
- "esbuild": "^0.28.1",
66
- "typescript": "^7.0.2"
67
- }
68
- }
1
+ {
2
+ "name": "@wafermapxyz/cli",
3
+ "version": "0.4.3",
4
+ "description": "Wafermap for agents: an encrypted local wallet that pays x402 resources, discovers and pays third-party x402 services (USDG on Robinhood Chain; USDC on Solana, Base, Arbitrum, Polygon), and swaps on Robinhood Chain and Base. CLI, SDK and MCP server.",
5
+ "type": "module",
6
+ "bin": {
7
+ "wafermap": "dist/cli.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "types": "dist/types/src/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/types/src/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "CHANGELOG.md",
21
+ "LICENSE"
22
+ ],
23
+ "engines": {
24
+ "node": ">=20"
25
+ },
26
+ "scripts": {
27
+ "build": "node build.mjs",
28
+ "test": "node --test"
29
+ },
30
+ "keywords": [
31
+ "x402",
32
+ "agents",
33
+ "mcp",
34
+ "wallet",
35
+ "robinhood-chain",
36
+ "base",
37
+ "arbitrum",
38
+ "polygon",
39
+ "solana",
40
+ "usdg",
41
+ "usdc"
42
+ ],
43
+ "homepage": "https://wafermap.xyz/docs#cli",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/wafermap/wafermap-cli.git"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/wafermap/wafermap-cli/issues"
50
+ },
51
+ "license": "MIT",
52
+ "publishConfig": {
53
+ "access": "public",
54
+ "provenance": true
55
+ },
56
+ "dependencies": {
57
+ "@modelcontextprotocol/sdk": "1.32.1",
58
+ "@noble/curves": "^1.9.1",
59
+ "@noble/hashes": "^1.8.0",
60
+ "@scure/base": "^1.2.6",
61
+ "viem": "^2.56.8",
62
+ "zod": "4.6.7"
63
+ },
64
+ "devDependencies": {
65
+ "@types/node": "^22.10.0",
66
+ "esbuild": "^0.28.1",
67
+ "typescript": "^7.0.2"
68
+ }
69
+ }