x402-safe-fetch 0.17.0 → 0.19.0

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/README.md CHANGED
@@ -23,12 +23,15 @@ npm install x402-safe-fetch @x402/fetch @x402/evm viem
23
23
  An agent that works in a terminal (or you) can pay for one API call with one command, with the same checks:
24
24
 
25
25
  ```sh
26
+ npx x402-safe-fetch connect # once: a wallet for this computer, linked to your Fizzl wallet
26
27
  npx x402-safe-fetch find "bitcoin price in usd" # free: who sells it, the price, how often it delivered
27
28
  npx x402-safe-fetch check <url> # free: the price per network and x402 Doctor's free check; pays nothing
28
- EVM_PRIVATE_KEY=0x… npx x402-safe-fetch pay <url> --max 0.05 --daily 1
29
+ npx x402-safe-fetch pay <url> --max 0.05 --daily 1
29
30
  npx x402-safe-fetch spent # what was paid today, and what is left
30
31
  ```
31
32
 
33
+ **No key to copy.** `connect` makes a wallet for this computer (its key stays in `~/.x402-safe-fetch/wallet.json`, readable by you only) and shows a link and a code: open it on your phone, sign in to [wallet.fizzl.eu](https://wallet.fizzl.eu) and approve. From then on every payment is first reserved on your Fizzl wallet: your limits there apply, and above them you get Approve / Deny on your phone (Telegram or the dashboard) before anything is signed; the key never leaves this computer. Then add a few dollars of USDC on Base to the address it shows (`wallet` shows it again, with the balance): no crypto yet? It also prints a link to buy $5 of USDC with a card at Coinbase, sent straight to that address (the link starts a Coinbase Onramp session through x402 Doctor; you pay Coinbase, Fizzl never holds the money). Or send USDC on Base from Coinbase, a wallet app or an exchange. `init` makes the wallet without linking it, and `EVM_PRIVATE_KEY` still overrides both.
34
+
32
35
  `pay` pays USDC on Base, only when x402 Doctor's preflight says go: the cheapest option within `--max`, all calls of the UTC day together within `--daily` (preflights included), and the paid answer checked for instructions aimed at your agent (`--no-answer-check` turns that off). An endpoint that took a payment without delivering isn't paid again for 30 minutes, also by the next command and whatever is added to its URL. `--method POST --data '{"q":1}'` sends a body, `--dry-run` is `check`, and `--json` prints one line of JSON (with the answer, the transaction hash and the day's spend). Exit code 0 is done, 1 is not paid (a check said no, or a limit), 2 is a usage error. The day's spend is kept in `~/.x402-safe-fetch/state.json` (`X402_SAFE_FETCH_HOME` moves it). Use a dedicated wallet with a few dollars.
33
36
 
34
37
  ## Use
package/cli.js CHANGED
@@ -6,16 +6,19 @@
6
6
  //
7
7
  // npx x402-safe-fetch find "bitcoin price in usd" free: who sells it, at what price, and whether it delivers
8
8
  // npx x402-safe-fetch check <url> free: the price and x402 Doctor's free check; pays nothing
9
- // EVM_PRIVATE_KEY=0x… npx x402-safe-fetch pay <url> --max 0.05 --daily 1
9
+ // npx x402-safe-fetch connect a wallet for this computer, linked to your Fizzl wallet
10
+ // npx x402-safe-fetch pay <url> --max 0.05 --daily 1
10
11
  // npx x402-safe-fetch spent what was paid today, and what is left
11
12
  //
12
- // Pays USDC on Base from EVM_PRIVATE_KEY (use a dedicated wallet with a few dollars). The day's spend and the
13
+ // Pays USDC on Base from this computer's own wallet (init or connect make it; EVM_PRIVATE_KEY overrides it). Linked
14
+ // to a Fizzl Agent Wallet (connect), the owner's limits and approvals apply to every payment (wallet.js). The day's spend and the
13
15
  // endpoints that took money without delivering are kept in ~/.x402-safe-fetch/state.json (X402_SAFE_FETCH_HOME
14
16
  // moves it), so the limits and the paid-twice guard hold across commands.
15
17
 
16
18
  import fs from "node:fs";
17
19
  import os from "node:os";
18
20
  import path from "node:path";
21
+ import { ensureWallet, readWallet, writeWallet, pair, serverSigner, usdcBalance, buyUsdcUrl, WALLET_URL } from "./wallet.js";
19
22
  import {
20
23
  createSafeFetch, SafePayError, receiptOf, readOffer, usdOfOption, answerCheckOf, purchaseKey, paidTwiceMessage,
21
24
  PAID_ERROR_MS, DOCTOR_URL, VERSION,
@@ -26,9 +29,13 @@ const PREFLIGHT_USD = 0.001;
26
29
 
27
30
  const HELP = `x402-safe-fetch ${VERSION}: pay for an API call from the command line, checked first.
28
31
 
32
+ connect Start here. Makes a wallet for this computer and links it to your Fizzl wallet
33
+ (wallet.fizzl.eu): approve the code on your phone. Your limits and approvals then apply.
34
+ init Only the wallet for this computer, not linked: shows its address and a link to buy USDC with a card.
35
+ wallet This computer's wallet: address, USDC balance, and whether it is linked.
29
36
  find <need> Free. Who sells it (Fizzl Buy's pick): URL, price, and how often it delivered.
30
37
  check <url> Free, pays nothing. The price per network and x402 Doctor's free check of the endpoint.
31
- pay <url> Pays one call in USDC on Base from EVM_PRIVATE_KEY, only if x402 Doctor's preflight says go.
38
+ pay <url> Pays one call in USDC on Base, only if x402 Doctor's preflight says go.
32
39
  spent What was paid today and what is left of the day's limit.
33
40
 
34
41
  Options for pay (check takes --method and --data too):
@@ -83,7 +90,7 @@ export function stateStore({ home = process.env.X402_SAFE_FETCH_HOME || path.joi
83
90
  fs.mkdirSync(home, { recursive: true, mode: 0o700 });
84
91
  fs.writeFileSync(file, JSON.stringify(s, null, 2), { mode: 0o600 });
85
92
  }
86
- return { file, read, write, now };
93
+ return { home, file, read, write, now };
87
94
  }
88
95
 
89
96
  function requestInit(flags) {
@@ -133,7 +140,7 @@ async function check(args, { fetch }) {
133
140
  return { ok: true, url, status: 402, paid: false, options, doctor };
134
141
  }
135
142
 
136
- async function pay(args, { fetch, env, store, createPayingFetch, safeFetchOptions = {} }) {
143
+ async function pay(args, { fetch, env, store, createPayingFetch, safeFetchOptions = {}, say = () => {} }) {
137
144
  if (args.flags["dry-run"]) return check(args, { fetch });
138
145
  const url = targetUrl(args);
139
146
  const init = requestInit(args.flags);
@@ -150,9 +157,12 @@ async function pay(args, { fetch, env, store, createPayingFetch, safeFetchOption
150
157
  if (earlier) return { ok: false, url, code: "paid_no_delivery", message: paidTwiceMessage(url, earlier), earlier };
151
158
 
152
159
  let register;
160
+ let linked = null; // the Fizzl wallet's signer, when this computer's wallet is linked
153
161
  if (!createPayingFetch) {
154
- const raw = String(env.EVM_PRIVATE_KEY || "").trim();
155
- if (!raw) throw new UsageError("set EVM_PRIVATE_KEY: a dedicated wallet with a few dollars of USDC on Base");
162
+ const envKey = String(env.EVM_PRIVATE_KEY || "").trim();
163
+ const own = envKey ? null : readWallet(store.home);
164
+ const raw = envKey || own?.privateKey;
165
+ if (!raw) throw new UsageError("no wallet yet: run npx x402-safe-fetch connect (or init), or set EVM_PRIVATE_KEY");
156
166
  let ExactEvmScheme, privateKeyToAccount;
157
167
  try {
158
168
  ({ ExactEvmScheme } = await import("@x402/evm/exact/client"));
@@ -161,7 +171,10 @@ async function pay(args, { fetch, env, store, createPayingFetch, safeFetchOption
161
171
  throw new UsageError("paying needs @x402/evm and viem: npx -p x402-safe-fetch -p @x402/evm -p viem x402-safe-fetch pay <url>");
162
172
  }
163
173
  const account = privateKeyToAccount(raw.startsWith("0x") ? raw : `0x${raw}`);
164
- register = (client) => client.register("eip155:8453", new ExactEvmScheme(account));
174
+ if (own?.server?.key) {
175
+ linked = serverSigner({ account, server: own.server, fetch, purchase: { url, description: "x402-safe-fetch pay" }, onWait: (summary) => say(`Waiting for your approval (${summary}): check Telegram or ${own.server.url}`) });
176
+ }
177
+ register = (client) => client.register("eip155:8453", new ExactEvmScheme(linked ?? account));
165
178
  }
166
179
 
167
180
  let preflights = 0;
@@ -194,7 +207,7 @@ async function pay(args, { fetch, env, store, createPayingFetch, safeFetchOption
194
207
  const verdict = () => preflight && { verdict: preflight.verdict, summary: preflight.summary };
195
208
 
196
209
  try {
197
- const res = await safeFetch(url, init);
210
+ const res = await safeFetch(url, init).finally(() => linked?.finish("x402-safe-fetch"));
198
211
  const text = await res.text();
199
212
  const receipt = receiptOf(res);
200
213
  // A response after a preflight means the endpoint was paid (or a payment was sent): a 2xx clears it, anything else
@@ -211,12 +224,43 @@ async function pay(args, { fetch, env, store, createPayingFetch, safeFetchOption
211
224
  ...(delivered ? {} : { code: "not_delivered", message: `paid, but the endpoint answered HTTP ${res.status}; it won't be paid again for 30 minutes` }),
212
225
  };
213
226
  } catch (err) {
227
+ const refused = linked?.state.denied;
228
+ if (refused) {
229
+ const totals = settle(null);
230
+ return { ok: false, url, code: refused.code, message: `not paid: your Fizzl wallet says no: ${refused.summary}`, preflight: verdict(), ...totals };
231
+ }
214
232
  if (!(err instanceof SafePayError)) throw err;
215
233
  const totals = settle(null);
216
234
  return { ok: false, url, code: err.code, message: err.message, preflight: verdict() ?? (err.preflight ? { verdict: err.preflight.verdict, summary: err.preflight.summary } : null), reasons: (err.preflight?.reasons || []).filter((r) => r.level !== "info").map((r) => ({ level: r.level, code: r.code, message: r.message })), ...totals };
217
235
  }
218
236
  }
