@wafermapxyz/cli 0.4.11 → 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,15 @@
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
+
3
13
  ## 0.4.11 (15 Nov 2026)
4
14
 
5
15
  **`wafermap statement`: a monthly statement you can check against the chains.**
package/README.md CHANGED
@@ -86,6 +86,8 @@ Pass `approve: (a) => boolean` to confirm each payment or swap yourself, and `le
86
86
 
87
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`.
88
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
+
89
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.
90
92
 
91
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,7 @@ var VERSION;
1288
1382
  var init_version = __esm({
1289
1383
  "src/version.ts"() {
1290
1384
  "use strict";
1291
- VERSION = "0.4.11";
1385
+ VERSION = "0.4.12";
1292
1386
  }
1293
1387
  });
1294
1388
 
@@ -1504,12 +1598,13 @@ async function runMcp() {
1504
1598
  resource: z.string().describe("Resource id, e.g. market-brief, prediction-markets, stock-signal, flux-2-dev."),
1505
1599
  input: z.record(z.string(), z.any()).optional().describe("Input fields for the resource."),
1506
1600
  network: NET,
1507
- 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.")
1508
1603
  }
1509
1604
  },
1510
- async ({ resource, input, network, payIn }) => {
1605
+ async ({ resource, input, network, payIn, metered }) => {
1511
1606
  try {
1512
- return result(await wm.call(resource, input ?? {}, { network, payIn }));
1607
+ return result(await wm.call(resource, input ?? {}, { network, payIn, metered }));
1513
1608
  } catch (e) {
1514
1609
  return failure(e);
1515
1610
  }
@@ -1965,6 +2060,7 @@ Pay
1965
2060
  wafermap resources [words] Wafermap's pay-per-call catalog
1966
2061
  wafermap call <id> [field=value ...] run a resource and pay its price (--json '{...}' for complex input)
1967
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)
1968
2064
  wafermap call <llm> ... --stream print the answer as it arrives; you pay only once the model finishes
1969
2065
  wafermap approve-wafermap one-time approval so the wallet can pay in $WAFERMAP (Robinhood Chain, ETH gas)
1970
2066
  wafermap holder what this wallet's $WAFERMAP unlocks: holder price, Pro free calls left today
@@ -1987,7 +2083,7 @@ function parse(argv) {
1987
2083
  if (a.startsWith("--")) {
1988
2084
  const [k, v] = a.slice(2).split("=", 2);
1989
2085
  if (v !== void 0) out.flags[k] = v;
1990
- 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];
1991
2087
  else out.flags[k] = true;
1992
2088
  } else out._.push(a);
1993
2089
  }
@@ -2271,9 +2367,11 @@ ${month}: ${out.totals.calls} paid calls, $${out.totals.usd}.${out.lines.length
2271
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)");
2272
2368
  let streamed = false;
2273
2369
  const onDelta = a.flags.stream === true ? (t) => (streamed = true, process.stdout.write(t)) : void 0;
2274
- 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 });
2275
2372
  if (streamed) process.stdout.write("\n");
2276
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}`);
2277
2375
  else if (out.receipt) console.error(`\u2713 Paid $${out.receipt.amountUsd} ${out.receipt.asset} \xB7 ${EXPLORER(out.receipt.network)}/tx/${out.receipt.tx}`);
2278
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.`);
2279
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";
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wafermapxyz/cli",
3
- "version": "0.4.11",
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": {