@wafermapxyz/cli 0.4.4 → 0.4.5

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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.5 (2 Nov 2026)
4
+
5
+ **Pay Wafermap in $WAFERMAP, at 50% off. The tokens are burned.**
6
+
7
+ - `wafermap call <id> --pay wafermap` (MCP `wafermap_call` with `payIn: "wafermap"`, SDK `call(id, input, { payIn: "wafermap" })`) pays one of Wafermap's own resources in $WAFERMAP instead of USDG/USDC. $WAFERMAP has no EIP-3009, so the payment is a Uniswap Permit2 signature for one exact amount, spendable for two minutes only by Wafermap's relayer, which sends it to the dead address once the call succeeded. A failed call moves nothing.
8
+ - `wafermap approve-wafermap`: the one-time Permit2 approval (one Robinhood Chain transaction, paid in ETH).
9
+ - What the client checks before it signs, trusting the server for none of it: the spender is Wafermap's relayer as fixed in this release (`WAFERMAP_RELAYERS`); the option names the canonical Permit2, the $WAFERMAP token and the dead address; the amount is valued at a price this client reads itself (GeckoTerminal) and refused when it is worth more than the quote states. The caps count the higher of the two values.
10
+ - Only `call()` to Wafermap ever signs a $WAFERMAP permit. `pay()` to third-party services never does, whatever they offer, because a Permit2 spender chooses where the tokens go.
11
+
3
12
  ## 0.4.4 (2 Nov 2026)
4
13
 
5
14
  **$WAFERMAP holder tiers.**
