@wafermapxyz/cli 0.4.10 → 0.4.12

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,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.12 (16 Nov 2026)
4
+
5
+ **`wafermap call <llm> --metered`: pay only the tokens the model used.**
6
+
7
+ - Signs one ceiling (x402 scheme `upto`, a Permit2 transfer of at most that much) instead of the flat price. After the model finishes, Wafermap settles only the input and output tokens it reported, never more than the ceiling and never less than $0.001. The confirmation shows the ceiling; the receipt line shows the token counts, what was charged and what was signed.
8
+ - USDC on `base`, `arbitrum` or `polygon` (pick one with `--network`). A wallet that never approved Permit2 for USDC signs an EIP-2612 permit with the payment, so no gas and no extra transaction is needed.
9
+ - The signature can only be spent by the x402 upto proxy (its address is fixed in the client), only to the quote's recipient and only through the facilitator named in the quote, and it expires after at most 5 minutes. Spending caps count the full ceiling, because that is what was signed.
10
+ - Works with `--stream`. Cannot be combined with `--pay wafermap`.
11
+ - SDK: `call(id, input, { metered: true })`; the receipt carries `metered` (`inputTokens`, `outputTokens`, `estimated`, `ceilingUsd`, `chargedUsd`). MCP: `wafermap_call` takes `metered`.
12
+
13
+ ## 0.4.11 (15 Nov 2026)
14
+
15
+ **`wafermap statement`: a monthly statement you can check against the chains.**
16
+
17
+ - `wafermap statement --month 2026-11 --out statement.json` downloads one month of this wallet's payments to Wafermap and to marketplace sellers (EVM and Solana sides together), from https://wafermap.xyz/api/statement. `--csv` gives the accounting version; without `--out` it prints the lines and the total.
18
+ - `wafermap statement --verify statement.json` reads every transaction from its own chain, not from Wafermap: an EVM line passes only if the receipt holds a Transfer of exactly that amount of that token from the payer to the recipient; a Solana line only if the transaction succeeded and moved exactly that amount between those two owners. It also checks that the totals add up to the lines, and exits with code 2 on any mismatch, failed or missing transaction. Public RPCs by default; set `WAFERMAP_RPC_<NETWORK>` to use your own.
19
+ - Works on statements downloaded anywhere: the Wallet page, a team owner's statement from /teams, or the API.
20
+ - SDK: `fetchStatement`, `parseStatement`, `verifyStatement`, `verifyLine`.
21
+
3
22
  ## 0.4.10 (12 Nov 2026)
4
23
 
5
24
  **`wafermap discover` shows the price on every network a service accepts.**
package/README.md CHANGED
@@ -33,6 +33,8 @@ wafermap discover weather
33
33
  wafermap pay https://x402.agentindex.world/weather city=Jakarta
34
34
  wafermap swap 0.01 ETH USDG
35
35
  wafermap history
36
+ wafermap statement --month 2026-11 --out nov.json
37
+ wafermap statement --verify nov.json
36
38
  ```
37
39
 
38
40
  Add `--yes` to skip the confirmation prompt (caps still apply), `--network base`, `arbitrum`, `polygon`, `arc`, `soneium` 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.
@@ -84,6 +86,8 @@ Pass `approve: (a) => boolean` to confirm each payment or swap yourself, and `le
84
86
 
85
87
  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, Arc, Soneium). 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
88
 
89
+ LLM resources can also be **metered**: `wafermap call llama-4-scout prompt="hi" --metered --network base` signs a ceiling (shown before you confirm) and pays only the tokens the model used, at least $0.001, in USDC on Base, Arbitrum or Polygon. No gas needed. Caps count the ceiling. Details: https://wafermap.xyz/docs#metered
90
+
87
91
  LLM resources can **stream**: `wafermap call gemma-4 prompt="hi" --stream` prints the answer as it arrives, and the payment settles only once the model finishes.
88
92
 
89
93
  Wafermap's own resources can also be **paid in $WAFERMAP at 50% off, and the tokens are burned**: `wafermap approve-wafermap` once (a Permit2 approval, one Robinhood Chain transaction), then `wafermap call <id> --pay wafermap`. Each payment is a Permit2 signature for one exact amount, usable for two minutes only by Wafermap's relayer (fixed in the client), checked against a market price the client reads itself.
package/dist/cli.js CHANGED
@@ -374,6 +374,36 @@ var init_solana = __esm({
374
374
  });
375
375
 
376
376
  // shared/x402client.ts
377
+ async function signUptoPayment(signer, req, resource, opts = {}) {
378
+ const fac = String(req.raw?.extra?.facilitatorAddress ?? "");
379
+ if (!/^0x[0-9a-fA-F]{40}$/.test(fac)) throw new X402Error("bad_quote", "The metered quote names no facilitator. Nothing was signed.");
380
+ const nonce = BigInt(randomNonce()) >> 8n;
381
+ const deadline = BigInt(Math.floor(Date.now() / 1e3) + Math.min(req.maxTimeoutSeconds, MAX_METERED_SECONDS));
382
+ const max = BigInt(req.amount);
383
+ const witness = { to: req.payTo, facilitator: fac, validAfter: 0n };
384
+ let signature;
385
+ try {
386
+ signature = await signer.signTypedData({ domain: { name: "Permit2", chainId: req.chain.id, verifyingContract: PERMIT2 }, types: UPTO_TYPES, primaryType: "PermitWitnessTransferFrom", message: { permitted: { token: req.asset, amount: max }, spender: UPTO_PROXY, nonce, deadline, witness } });
387
+ } catch (e) {
388
+ throw new X402Error("sign_rejected", e?.shortMessage ?? e?.message ?? "Signature rejected.");
389
+ }
390
+ let extensions;
391
+ if (opts.gasless && opts.reader) {
392
+ const allowance = await opts.reader.allowance(req.chain, signer.address, req.asset).catch(() => 0n);
393
+ if (allowance < max) {
394
+ const pn = await opts.reader.permitNonce(req.chain, signer.address, req.asset);
395
+ let ps;
396
+ try {
397
+ ps = await signer.signTypedData({ domain: { name: req.extra.name, version: req.extra.version, chainId: req.chain.id, verifyingContract: req.asset }, types: EIP2612_TYPES, primaryType: "Permit", message: { owner: signer.address, spender: PERMIT2, value: max, nonce: pn, deadline } });
398
+ } catch (e) {
399
+ throw new X402Error("sign_rejected", e?.shortMessage ?? e?.message ?? "Signature rejected.");
400
+ }
401
+ extensions = { eip2612GasSponsoring: { info: { from: signer.address, asset: req.asset, spender: PERMIT2, amount: max.toString(), nonce: pn.toString(), deadline: deadline.toString(), signature: ps, version: "1" } } };
402
+ }
403
+ }
404
+ const permit2Authorization = { from: signer.address, permitted: { token: req.asset, amount: req.amount }, spender: UPTO_PROXY, nonce: nonce.toString(), deadline: deadline.toString(), witness: { to: req.payTo, facilitator: fac, validAfter: "0" } };
405
+ return { name: "PAYMENT-SIGNATURE", value: b64encode(JSON.stringify({ x402Version: 2, resource, accepted: req.raw, payload: { signature, permit2Authorization }, ...extensions ? { extensions } : {} })), deadline: Number(deadline) };
406
+ }
377
407
  async function wafermapPriceUsd(f = fetch) {
378
408
  const res = await f(`https://api.geckoterminal.com/api/v2/simple/networks/robinhood/token_price/${WAFERMAP_TOKEN}`, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(15e3) });
379
409
  const j = await res.json().catch(() => null);
