@wafermapxyz/cli 0.4.2 → 0.4.4

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,46 +1,62 @@
1
- # Changelog
2
-
3
- ## 0.4.2 (2 Nov 2026)
4
-
5
- **$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.
6
-
7
- ## 0.4.1 (2 Nov 2026)
8
-
9
- 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.
10
-
11
- ## 0.4.0 (2 Nov 2026)
12
-
13
- From the second public review by @whoiskn0why, thank you.
14
-
15
- - **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.
16
- - **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.
17
- - **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.
18
- - **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.
19
- - An empty `WAFERMAP_PASSWORD` counts as not set. `wafermap keychain off` checks that the password is really gone.
20
- - 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.
21
- - The SDK exports `quoteSwap`, `executeSwap`, `decodeSwap`, `KYBER_ROUTER`, `MAX_VALIDITY_SECONDS` and `DEFAULT_TIMEOUT_MS`.
22
-
23
- 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`.
24
-
25
- ## 0.3.3 (1 Nov 2026)
26
-
27
- **Fix: the daily cap no longer trusts the server.** Reported publicly in a code review by @whoiskn0why, thank you.
28
-
29
- 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.
30
-
31
- Now:
32
- - 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.
33
- - 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).
34
- - `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.
35
- - `wafermap history` shows each payment's status and notes that log lines from before 0.3.3 hold what the server reported.
36
- - Tests (`npm test`): the caps against a local mock server that lies, errors, refuses or hangs up; CI on Node 20 and 22.
37
-
38
- Older `spend.jsonl` lines are still read; `mergeLog` joins the new two-line records.
39
-
40
- ## 0.3.2
41
-
42
- Opt-in per-payment confirmation for the MCP server (`wafermap limits --mcp-confirm on`).
43
-
44
- ## 0.3.1
45
-
46
- First public source release: CLI, SDK and MCP server.
1
+ # Changelog
2
+
3
+ ## 0.4.4 (2 Nov 2026)
4
+
5
+ **$WAFERMAP holder tiers.**
6
+
7
+ - **Pro free calls.** A wallet holding 1,000,000 $WAFERMAP on Robinhood Chain gets 10 free calls a day (UTC) to Wafermap resources listed at $0.01 or less. `call()` (CLI `wafermap call`, MCP `wafermap_call`) sends a **holder pass** to Wafermap with each call; when the call is free, the result comes back with `receipt: null` and `free: {left, perDay}`, no payment is signed and nothing is recorded as spending. Otherwise the call is quoted and paid as before.
8
+ - **Holder pass.** A message the wallet signs (`personal_sign`: no transaction, no cost) that proves it holds the wallet, valid for 24 hours and reused until its last 10 minutes (`Wafermap.holderPass()`). It goes to Wafermap only: never with `pay()` to third-party services, and never to marketplace listings (`m/...`). The SDK exports `PASS_HEADER`, `passMessage`, `encodePass`, `decodePass`, `makePass`.
9
+ - **`wafermap holder`** shows what the wallet's $WAFERMAP unlocks: holder price, tier, free calls left today, x402 Tracker Pro. Needs no password (`holderInfo()` in the SDK).
10
+ - **`wafermap pass`** prints a pass for scripts, e.g. the x402 Tracker Pro API (`/api/x402/pro/movers`, `/api/x402/pro/export`, for wallets holding 100,000 $WAFERMAP).
11
+
12
+ ## 0.4.3 (2 Nov 2026)
13
+
14
+ The two open points from the third public review by @whoiskn0why (8/10), thank you.
15
+
16
+ - **`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.
17
+ - **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.
18
+
19
+ ## 0.4.2 (2 Nov 2026)
20
+
21
+ **$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.
22
+
23
+ ## 0.4.1 (2 Nov 2026)
24
+
25
+ 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.
26
+
27
+ ## 0.4.0 (2 Nov 2026)
28
+
29
+ From the second public review by @whoiskn0why, thank you.
30
+
31
+ - **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.
32
+ - **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.
33
+ - **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.
34
+ - **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.
35
+ - An empty `WAFERMAP_PASSWORD` counts as not set. `wafermap keychain off` checks that the password is really gone.
36
+ - 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.
37
+ - The SDK exports `quoteSwap`, `executeSwap`, `decodeSwap`, `KYBER_ROUTER`, `MAX_VALIDITY_SECONDS` and `DEFAULT_TIMEOUT_MS`.
38
+
39
+ 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`.
40
+
41
+ ## 0.3.3 (1 Nov 2026)
42
+
43
+ **Fix: the daily cap no longer trusts the server.** Reported publicly in a code review by @whoiskn0why, thank you.
44
+
45
+ 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.
46
+
47
+ Now:
48
+ - 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.
49
+ - 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).
50
+ - `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.
51
+ - `wafermap history` shows each payment's status and notes that log lines from before 0.3.3 hold what the server reported.
52
+ - Tests (`npm test`): the caps against a local mock server that lies, errors, refuses or hangs up; CI on Node 20 and 22.
53
+
54
+ Older `spend.jsonl` lines are still read; `mergeLog` joins the new two-line records.
55
+
56
+ ## 0.3.2
57
+
58
+ Opt-in per-payment confirmation for the MCP server (`wafermap limits --mcp-confirm on`).
59
+
60
+ ## 0.3.1
61
+
62
+ First public source release: CLI, SDK and MCP server.
package/README.md CHANGED
@@ -1,118 +1,120 @@
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. 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)
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…, or run `wafermap holder`.
86
+
87
+ With 1,000,000 $WAFERMAP the wallet is **Pro**: 10 free calls a day (UTC) to resources listed at $0.01 or less. `wafermap call` sends a holder pass (a signed message, no transaction, valid 24 hours) to Wafermap only; when a call is free, nothing is signed for payment and it prints `✓ Free call`. Holders of 100,000 also unlock x402 Tracker Pro: `wafermap pass` prints a pass for its API. Details: https://wafermap.xyz/docs#holders
88
+
89
+ ## Where the limits live
90
+
91
+ 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
92
+
93
+ **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).)
94
+
95
+ **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.
96
+
97
+ **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.
98
+
99
+ **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.
100
+
101
+ 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.
102
+
103
+ ## Build from source
104
+
105
+ ```bash
106
+ git clone https://github.com/wafermap/wafermap-cli.git && cd wafermap-cli
107
+ npm ci
108
+ npm run build # dist/cli.js, dist/index.js and type declarations (strict tsc)
109
+ npm test # the caps against a mock server that misbehaves
110
+ node dist/cli.js --help
111
+ npm i -g . # optional: install this build as `wafermap`
112
+ ```
113
+
114
+ 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.
115
+
116
+ ## Files
117
+
118
+ `~/.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`.
119
+
120
+ Docs: https://wafermap.xyz/docs#cli · Security: [SECURITY.md](SECURITY.md) · License: [MIT](LICENSE)
package/dist/cli.js CHANGED
@@ -437,6 +437,47 @@ var init_x402client = __esm({
437
437
  }
438
438
  });
439
439
 
440
+ // shared/pass.ts
441
+ function passMessage(address, iat, exp) {
442
+ return [
443
+ "wafermap.xyz holder pass",
444
+ "",
445
+ `Wallet: ${address}`,
446
+ `Issued: ${new Date(iat * 1e3).toISOString()}`,
447
+ `Expires: ${new Date(exp * 1e3).toISOString()}`,
448
+ "",
449
+ "Proves you hold this wallet, so Wafermap can apply what your $WAFERMAP unlocks. It costs nothing and moves no funds."
450
+ ].join("\n");
451
+ }
452
+ function decodePass(s) {
453
+ if (!s || s.length > 1e3 || !/^[A-Za-z0-9_-]+$/.test(s)) return null;
454
+ try {
455
+ const p = JSON.parse(unb64url(s));
456
+ if (p?.v !== 1 || typeof p.address !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(p.address)) return null;
457
+ if (!Number.isInteger(p.iat) || !Number.isInteger(p.exp) || typeof p.sig !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(p.sig)) return null;
458
+ return { v: 1, address: p.address, iat: p.iat, exp: p.exp, sig: p.sig };
459
+ } catch {
460
+ return null;
461
+ }
462
+ }
463
+ async function makePass(signer, seconds = PASS_MAX_SECONDS, now = Math.floor(Date.now() / 1e3)) {
464
+ const iat = now;
465
+ const exp = now + Math.min(Math.max(60, seconds), PASS_MAX_SECONDS);
466
+ const sig = await signer.signMessage({ message: passMessage(signer.address, iat, exp) });
467
+ return { v: 1, address: signer.address, iat, exp, sig };
468
+ }
469
+ var PASS_HEADER, PASS_MAX_SECONDS, b64url, unb64url, encodePass;
470
+ var init_pass = __esm({
471
+ "shared/pass.ts"() {
472
+ "use strict";
473
+ PASS_HEADER = "x-wafermap-pass";
474
+ PASS_MAX_SECONDS = 86400;
475
+ b64url = (s) => btoa(unescape(encodeURIComponent(s))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
476
+ unb64url = (s) => decodeURIComponent(escape(atob(s.replace(/-/g, "+").replace(/_/g, "/"))));
477
+ encodePass = (p) => b64url(JSON.stringify(p));
478
+ }
479
+ });
480
+
440
481
  // src/core.ts
441
482
  var core_exports = {};
442
483
  __export(core_exports, {
@@ -446,7 +487,9 @@ __export(core_exports, {
446
487
  Wafermap: () => Wafermap,
447
488
  WafermapError: () => WafermapError,
448
489
  checkPayment: () => checkPayment,
490
+ holderInfo: () => holderInfo,
449
491
  isNet: () => isNet,
492
+ listResources: () => listResources,
450
493
  mergeLog: () => mergeLog
451
494
  });
452
495
  import { createPublicClient, createWalletClient, erc20Abi as erc20Abi2, formatUnits as formatUnits2, http } from "viem";
@@ -472,6 +515,29 @@ async function checkPayment(e, rpc) {
472
515
  return "error";
473
516
  }
474
517
  }
518
+ async function holderInfo(address, baseUrl = "https://wafermap.xyz", timeoutMs = DEFAULT_TIMEOUT_MS) {
519
+ const host = new URL(baseUrl).host;
520
+ const r = await fetch(`${baseUrl.replace(/\/$/, "")}/api/holder?address=${encodeURIComponent(address)}`, { signal: AbortSignal.timeout(timeoutMs) }).catch((e) => {
521
+ throw timedOut(e) ? new WafermapError("timeout", `${host} did not answer within ${Math.round(timeoutMs / 1e3)} s.`) : new WafermapError("network_error", `${host}: ${e?.message ?? "request failed"}`);
522
+ });
523
+ const j = await r.json().catch(() => null);
524
+ if (!r.ok || !j) throw new WafermapError(j?.error?.code ?? "network_error", j?.error?.message ?? `${host} answered HTTP ${r.status}.`);
525
+ return j;
526
+ }
527
+ async function listResources(baseUrl = "https://wafermap.xyz", timeoutMs = DEFAULT_TIMEOUT_MS) {
528
+ const host = new URL(baseUrl).host;
529
+ const gaveUp = () => new WafermapError("timeout", `${host} did not answer within ${Math.round(timeoutMs / 1e3)} s. Nothing was paid.`);
530
+ const signal = AbortSignal.timeout(timeoutMs);
531
+ const r = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/resources`, { signal }).catch((e) => {
532
+ throw timedOut(e) ? gaveUp() : new WafermapError("network_error", `${host}: ${e?.message ?? "request failed"}`);
533
+ });
534
+ const j = await r.json().catch((e) => {
535
+ if (timedOut(e)) throw gaveUp();
536
+ return null;
537
+ });
538
+ if (!r.ok || !Array.isArray(j?.data)) throw new WafermapError("network_error", `${host} returned no catalog (HTTP ${r.status}).`);
539
+ return j.data;
540
+ }
475
541
  var NETS, isNet, isSwapNet, WafermapError, CHAINS, AUTH_STATE_ABI, DEFAULT_LIMITS, DEFAULT_TIMEOUT_MS, timedOut, Wafermap;