package/README.md CHANGED
@@ -84,6 +84,8 @@ Pass `approve: (a) => boolean` to confirm each payment or swap yourself, and `le
84
84
 
85
85
  A wallet that holds at least 100,000 $WAFERMAP on Robinhood Chain pays 50% less for Wafermap's own resources when it pays on an EVM network (Robinhood Chain, Base, Arbitrum, Polygon). Nothing to set up: `wafermap call` names the wallet to Wafermap and the quote comes back discounted; the server re-checks the balance of the wallet that signs. Marketplace listings and Solana payments keep the regular price. Check a wallet: https://wafermap.xyz/api/holder?address=0x…, or run `wafermap holder`.
86
86
 
87
+ 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.
88
+
87
89
  With 1,000,000 $WAFERMAP the wallet is **Pro**: 10 free calls a day (UTC) to resources listed at $0.01 or less. `wafermap call` sends a holder pass (a signed message, no transaction, valid 24 hours) to Wafermap only; when a call is free, nothing is signed for payment and it prints `✓ Free call`. Holders of 100,000 also unlock x402 Tracker Pro: `wafermap pass` prints a pass for its API. Details: https://wafermap.xyz/docs#holders
88
90
 
89
91
  ## Where the limits live
package/dist/cli.js CHANGED
@@ -296,6 +296,34 @@ var init_solana = __esm({
296
296
  });
297
297
 
298
298
  // shared/x402client.ts
299
+ async function wafermapPriceUsd(f = fetch) {
300
+ 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) });
301
+ const j = await res.json().catch(() => null);
302
+ const p = Number(j?.data?.attributes?.token_prices?.[WAFERMAP_TOKEN]);
303
+ if (!res.ok || !(p > 0)) throw new X402Error("price_unavailable", "The $WAFERMAP market price could not be read, so the amount cannot be checked. Nothing was signed; pay in USDG or USDC.");
304
+ return p;
305
+ }
306
+ function tokenOption(r) {
307
+ const e = r?.extra ?? {};
308
+ const same = (a, b) => String(a ?? "").toLowerCase() === b.toLowerCase();
309
+ if (r?.scheme !== TOKEN_SCHEME || r.network !== "eip155:4663" || !same(r.asset, WAFERMAP_TOKEN) || !same(r.payTo, DEAD) || !same(e.permit2, PERMIT2)) return;
310
+ if (!/^\d{1,40}$/.test(String(r.amount)) || !/^0x[0-9a-fA-F]{40}$/.test(String(e.spender)) || !(Number(e.usd) > 0)) return;
311
+ if (!WAFERMAP_RELAYERS.some((a) => same(e.spender, a))) return;
312
+ return { amount: String(r.amount), usd: Number(e.usd), spender: e.spender, maxTimeoutSeconds: Number(r.maxTimeoutSeconds) || 120, raw: r };
313
+ }
314
+ async function signTokenPayment(signer, t, resource) {
315
+ const nonce = BigInt(randomNonce()) >> 8n;
316
+ const deadline = BigInt(Math.floor(Date.now() / 1e3) + Math.min(t.maxTimeoutSeconds, 120));
317
+ const permitted = { token: WAFERMAP_TOKEN, amount: BigInt(t.amount) };
318
+ let signature;
319
+ try {
320
+ signature = await signer.signTypedData({ domain: { name: "Permit2", chainId: 4663, verifyingContract: PERMIT2 }, types: PERMIT_TYPES, primaryType: "PermitTransferFrom", message: { permitted, spender: t.spender, nonce, deadline } });
321
+ } catch (e) {
322
+ throw new X402Error("sign_rejected", e?.shortMessage ?? e?.message ?? "Signature rejected.");
323
+ }
324
+ const payload = { owner: signer.address, permit: { permitted: { token: WAFERMAP_TOKEN, amount: t.amount }, nonce: nonce.toString(), deadline: deadline.toString() }, signature };
325
+ return { name: "PAYMENT-SIGNATURE", value: b64encode(JSON.stringify({ x402Version: 2, resource, accepted: t.raw, payload })), deadline: Number(deadline) };
326
+ }
299
327
  function chainOf(network) {
300
328
  const n = String(network ?? "").toLowerCase();
301
329
  return PAY_CHAINS.find((c) => c.caip.toLowerCase() === n || c.v1 === n || c.id > 0 && String(c.id) === n);
@@ -314,8 +342,13 @@ async function readQuote(res) {
314
342
  const version = Number(body.x402Version) === 1 ? 1 : 2;
315
343
  const accepts = [];
316
344
  const offered = [];
345
+ let token;
317
346
  for (const r of body.accepts) {
318
347
  offered.push(`${r.scheme ?? "?"}/${r.network ?? "?"}`);
348
+ if (r?.scheme === TOKEN_SCHEME) {
349
+ token ??= tokenOption(r);
350
+ continue;
351
+ }
319
352
  const chain = chainOf(r.network);
320
353
  if (!chain || r.scheme !== "exact") continue;
321
354
  if (chain.vm === "evm" ? String(r.asset ?? "").toLowerCase() !== chain.stable.toLowerCase() : r.asset !== chain.stable) continue;
@@ -329,7 +362,7 @@ async function readQuote(res) {
329
362
  accepts.push({ version, chain, amount, usd: Number(amount) / 1e6, payTo: r.payTo, asset: r.asset, maxTimeoutSeconds: Number(r.maxTimeoutSeconds) || 120, extra, raw: r });
330
363
  }
331
364
  const inputError = body.inputError?.message ? { code: String(body.inputError.code ?? "invalid_input"), message: String(body.inputError.message) } : void 0;
332
- return { version, resource: body.resource ?? null, accepts, offered, inputError };
365
+ return { version, resource: body.resource ?? null, accepts, offered, inputError, ...token ? { token } : {} };
333
366
  }
334
367
  function choose(q, prefer = CHAIN_KEYS) {
335
368
  for (const k of prefer) {
@@ -381,6 +414,23 @@ async function x402fetch(url, init, opts) {
381
414
  if (first.status !== 402) return { res: first, paid: null };
382
415
  const quote = await readQuote(first);
383
416
  if (quote.inputError) throw new X402Error(quote.inputError.code, quote.inputError.message);
417
+ if (opts.payIn === "wafermap") {
418
+ const t = quote.token;
419
+ if (!t) throw new X402Error("token_not_offered", "This call is not offered in $WAFERMAP right now (or not to a known Wafermap relayer). Pay in USDG or USDC.");
420
+ const value = Number(BigInt(t.amount) / 10n ** 12n) / 1e6 * await (opts.tokenPriceUsd ?? (() => wafermapPriceUsd(fetch)))();
421
+ if (value > t.usd * TOKEN_VALUE_SLACK + 5e-4) throw new X402Error("price_mismatch", `The quote asks for $WAFERMAP worth about $${value.toFixed(4)} at the market price, more than the $${t.usd} it states. Nothing was signed.`);
422
+ const usd = Math.max(t.usd, Math.round(value * 1e6) / 1e6);
423
+ const req2 = { version: 2, chain: PAY_CHAINS[0], amount: String(Math.round(usd * 1e6)), usd, payTo: DEAD, asset: WAFERMAP_TOKEN, maxTimeoutSeconds: t.maxTimeoutSeconds, extra: {}, raw: t.raw };
424
+ if (opts.approve) await opts.approve(req2);
425
+ const signer2 = typeof opts.signer === "function" ? await opts.signer() : opts.signer;
426
+ const header2 = await signTokenPayment(signer2, t, quote.resource);
427
+ const signed2 = { req: req2, from: signer2.address, token: { amount: t.amount } };
428
+ opts.onSigned?.(signed2);
429
+ const headers2 = new Headers(init.headers);
430
+ headers2.set(header2.name, header2.value);
431
+ const res2 = await f(url, { ...init, headers: headers2 });
432
+ return { res: res2, paid: { ...signed2, tx: readSettlement(res2)?.tx ?? null } };
433
+ }
384
434
  const req = choose(quote, (opts.prefer ?? CHAIN_KEYS).filter((k) => k !== "solana" || opts.solana));
385
435
  if (opts.approve) await opts.approve(req);
386
436
  const svm = req.chain.vm === "svm";
@@ -393,7 +443,7 @@ async function x402fetch(url, init, opts) {
393
443
  const res = await f(url, { ...init, headers });
394
444
  return { res, paid: { ...signed, tx: readSettlement(res)?.tx ?? null } };
395
445
  }
396
- var USDC_DOMAIN, PAY_CHAINS, EVM_CHAINS, CHAIN_KEYS, X402Error, b64encode, b64decode, TYPES, MAX_VALIDITY_SECONDS, randomNonce;
446
+ 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;
397
447
  var init_x402client = __esm({
398
448
  "shared/x402client.ts"() {
399
449
  "use strict";
@@ -408,6 +458,24 @@ var init_x402client = __esm({
408
458
  ];
409
459
  EVM_CHAINS = PAY_CHAINS.filter((c) => c.vm === "evm");
410
460
  CHAIN_KEYS = PAY_CHAINS.map((c) => c.key);
461
+ WAFERMAP_TOKEN = "0xfda080f64d84c989cfc50ff18253542cfe502db3";
462
+ PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
463
+ DEAD = "0x000000000000000000000000000000000000dEaD";
464
+ TOKEN_SCHEME = "wafermap-permit2";
465
+ WAFERMAP_RELAYERS = ["0xD21547a387a9CfFb747211983c91f457742Ef00D"];
466
+ TOKEN_VALUE_SLACK = 1.25;
467
+ PERMIT_TYPES = {
468
+ PermitTransferFrom: [
469
+ { name: "permitted", type: "TokenPermissions" },
470
+ { name: "spender", type: "address" },
471
+ { name: "nonce", type: "uint256" },
472
+ { name: "deadline", type: "uint256" }
473
+ ],
474
+ TokenPermissions: [
475
+ { name: "token", type: "address" },
476
+ { name: "amount", type: "uint256" }
477
+ ]
478
+ };
411
479
  X402Error = class extends Error {
412
480
  constructor(code, message) {
413
481
  super(message);
@@ -575,6 +643,7 @@ var init_core = __esm({
575
643
  timeoutMs;
576
644
  /** fetch with this wallet's time limit: a server that never answers cannot hold a call forever. */
577
645
  fetch;
646
+ tokenPriceUsd;
578
647
  constructor(o) {
579
648
  const account = o.account ?? (o.privateKey ? privateKeyToAccount(o.privateKey) : null);
580
649
  if (!account) throw new WafermapError("no_account", "Pass privateKey or account.");
@@ -587,6 +656,7 @@ var init_core = __esm({
587
656
  this.approveFn = o.approve;
588
657
  this.rpc = o.rpc ?? {};
589
658
  this.timeoutMs = o.timeoutMs && o.timeoutMs > 0 ? o.timeoutMs : DEFAULT_TIMEOUT_MS;
659
+ this.tokenPriceUsd = o.tokenPriceUsd;
590
660
  this.fetch = (input, init) => fetch(input, { ...init, signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs) });
591
661
  const sol = o.solana ?? (o.privateKey ? solanaKeyFromEvm(o.privateKey) : null);
592
662
  const solRpc = this.rpc.solana ?? `${this.baseUrl}/api/solana`;
@@ -660,7 +730,7 @@ var init_core = __esm({
660
730
  this.queue = prev.then(() => held);
661
731
  return prev.then(() => release);
662
732
  }
663
- async paidFetch(kind, what, url, init, network) {
733
+ async paidFetch(kind, what, url, init, network, payIn) {
664
734
  let id = null;
665
735
  let release = null;
666
736
  const unlock = () => {
@@ -673,9 +743,11 @@ var init_core = __esm({
673
743
  solana: this.solana ?? void 0,
674
744
  prefer: this.prefer(network),
675
745
  fetch: this.fetch,
746
+ payIn,
747
+ tokenPriceUsd: this.tokenPriceUsd,
676
748
  approve: async (req) => {
677
749
  release = await this.acquire();
678
- await this.gate({ kind, what, usd: req.usd, network: req.chain.key, detail: `${req.chain.symbol} to ${req.payTo}` });
750
+ 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}` });
679
751
  },