219
237
 
238
+ async function init(args, { store, fetch }) {
239
+ const { wallet, created } = await ensureWallet(store.home);
240
+ const usdc = await usdcBalance(wallet.address, { fetch });
241
+ return { ok: true, created, address: wallet.address, usdc_base: usdc, buy_usdc_url: buyUsdcUrl(wallet.address), linked: wallet.server ? { url: wallet.server.url, agent: wallet.server.agent } : null, file: path.join(store.home, "wallet.json") };
242
+ }
243
+
244
+ async function connect(args, { store, fetch, say, sleep }) {
245
+ const { wallet } = await ensureWallet(store.home);
246
+ const url = String(args.flags.server || WALLET_URL);
247
+ const label = String(args.flags.name || `x402-safe-fetch on ${os.hostname()}`).slice(0, 40);
248
+ const linked = await pair({
249
+ address: wallet.address, label, url, fetch, sleep,
250
+ onCode: ({ code, url: link }) => say(`Open this on your phone and approve:\n\n ${link}\n\n code ${code} (valid 10 minutes)\n\nNo Fizzl wallet yet? One is made when you sign in. Waiting...`),
251
+ });
252
+ writeWallet(store.home, { ...wallet, server: linked });
253
+ const usdc = await usdcBalance(wallet.address, { fetch });
254
+ return { ok: true, address: wallet.address, linked: { url: linked.url, agent: linked.agent }, usdc_base: usdc, buy_usdc_url: buyUsdcUrl(wallet.address) };
255
+ }
256
+
257
+ async function walletInfo(args, { store, fetch }) {
258
+ const wallet = readWallet(store.home);
259
+ if (!wallet) return { ok: false, code: "no_wallet", message: "no wallet yet: run npx x402-safe-fetch connect (or init)" };
260
+ const usdc = await usdcBalance(wallet.address, { fetch });
261
+ return { ok: true, address: wallet.address, usdc_base: usdc, buy_usdc_url: buyUsdcUrl(wallet.address), linked: wallet.server ? { url: wallet.server.url, agent: wallet.server.agent } : null };
262
+ }
263
+
220
264
  function spent(args, { store }) {
221
265
  const s = store.read();
222
266
  const daily = money(args.flags.daily, "daily") ?? 1;
@@ -252,6 +296,20 @@ function text(cmd, r) {
252
296
  if (r.status && r.body !== undefined) lines.push(typeof r.body === "string" ? r.body.slice(0, 600) : JSON.stringify(r.body).slice(0, 600));
253
297
  }
254
298
  if (r.spent_today_usd !== undefined) lines.push(`Today: $${r.spent_today_usd} of $${r.daily_limit_usd}`);
299
+ } else if (cmd === "init" || cmd === "connect" || cmd === "wallet") {
300
+ if (!r.ok) return r.message;
301
+ if (cmd === "init" && r.created) lines.push("Made a wallet for this computer (the key stays here, readable by you only).");
302
+ if (cmd === "connect") lines.push(`Linked to your Fizzl wallet${r.linked?.agent ? ` as "${r.linked.agent}"` : ""}: your limits and approvals now apply.`);
303
+ lines.push(`Address: ${r.address}`, `USDC on Base: ${r.usdc_base === null ? "?" : `$${r.usdc_base}`}`);
304
+ if (cmd !== "connect") lines.push(r.linked ? `Linked to ${r.linked.url}${r.linked.agent ? ` as "${r.linked.agent}"` : ""}` : "Not linked to a Fizzl wallet (run connect for limits and phone approvals).");
305
+ if (!r.usdc_base) {
306
+ lines.push(
307
+ "To pay, add a few dollars of USDC on Base to the address above:",
308
+ ` with a card (Coinbase, $5; open it in your browser): ${r.buy_usdc_url}`,
309
+ " or send USDC from Coinbase, a wallet app or an exchange (choose the Base network).",
310
+ );
311
+ }
312
+ lines.push('Then: npx x402-safe-fetch find "what you need"');
255
313
  } else if (cmd === "spent") {
256
314
  lines.push(`${r.day}: $${r.spent_today_usd} paid in ${r.calls} call(s), $${r.left_usd} left of $${r.daily_limit_usd}`);
257
315
  if (r.blocked_endpoints.length) lines.push("Not paid again for now (took money without delivering):", ...r.blocked_endpoints.map((b) => ` ${b}`));
@@ -259,14 +317,15 @@ function text(cmd, r) {
259
317
  return lines.join("\n");
260
318
  }
261
319
 
262
- export async function main(argv, { fetch = globalThis.fetch, env = process.env, store = stateStore(), createPayingFetch, safeFetchOptions, out = console.log, err = console.error } = {}) {
320
+ export async function main(argv, { fetch = globalThis.fetch, env = process.env, store = stateStore(), createPayingFetch, safeFetchOptions, sleep, out = console.log, err = console.error } = {}) {
263
321
  const args = parseArgs(argv);
264
322
  const cmd = args._[0];
265
323
  if (!cmd || args.flags.help || cmd === "help") { out(HELP); return cmd || args.flags.help ? 0 : 2; }
266
- const commands = { find, check, pay, spent };
324
+ const commands = { connect, init, wallet: walletInfo, find, check, pay, spent };
267
325
  if (!commands[cmd]) { err(`unknown command "${cmd}"\n\n${HELP}`); return 2; }
268
326
  try {
269
- const r = await commands[cmd](args, { fetch, env, store, createPayingFetch, safeFetchOptions });
327
+ const say = args.flags.json ? () => {} : (s) => err(s);
328
+ const r = await commands[cmd](args, { fetch, env, store, createPayingFetch, safeFetchOptions, say, sleep });
270
329
  out(args.flags.json ? JSON.stringify(r) : text(cmd === "pay" && args.flags["dry-run"] ? "check" : cmd, r));
271
330
  return r.ok ? 0 : 1;
272
331
  } catch (e) {
package/index.js CHANGED
@@ -51,7 +51,7 @@ export const PRESIGN_GUARD_URL = "https://presign-guard.fizzl.eu";
51
51
  export const INJECTION_CAP = "$0.02"; // presign-guard's injection check costs $0.01; never more than twice that
52
52
  // The header a checked answer carries: green, orange, red (only with onInjection "flag" or a function) or unchecked.
53
53
  export const ANSWER_CHECK_HEADER = "x-answer-check";
54
- export const VERSION = "0.17.0";
54
+ export const VERSION = "0.19.0";
55
55
  // Doctor sees which calls come from this package (in its usage counts), nothing more.
56
56
  const DOCTOR_HEADERS = { accept: "application/json", "user-agent": `x402-safe-fetch/${VERSION}` };
57
57
  export const BASE = "eip155:8453";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-safe-fetch",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "A fetch for AI agents that checks an unknown x402 endpoint ($0.001 x402 Doctor preflight) before paying it, and never pays above your budget.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,6 +21,7 @@
21
21
  "files": [
22
22
  "index.js",
23
23
  "cli.js",
24
+ "wallet.js",
24
25
  "receipt.js",
25
26
  "paid-guard.js",
26
27
  "hook.js",
package/wallet.js ADDED
@@ -0,0 +1,144 @@
1
+ // The command line's own wallet, and its link to a Fizzl Agent Wallet (wallet.fizzl.eu).
2
+ //
3
+ // init makes a dedicated wallet for the command line: a new key in ~/.x402-safe-fetch/wallet.json (readable by you
4
+ // only), so nobody has to paste a private key. connect links that wallet to the owner's Fizzl Agent Wallet with a
5
+ // code the owner approves on their phone (the same link Claude Desktop's wallet uses: /v1/pair). Once linked, every
6
+ // payment is first reserved on the wallet server: the owner's limits apply, and above them the owner is asked
7
+ // (Telegram or the dashboard) before anything is signed. The key never leaves this computer.
8
+
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { SafePayError } from "./index.js";
12
+
13
+ export const WALLET_URL = "https://wallet.fizzl.eu";
14
+ export const BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
15
+ const BASE_RPC = "https://mainnet.base.org";
16
+ export const ONRAMP_URL = "https://x402-doctor.fizzl.eu/api/onramp";
17
+
18
+ // A link to buy USDC on Base with a card at Coinbase, delivered to `address`. Each visit starts a new Coinbase session
19
+ // (x402 Doctor asks Coinbase Onramp for it); the buyer pays Coinbase and Fizzl never holds the money.
20
+ export function buyUsdcUrl(address, { amount = 5 } = {}) {
21
+ return `${ONRAMP_URL}?${new URLSearchParams({ address, amount: String(amount) })}`;
22
+ }
23
+
24
+ export function walletFile(home) {
25
+ return path.join(home, "wallet.json");
26
+ }
27
+
28
+ export function readWallet(home) {
29
+ try {
30
+ const w = JSON.parse(fs.readFileSync(walletFile(home), "utf8"));
31
+ return /^0x[0-9a-fA-F]{64}$/.test(w?.privateKey ?? "") ? w : null;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ export function writeWallet(home, wallet) {
38
+ fs.mkdirSync(home, { recursive: true, mode: 0o700 });
39
+ const file = walletFile(home);
40
+ fs.writeFileSync(file, JSON.stringify(wallet, null, 2), { mode: 0o600 });
41
+ try { fs.chmodSync(file, 0o600); } catch { /* not on every file system */ }
42
+ }
43
+
44
+ // A wallet for this computer: the existing one, or a new key. Needs viem (a peer dependency of x402-safe-fetch).
45
+ export async function ensureWallet(home) {
46
+ const existing = readWallet(home);
47
+ if (existing) return { wallet: existing, created: false };
48
+ const { generatePrivateKey, privateKeyToAccount } = await import("viem/accounts");
49
+ const privateKey = generatePrivateKey();
50
+ const wallet = { address: privateKeyToAccount(privateKey).address, privateKey, createdAt: new Date().toISOString() };
51
+ writeWallet(home, wallet);
52
+ return { wallet, created: true };
53
+ }
54
+
55
+ // USDC on Base held by `address`, in dollars (null when the network can't be read).
56
+ export async function usdcBalance(address, { fetch = globalThis.fetch, rpc = BASE_RPC } = {}) {
57
+ try {
58
+ const data = `0x70a08231${address.slice(2).toLowerCase().padStart(64, "0")}`;
59
+ const res = await fetch(rpc, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_call", params: [{ to: BASE_USDC, data }, "latest"] }) });
60
+ const j = await res.json();
61
+ return typeof j?.result === "string" && j.result.startsWith("0x") ? Number(BigInt(j.result)) / 1e6 : null;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ async function call(fetch, base, key, method, p, body) {
68
+ const res = await fetch(`${base}${p}`, {
69
+ method,
70
+ headers: { ...(key ? { authorization: `Bearer ${key}` } : {}), "content-type": "application/json", accept: "application/json" },
71
+ body: body === undefined ? undefined : JSON.stringify(body, (_k, v) => (typeof v === "bigint" ? v.toString() : v)),
72
+ });
73
+ const data = await res.json().catch(() => null);
74
+ if (!res.ok) throw new Error(`wallet server ${p}: ${data?.message ?? data?.error ?? `HTTP ${res.status}`}`);
75
+ return data;
76
+ }
77
+
78
+ /**
79
+ * Links this wallet to a Fizzl Agent Wallet: asks for a code, shows it (onCode), and waits until the owner approves it
80
+ * where they are signed in. Returns { url, key, agent }. The code is useless without the token kept here.
81
+ */
82
+ export async function pair({ address, label = "x402-safe-fetch", url = WALLET_URL, fetch = globalThis.fetch, onCode, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), now = Date.now } = {}) {
83
+ const base = url.replace(/\/$/, "");
84
+ const start = await call(fetch, base, null, "POST", "/v1/pair/start", { address, label });
85
+ if (!start?.code || !start?.token) throw new Error("wallet server: no pairing code");
86
+ const link = start.url || `${base}/pair?code=${start.code}`;
87
+ onCode?.({ code: start.code, url: link, expiresIn: start.expiresIn ?? 600 });
88
+ const until = now() + (Number(start.expiresIn) || 600) * 1000;
89
+ const every = Math.max(1, Number(start.interval) || 3) * 1000;
90
+ while (now() < until) {
91
+ await sleep(every);
92
+ const p = await call(fetch, base, null, "POST", "/v1/pair/poll", { token: start.token });
93
+ if (p?.status === "linked" && typeof p.key === "string") return { url: base, key: p.key, agent: p.agent ?? null };
94
+ }
95
+ throw new Error("the code expired before it was approved (10 minutes); run connect again");
96
+ }
97
+
98
+ /**
99
+ * A signer that asks the owner's wallet server before each signature (reserve), waits for the owner's approval when
100
+ * a limit is reached, and reports what was signed afterwards (finish). Throws SafePayError wallet_denied or
101
+ * wallet_paused when the server says no; `denied` keeps the last refusal for the caller.
102
+ */
103
+ export function serverSigner({ account, server, fetch = globalThis.fetch, purchase, onWait, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), now = Date.now }) {
104
+ const base = server.url.replace(/\/$/, "");
105
+ const reserved = [];
106
+ const state = { denied: null };
107
+ const deny = (code, summary) => {
108
+ state.denied = { code, summary };
109
+ throw new SafePayError(`not paid: your Fizzl wallet says no: ${summary}`, { code });
110
+ };
111
+ return {
112
+ address: account.address,
113
+ state,
114
+ async signTypedData(typedData) {
115
+ const { domain, types, primaryType, message } = typedData;
116
+ const request = { type: "signature", chainId: Number(domain?.chainId ?? 8453), typedData: { domain, types, primaryType, message } };
117
+ let r = await call(fetch, base, server.key, "POST", "/v1/reserve", { method: "signTypedData", request, verdict: null, purchase });
118
+ if (r.status === "pending") {
119
+ onWait?.(r.summary);
120
+ const deadline = (r.expiresAt ?? now() + 600_000) + 10_000;
121
+ let a = null;
122
+ while (now() < deadline) {
123
+ a = await call(fetch, base, server.key, "GET", `/v1/approvals/${encodeURIComponent(r.approvalId)}?wait=25`);
124
+ if (a.status !== "pending") break;
125
+ await sleep(1000);
126
+ }
127
+ if (a?.status === "approved") r = { status: "ok", entries: a.entries ?? [], purchaseId: a.purchaseId ?? null };
128
+ else deny("wallet_denied", `${r.summary} (${a?.status === "denied" ? "denied by the owner" : "no answer in time"})`);
129
+ }
130
+ if (r.status === "paused") deny("wallet_paused", r.summary || "paused");
131
+ if (r.status === "denied") deny("wallet_denied", r.summary || "over a hard limit");
132
+ if (r.status !== "ok") throw new Error(`wallet server: unexpected answer ${r.status}`);
133
+ reserved.push({ entries: r.entries ?? [], purchaseId: r.purchaseId ?? null });
134
+ return account.signTypedData(typedData);
135
+ },
136
+ // After the call: every signed payment counts as spent (a signed authorization can be settled by the seller).
137
+ async finish(result) {
138
+ for (const r of reserved.splice(0)) {
139
+ if (!r.entries.length) continue;
140
+ await call(fetch, base, server.key, "POST", "/v1/spent", { entries: r.entries, result: String(result ?? ""), ...(r.purchaseId ? { purchaseId: r.purchaseId } : {}) }).catch(() => {});
141
+ }
142
+ },
143
+ };
144
+ }