@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 +62 -46
- package/README.md +120 -118
- package/dist/cli.js +107 -10
- package/dist/index.js +78 -7
- package/dist/types/shared/pass.d.ts +21 -0
- package/dist/types/src/core.d.ts +14 -0
- package/dist/types/src/index.d.ts +2 -1
- package/package.json +69 -68
package/CHANGELOG.md
CHANGED
|
@@ -1,46 +1,62 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 0.4.
|
|
4
|
-
|
|
5
|
-
**$WAFERMAP holder
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
- **
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
- `wafermap
|
|
35
|
-
-
|
|
36
|
-
- Tests (`
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
**
|
|
94
|
-
|
|
95
|
-
**
|
|
96
|
-
|
|
97
|
-
**
|
|
98
|
-
|
|
99
|
-
The checks
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
npm
|
|
108
|
-
|
|
109
|
-
npm
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
## $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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
|
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.
|
|
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
|
|
1684
|
-
for (const x of
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
|
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>;
|
package/dist/types/src/core.d.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
"@
|
|
58
|
-
"@noble/
|
|
59
|
-
"@
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
"
|
|
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
|
+
}
|