680
752
  onSigned: (s) => {
681
753
  id = this.booked(kind, what, s);
@@ -711,9 +783,33 @@ var init_core = __esm({
711
783
  this.passCache = encodePass(await makePass({ address: this.account.address, signMessage: (a) => this.account.signMessage(a) }));
712
784
  return this.passCache;
713
785
  }
786
+ // ---- Paying in $WAFERMAP (Permit2; the tokens are burned) ----
787
+ /** Whether this wallet has approved Permit2 to move its $WAFERMAP (needed once before paying in $WAFERMAP). */
788
+ async wafermapApproved() {
789
+ const a = await this.client("robinhood").readContract({ address: WAFERMAP_TOKEN, abi: erc20Abi2, functionName: "allowance", args: [this.address, PERMIT2] });
790
+ return a >= 10n ** 30n;
791
+ }
792
+ /** Approves Permit2 (Uniswap's, at its canonical address) to move this wallet's $WAFERMAP: one on-chain
793
+ * transaction on Robinhood Chain, paid in ETH. Each payment after that is a signature for one exact amount,
794
+ * spendable only by Wafermap's relayer for two minutes. */
795
+ async approveWafermap() {
796
+ const wallet = createWalletClient({ account: this.account, chain: CHAINS.robinhood, transport: http(this.rpc.robinhood) });
797
+ try {
798
+ const tx = await wallet.writeContract({ address: WAFERMAP_TOKEN, abi: erc20Abi2, functionName: "approve", args: [PERMIT2, 2n ** 256n - 1n] });
799
+ const rc = await this.client("robinhood").waitForTransactionReceipt({ hash: tx, timeout: 6e4 });
800
+ if (rc.status !== "success") throw new WafermapError("approve_failed", `The approval reverted: ${tx}`);
801
+ return tx;
802
+ } catch (e) {
803
+ if (e instanceof WafermapError) throw e;
804
+ throw new WafermapError("approve_failed", e?.shortMessage ?? e?.message ?? "The approval failed.");
805
+ }
806
+ }
714
807
  async call(id, input = {}, o = {}) {
808
+ const inToken = o.payIn === "wafermap";
809
+ 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.");
810
+ 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.");
715
811
  const pass = id.startsWith("m/") ? null : await this.holderPass();
716
- const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address, ...pass ? { [PASS_HEADER]: pass } : {} }, body: JSON.stringify(input) }, o.network);
812
+ const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address, ...pass ? { [PASS_HEADER]: pass } : {} }, body: JSON.stringify(input) }, o.network, inToken ? "wafermap" : void 0);
717
813
  const settle = (r2, out2) => {
718
814
  if (!r2.paid || !r2.id) return;
719
815
  const status = r2.res.status === 402 ? "refused" : r2.res.ok ? "ok" : "failed";
@@ -1023,7 +1119,7 @@ var VERSION;
1023
1119
  var init_version = __esm({
1024
1120
  "src/version.ts"() {
1025
1121
  "use strict";
1026
- VERSION = "0.4.4";
1122
+ VERSION = "0.4.5";
1027
1123
  }
1028
1124
  });
1029
1125
 
@@ -1123,11 +1219,16 @@ async function runMcp() {
1123
1219
  {
1124
1220
  title: "Call a Wafermap resource",
1125
1221
  description: "Run one Wafermap resource and pay its exact price over x402 from the local wallet, within the per-payment and daily caps. Failed calls are not charged.",
1126
- inputSchema: { resource: z.string().describe("Resource id, e.g. market-brief, prediction-markets, stock-signal, flux-2-dev."), input: z.record(z.string(), z.any()).optional().describe("Input fields for the resource."), network: NET }
1222
+ inputSchema: {
1223
+ resource: z.string().describe("Resource id, e.g. market-brief, prediction-markets, stock-signal, flux-2-dev."),
1224
+ input: z.record(z.string(), z.any()).optional().describe("Input fields for the resource."),
1225
+ network: NET,
1226
+ 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.")
1227
+ }
1127
1228
  },
1128
- async ({ resource, input, network }) => {
1229
+ async ({ resource, input, network, payIn }) => {
1129
1230
  try {
1130
- return result(await wm.call(resource, input ?? {}, { network }));
1231
+ return result(await wm.call(resource, input ?? {}, { network, payIn }));
1131
1232
  } catch (e) {
1132
1233
  return failure(e);
1133
1234
  }
@@ -1578,6 +1679,8 @@ Wallet
1578
1679
  Pay
1579
1680
  wafermap resources [words] Wafermap's pay-per-call catalog
1580
1681
  wafermap call <id> [field=value ...] run a resource and pay its price (--json '{...}' for complex input)
1682
+ wafermap call <id> ... --pay wafermap pay a Wafermap resource in $WAFERMAP at 50% off (the tokens are burned)
1683
+ wafermap approve-wafermap one-time approval so the wallet can pay in $WAFERMAP (Robinhood Chain, ETH gas)
1581
1684
  wafermap holder what this wallet's $WAFERMAP unlocks: holder price, Pro free calls left today
1582
1685
  wafermap pass print a holder pass (X-Wafermap-Pass) for scripts and x402 Tracker Pro
1583
1686
  wafermap discover <words> search third-party x402 services
@@ -1695,6 +1798,14 @@ async function main() {
1695
1798
  Caps: $0.10 per payment, $1 per day, $25 per swap. Change with \`wafermap limits\`.`);
1696
1799
  return;
1697
1800
  }
1801
+ case "approve-wafermap": {
1802
+ const wm = openWallet(await password());
1803
+ if (await wm.wafermapApproved()) return console.log("\u2713 Already approved: this wallet can pay Wafermap in $WAFERMAP (wafermap call <id> --pay wafermap).");
1804
+ console.error(`This sends one transaction on Robinhood Chain from ${wm.address}, paid in ETH: approve Permit2 (Uniswap, 0x000000000022D473030F116dDEE9F6B43aC78BA3) to move this wallet's $WAFERMAP. Each payment after that is a separate signature for one exact amount, usable only by Wafermap's relayer for two minutes.`);
1805
+ if (!yes && !/^y(es)?$/i.test((await ask("Send the approval? [y/N] ")).trim())) die("Cancelled. Nothing was sent.");
1806
+ const tx = await wm.approveWafermap();
1807
+ return console.log(`\u2713 Approved \xB7 ${EXPLORER("robinhood")}/tx/${tx}`);
1808
+ }
1698
1809
  case "holder": {
1699
1810
  const addr = keystoreAddress() ?? die("No wallet yet. Run `wafermap init`.");
1700
1811
  const j = await holderInfo(addr, process.env.WAFERMAP_URL, Number(process.env.WAFERMAP_TIMEOUT_MS) || void 0);
@@ -1794,8 +1905,10 @@ Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment
1794
1905
  case "call": {
1795
1906
  const id = a._[0] ?? die("Which resource? e.g. wafermap call gas-now network=base");
1796
1907
  const wm = openWallet(await password(), approver(yes));
1797
- const out = await wm.call(id, inputOf(a, 1), { network: net(a.flags.network) });
1798
- if (out.receipt) console.error(`\u2713 Paid $${out.receipt.amountUsd} ${out.receipt.asset} \xB7 ${EXPLORER(out.receipt.network)}/tx/${out.receipt.tx}`);
1908
+ 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)");
1909
+ const out = await wm.call(id, inputOf(a, 1), { network: net(a.flags.network), payIn });
1910
+ 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}`);
1911
+ else if (out.receipt) console.error(`\u2713 Paid $${out.receipt.amountUsd} ${out.receipt.asset} \xB7 ${EXPLORER(out.receipt.network)}/tx/${out.receipt.tx}`);
1799
1912
  else if (out.free) console.error(`\u2713 Free call ($WAFERMAP Pro) \xB7 ${out.free.left} of ${out.free.perDay} left today. Nothing was paid.`);
1800
1913
  if (typeof a.flags.out === "string") await saveMedia(out.result, a.flags.out);
1801
1914
  return printResult(out.result);
package/dist/index.js CHANGED
@@ -283,6 +283,52 @@ var PAY_CHAINS = [
283
283
  ];
284
284
  var EVM_CHAINS = PAY_CHAINS.filter((c) => c.vm === "evm");
285
285
  var CHAIN_KEYS = PAY_CHAINS.map((c) => c.key);
286
+ var WAFERMAP_TOKEN = "0xfda080f64d84c989cfc50ff18253542cfe502db3";
287
+ var PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
288
+ var DEAD = "0x000000000000000000000000000000000000dEaD";
289
+ var TOKEN_SCHEME = "wafermap-permit2";
290
+ var WAFERMAP_RELAYERS = ["0xD21547a387a9CfFb747211983c91f457742Ef00D"];
291
+ var TOKEN_VALUE_SLACK = 1.25;
292
+ async function wafermapPriceUsd(f = fetch) {
293
+ 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) });
294
+ const j = await res.json().catch(() => null);
295
+ const p = Number(j?.data?.attributes?.token_prices?.[WAFERMAP_TOKEN]);
296
+ if (!res.ok || !(p > 0)) throw new X402Error("price_unavailable", "The $WAFERMAP market price could not be read, so the amount cannot be checked. Nothing was signed; pay in USDG or USDC.");
297
+ return p;
298
+ }
299
+ var PERMIT_TYPES = {
300
+ PermitTransferFrom: [
301
+ { name: "permitted", type: "TokenPermissions" },
302
+ { name: "spender", type: "address" },
303
+ { name: "nonce", type: "uint256" },
304
+ { name: "deadline", type: "uint256" }
305
+ ],
306
+ TokenPermissions: [
307
+ { name: "token", type: "address" },
308
+ { name: "amount", type: "uint256" }
309
+ ]
310
+ };
311
+ function tokenOption(r) {
312
+ const e = r?.extra ?? {};
313
+ const same = (a, b) => String(a ?? "").toLowerCase() === b.toLowerCase();
314
+ if (r?.scheme !== TOKEN_SCHEME || r.network !== "eip155:4663" || !same(r.asset, WAFERMAP_TOKEN) || !same(r.payTo, DEAD) || !same(e.permit2, PERMIT2)) return;
315
+ if (!/^\d{1,40}$/.test(String(r.amount)) || !/^0x[0-9a-fA-F]{40}$/.test(String(e.spender)) || !(Number(e.usd) > 0)) return;
316
+ if (!WAFERMAP_RELAYERS.some((a) => same(e.spender, a))) return;
317
+ return { amount: String(r.amount), usd: Number(e.usd), spender: e.spender, maxTimeoutSeconds: Number(r.maxTimeoutSeconds) || 120, raw: r };
318
+ }
319
+ async function signTokenPayment(signer, t, resource) {
320
+ const nonce = BigInt(randomNonce()) >> 8n;
321
+ const deadline = BigInt(Math.floor(Date.now() / 1e3) + Math.min(t.maxTimeoutSeconds, 120));
322
+ const permitted = { token: WAFERMAP_TOKEN, amount: BigInt(t.amount) };
323
+ let signature;
324
+ try {
325
+ signature = await signer.signTypedData({ domain: { name: "Permit2", chainId: 4663, verifyingContract: PERMIT2 }, types: PERMIT_TYPES, primaryType: "PermitTransferFrom", message: { permitted, spender: t.spender, nonce, deadline } });
326
+ } catch (e) {
327
+ throw new X402Error("sign_rejected", e?.shortMessage ?? e?.message ?? "Signature rejected.");
328
+ }
329
+ const payload = { owner: signer.address, permit: { permitted: { token: WAFERMAP_TOKEN, amount: t.amount }, nonce: nonce.toString(), deadline: deadline.toString() }, signature };
330
+ return { name: "PAYMENT-SIGNATURE", value: b64encode(JSON.stringify({ x402Version: 2, resource, accepted: t.raw, payload })), deadline: Number(deadline) };
331
+ }
286
332
  var X402Error = class extends Error {
287
333
  constructor(code, message) {
288
334
  super(message);
@@ -315,8 +361,13 @@ async function readQuote(res) {
315
361
  const version = Number(body.x402Version) === 1 ? 1 : 2;
316
362
  const accepts = [];
317
363
  const offered = [];
364
+ let token;
318
365
  for (const r of body.accepts) {
319
366
  offered.push(`${r.scheme ?? "?"}/${r.network ?? "?"}`);
367
+ if (r?.scheme === TOKEN_SCHEME) {
368
+ token ??= tokenOption(r);
369
+ continue;
370
+ }
320
371
  const chain = chainOf(r.network);
321
372
  if (!chain || r.scheme !== "exact") continue;
322
373
  if (chain.vm === "evm" ? String(r.asset ?? "").toLowerCase() !== chain.stable.toLowerCase() : r.asset !== chain.stable) continue;
@@ -330,7 +381,7 @@ async function readQuote(res) {
330
381
  accepts.push({ version, chain, amount, usd: Number(amount) / 1e6, payTo: r.payTo, asset: r.asset, maxTimeoutSeconds: Number(r.maxTimeoutSeconds) || 120, extra, raw: r });
331
382
  }
332
383
  const inputError = body.inputError?.message ? { code: String(body.inputError.code ?? "invalid_input"), message: String(body.inputError.message) } : void 0;
333
- return { version, resource: body.resource ?? null, accepts, offered, inputError };
384
+ return { version, resource: body.resource ?? null, accepts, offered, inputError, ...token ? { token } : {} };
334
385
  }
335
386
  function choose(q, prefer = CHAIN_KEYS) {
336
387
  for (const k of prefer) {
@@ -394,6 +445,23 @@ async function x402fetch(url, init, opts) {
394
445
  if (first.status !== 402) return { res: first, paid: null };
395
446
  const quote = await readQuote(first);
396
447
  if (quote.inputError) throw new X402Error(quote.inputError.code, quote.inputError.message);
448
+ if (opts.payIn === "wafermap") {
449
+ const t = quote.token;
450
+ if (!t) throw new X402Error("token_not_offered", "This call is not offered in $WAFERMAP right now (or not to a known Wafermap relayer). Pay in USDG or USDC.");
451
+ const value = Number(BigInt(t.amount) / 10n ** 12n) / 1e6 * await (opts.tokenPriceUsd ?? (() => wafermapPriceUsd(fetch)))();
452
+ if (value > t.usd * TOKEN_VALUE_SLACK + 5e-4) throw new X402Error("price_mismatch", `The quote asks for $WAFERMAP worth about $${value.toFixed(4)} at the market price, more than the $${t.usd} it states. Nothing was signed.`);
453
+ const usd = Math.max(t.usd, Math.round(value * 1e6) / 1e6);
454
+ const req2 = { version: 2, chain: PAY_CHAINS[0], amount: String(Math.round(usd * 1e6)), usd, payTo: DEAD, asset: WAFERMAP_TOKEN, maxTimeoutSeconds: t.maxTimeoutSeconds, extra: {}, raw: t.raw };
455
+ if (opts.approve) await opts.approve(req2);
456
+ const signer2 = typeof opts.signer === "function" ? await opts.signer() : opts.signer;
457
+ const header2 = await signTokenPayment(signer2, t, quote.resource);
458
+ const signed2 = { req: req2, from: signer2.address, token: { amount: t.amount } };
459
+ opts.onSigned?.(signed2);
460
+ const headers2 = new Headers(init.headers);
461
+ headers2.set(header2.name, header2.value);
462
+ const res2 = await f(url, { ...init, headers: headers2 });
463
+ return { res: res2, paid: { ...signed2, tx: readSettlement(res2)?.tx ?? null } };
464
+ }
397
465
  const req = choose(quote, (opts.prefer ?? CHAIN_KEYS).filter((k) => k !== "solana" || opts.solana));
398
466
  if (opts.approve) await opts.approve(req);
399
467
  const svm = req.chain.vm === "svm";
@@ -514,6 +582,7 @@ var Wafermap = class {
514
582
  timeoutMs;
515
583
  /** fetch with this wallet's time limit: a server that never answers cannot hold a call forever. */
516
584
  fetch;
585
+ tokenPriceUsd;
517
586
  constructor(o) {
518
587
  const account = o.account ?? (o.privateKey ? privateKeyToAccount(o.privateKey) : null);
519
588
  if (!account) throw new WafermapError("no_account", "Pass privateKey or account.");
@@ -526,6 +595,7 @@ var Wafermap = class {
526
595
  this.approveFn = o.approve;
527
596
  this.rpc = o.rpc ?? {};
528
597
  this.timeoutMs = o.timeoutMs && o.timeoutMs > 0 ? o.timeoutMs : DEFAULT_TIMEOUT_MS;
598
+ this.tokenPriceUsd = o.tokenPriceUsd;
529
599
  this.fetch = (input, init) => fetch(input, { ...init, signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs) });
530
600
  const sol = o.solana ?? (o.privateKey ? solanaKeyFromEvm(o.privateKey) : null);
531
601
  const solRpc = this.rpc.solana ?? `${this.baseUrl}/api/solana`;
@@ -599,7 +669,7 @@ var Wafermap = class {
599
669
  this.queue = prev.then(() => held);
600
670
  return prev.then(() => release);
601
671
  }
602
- async paidFetch(kind, what, url, init, network) {
672
+ async paidFetch(kind, what, url, init, network, payIn) {
603
673
  let id = null;
604
674
  let release = null;
605
675
  const unlock = () => {
@@ -612,9 +682,11 @@ var Wafermap = class {
612
682
  solana: this.solana ?? void 0,
613
683
  prefer: this.prefer(network),
614
684
  fetch: this.fetch,
685
+ payIn,
686
+ tokenPriceUsd: this.tokenPriceUsd,
615
687
  approve: async (req) => {
616
688
  release = await this.acquire();
617
- await this.gate({ kind, what, usd: req.usd, network: req.chain.key, detail: `${req.chain.symbol} to ${req.payTo}` });
689
+ 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}` });
618
690
  },