476
542
  var init_core = __esm({
477
543
  "src/core.ts"() {
@@ -479,6 +545,7 @@ var init_core = __esm({
479
545
  init_swap();
480
546
  init_solana();
481
547
  init_x402client();
548
+ init_pass();
482
549
  NETS = CHAIN_KEYS;
483
550
  isNet = (v) => NETS.includes(v);
484
551
  isSwapNet = (v) => v === "robinhood" || v === "base";
@@ -632,15 +699,21 @@ var init_core = __esm({
632
699
  }
633
700
  // ---- Wafermap resources ----
634
701
  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 ?? [];
702
+ return listResources(this.baseUrl, this.timeoutMs);
703
+ }
704
+ passCache = null;
705
+ /** A holder pass (shared/pass.ts) signed by this wallet, valid 24 h and reused until its last 10 minutes. It
706
+ * proves the wallet to Wafermap for what its $WAFERMAP unlocks (Pro free calls, x402 Tracker Pro). Signing
707
+ * a message moves no funds. */
708
+ async holderPass() {
709
+ const p = decodePass(this.passCache);
710
+ if (this.passCache && p && p.exp - 600 > Date.now() / 1e3) return this.passCache;
711
+ this.passCache = encodePass(await makePass({ address: this.account.address, signMessage: (a) => this.account.signMessage(a) }));
712
+ return this.passCache;
641
713
  }
642
714
  async call(id, input = {}, o = {}) {
643
- 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);
715
+ const pass = id.startsWith("m/") ? null : await this.holderPass();
716
+ const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address, ...pass ? { [PASS_HEADER]: pass } : {} }, body: JSON.stringify(input) }, o.network);
644
717
  const settle = (r2, out2) => {
645
718
  if (!r2.paid || !r2.id) return;
646
719
  const status = r2.res.status === 402 ? "refused" : r2.res.ok ? "ok" : "failed";
@@ -927,6 +1000,7 @@ function openWallet(password2, approve) {
927
1000
  const wm = new Wafermap({
928
1001
  privateKey: loadKey(password2),
929
1002
  baseUrl: process.env.WAFERMAP_URL,
1003
+ timeoutMs: Number(process.env.WAFERMAP_TIMEOUT_MS) || void 0,
930
1004
  network: c.network,
931
1005
  limits: c.limits,
932
1006
  ledger: { entries: spendLog, record: appendSpend },
@@ -949,7 +1023,7 @@ var VERSION;
949
1023
  var init_version = __esm({
950
1024
  "src/version.ts"() {
951
1025
  "use strict";
952
- VERSION = "0.4.2";
1026
+ VERSION = "0.4.4";
953
1027
  }
954
1028
  });
955
1029
 
@@ -1504,6 +1578,8 @@ Wallet
1504
1578
  Pay
1505
1579
  wafermap resources [words] Wafermap's pay-per-call catalog
1506
1580
  wafermap call <id> [field=value ...] run a resource and pay its price (--json '{...}' for complex input)
1581
+ wafermap holder what this wallet's $WAFERMAP unlocks: holder price, Pro free calls left today
1582
+ wafermap pass print a holder pass (X-Wafermap-Pass) for scripts and x402 Tracker Pro
1507
1583
  wafermap discover <words> search third-party x402 services
1508
1584
  wafermap pay <url> [field=value ...] call any x402 URL and pay it (--post to send a JSON body)
1509
1585
 
@@ -1619,6 +1695,26 @@ async function main() {
1619
1695
  Caps: $0.10 per payment, $1 per day, $25 per swap. Change with \`wafermap limits\`.`);
1620
1696
  return;
1621
1697
  }
1698
+ case "holder": {
1699
+ const addr = keystoreAddress() ?? die("No wallet yet. Run `wafermap init`.");
1700
+ const j = await holderInfo(addr, process.env.WAFERMAP_URL, Number(process.env.WAFERMAP_TIMEOUT_MS) || void 0);
1701
+ if (!j.active) return console.log("The $WAFERMAP holder program is off.");
1702
+ const n = (v) => Math.floor(v).toLocaleString("en-US");
1703
+ const k = j.perks;
1704
+ console.log(`${addr}
1705
+ holds ${n(j.balance)} $WAFERMAP on Robinhood Chain`);
1706
+ console.log(` tier ${j.tier === "pro" ? "Pro" : j.tier === "holder" ? "Holder" : "none"}`);
1707
+ console.log(` price ${j.holder ? `${j.percent}% off Wafermap resources on EVM networks` : `regular (hold ${n(j.minTokens)} for ${j.percent}% off)`}`);
1708
+ if (k) console.log(` free ${j.tier === "pro" ? `${k.freeLeftToday ?? "?"} of ${k.freeCallsPerDay} free calls left today (resources up to $${k.freeMaxUsd}); \`wafermap call\` uses them first` : `hold ${n(k.proMinTokens)} for ${k.freeCallsPerDay} free calls a day`}`);
1709
+ return console.log(` tracker ${j.holder ? "x402 Tracker Pro unlocked: wafermap.xyz/x402 (or `wafermap pass` for the API)" : `x402 Tracker Pro needs ${n(j.minTokens)}`}`);
1710
+ }
1711
+ case "pass": {
1712
+ const wm = openWallet(await password());
1713
+ const v = await wm.holderPass();
1714
+ console.error(`Holder pass for ${wm.address}, valid 24 hours. It proves you hold the wallet and moves no funds; anyone who has it can use your free calls until it expires, so keep it private. Send it as X-Wafermap-Pass:`);
1715
+ console.error(` curl -H "X-Wafermap-Pass: $(wafermap pass)" https://wafermap.xyz/api/x402/pro/movers`);
1716
+ return console.log(v);
1717
+ }
1622
1718
  case "address": {
1623
1719
  const evm = keystoreAddress() ?? die("No wallet yet. Run `wafermap init`.");
1624
1720
  let sol = keystoreSolana();
@@ -1680,8 +1776,8 @@ Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment
1680
1776
  }
1681
1777
  case "resources": {
1682
1778
  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}`);
1779
+ const list = await listResources(process.env.WAFERMAP_URL, Number(process.env.WAFERMAP_TIMEOUT_MS) || void 0);
1780
+ 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
1781
  return;
1686
1782
  }
1687
1783
  case "discover": {
@@ -1700,6 +1796,7 @@ Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment
1700
1796
  const wm = openWallet(await password(), approver(yes));
1701
1797
  const out = await wm.call(id, inputOf(a, 1), { network: net(a.flags.network) });
1702
1798
  if (out.receipt) console.error(`\u2713 Paid $${out.receipt.amountUsd} ${out.receipt.asset} \xB7 ${EXPLORER(out.receipt.network)}/tx/${out.receipt.tx}`);
1799
+ else if (out.free) console.error(`\u2713 Free call ($WAFERMAP Pro) \xB7 ${out.free.left} of ${out.free.perDay} left today. Nothing was paid.`);
1703
1800
  if (typeof a.flags.out === "string") await saveMedia(out.result, a.flags.out);
1704
1801
  return printResult(out.result);
1705
1802
  }
package/dist/index.js CHANGED
@@ -407,6 +407,41 @@ async function x402fetch(url, init, opts) {
407
407
  return { res, paid: { ...signed, tx: readSettlement(res)?.tx ?? null } };
408
408
  }
409
409
 
410
+ // shared/pass.ts
411
+ var PASS_HEADER = "x-wafermap-pass";
412
+ var PASS_MAX_SECONDS = 86400;
413
+ function passMessage(address, iat, exp) {
414
+ return [
415
+ "wafermap.xyz holder pass",
416
+ "",
417
+ `Wallet: ${address}`,
418
+ `Issued: ${new Date(iat * 1e3).toISOString()}`,
419
+ `Expires: ${new Date(exp * 1e3).toISOString()}`,
420
+ "",
421
+ "Proves you hold this wallet, so Wafermap can apply what your $WAFERMAP unlocks. It costs nothing and moves no funds."
422
+ ].join("\n");
423
+ }
424
+ var b64url = (s) => btoa(unescape(encodeURIComponent(s))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
425
+ var unb64url = (s) => decodeURIComponent(escape(atob(s.replace(/-/g, "+").replace(/_/g, "/"))));
426
+ var encodePass = (p) => b64url(JSON.stringify(p));
427
+ function decodePass(s) {
428
+ if (!s || s.length > 1e3 || !/^[A-Za-z0-9_-]+$/.test(s)) return null;
429
+ try {
430
+ const p = JSON.parse(unb64url(s));
431
+ if (p?.v !== 1 || typeof p.address !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(p.address)) return null;
432
+ if (!Number.isInteger(p.iat) || !Number.isInteger(p.exp) || typeof p.sig !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(p.sig)) return null;
433
+ return { v: 1, address: p.address, iat: p.iat, exp: p.exp, sig: p.sig };
434
+ } catch {
435
+ return null;
436
+ }
437
+ }
438
+ async function makePass(signer, seconds = PASS_MAX_SECONDS, now = Math.floor(Date.now() / 1e3)) {
439
+ const iat = now;
440
+ const exp = now + Math.min(Math.max(60, seconds), PASS_MAX_SECONDS);
441
+ const sig = await signer.signMessage({ message: passMessage(signer.address, iat, exp) });
442
+ return { v: 1, address: signer.address, iat, exp, sig };
443
+ }
444
+
410
445
  // src/core.ts
411
446
  var NETS = CHAIN_KEYS;
412
447
  var isSwapNet = (v) => v === "robinhood" || v === "base";
@@ -442,6 +477,29 @@ async function checkPayment(e, rpc) {
442
477
  var DEFAULT_LIMITS = { perPayment: 0.1, perDay: 1, perSwap: 25 };
443
478
  var DEFAULT_TIMEOUT_MS = 6e4;
444
479
  var timedOut = (e) => e?.name === "TimeoutError" || e?.cause?.name === "TimeoutError";
480
+ async function holderInfo(address, baseUrl = "https://wafermap.xyz", timeoutMs = DEFAULT_TIMEOUT_MS) {
481
+ const host = new URL(baseUrl).host;
482
+ const r = await fetch(`${baseUrl.replace(/\/$/, "")}/api/holder?address=${encodeURIComponent(address)}`, { signal: AbortSignal.timeout(timeoutMs) }).catch((e) => {
483
+ throw timedOut(e) ? new WafermapError("timeout", `${host} did not answer within ${Math.round(timeoutMs / 1e3)} s.`) : new WafermapError("network_error", `${host}: ${e?.message ?? "request failed"}`);
484
+ });
485
+ const j = await r.json().catch(() => null);
486
+ if (!r.ok || !j) throw new WafermapError(j?.error?.code ?? "network_error", j?.error?.message ?? `${host} answered HTTP ${r.status}.`);
487
+ return j;
488
+ }
489
+ async function listResources(baseUrl = "https://wafermap.xyz", timeoutMs = DEFAULT_TIMEOUT_MS) {
490
+ const host = new URL(baseUrl).host;
491
+ const gaveUp = () => new WafermapError("timeout", `${host} did not answer within ${Math.round(timeoutMs / 1e3)} s. Nothing was paid.`);
492
+ const signal = AbortSignal.timeout(timeoutMs);
493
+ const r = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/resources`, { signal }).catch((e) => {
494
+ throw timedOut(e) ? gaveUp() : new WafermapError("network_error", `${host}: ${e?.message ?? "request failed"}`);
495
+ });
496
+ const j = await r.json().catch((e) => {
497
+ if (timedOut(e)) throw gaveUp();
498
+ return null;
499
+ });
500
+ if (!r.ok || !Array.isArray(j?.data)) throw new WafermapError("network_error", `${host} returned no catalog (HTTP ${r.status}).`);
501
+ return j.data;
502
+ }
445
503
  var Wafermap = class {
446
504
  account;
447
505
  baseUrl;
@@ -580,15 +638,21 @@ var Wafermap = class {
580
638
  }
581
639
  // ---- Wafermap resources ----
582
640
  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 ?? [];
641
+ return listResources(this.baseUrl, this.timeoutMs);
642
+ }
643
+ passCache = null;
644
+ /** A holder pass (shared/pass.ts) signed by this wallet, valid 24 h and reused until its last 10 minutes. It
645
+ * proves the wallet to Wafermap for what its $WAFERMAP unlocks (Pro free calls, x402 Tracker Pro). Signing
646
+ * a message moves no funds. */
647
+ async holderPass() {
648
+ const p = decodePass(this.passCache);
649
+ if (this.passCache && p && p.exp - 600 > Date.now() / 1e3) return this.passCache;
650
+ this.passCache = encodePass(await makePass({ address: this.account.address, signMessage: (a) => this.account.signMessage(a) }));
651
+ return this.passCache;
589
652
  }
590
653
  async call(id, input = {}, o = {}) {
591
- 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);
654
+ const pass = id.startsWith("m/") ? null : await this.holderPass();
655
+ const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address, ...pass ? { [PASS_HEADER]: pass } : {} }, body: JSON.stringify(input) }, o.network);
592
656
  const settle = (r2, out2) => {
593
657
  if (!r2.paid || !r2.id) return;
594
658
  const status = r2.res.status === 402 ? "refused" : r2.res.ok ? "ok" : "failed";
@@ -702,6 +766,7 @@ export {
702
766
  DEFAULT_TIMEOUT_MS,
703
767
  KYBER_ROUTER,
704
768
  MAX_VALIDITY_SECONDS,
769
+ PASS_HEADER,
705
770
  PAY_CHAINS,
706
771
  SwapError,
707
772
  Wafermap,
@@ -709,9 +774,15 @@ export {
709
774
  X402Error,
710
775
  checkPayment,
711
776
  choose,
777
+ decodePass,
712
778
  decodeSwap,
779
+ encodePass,
713
780
  executeSwap,
781
+ holderInfo,
782
+ listResources,
783
+ makePass,
714
784
  mergeLog,
785
+ passMessage,
715
786
  quoteSwap,
716
787
  readQuote,
717
788
  signPayment,
@@ -0,0 +1,21 @@
1
+ export declare const PASS_HEADER = "x-wafermap-pass";
2
+ export declare const PASS_MAX_SECONDS = 86400;
3
+ export type Pass = {
4
+ v: 1;
5
+ address: string;
6
+ iat: number;
7
+ exp: number;
8
+ sig: string;
9
+ };
10
+ /** The exact text the wallet signs. */
11
+ export declare function passMessage(address: string, iat: number, exp: number): string;
12
+ export declare const encodePass: (p: Pass) => string;
13
+ /** The pass's fields, or null when it is not a well-formed pass (the signature is not checked here). */
14
+ export declare function decodePass(s: string | undefined | null): Pass | null;
15
+ /** Signs a new pass with any signer that can personal_sign. Valid for `seconds` (at most a day). */
16
+ export declare function makePass(signer: {
17
+ address: string;
18
+ signMessage: (a: {
19
+ message: string;
20
+ }) => Promise<string>;
21
+ }, seconds?: number, now?: number): Promise<Pass>;
@@ -87,6 +87,10 @@ 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
+ /** What a wallet's $WAFERMAP unlocks (holder price, Pro free calls left today), from /api/holder. Needs no key. */
91
+ export declare function holderInfo(address: string, baseUrl?: string, timeoutMs?: number): Promise<any>;
92
+ /** Wafermap's resource catalog. Needs no wallet; gives up after `timeoutMs` like every other request. */
93
+ export declare function listResources(baseUrl?: string, timeoutMs?: number): Promise<any[]>;
90
94
  export declare class Wafermap {
91
95
  readonly account: LocalAccount;
92
96
  readonly baseUrl: string;
@@ -124,6 +128,11 @@ export declare class Wafermap {
124
128
  private acquire;
125
129
  private paidFetch;
126
130
  resources(): Promise<any[]>;
131
+ private passCache;
132
+ /** A holder pass (shared/pass.ts) signed by this wallet, valid 24 h and reused until its last 10 minutes. It
133
+ * proves the wallet to Wafermap for what its $WAFERMAP unlocks (Pro free calls, x402 Tracker Pro). Signing
134
+ * a message moves no funds. */
135
+ holderPass(): Promise<string>;
127
136
  call(id: string, input?: Record<string, unknown>, o?: {
128
137
  network?: Net;
129
138
  }): Promise<{
@@ -137,6 +146,11 @@ export declare class Wafermap {
137
146
  tx: string;
138
147
  ms: number;
139
148
  } | null;
149
+ free?: {
150
+ wallet: string;
151
+ left: number;
152
+ perDay: number;
153
+ };
140
154
  }>;
141
155
  discover(query: string, limit?: number): Promise<{
142
156
  url: string;
@@ -1,4 +1,5 @@
1
- export { Wafermap, WafermapError, DEFAULT_LIMITS, DEFAULT_TIMEOUT_MS, checkPayment, mergeLog } from "./core.js";
1
+ export { Wafermap, WafermapError, DEFAULT_LIMITS, DEFAULT_TIMEOUT_MS, checkPayment, holderInfo, listResources, mergeLog } from "./core.js";
2
+ export { PASS_HEADER, passMessage, encodePass, decodePass, makePass } from "../shared/pass";
2
3
  export type { Approval, Limits, Net, PaymentStatus, SpendEntry, SwapQuote, Token, WafermapOptions } from "./core.js";
3
4
  export { x402fetch, readQuote, choose, signPayment, PAY_CHAINS, MAX_VALIDITY_SECONDS, X402Error } from "../shared/x402client";
4
5
  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.2",
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.4",
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
+ }