@@ -421,12 +451,19 @@ async function readQuote(res) {
421
451
  const accepts = [];
422
452
  const offered = [];
423
453
  let token;
454
+ const metered = [];
424
455
  for (const r of body.accepts) {
425
456
  offered.push(`${r.scheme ?? "?"}/${r.network ?? "?"}`);
426
457
  if (r?.scheme === TOKEN_SCHEME) {
427
458
  token ??= tokenOption(r);
428
459
  continue;
429
460
  }
461
+ if (r?.scheme === "upto" && version === 2) {
462
+ const ch = chainOf(r.network);
463
+ if (ch && ch.vm === "evm" && String(r.asset ?? "").toLowerCase() === ch.stable.toLowerCase() && /^\d+$/.test(String(r.amount)) && /^0x[0-9a-fA-F]{40}$/.test(String(r.extra?.facilitatorAddress ?? "")) && /^0x[0-9a-fA-F]{40}$/.test(String(r.payTo ?? "")))
464
+ metered.push({ version, chain: ch, amount: String(r.amount), usd: Number(r.amount) / 1e6, payTo: r.payTo, asset: r.asset, maxTimeoutSeconds: Number(r.maxTimeoutSeconds) || 120, extra: r.extra?.name && r.extra?.version ? { name: String(r.extra.name), version: String(r.extra.version) } : ch.domain, raw: r });
465
+ continue;
466
+ }
430
467
  const chain = chainOf(r.network);
431
468
  if (!chain || r.scheme !== "exact") continue;
432
469
  if (chain.vm === "evm" ? String(r.asset ?? "").toLowerCase() !== chain.stable.toLowerCase() : r.asset !== chain.stable) continue;
@@ -440,7 +477,10 @@ async function readQuote(res) {
440
477
  accepts.push({ version, chain, amount, usd: Number(amount) / 1e6, payTo: r.payTo, asset: r.asset, maxTimeoutSeconds: Number(r.maxTimeoutSeconds) || 120, extra, raw: r });
441
478
  }
442
479
  const inputError = body.inputError?.message ? { code: String(body.inputError.code ?? "invalid_input"), message: String(body.inputError.message) } : void 0;
443
- return { version, resource: body.resource ?? null, accepts, offered, inputError, ...token ? { token } : {} };
480
+ const m = body.metered;
481
+ const meterInfo = m && Number.isFinite(Number(m.ceilingUsd)) ? { inputPer1kTokensUsd: Number(m.inputPer1kTokensUsd), outputPer1kTokensUsd: Number(m.outputPer1kTokensUsd), ceilingUsd: Number(m.ceilingUsd), minimumUsd: Number(m.minimumUsd) } : void 0;
482
+ const gasless = !!body.extensions?.eip2612GasSponsoring;
483
+ return { version, resource: body.resource ?? null, accepts, offered, inputError, ...token ? { token } : {}, ...metered.length ? { metered: metered.map((q) => ({ ...q, gasless })) } : {}, ...meterInfo ? { meterInfo } : {} };
444
484
  }
445
485
  function choose(q, prefer = CHAIN_KEYS) {
446
486
  for (const k of prefer) {
@@ -512,6 +552,20 @@ async function x402fetch(url, init, opts) {
512
552
  const res2 = await f(url, { ...init, headers: headers2 });
513
553
  return { res: res2, paid: { ...signed2, tx: readSettlement(res2)?.tx ?? null } };
514
554
  }
555
+ if (opts.meter) {
556
+ const pref = (opts.prefer ?? CHAIN_KEYS).filter((k) => k !== "solana");
557
+ const req2 = pref.map((k) => quote.metered?.find((q) => q.chain.key === k)).find(Boolean);
558
+ if (!req2) throw new X402Error("metered_not_offered", quote.metered?.length ? `Metered calls are offered on ${quote.metered.map((q) => q.chain.name).join(", ")}; pick one of those networks.` : "This call is not offered as a metered (pay per token) call. Pay the flat price instead.");
559
+ if (opts.approve) await opts.approve(req2);
560
+ const signer2 = typeof opts.signer === "function" ? await opts.signer() : opts.signer;
561
+ const header2 = await signUptoPayment(signer2, req2, quote.resource, { reader: opts.reader, gasless: !!req2.gasless });
562
+ const signed2 = { req: req2, from: signer2.address, metered: { ceiling: req2.amount, deadline: header2.deadline } };
563
+ opts.onSigned?.(signed2);
564
+ const headers2 = new Headers(init.headers);
565
+ headers2.set(header2.name, header2.value);
566
+ const res2 = await f(url, { ...init, headers: headers2 });
567
+ return { res: res2, paid: { ...signed2, tx: readSettlement(res2)?.tx ?? null } };
568
+ }
515
569
  const req = choose(quote, (opts.prefer ?? CHAIN_KEYS).filter((k) => k !== "solana" || opts.solana));
516
570
  if (opts.approve) await opts.approve(req);
517
571
  const svm = req.chain.vm === "svm";
@@ -524,7 +578,7 @@ async function x402fetch(url, init, opts) {
524
578
  const res = await f(url, { ...init, headers });
525
579
  return { res, paid: { ...signed, tx: readSettlement(res)?.tx ?? null } };
526
580
  }
527
- var USDC_DOMAIN, PAY_CHAINS, EVM_CHAINS, CHAIN_KEYS, WAFERMAP_TOKEN, PERMIT2, DEAD, TOKEN_SCHEME, WAFERMAP_RELAYERS, TOKEN_VALUE_SLACK, PERMIT_TYPES, X402Error, b64encode, b64decode, TYPES, MAX_VALIDITY_SECONDS, randomNonce;
581
+ var USDC_DOMAIN, PAY_CHAINS, EVM_CHAINS, CHAIN_KEYS, UPTO_PROXY, MAX_METERED_SECONDS, UPTO_TYPES, EIP2612_TYPES, WAFERMAP_TOKEN, PERMIT2, DEAD, TOKEN_SCHEME, WAFERMAP_RELAYERS, TOKEN_VALUE_SLACK, PERMIT_TYPES, X402Error, b64encode, b64decode, TYPES, MAX_VALIDITY_SECONDS, randomNonce;
528
582
  var init_x402client = __esm({
529
583
  "shared/x402client.ts"() {
530
584
  "use strict";
@@ -542,6 +596,35 @@ var init_x402client = __esm({
542
596
  ];
543
597
  EVM_CHAINS = PAY_CHAINS.filter((c) => c.vm === "evm");
544
598
  CHAIN_KEYS = PAY_CHAINS.map((c) => c.key);
599
+ UPTO_PROXY = "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002";
600
+ MAX_METERED_SECONDS = 300;
601
+ UPTO_TYPES = {
602
+ PermitWitnessTransferFrom: [
603
+ { name: "permitted", type: "TokenPermissions" },
604
+ { name: "spender", type: "address" },
605
+ { name: "nonce", type: "uint256" },
606
+ { name: "deadline", type: "uint256" },
607
+ { name: "witness", type: "Witness" }
608
+ ],
609
+ TokenPermissions: [
610
+ { name: "token", type: "address" },
611
+ { name: "amount", type: "uint256" }
612
+ ],
613
+ Witness: [
614
+ { name: "to", type: "address" },
615
+ { name: "facilitator", type: "address" },
616
+ { name: "validAfter", type: "uint256" }
617
+ ]
618
+ };
619
+ EIP2612_TYPES = {
620
+ Permit: [
621
+ { name: "owner", type: "address" },
622
+ { name: "spender", type: "address" },
623
+ { name: "value", type: "uint256" },
624
+ { name: "nonce", type: "uint256" },
625
+ { name: "deadline", type: "uint256" }
626
+ ]
627
+ };
545
628
  WAFERMAP_TOKEN = "0xfda080f64d84c989cfc50ff18253542cfe502db3";
546
629
  PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
547
630
  DEAD = "0x000000000000000000000000000000000000dEaD";
@@ -858,7 +941,15 @@ var init_core = __esm({
858
941
  this.queue = prev.then(() => held);
859
942
  return prev.then(() => release);
860
943
  }
861
- async paidFetch(kind, what, url, init, network, payIn) {
944
+ /** Chain reads for a metered payment's EIP-2612 permit: Permit2 allowance and the token's permit nonce. */
945
+ reader() {
946
+ const nonces = [{ type: "function", name: "nonces", stateMutability: "view", inputs: [{ name: "owner", type: "address" }], outputs: [{ type: "uint256" }] }];
947
+ return {
948
+ allowance: (chain, owner, token) => this.client(chain.key).readContract({ address: token, abi: erc20Abi2, functionName: "allowance", args: [owner, PERMIT2] }),
949
+ permitNonce: (chain, owner, token) => this.client(chain.key).readContract({ address: token, abi: nonces, functionName: "nonces", args: [owner] })
950
+ };
951
+ }
952
+ async paidFetch(kind, what, url, init, network, payIn, meter) {
862
953
  let id = null;
863
954
  let release = null;
864
955
  const unlock = () => {
@@ -872,10 +963,12 @@ var init_core = __esm({
872
963
  prefer: this.prefer(network),
873
964
  fetch: this.fetch,
874
965
  payIn,
966
+ meter,
967
+ reader: meter ? this.reader() : void 0,
875
968
  tokenPriceUsd: this.tokenPriceUsd,
876
969
  approve: async (req) => {
877
970
  release = await this.acquire();
878
- await this.gate({ kind, what, usd: req.usd, network: req.chain.key, detail: req.asset === WAFERMAP_TOKEN ? "in $WAFERMAP at 50% off, burned" : `${req.chain.symbol} to ${req.payTo}` });
971
+ await this.gate({ kind, what, usd: req.usd, network: req.chain.key, detail: req.asset === WAFERMAP_TOKEN ? "in $WAFERMAP at 50% off, burned" : meter ? `at most ${req.chain.symbol} ${req.usd}, charged for the tokens used, to ${req.payTo}` : `${req.chain.symbol} to ${req.payTo}` });
879
972
  },
880
973
  onSigned: (s) => {
881
974
  id = this.booked(kind, what, s);
@@ -963,11 +1056,12 @@ var init_core = __esm({
963
1056
  }
964
1057
  async call(id, input = {}, o = {}) {
965
1058
  const inToken = o.payIn === "wafermap";
1059
+ if (o.metered && inToken) throw new WafermapError("bad_options", "A metered call is paid in USDC; it cannot also be paid in $WAFERMAP.");
966
1060
  const stream = !!o.onDelta && !inToken;
967
1061
  if (inToken && id.startsWith("m/")) throw new WafermapError("token_not_offered", "Marketplace listings are paid in USDG or USDC to their seller, not in $WAFERMAP.");
968
1062
  if (inToken && !await this.wafermapApproved().catch(() => true)) throw new WafermapError("permit2_not_approved", "Paying in $WAFERMAP needs a one-time approval of Permit2. Run `wafermap approve-wafermap` (one transaction on Robinhood Chain, paid in ETH), then call again.");
969
1063
  const pass = id.startsWith("m/") ? null : await this.holderPass();
970
- const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address, ...stream ? { accept: "text/event-stream" } : {}, ...pass ? { [PASS_HEADER]: pass } : {} }, body: JSON.stringify(input) }, o.network, inToken ? "wafermap" : void 0);
1064
+ const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address, ...stream ? { accept: "text/event-stream" } : {}, ...pass ? { [PASS_HEADER]: pass } : {} }, body: JSON.stringify(input) }, o.network, inToken ? "wafermap" : void 0, o.metered);
971
1065
  const settle = (r2, out2) => {
972
1066
  if (!r2.paid || !r2.id) return;
973
1067
  const status = r2.res.status === 402 ? "refused" : r2.res.ok && !out2?.streamError ? "ok" : "failed";
@@ -1288,7 +1382,119 @@ var VERSION;
1288
1382
  var init_version = __esm({
1289
1383
  "src/version.ts"() {
1290
1384
  "use strict";
1291
- VERSION = "0.4.10";
1385
+ VERSION = "0.4.12";
1386
+ }
1387
+ });
1388
+
1389
+ // src/statement.ts
1390
+ var statement_exports = {};
1391
+ __export(statement_exports, {
1392
+ defaultClients: () => defaultClients,
1393
+ fetchStatement: () => fetchStatement,
1394
+ parseStatement: () => parseStatement,
1395
+ verifyLine: () => verifyLine,
1396
+ verifyStatement: () => verifyStatement
1397
+ });
1398
+ import { createPublicClient as createPublicClient2, getAddress as getAddress3, http as http2, isAddress as isAddress3, parseAbiItem as parseAbiItem2, toEventSelector as toEventSelector2 } from "viem";
1399
+ import { arbitrum as arbitrum2, arc as arc2, base as base2, polygon as polygon2, robinhood as robinhood2, soneium as soneium2 } from "viem/chains";
1400
+ async function fetchStatement(wallets, month, opts = {}) {
1401
+ const url = `${(opts.baseUrl ?? "https://wafermap.xyz").replace(/\/$/, "")}/api/statement?wallet=${encodeURIComponent(wallets.join(","))}&month=${encodeURIComponent(month)}${opts.csv ? "&format=csv" : ""}`;
1402
+ const r = await fetch(url, { signal: AbortSignal.timeout(opts.timeoutMs ?? 6e4) });
1403
+ if (!r.ok) {
1404
+ const j = await r.json().catch(() => null);
1405
+ throw new Error(j?.error?.message ?? `Statement request failed (${r.status}).`);
1406
+ }
1407
+ return opts.csv ? r.text() : await r.json();
1408
+ }
1409
+ function parseStatement(text2) {
1410
+ let s;
1411
+ try {
1412
+ s = JSON.parse(text2);
1413
+ } catch {
1414
+ throw new Error("That file is not JSON. Download the JSON statement (not the CSV) to verify it.");
1415
+ }
1416
+ if (s?.kind !== "wafermap-statement" || !Array.isArray(s.lines)) throw new Error("That file is not a Wafermap statement.");
1417
+ return s;
1418
+ }
1419
+ function defaultClients(env = process.env) {
1420
+ const cache = /* @__PURE__ */ new Map();
1421
+ return {
1422
+ evm: (network) => {
1423
+ const chain = EVM[network];
1424
+ if (!chain) return null;
1425
+ if (!cache.has(network)) cache.set(network, createPublicClient2({ chain, transport: http2(env[ENV_RPC[network]] || void 0, { timeout: 3e4 }) }));
1426
+ return cache.get(network);
1427
+ },
1428
+ solana: (method, params) => solanaRpc(env.WAFERMAP_RPC_SOLANA || SOLANA_PUBLIC_RPC, method, params)
1429
+ };
1430
+ }
1431
+ async function verifyLine(l, clients) {
1432
+ try {
1433
+ if (!/^\d+$/.test(l.amountUnits)) return "mismatch";
1434
+ const want = BigInt(l.amountUnits);
1435
+ if (l.network.startsWith("solana:")) {
1436
+ const tx = await clients.solana("getTransaction", [l.tx, { encoding: "jsonParsed", maxSupportedTransactionVersion: 0, commitment: "finalized" }]);
1437
+ if (!tx) return "not_found";
1438
+ if (tx.meta?.err) return "failed";
1439
+ const delta = (owner) => {
1440
+ const sum = (xs) => (xs ?? []).filter((b) => b.owner === owner && b.mint === l.token).reduce((a, b) => a + BigInt(b.uiTokenAmount?.amount ?? "0"), 0n);
1441
+ return sum(tx.meta?.postTokenBalances) - sum(tx.meta?.preTokenBalances);
1442
+ };
1443
+ return delta(l.from) === -want && delta(l.to) === want ? "ok" : "mismatch";
1444
+ }
1445
+ const client = clients.evm(l.network);
1446
+ if (!client) return "unsupported";
1447
+ if (!/^0x[0-9a-fA-F]{64}$/.test(l.tx) || !isAddress3(l.token) || !isAddress3(l.from) || !isAddress3(l.to)) return "mismatch";
1448
+ const receipt = await client.getTransactionReceipt({ hash: l.tx }).catch((e) => /not.*found|could not be found/i.test(String(e?.message)) ? null : Promise.reject(e));
1449
+ if (!receipt) return "not_found";
1450
+ if (receipt.status !== "success") return "failed";
1451
+ const hit = receipt.logs.some(
1452
+ (g) => g.address.toLowerCase() === l.token.toLowerCase() && g.topics[0] === TRANSFER && topicAddr(g.topics[1]) === getAddress3(l.from) && topicAddr(g.topics[2]) === getAddress3(l.to) && BigInt(g.data) === want
1453
+ );
1454
+ return hit ? "ok" : "mismatch";
1455
+ } catch {
1456
+ return "error";
1457
+ }
1458
+ }
1459
+ async function verifyStatement(s, clients = defaultClients(), onLine) {
1460
+ const results = new Array(s.lines.length);
1461
+ let next = 0;
1462
+ await Promise.all(
1463
+ Array.from({ length: 4 }, async () => {
1464
+ while (next < s.lines.length) {
1465
+ const i = next++;
1466
+ results[i] = await verifyLine(s.lines[i], clients);
1467
+ onLine?.(i, results[i]);
1468
+ }
1469
+ })
1470
+ );
1471
+ const usd = (pick) => Math.round(s.lines.reduce((a, l, i) => a + (pick(results[i]) ? Number(l.amountUsd) : 0), 0) * 1e6) / 1e6;
1472
+ const count = (r) => results.filter((x) => x === r).length;
1473
+ const linesUsd = usd(() => true);
1474
+ return {
1475
+ results,
1476
+ lines: s.lines.length,
1477
+ ok: count("ok"),
1478
+ mismatch: count("mismatch"),
1479
+ failed: count("failed"),
1480
+ notFound: count("not_found"),
1481
+ unsupported: count("unsupported"),
1482
+ error: count("error"),
1483
+ confirmedUsd: usd((r) => r === "ok"),
1484
+ linesUsd,
1485
+ // The totals block must add up to the lines it sits next to.
1486
+ totalsMatch: s.totals?.calls === s.lines.length && Math.abs((s.totals?.usd ?? NaN) - linesUsd) < 1e-6
1487
+ };
1488
+ }
1489
+ var EVM, ENV_RPC, TRANSFER, topicAddr;
1490
+ var init_statement = __esm({
1491
+ "src/statement.ts"() {
1492
+ "use strict";
1493
+ init_solana();
1494
+ EVM = { "eip155:4663": robinhood2, "eip155:8453": base2, "eip155:42161": arbitrum2, "eip155:137": polygon2, "eip155:5042": arc2, "eip155:1868": soneium2 };
1495
+ ENV_RPC = { "eip155:4663": "WAFERMAP_RPC_ROBINHOOD", "eip155:8453": "WAFERMAP_RPC_BASE", "eip155:42161": "WAFERMAP_RPC_ARBITRUM", "eip155:137": "WAFERMAP_RPC_POLYGON", "eip155:5042": "WAFERMAP_RPC_ARC", "eip155:1868": "WAFERMAP_RPC_SONEIUM", solana: "WAFERMAP_RPC_SOLANA" };
1496
+ TRANSFER = toEventSelector2(parseAbiItem2("event Transfer(address indexed from, address indexed to, uint256 value)"));
1497
+ topicAddr = (t) => t ? getAddress3(`0x${t.slice(-40)}`) : null;
1292
1498
  }
1293
1499
  });
1294
1500
 
@@ -1392,12 +1598,13 @@ async function runMcp() {
1392
1598
  resource: z.string().describe("Resource id, e.g. market-brief, prediction-markets, stock-signal, flux-2-dev."),
1393
1599
  input: z.record(z.string(), z.any()).optional().describe("Input fields for the resource."),
1394
1600
  network: NET,
1395
- payIn: z.enum(["stablecoin", "wafermap"]).optional().describe("wafermap: pay Wafermap's own resources in $WAFERMAP at 50% off (the tokens are burned; needs `wafermap approve-wafermap` once). Default: USDG/USDC.")
1601
+ payIn: z.enum(["stablecoin", "wafermap"]).optional().describe("wafermap: pay Wafermap's own resources in $WAFERMAP at 50% off (the tokens are burned; needs `wafermap approve-wafermap` once). Default: USDG/USDC."),
1602
+ metered: z.boolean().optional().describe("LLM resources only: sign a ceiling and pay only the tokens the model used (USDC on base, arbitrum or polygon; no gas needed). Caps count the ceiling.")
1396
1603
  }
1397
1604
  },
1398
- async ({ resource, input, network, payIn }) => {
1605
+ async ({ resource, input, network, payIn, metered }) => {
1399
1606
  try {
1400
- return result(await wm.call(resource, input ?? {}, { network, payIn }));
1607
+ return result(await wm.call(resource, input ?? {}, { network, payIn, metered }));
1401
1608
  } catch (e) {
1402
1609
  return failure(e);
1403
1610
  }
@@ -1844,6 +2051,8 @@ Wallet
1844
2051
  wafermap balance USDG / USDC and gas on every payment network
1845
2052
  wafermap limits [--per-payment 0.1] [--per-day 1] [--per-swap 25] [--network <net>] [--mcp-swaps on|off] [--mcp-confirm on|off]
1846
2053
  wafermap history [--days 7] [--verify] payments and swaps from this machine; --verify checks on-chain
2054
+ wafermap statement [--month 2026-11] [--csv] [--out file] a month of payments to Wafermap and its sellers
2055
+ wafermap statement --verify <file.json> check every line of a statement against the chain
1847
2056
  wafermap export-key [--solana] print the private key, or the Solana key for Phantom/Solflare
1848
2057
  wafermap keychain [on|off] keep the password for \`wafermap mcp\` in the OS keychain, not in plain text
1849
2058
 
@@ -1851,6 +2060,7 @@ Pay
1851
2060
  wafermap resources [words] Wafermap's pay-per-call catalog
1852
2061
  wafermap call <id> [field=value ...] run a resource and pay its price (--json '{...}' for complex input)
1853
2062
  wafermap call <id> ... --pay wafermap pay a Wafermap resource in $WAFERMAP at 50% off (the tokens are burned)
2063
+ wafermap call <llm> ... --metered pay only the tokens used, up to a ceiling you sign (USDC on base, arbitrum, polygon)
1854
2064
  wafermap call <llm> ... --stream print the answer as it arrives; you pay only once the model finishes
1855
2065
  wafermap approve-wafermap one-time approval so the wallet can pay in $WAFERMAP (Robinhood Chain, ETH gas)
1856
2066
  wafermap holder what this wallet's $WAFERMAP unlocks: holder price, Pro free calls left today
@@ -1873,7 +2083,7 @@ function parse(argv) {
1873
2083
  if (a.startsWith("--")) {
1874
2084
  const [k, v] = a.slice(2).split("=", 2);
1875
2085
  if (v !== void 0) out.flags[k] = v;
1876
- else if (argv[i + 1] && !argv[i + 1].startsWith("--") && !["yes", "post", "import", "stream"].includes(k)) out.flags[k] = argv[++i];
2086
+ else if (argv[i + 1] && !argv[i + 1].startsWith("--") && !["yes", "post", "import", "stream", "metered"].includes(k)) out.flags[k] = argv[++i];
1877
2087
  else out.flags[k] = true;
1878
2088
  } else out._.push(a);
1879
2089
  }
@@ -2080,6 +2290,60 @@ Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment
2080
2290
  } else console.log("Add --verify to check on-chain which signed payments were executed.");
2081
2291
  return;
2082
2292
  }
2293
+ case "statement": {
2294
+ const { fetchStatement: fetchStatement2, parseStatement: parseStatement2, verifyStatement: verifyStatement2 } = await Promise.resolve().then(() => (init_statement(), statement_exports));
2295
+ const { readFileSync: readFileSync2, writeFileSync: writeFileSync2 } = await import("node:fs");
2296
+ if (a.flags.verify !== void 0) {
2297
+ const file2 = typeof a.flags.verify === "string" ? a.flags.verify : a._[0];
2298
+ if (!file2) die("Which file? wafermap statement --verify wafermap-statement-2026-11.json");
2299
+ let text2;
2300
+ try {
2301
+ text2 = readFileSync2(file2, "utf8");
2302
+ } catch {
2303
+ die(`Cannot read ${file2}.`);
2304
+ }
2305
+ let st;
2306
+ try {
2307
+ st = parseStatement2(text2);
2308
+ } catch (e) {
2309
+ die(e.message);
2310
+ }
2311
+ console.error(`Checking ${st.lines.length} payments from ${st.month} on-chain\u2026`);
2312
+ const r = await verifyStatement2(st);
2313
+ st.lines.forEach((l, i) => {
2314
+ if (r.results[i] !== "ok") console.log(`${r.results[i].toUpperCase().padEnd(11)} ${l.date.slice(0, 16).replace("T", " ")} $${l.amountUsd.padEnd(8)} ${l.resource} ${l.explorer ?? l.tx}`);
2315
+ });
2316
+ console.log(`
2317
+ ${r.ok} of ${r.lines} payments confirmed on-chain, $${r.confirmedUsd} of $${r.linesUsd}.${r.totalsMatch ? " The totals match the lines." : " The totals block does NOT match the lines."}${st.truncated ? " This statement was cut at 10,000 lines." : ""}`);
2318
+ if (r.ok !== r.lines)
2319
+ console.log(`ok = the transaction moved exactly that amount from the payer to the recipient \xB7 mismatch = it did not \xB7 failed = the transaction reverted \xB7 not_found = no such transaction \xB7 error = the RPC did not answer, try again (set WAFERMAP_RPC_<NETWORK> to your own RPC) \xB7 unsupported = a network this CLI cannot read.`);
2320
+ if (r.mismatch || r.failed || r.notFound || !r.totalsMatch) process.exitCode = 2;
2321
+ return;
2322
+ }
2323
+ const evm = keystoreAddress() ?? die("No wallet yet. Run `wafermap init`, or check a downloaded file with --verify.");
2324
+ const wallets = [evm, ...keystoreSolana() ? [keystoreSolana()] : []];
2325
+ const month = typeof a.flags.month === "string" ? a.flags.month : (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
2326
+ if (!/^20\d\d-(0[1-9]|1[0-2])$/.test(month)) die("--month must be YYYY-MM, for example 2026-11.");
2327
+ const csv = a.flags.csv === true;
2328
+ const out = await fetchStatement2(wallets, month, { baseUrl: process.env.WAFERMAP_URL, csv, timeoutMs: Number(process.env.WAFERMAP_TIMEOUT_MS) || void 0 });
2329
+ const file = typeof a.flags.out === "string" ? a.flags.out : null;
2330
+ if (typeof out === "string") {
2331
+ if (file) writeFileSync2(file, out);
2332
+ else process.stdout.write(out);
2333
+ if (file) console.error(`Saved ${file}.`);
2334
+ return;
2335
+ }
2336
+ if (file) {
2337
+ writeFileSync2(file, JSON.stringify(out, null, 2) + "\n");
2338
+ console.error(`Saved ${file}. Check it any time with: wafermap statement --verify ${file}`);
2339
+ }
2340
+ if (!file || a.flags.print !== void 0) {
2341
+ for (const l of out.lines) console.log(`${l.date.slice(0, 16).replace("T", " ")} $${l.amountUsd.padEnd(8)} ${l.network.padEnd(14).slice(0, 14)} ${l.resource} ${l.explorer ?? l.tx}`);
2342
+ console.log(`
2343
+ ${month}: ${out.totals.calls} paid calls, $${out.totals.usd}.${out.lines.length ? " Save it with --out statement.json, then check it with --verify." : ""}`);
2344
+ }
2345
+ return;
2346
+ }
2083
2347
  case "resources": {
2084
2348
  const q = a._.join(" ").toLowerCase();
2085
2349
  const list = await listResources(process.env.WAFERMAP_URL, Number(process.env.WAFERMAP_TIMEOUT_MS) || void 0);
@@ -2103,9 +2367,11 @@ Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment
2103
2367
  const payIn = a.flags.pay === "wafermap" ? "wafermap" : a.flags.pay === void 0 || a.flags.pay === "usd" ? void 0 : die("--pay must be wafermap (or leave it out to pay in USDG/USDC)");
2104
2368
  let streamed = false;
2105
2369
  const onDelta = a.flags.stream === true ? (t) => (streamed = true, process.stdout.write(t)) : void 0;
2106
- const out = await wm.call(id, inputOf(a, 1), { network: net(a.flags.network), payIn, onDelta });
2370
+ const metered = a.flags.metered === true;
2371
+ const out = await wm.call(id, inputOf(a, 1), { network: net(a.flags.network), payIn, onDelta, metered });
2107
2372
  if (streamed) process.stdout.write("\n");
2108
2373
  if (out.receipt?.asset === "WAFERMAP") console.error(`\u2713 Paid ${out.receipt.amountTokens} $WAFERMAP (\u2248 $${out.receipt.amountUsd}), burned \xB7 ${EXPLORER(out.receipt.network)}/tx/${out.receipt.tx}`);
2374
+ else if (out.receipt?.metered) console.error(`\u2713 Paid $${out.receipt.amountUsd} ${out.receipt.asset} for ${out.receipt.metered.inputTokens} in + ${out.receipt.metered.outputTokens} out tokens${out.receipt.metered.estimated ? " (estimated)" : ""}, signed up to $${out.receipt.metered.ceilingUsd} \xB7 ${EXPLORER(out.receipt.network)}/tx/${out.receipt.tx}`);
2109
2375
  else if (out.receipt) console.error(`\u2713 Paid $${out.receipt.amountUsd} ${out.receipt.asset} \xB7 ${EXPLORER(out.receipt.network)}/tx/${out.receipt.tx}`);
2110
2376
  else if (out.free) console.error(`\u2713 Free call ($WAFERMAP Pro) \xB7 ${out.free.left} of ${out.free.perDay} left today. Nothing was paid.`);
2111
2377
  if (typeof a.flags.out === "string") await saveMedia(out.result, a.flags.out);
package/dist/index.js CHANGED
@@ -364,6 +364,65 @@ var PAY_CHAINS = [
364
364
  ];
365
365
  var EVM_CHAINS = PAY_CHAINS.filter((c) => c.vm === "evm");
366
366
  var CHAIN_KEYS = PAY_CHAINS.map((c) => c.key);
367
+ var UPTO_PROXY = "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002";
368
+ var MAX_METERED_SECONDS = 300;
369
+ var UPTO_TYPES = {
370
+ PermitWitnessTransferFrom: [
371
+ { name: "permitted", type: "TokenPermissions" },
372
+ { name: "spender", type: "address" },
373
+ { name: "nonce", type: "uint256" },
374
+ { name: "deadline", type: "uint256" },
375
+ { name: "witness", type: "Witness" }
376
+ ],
377
+ TokenPermissions: [
378
+ { name: "token", type: "address" },
379
+ { name: "amount", type: "uint256" }
380
+ ],
381
+ Witness: [
382
+ { name: "to", type: "address" },
383
+ { name: "facilitator", type: "address" },
384
+ { name: "validAfter", type: "uint256" }
385
+ ]
386
+ };
387
+ var EIP2612_TYPES = {
388
+ Permit: [
389
+ { name: "owner", type: "address" },
390
+ { name: "spender", type: "address" },
391
+ { name: "value", type: "uint256" },
392
+ { name: "nonce", type: "uint256" },
393
+ { name: "deadline", type: "uint256" }
394
+ ]
395
+ };
396
+ async function signUptoPayment(signer, req, resource, opts = {}) {
397
+ const fac = String(req.raw?.extra?.facilitatorAddress ?? "");
398
+ if (!/^0x[0-9a-fA-F]{40}$/.test(fac)) throw new X402Error("bad_quote", "The metered quote names no facilitator. Nothing was signed.");
399
+ const nonce = BigInt(randomNonce()) >> 8n;
400
+ const deadline = BigInt(Math.floor(Date.now() / 1e3) + Math.min(req.maxTimeoutSeconds, MAX_METERED_SECONDS));
401
+ const max = BigInt(req.amount);
402
+ const witness = { to: req.payTo, facilitator: fac, validAfter: 0n };
403
+ let signature;
404
+ try {
405
+ signature = await signer.signTypedData({ domain: { name: "Permit2", chainId: req.chain.id, verifyingContract: PERMIT2 }, types: UPTO_TYPES, primaryType: "PermitWitnessTransferFrom", message: { permitted: { token: req.asset, amount: max }, spender: UPTO_PROXY, nonce, deadline, witness } });
406
+ } catch (e) {
407
+ throw new X402Error("sign_rejected", e?.shortMessage ?? e?.message ?? "Signature rejected.");
408
+ }
409
+ let extensions;
410
+ if (opts.gasless && opts.reader) {
411
+ const allowance = await opts.reader.allowance(req.chain, signer.address, req.asset).catch(() => 0n);
412
+ if (allowance < max) {
413
+ const pn = await opts.reader.permitNonce(req.chain, signer.address, req.asset);
414
+ let ps;
415
+ try {
416
+ ps = await signer.signTypedData({ domain: { name: req.extra.name, version: req.extra.version, chainId: req.chain.id, verifyingContract: req.asset }, types: EIP2612_TYPES, primaryType: "Permit", message: { owner: signer.address, spender: PERMIT2, value: max, nonce: pn, deadline } });
417
+ } catch (e) {
418
+ throw new X402Error("sign_rejected", e?.shortMessage ?? e?.message ?? "Signature rejected.");
419
+ }
420
+ extensions = { eip2612GasSponsoring: { info: { from: signer.address, asset: req.asset, spender: PERMIT2, amount: max.toString(), nonce: pn.toString(), deadline: deadline.toString(), signature: ps, version: "1" } } };
421
+ }
422
+ }
423
+ const permit2Authorization = { from: signer.address, permitted: { token: req.asset, amount: req.amount }, spender: UPTO_PROXY, nonce: nonce.toString(), deadline: deadline.toString(), witness: { to: req.payTo, facilitator: fac, validAfter: "0" } };
424
+ return { name: "PAYMENT-SIGNATURE", value: b64encode(JSON.stringify({ x402Version: 2, resource, accepted: req.raw, payload: { signature, permit2Authorization }, ...extensions ? { extensions } : {} })), deadline: Number(deadline) };
425
+ }
367
426
  var WAFERMAP_TOKEN = "0xfda080f64d84c989cfc50ff18253542cfe502db3";
368
427
  var PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
369
428
  var DEAD = "0x000000000000000000000000000000000000dEaD";
@@ -443,12 +502,19 @@ async function readQuote(res) {
443
502
  const accepts = [];
444
503
  const offered = [];
445
504
  let token;
505
+ const metered = [];
446
506
  for (const r of body.accepts) {
447
507
  offered.push(`${r.scheme ?? "?"}/${r.network ?? "?"}`);
448
508
  if (r?.scheme === TOKEN_SCHEME) {
449
509
  token ??= tokenOption(r);
450
510
  continue;
451
511
  }
512
+ if (r?.scheme === "upto" && version === 2) {
513
+ const ch = chainOf(r.network);
514
+ if (ch && ch.vm === "evm" && String(r.asset ?? "").toLowerCase() === ch.stable.toLowerCase() && /^\d+$/.test(String(r.amount)) && /^0x[0-9a-fA-F]{40}$/.test(String(r.extra?.facilitatorAddress ?? "")) && /^0x[0-9a-fA-F]{40}$/.test(String(r.payTo ?? "")))
515
+ metered.push({ version, chain: ch, amount: String(r.amount), usd: Number(r.amount) / 1e6, payTo: r.payTo, asset: r.asset, maxTimeoutSeconds: Number(r.maxTimeoutSeconds) || 120, extra: r.extra?.name && r.extra?.version ? { name: String(r.extra.name), version: String(r.extra.version) } : ch.domain, raw: r });
516
+ continue;
517
+ }
452
518
  const chain = chainOf(r.network);
453
519
  if (!chain || r.scheme !== "exact") continue;
454
520
  if (chain.vm === "evm" ? String(r.asset ?? "").toLowerCase() !== chain.stable.toLowerCase() : r.asset !== chain.stable) continue;
@@ -462,7 +528,10 @@ async function readQuote(res) {
462
528
  accepts.push({ version, chain, amount, usd: Number(amount) / 1e6, payTo: r.payTo, asset: r.asset, maxTimeoutSeconds: Number(r.maxTimeoutSeconds) || 120, extra, raw: r });
463
529
  }
464
530
  const inputError = body.inputError?.message ? { code: String(body.inputError.code ?? "invalid_input"), message: String(body.inputError.message) } : void 0;
465
- return { version, resource: body.resource ?? null, accepts, offered, inputError, ...token ? { token } : {} };
531
+ const m = body.metered;
532
+ const meterInfo = m && Number.isFinite(Number(m.ceilingUsd)) ? { inputPer1kTokensUsd: Number(m.inputPer1kTokensUsd), outputPer1kTokensUsd: Number(m.outputPer1kTokensUsd), ceilingUsd: Number(m.ceilingUsd), minimumUsd: Number(m.minimumUsd) } : void 0;
533
+ const gasless = !!body.extensions?.eip2612GasSponsoring;
534
+ return { version, resource: body.resource ?? null, accepts, offered, inputError, ...token ? { token } : {}, ...metered.length ? { metered: metered.map((q) => ({ ...q, gasless })) } : {}, ...meterInfo ? { meterInfo } : {} };
466
535
  }
467
536
  function choose(q, prefer = CHAIN_KEYS) {
468
537
  for (const k of prefer) {
@@ -546,6 +615,20 @@ async function x402fetch(url, init, opts) {
546
615
  const res2 = await f(url, { ...init, headers: headers2 });
547
616
  return { res: res2, paid: { ...signed2, tx: readSettlement(res2)?.tx ?? null } };
548
617
  }
618
+ if (opts.meter) {
619
+ const pref = (opts.prefer ?? CHAIN_KEYS).filter((k) => k !== "solana");
620
+ const req2 = pref.map((k) => quote.metered?.find((q) => q.chain.key === k)).find(Boolean);
621
+ if (!req2) throw new X402Error("metered_not_offered", quote.metered?.length ? `Metered calls are offered on ${quote.metered.map((q) => q.chain.name).join(", ")}; pick one of those networks.` : "This call is not offered as a metered (pay per token) call. Pay the flat price instead.");
622
+ if (opts.approve) await opts.approve(req2);
623
+ const signer2 = typeof opts.signer === "function" ? await opts.signer() : opts.signer;
624
+ const header2 = await signUptoPayment(signer2, req2, quote.resource, { reader: opts.reader, gasless: !!req2.gasless });
625
+ const signed2 = { req: req2, from: signer2.address, metered: { ceiling: req2.amount, deadline: header2.deadline } };
626
+ opts.onSigned?.(signed2);
627
+ const headers2 = new Headers(init.headers);
628
+ headers2.set(header2.name, header2.value);
629
+ const res2 = await f(url, { ...init, headers: headers2 });
630
+ return { res: res2, paid: { ...signed2, tx: readSettlement(res2)?.tx ?? null } };
631
+ }
549
632
  const req = choose(quote, (opts.prefer ?? CHAIN_KEYS).filter((k) => k !== "solana" || opts.solana));
550
633
  if (opts.approve) await opts.approve(req);
551
634
  const svm = req.chain.vm === "svm";
@@ -797,7 +880,15 @@ var Wafermap = class {
797
880
  this.queue = prev.then(() => held);
798
881
  return prev.then(() => release);
799
882
  }
800
- async paidFetch(kind, what, url, init, network, payIn) {
883
+ /** Chain reads for a metered payment's EIP-2612 permit: Permit2 allowance and the token's permit nonce. */
884
+ reader() {
885
+ const nonces = [{ type: "function", name: "nonces", stateMutability: "view", inputs: [{ name: "owner", type: "address" }], outputs: [{ type: "uint256" }] }];
886
+ return {
887
+ allowance: (chain, owner, token) => this.client(chain.key).readContract({ address: token, abi: erc20Abi2, functionName: "allowance", args: [owner, PERMIT2] }),
888
+ permitNonce: (chain, owner, token) => this.client(chain.key).readContract({ address: token, abi: nonces, functionName: "nonces", args: [owner] })
889
+ };
890
+ }
891
+ async paidFetch(kind, what, url, init, network, payIn, meter) {
801
892
  let id = null;
802
893
  let release = null;
803
894
  const unlock = () => {
@@ -811,10 +902,12 @@ var Wafermap = class {
811
902
  prefer: this.prefer(network),
812
903
  fetch: this.fetch,
813
904
  payIn,
905
+ meter,
906
+ reader: meter ? this.reader() : void 0,
814
907
  tokenPriceUsd: this.tokenPriceUsd,
815
908
  approve: async (req) => {
816
909
  release = await this.acquire();
817
- await this.gate({ kind, what, usd: req.usd, network: req.chain.key, detail: req.asset === WAFERMAP_TOKEN ? "in $WAFERMAP at 50% off, burned" : `${req.chain.symbol} to ${req.payTo}` });
910
+ await this.gate({ kind, what, usd: req.usd, network: req.chain.key, detail: req.asset === WAFERMAP_TOKEN ? "in $WAFERMAP at 50% off, burned" : meter ? `at most ${req.chain.symbol} ${req.usd}, charged for the tokens used, to ${req.payTo}` : `${req.chain.symbol} to ${req.payTo}` });
818
911
  },
819
912
  onSigned: (s) => {
820
913
  id = this.booked(kind, what, s);
@@ -902,11 +995,12 @@ var Wafermap = class {
902
995
  }
903
996
  async call(id, input = {}, o = {}) {
904
997
  const inToken = o.payIn === "wafermap";
998
+ if (o.metered && inToken) throw new WafermapError("bad_options", "A metered call is paid in USDC; it cannot also be paid in $WAFERMAP.");
905
999
  const stream = !!o.onDelta && !inToken;
906
1000
  if (inToken && id.startsWith("m/")) throw new WafermapError("token_not_offered", "Marketplace listings are paid in USDG or USDC to their seller, not in $WAFERMAP.");
907
1001
  if (inToken && !await this.wafermapApproved().catch(() => true)) throw new WafermapError("permit2_not_approved", "Paying in $WAFERMAP needs a one-time approval of Permit2. Run `wafermap approve-wafermap` (one transaction on Robinhood Chain, paid in ETH), then call again.");
908
1002
  const pass = id.startsWith("m/") ? null : await this.holderPass();
909
- const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address, ...stream ? { accept: "text/event-stream" } : {}, ...pass ? { [PASS_HEADER]: pass } : {} }, body: JSON.stringify(input) }, o.network, inToken ? "wafermap" : void 0);
1003
+ const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address, ...stream ? { accept: "text/event-stream" } : {}, ...pass ? { [PASS_HEADER]: pass } : {} }, body: JSON.stringify(input) }, o.network, inToken ? "wafermap" : void 0, o.metered);
910
1004
  const settle = (r2, out2) => {
911
1005
  if (!r2.paid || !r2.id) return;
912
1006
  const status = r2.res.status === 402 ? "refused" : r2.res.ok && !out2?.streamError ? "ok" : "failed";
@@ -1026,6 +1120,103 @@ var Wafermap = class {
1026
1120
  }
1027
1121
  }
1028
1122
  };
1123
+
1124
+ // src/statement.ts
1125
+ import { createPublicClient as createPublicClient2, getAddress as getAddress3, http as http2, isAddress as isAddress3, parseAbiItem as parseAbiItem2, toEventSelector as toEventSelector2 } from "viem";
1126
+ import { arbitrum as arbitrum2, arc as arc2, base as base2, polygon as polygon2, robinhood as robinhood2, soneium as soneium2 } from "viem/chains";
1127
+ var EVM = { "eip155:4663": robinhood2, "eip155:8453": base2, "eip155:42161": arbitrum2, "eip155:137": polygon2, "eip155:5042": arc2, "eip155:1868": soneium2 };
1128
+ var ENV_RPC = { "eip155:4663": "WAFERMAP_RPC_ROBINHOOD", "eip155:8453": "WAFERMAP_RPC_BASE", "eip155:42161": "WAFERMAP_RPC_ARBITRUM", "eip155:137": "WAFERMAP_RPC_POLYGON", "eip155:5042": "WAFERMAP_RPC_ARC", "eip155:1868": "WAFERMAP_RPC_SONEIUM", solana: "WAFERMAP_RPC_SOLANA" };
1129
+ var TRANSFER = toEventSelector2(parseAbiItem2("event Transfer(address indexed from, address indexed to, uint256 value)"));
1130
+ var topicAddr = (t) => t ? getAddress3(`0x${t.slice(-40)}`) : null;
1131
+ async function fetchStatement(wallets, month, opts = {}) {
1132
+ const url = `${(opts.baseUrl ?? "https://wafermap.xyz").replace(/\/$/, "")}/api/statement?wallet=${encodeURIComponent(wallets.join(","))}&month=${encodeURIComponent(month)}${opts.csv ? "&format=csv" : ""}`;
1133
+ const r = await fetch(url, { signal: AbortSignal.timeout(opts.timeoutMs ?? 6e4) });
1134
+ if (!r.ok) {
1135
+ const j = await r.json().catch(() => null);
1136
+ throw new Error(j?.error?.message ?? `Statement request failed (${r.status}).`);
1137
+ }
1138
+ return opts.csv ? r.text() : await r.json();
1139
+ }
1140
+ function parseStatement(text) {
1141
+ let s;
1142
+ try {
1143
+ s = JSON.parse(text);
1144
+ } catch {
1145
+ throw new Error("That file is not JSON. Download the JSON statement (not the CSV) to verify it.");
1146
+ }
1147
+ if (s?.kind !== "wafermap-statement" || !Array.isArray(s.lines)) throw new Error("That file is not a Wafermap statement.");
1148
+ return s;
1149
+ }
1150
+ function defaultClients(env = process.env) {
1151
+ const cache = /* @__PURE__ */ new Map();
1152
+ return {
1153
+ evm: (network) => {
1154
+ const chain = EVM[network];
1155
+ if (!chain) return null;
1156
+ if (!cache.has(network)) cache.set(network, createPublicClient2({ chain, transport: http2(env[ENV_RPC[network]] || void 0, { timeout: 3e4 }) }));
1157
+ return cache.get(network);
1158
+ },
1159
+ solana: (method, params) => solanaRpc(env.WAFERMAP_RPC_SOLANA || SOLANA_PUBLIC_RPC, method, params)
1160
+ };
1161
+ }
1162
+ async function verifyLine(l, clients) {
1163
+ try {
1164
+ if (!/^\d+$/.test(l.amountUnits)) return "mismatch";
1165
+ const want = BigInt(l.amountUnits);
1166
+ if (l.network.startsWith("solana:")) {
1167
+ const tx = await clients.solana("getTransaction", [l.tx, { encoding: "jsonParsed", maxSupportedTransactionVersion: 0, commitment: "finalized" }]);
1168
+ if (!tx) return "not_found";
1169
+ if (tx.meta?.err) return "failed";
1170
+ const delta = (owner) => {
1171
+ const sum = (xs) => (xs ?? []).filter((b) => b.owner === owner && b.mint === l.token).reduce((a, b) => a + BigInt(b.uiTokenAmount?.amount ?? "0"), 0n);
1172
+ return sum(tx.meta?.postTokenBalances) - sum(tx.meta?.preTokenBalances);
1173
+ };
1174
+ return delta(l.from) === -want && delta(l.to) === want ? "ok" : "mismatch";
1175
+ }
1176
+ const client = clients.evm(l.network);
1177
+ if (!client) return "unsupported";
1178
+ if (!/^0x[0-9a-fA-F]{64}$/.test(l.tx) || !isAddress3(l.token) || !isAddress3(l.from) || !isAddress3(l.to)) return "mismatch";
1179
+ const receipt = await client.getTransactionReceipt({ hash: l.tx }).catch((e) => /not.*found|could not be found/i.test(String(e?.message)) ? null : Promise.reject(e));
1180
+ if (!receipt) return "not_found";
1181
+ if (receipt.status !== "success") return "failed";
1182
+ const hit = receipt.logs.some(
1183
+ (g) => g.address.toLowerCase() === l.token.toLowerCase() && g.topics[0] === TRANSFER && topicAddr(g.topics[1]) === getAddress3(l.from) && topicAddr(g.topics[2]) === getAddress3(l.to) && BigInt(g.data) === want
1184
+ );
1185
+ return hit ? "ok" : "mismatch";
1186
+ } catch {
1187
+ return "error";
1188
+ }
1189
+ }
1190
+ async function verifyStatement(s, clients = defaultClients(), onLine) {
1191
+ const results = new Array(s.lines.length);
1192
+ let next = 0;
1193
+ await Promise.all(
1194
+ Array.from({ length: 4 }, async () => {
1195
+ while (next < s.lines.length) {
1196
+ const i = next++;
1197
+ results[i] = await verifyLine(s.lines[i], clients);
1198
+ onLine?.(i, results[i]);
1199
+ }
1200
+ })
1201
+ );
1202
+ const usd = (pick) => Math.round(s.lines.reduce((a, l, i) => a + (pick(results[i]) ? Number(l.amountUsd) : 0), 0) * 1e6) / 1e6;
1203
+ const count = (r) => results.filter((x) => x === r).length;
1204
+ const linesUsd = usd(() => true);
1205
+ return {
1206
+ results,
1207
+ lines: s.lines.length,
1208
+ ok: count("ok"),
1209
+ mismatch: count("mismatch"),
1210
+ failed: count("failed"),
1211
+ notFound: count("not_found"),
1212
+ unsupported: count("unsupported"),
1213
+ error: count("error"),
1214
+ confirmedUsd: usd((r) => r === "ok"),
1215
+ linesUsd,
1216
+ // The totals block must add up to the lines it sits next to.
1217
+ totalsMatch: s.totals?.calls === s.lines.length && Math.abs((s.totals?.usd ?? NaN) - linesUsd) < 1e-6
1218
+ };
1219
+ }
1029
1220
  export {
1030
1221
  DEFAULT_LIMITS,
1031
1222
  DEFAULT_TIMEOUT_MS,
@@ -1048,15 +1239,19 @@ export {
1048
1239
  decodeSwap,
1049
1240
  encodePass,
1050
1241
  executeSwap,
1242
+ fetchStatement,
1051
1243
  holderInfo,
1052
1244
  listResources,
1053
1245
  makePass,
1054
1246
  mergeLog,
1247
+ parseStatement,
1055
1248
  passMessage,
1056
1249
  quoteSwap,
1057
1250
  readQuote,
1058
1251
  signPayment,
1059
1252
  signTokenPayment,
1253
+ verifyLine,
1254
+ verifyStatement,
1060
1255
  wafermapPriceUsd,
1061
1256
  x402fetch
1062
1257
  };
@@ -46,6 +46,8 @@ export type Requirement = {
46
46
  };
47
47
  /** The requirement exactly as the server sent it (echoed back in the payment). */
48
48
  raw: any;
49
+ /** Metered quotes only: the server announced eip2612GasSponsoring, so a missing Permit2 approval can be signed. */
50
+ gasless?: boolean;
49
51
  };
50
52
  export type Quote = {
51
53
  version: 1 | 2;
@@ -57,7 +59,33 @@ export type Quote = {
57
59
  message: string;
58
60
  };
59
61
  token?: TokenRequirement;
62
+ metered?: Requirement[];
63
+ meterInfo?: MeterInfo;
60
64
  };
65
+ export declare const UPTO_PROXY = "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002";
66
+ /** The longest a metered authorization stays valid: long answers settle after the model finishes. */
67
+ export declare const MAX_METERED_SECONDS = 300;
68
+ export type MeterInfo = {
69
+ inputPer1kTokensUsd: number;
70
+ outputPer1kTokensUsd: number;
71
+ ceilingUsd: number;
72
+ minimumUsd: number;
73
+ };
74
+ /** Reads the client needs for an EIP-2612 permit: the owner's Permit2 allowance and its permit nonce on the token. */
75
+ export type ChainReader = {
76
+ allowance: (chain: Chain, owner: Hex, token: Hex) => Promise<bigint>;
77
+ permitNonce: (chain: Chain, owner: Hex, token: Hex) => Promise<bigint>;
78
+ };
79
+ /** Signs a metered payment for at most `req.amount` (the ceiling). Adds an EIP-2612 permit when the Permit2
80
+ * allowance is short and the server announced eip2612GasSponsoring. */
81
+ export declare function signUptoPayment(signer: Signer, req: Requirement, resource: any, opts?: {
82
+ reader?: ChainReader;
83
+ gasless?: boolean;
84
+ }): Promise<{
85
+ name: string;
86
+ value: string;
87
+ deadline: number;
88
+ }>;
61
89
  export declare const WAFERMAP_TOKEN = "0xfda080f64d84c989cfc50ff18253542cfe502db3";
62
90
  export declare const PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
63
91
  export declare const DEAD = "0x000000000000000000000000000000000000dEaD";
@@ -123,6 +151,10 @@ export type SignedSol = {
123
151
  export type Signed = {
124
152
  req: Requirement;
125
153
  from: string;
154
+ metered?: {
155
+ ceiling: string;
156
+ deadline: number;
157
+ };
126
158
  auth?: SignedAuth;
127
159
  sol?: SignedSol;
128
160
  token?: {
@@ -151,4 +183,6 @@ export declare function x402fetch(url: string, init: RequestInit, opts: {
151
183
  fetch?: typeof fetch;
152
184
  payIn?: "stablecoin" | "wafermap";
153
185
  tokenPriceUsd?: () => Promise<number>;
186
+ meter?: boolean;
187
+ reader?: ChainReader;
154
188
  }): Promise<PaidCall>;
@@ -142,6 +142,8 @@ export declare class Wafermap {
142
142
  private body;
143
143
  private queue;
144
144
  private acquire;
145
+ /** Chain reads for a metered payment's EIP-2612 permit: Permit2 allowance and the token's permit nonce. */
146
+ private reader;
145
147
  private paidFetch;
146
148
  resources(): Promise<any[]>;
147
149
  private passCache;
@@ -161,6 +163,7 @@ export declare class Wafermap {
161
163
  network?: Net;
162
164
  payIn?: "stablecoin" | "wafermap";
163
165
  onDelta?: (text: string) => void;
166
+ metered?: boolean;
164
167
  }): Promise<{
165
168
  resource: string;
166
169
  result: any;
@@ -173,6 +176,13 @@ export declare class Wafermap {
173
176
  payer: string;
174
177
  tx: string;
175
178
  ms: number;
179
+ metered?: {
180
+ inputTokens: number;
181
+ outputTokens: number;
182
+ estimated: boolean;
183
+ ceilingUsd: number;
184
+ chargedUsd: number;
185
+ };
176
186
  } | null;
177
187
  free?: {
178
188
  wallet: string;
@@ -6,3 +6,5 @@ export { quoteSwap, executeSwap, decodeSwap, KYBER_ROUTER, SwapError } from "../
6
6
  export type { Requirement, Quote, Signed, SignedAuth, SignedSol, Signer } from "../shared/x402client";
7
7
  export { checkSolanaPayment, SOLANA_PUBLIC_RPC } from "../shared/solana";
8
8
  export type { SolanaCheck } from "../shared/solana";
9
+ export { fetchStatement, parseStatement, verifyStatement, verifyLine } from "./statement.js";
10
+ export type { Statement, StatementLine, LineCheck } from "./statement.js";
@@ -0,0 +1,58 @@
1
+ import { type PublicClient } from "viem";
2
+ export type StatementLine = {
3
+ date: string;
4
+ resource: string;
5
+ network: string;
6
+ asset: string;
7
+ amountUsd: string;
8
+ token: string;
9
+ amountUnits: string;
10
+ from: string;
11
+ to: string;
12
+ tx: string;
13
+ explorer?: string | null;
14
+ };
15
+ export type Statement = {
16
+ kind: "wafermap-statement";
17
+ version: number;
18
+ month: string;
19
+ wallets: string[];
20
+ totals: {
21
+ calls: number;
22
+ usd: number;
23
+ };
24
+ lines: StatementLine[];
25
+ truncated?: boolean;
26
+ };
27
+ export type LineCheck = "ok" | "mismatch" | "failed" | "not_found" | "unsupported" | "error";
28
+ /** Downloads a statement. `csv` returns the CSV text, otherwise the parsed JSON. */
29
+ export declare function fetchStatement(wallets: string[], month: string, opts?: {
30
+ baseUrl?: string;
31
+ csv?: boolean;
32
+ timeoutMs?: number;
33
+ }): Promise<Statement | string>;
34
+ /** Reads and sanity-checks a statement file's content. Throws on anything that is not a statement. */
35
+ export declare function parseStatement(text: string): Statement;
36
+ type Clients = {
37
+ evm: (network: string) => Pick<PublicClient, "getTransactionReceipt"> | null;
38
+ solana: (method: string, params: unknown[]) => Promise<any>;
39
+ };
40
+ /** Clients from env RPCs (WAFERMAP_RPC_<NET>) or the chains' public RPCs. */
41
+ export declare function defaultClients(env?: Record<string, string | undefined>): Clients;
42
+ /** Checks one line against its chain. */
43
+ export declare function verifyLine(l: StatementLine, clients: Clients): Promise<LineCheck>;
44
+ /** Checks every line, four at a time (public RPCs refuse bursts), and adds up what the chain confirms. */
45
+ export declare function verifyStatement(s: Statement, clients?: Clients, onLine?: (i: number, r: LineCheck) => void): Promise<{
46
+ results: LineCheck[];
47
+ lines: number;
48
+ ok: number;
49
+ mismatch: number;
50
+ failed: number;
51
+ notFound: number;
52
+ unsupported: number;
53
+ error: number;
54
+ confirmedUsd: number;
55
+ linesUsd: number;
56
+ totalsMatch: boolean;
57
+ }>;
58
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wafermapxyz/cli",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
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, Arc, Soneium), and swaps on Robinhood Chain and Base. CLI, SDK and MCP server.",
5
5
  "type": "module",
6
6
  "bin": {