619
691
  onSigned: (s) => {
620
692
  id = this.booked(kind, what, s);
@@ -650,9 +722,33 @@ var Wafermap = class {
650
722
  this.passCache = encodePass(await makePass({ address: this.account.address, signMessage: (a) => this.account.signMessage(a) }));
651
723
  return this.passCache;
652
724
  }
725
+ // ---- Paying in $WAFERMAP (Permit2; the tokens are burned) ----
726
+ /** Whether this wallet has approved Permit2 to move its $WAFERMAP (needed once before paying in $WAFERMAP). */
727
+ async wafermapApproved() {
728
+ const a = await this.client("robinhood").readContract({ address: WAFERMAP_TOKEN, abi: erc20Abi2, functionName: "allowance", args: [this.address, PERMIT2] });
729
+ return a >= 10n ** 30n;
730
+ }
731
+ /** Approves Permit2 (Uniswap's, at its canonical address) to move this wallet's $WAFERMAP: one on-chain
732
+ * transaction on Robinhood Chain, paid in ETH. Each payment after that is a signature for one exact amount,
733
+ * spendable only by Wafermap's relayer for two minutes. */
734
+ async approveWafermap() {
735
+ const wallet = createWalletClient({ account: this.account, chain: CHAINS.robinhood, transport: http(this.rpc.robinhood) });
736
+ try {
737
+ const tx = await wallet.writeContract({ address: WAFERMAP_TOKEN, abi: erc20Abi2, functionName: "approve", args: [PERMIT2, 2n ** 256n - 1n] });
738
+ const rc = await this.client("robinhood").waitForTransactionReceipt({ hash: tx, timeout: 6e4 });
739
+ if (rc.status !== "success") throw new WafermapError("approve_failed", `The approval reverted: ${tx}`);
740
+ return tx;
741
+ } catch (e) {
742
+ if (e instanceof WafermapError) throw e;
743
+ throw new WafermapError("approve_failed", e?.shortMessage ?? e?.message ?? "The approval failed.");
744
+ }
745
+ }
653
746
  async call(id, input = {}, o = {}) {
747
+ const inToken = o.payIn === "wafermap";
748
+ 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.");
749
+ 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.");
654
750
  const pass = id.startsWith("m/") ? null : await this.holderPass();
655
- const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address, ...pass ? { [PASS_HEADER]: pass } : {} }, body: JSON.stringify(input) }, o.network);
751
+ const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address, ...pass ? { [PASS_HEADER]: pass } : {} }, body: JSON.stringify(input) }, o.network, inToken ? "wafermap" : void 0);
656
752
  const settle = (r2, out2) => {
657
753
  if (!r2.paid || !r2.id) return;
658
754
  const status = r2.res.status === 402 ? "refused" : r2.res.ok ? "ok" : "failed";
@@ -768,7 +864,10 @@ export {
768
864
  MAX_VALIDITY_SECONDS,
769
865
  PASS_HEADER,
770
866
  PAY_CHAINS,
867
+ PERMIT2,
771
868
  SwapError,
869
+ WAFERMAP_RELAYERS,
870
+ WAFERMAP_TOKEN,
772
871
  Wafermap,
773
872
  WafermapError,
774
873
  X402Error,
@@ -786,5 +885,7 @@ export {
786
885
  quoteSwap,
787
886
  readQuote,
788
887
  signPayment,
888
+ signTokenPayment,
889
+ wafermapPriceUsd,
789
890
  x402fetch
790
891
  };
@@ -56,7 +56,30 @@ export type Quote = {
56
56
  code: string;
57
57
  message: string;
58
58
  };
59
+ token?: TokenRequirement;
59
60
  };
61
+ export declare const WAFERMAP_TOKEN = "0xfda080f64d84c989cfc50ff18253542cfe502db3";
62
+ export declare const PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
63
+ export declare const DEAD = "0x000000000000000000000000000000000000dEaD";
64
+ export declare const TOKEN_SCHEME = "wafermap-permit2";
65
+ /** Wafermap's relayer wallets, the only spenders this client signs a $WAFERMAP permit for. Empty: paying in
66
+ * $WAFERMAP is off in this build. A new relayer address ships as a new client release. */
67
+ export declare const WAFERMAP_RELAYERS: string[];
68
+ /** $WAFERMAP in USD, read by the client itself from GeckoTerminal. Throws when it cannot be read. */
69
+ export declare function wafermapPriceUsd(f?: typeof fetch): Promise<number>;
70
+ export type TokenRequirement = {
71
+ amount: string;
72
+ usd: number;
73
+ spender: Hex;
74
+ maxTimeoutSeconds: number;
75
+ raw: any;
76
+ };
77
+ /** Signs a Permit2 transfer of exactly the quoted $WAFERMAP to Wafermap's relayer, valid for at most 120 s. */
78
+ export declare function signTokenPayment(signer: Signer, t: TokenRequirement, resource: any): Promise<{
79
+ name: string;
80
+ value: string;
81
+ deadline: number;
82
+ }>;
60
83
  export declare class X402Error extends Error {
61
84
  code: string;
62
85
  constructor(code: string, message: string);
@@ -92,6 +115,9 @@ export type Signed = {
92
115
  req: Requirement;
93
116
  from: string;
94
117
  auth?: SignedAuth;
118
+ token?: {
119
+ amount: string;
120
+ };
95
121
  };
96
122
  export type PaidCall = {
97
123
  res: Response;
@@ -113,4 +139,6 @@ export declare function x402fetch(url: string, init: RequestInit, opts: {
113
139
  approve?: (req: Requirement) => Promise<void> | void;
114
140
  onSigned?: (s: Signed) => void;
115
141
  fetch?: typeof fetch;
142
+ payIn?: "stablecoin" | "wafermap";
143
+ tokenPriceUsd?: () => Promise<number>;
116
144
  }): Promise<PaidCall>;
@@ -71,6 +71,9 @@ export type WafermapOptions = {
71
71
  /** Give up on any HTTP request (quote, paid call, discovery, swap API) after this long. Default 60000 ms.
72
72
  * A paid call that times out after signing stays counted until its authorization expires. */
73
73
  timeoutMs?: number;
74
+ /** Where the $WAFERMAP price comes from when paying in $WAFERMAP (default: GeckoTerminal, read by this client).
75
+ * The signed amount is valued at this price and refused when it is worth more than the quote states. */
76
+ tokenPriceUsd?: () => Promise<number>;
74
77
  };
75
78
  export declare class WafermapError extends Error {
76
79
  code: string;
@@ -105,6 +108,7 @@ export declare class Wafermap {
105
108
  readonly timeoutMs: number;
106
109
  /** fetch with this wallet's time limit: a server that never answers cannot hold a call forever. */
107
110
  readonly fetch: typeof fetch;
111
+ private readonly tokenPriceUsd?;
108
112
  constructor(o: WafermapOptions);
109
113
  get address(): `0x${string}`;
110
114
  client(n: EvmNet): PublicClient;
@@ -133,8 +137,15 @@ export declare class Wafermap {
133
137
  * proves the wallet to Wafermap for what its $WAFERMAP unlocks (Pro free calls, x402 Tracker Pro). Signing
134
138
  * a message moves no funds. */
135
139
  holderPass(): Promise<string>;
140
+ /** Whether this wallet has approved Permit2 to move its $WAFERMAP (needed once before paying in $WAFERMAP). */
141
+ wafermapApproved(): Promise<boolean>;
142
+ /** Approves Permit2 (Uniswap's, at its canonical address) to move this wallet's $WAFERMAP: one on-chain
143
+ * transaction on Robinhood Chain, paid in ETH. Each payment after that is a signature for one exact amount,
144
+ * spendable only by Wafermap's relayer for two minutes. */
145
+ approveWafermap(): Promise<string>;
136
146
  call(id: string, input?: Record<string, unknown>, o?: {
137
147
  network?: Net;
148
+ payIn?: "stablecoin" | "wafermap";
138
149
  }): Promise<{
139
150
  resource: string;
140
151
  result: any;
@@ -142,6 +153,8 @@ export declare class Wafermap {
142
153
  network: string;
143
154
  asset: string;
144
155
  amountUsd: number;
156
+ amountTokens?: number;
157
+ burned?: boolean;
145
158
  payer: string;
146
159
  tx: string;
147
160
  ms: number;
@@ -1,6 +1,6 @@
1
1
  export { Wafermap, WafermapError, DEFAULT_LIMITS, DEFAULT_TIMEOUT_MS, checkPayment, holderInfo, listResources, mergeLog } from "./core.js";
2
2
  export { PASS_HEADER, passMessage, encodePass, decodePass, makePass } from "../shared/pass";
3
3
  export type { Approval, Limits, Net, PaymentStatus, SpendEntry, SwapQuote, Token, WafermapOptions } from "./core.js";
4
- export { x402fetch, readQuote, choose, signPayment, PAY_CHAINS, MAX_VALIDITY_SECONDS, X402Error } from "../shared/x402client";
4
+ export { x402fetch, readQuote, choose, signPayment, signTokenPayment, wafermapPriceUsd, PAY_CHAINS, MAX_VALIDITY_SECONDS, WAFERMAP_RELAYERS, WAFERMAP_TOKEN, PERMIT2, X402Error } from "../shared/x402client";
5
5
  export { quoteSwap, executeSwap, decodeSwap, KYBER_ROUTER, SwapError } from "../shared/swap";
6
6
  export type { Requirement, Quote, Signed, SignedAuth, Signer } from "../shared/x402client";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wafermapxyz/cli",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Wafermap for agents: an encrypted local wallet that pays x402 resources, discovers and pays third-party x402 services (USDG on Robinhood Chain; USDC on Solana, Base, Arbitrum, Polygon), and swaps on Robinhood Chain and Base. CLI, SDK and MCP server.",
5
5
  "type": "module",
6
6
  "bin": {