@blockrun/llm 3.18.0 → 3.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/dist/index.cjs CHANGED
@@ -284,7 +284,7 @@ var TRANSIENT_RETRIES = 2;
284
284
  var TRANSIENT_BACKOFF_MS = 1e3;
285
285
  function retryDelay(ms, signal) {
286
286
  signal?.throwIfAborted();
287
- return new Promise((resolve, reject) => {
287
+ return new Promise((resolve2, reject) => {
288
288
  const onAbort = () => {
289
289
  clearTimeout(timer);
290
290
  signal?.removeEventListener("abort", onAbort);
@@ -292,7 +292,7 @@ function retryDelay(ms, signal) {
292
292
  };
293
293
  const timer = setTimeout(() => {
294
294
  signal?.removeEventListener("abort", onAbort);
295
- resolve();
295
+ resolve2();
296
296
  }, ms);
297
297
  signal?.addEventListener("abort", onAbort, { once: true });
298
298
  });
@@ -328,8 +328,8 @@ var ApiKeyAuth = class {
328
328
  this.#key = key;
329
329
  }
330
330
  /** Resolve gateway poll paths without forwarding credentials to another origin. */
331
- resolveUrl(path6) {
332
- const url = new URL(path6, this.apiUrl + "/");
331
+ resolveUrl(path7) {
332
+ const url = new URL(path7, this.apiUrl + "/");
333
333
  if (url.origin !== new URL(this.apiUrl).origin || url.username || url.password) {
334
334
  throw new Error("Refusing to send a BlockRun API key to a different origin.");
335
335
  }
@@ -395,7 +395,7 @@ var ApiKeyAuth = class {
395
395
  if (["failed", "cancelled", "canceled"].includes(String(data.status))) {
396
396
  throw new APIError("Account API job failed or was cancelled", 502, sanitizeErrorResponse(data));
397
397
  }
398
- await new Promise((resolve) => setTimeout(resolve, Math.min(interval, Math.max(0, deadline - Date.now()))));
398
+ await new Promise((resolve2) => setTimeout(resolve2, Math.min(interval, Math.max(0, deadline - Date.now()))));
399
399
  const remaining = deadline - Date.now();
400
400
  if (remaining <= 0) break;
401
401
  try {
@@ -4660,7 +4660,7 @@ function getCostSummary() {
4660
4660
  }
4661
4661
 
4662
4662
  // src/version.ts
4663
- var SDK_VERSION = "3.18.0";
4663
+ var SDK_VERSION = "3.19.0";
4664
4664
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
4665
4665
 
4666
4666
  // src/client.ts
@@ -5769,8 +5769,8 @@ var LLMClient = class _LLMClient {
5769
5769
  * @example
5770
5770
  * const results = await client.exa("search", { query: "latest AI research", numResults: 5 });
5771
5771
  */
5772
- async exa(path6, body) {
5773
- return this.requestWithPaymentRaw(`/v1/exa/${path6}`, body);
5772
+ async exa(path7, body) {
5773
+ return this.requestWithPaymentRaw(`/v1/exa/${path7}`, body);
5774
5774
  }
5775
5775
  /**
5776
5776
  * Neural web search via Exa. Returns semantically relevant URLs and metadata.
@@ -5918,8 +5918,8 @@ var LLMClient = class _LLMClient {
5918
5918
  * const market = await client.pm("kalshi/markets/KXBTC-25MAR14");
5919
5919
  * const results = await client.pm("polymarket/search", { q: "bitcoin" });
5920
5920
  */
5921
- async pm(path6, params) {
5922
- return this.getWithPaymentRaw(`/v1/pm/${path6}`, params);
5921
+ async pm(path7, params) {
5922
+ return this.getWithPaymentRaw(`/v1/pm/${path7}`, params);
5923
5923
  }
5924
5924
  /**
5925
5925
  * Structured query for Predexon prediction market data (POST endpoints).
@@ -5935,8 +5935,8 @@ var LLMClient = class _LLMClient {
5935
5935
  * addresses: ["0xabc...", "0xdef..."],
5936
5936
  * });
5937
5937
  */
5938
- async pmQuery(path6, query) {
5939
- return this.requestWithPaymentRaw(`/v1/pm/${path6}`, query);
5938
+ async pmQuery(path7, query) {
5939
+ return this.requestWithPaymentRaw(`/v1/pm/${path7}`, query);
5940
5940
  }
5941
5941
  // ── PM convenience helpers (Predexon v2) ─────────────────────────────────
5942
5942
  // Thin wrappers over pm() / pmQuery() for the most common v2 endpoints.
@@ -6024,8 +6024,8 @@ var LLMClient = class _LLMClient {
6024
6024
  * "prices/coingecko:bitcoin,base:0x..."
6025
6025
  * @param params - Query parameters passed through to DefiLlama
6026
6026
  */
6027
- async defi(path6, params) {
6028
- return this.getWithPaymentRaw(`/v1/defillama/${path6}`, params);
6027
+ async defi(path7, params) {
6028
+ return this.getWithPaymentRaw(`/v1/defillama/${path7}`, params);
6029
6029
  }
6030
6030
  /** All DeFi protocols with TVL. */
6031
6031
  async defiProtocols() {
@@ -6061,9 +6061,9 @@ var LLMClient = class _LLMClient {
6061
6061
  * @param params - Query params (chainId, sellToken, buyToken, sellAmount, taker, ...)
6062
6062
  * @param body - JSON body — pass to switch to POST (gasless/submit only)
6063
6063
  */
6064
- async dex(path6, params, body) {
6065
- if (body) return this.requestWithPaymentRaw(`/v1/zerox/${path6}`, body);
6066
- return this.getWithPaymentRaw(`/v1/zerox/${path6}`, params);
6064
+ async dex(path7, params, body) {
6065
+ if (body) return this.requestWithPaymentRaw(`/v1/zerox/${path7}`, body);
6066
+ return this.getWithPaymentRaw(`/v1/zerox/${path7}`, params);
6067
6067
  }
6068
6068
  /** Indicative Permit2 swap price — no commitment (free). */
6069
6069
  async dexPrice(params) {
@@ -6107,8 +6107,8 @@ var LLMClient = class _LLMClient {
6107
6107
  * "sandbox/status", "sandbox/terminate"
6108
6108
  * @param body - JSON body for the endpoint
6109
6109
  */
6110
- async modal(path6, body) {
6111
- return this.requestWithPaymentRaw(`/v1/modal/${path6}`, body ?? {});
6110
+ async modal(path7, body) {
6111
+ return this.requestWithPaymentRaw(`/v1/modal/${path7}`, body ?? {});
6112
6112
  }
6113
6113
  /** Create a sandboxed compute environment (CPU / GPU tiers). */
6114
6114
  async modalSandboxCreate(body) {
@@ -6416,7 +6416,7 @@ var ImageClient = class {
6416
6416
  const pollUrl = pollPath.startsWith("/api/") ? `${this.apiUrl.replace(/\/api$/, "")}${pollPath}` : `${this.apiUrl}${pollPath.startsWith("/") ? "" : "/"}${pollPath}`;
6417
6417
  const startedAt = Date.now();
6418
6418
  while (Date.now() - startedAt < POLL_MAX_DURATION_MS) {
6419
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
6419
+ await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
6420
6420
  const pollResp = await this.fetchWithTimeout(pollUrl, {
6421
6421
  method: "GET",
6422
6422
  headers: {
@@ -7614,8 +7614,8 @@ var PhoneClient = class {
7614
7614
  return data;
7615
7615
  }
7616
7616
  // ─── Internals ─────────────────────────────────────────────────────────
7617
- async request(path6, body) {
7618
- const url = `${this.apiUrl}/v1/phone/${path6}`;
7617
+ async request(path7, body) {
7618
+ const url = `${this.apiUrl}/v1/phone/${path7}`;
7619
7619
  const response = await this.fetchWithTimeout(url, {
7620
7620
  method: "POST",
7621
7621
  headers: { "Content-Type": "application/json" },
@@ -8056,10 +8056,10 @@ var PriceClient = class {
8056
8056
  }
8057
8057
  async price(category, symbol, options) {
8058
8058
  if (!symbol) throw new Error("symbol is required");
8059
- const path6 = categoryPath(category, options?.market, "price", symbol);
8059
+ const path7 = categoryPath(category, options?.market, "price", symbol);
8060
8060
  const query = {};
8061
8061
  if (options?.session) query.session = options.session;
8062
- const data = await this.getWithPayment(path6, query);
8062
+ const data = await this.getWithPayment(path7, query);
8063
8063
  return {
8064
8064
  symbol: data.symbol ?? symbol.toUpperCase(),
8065
8065
  price: data.price,
@@ -8078,14 +8078,14 @@ var PriceClient = class {
8078
8078
  if (!options.from || options.from <= 0) {
8079
8079
  throw new Error("history requires options.from (unix seconds)");
8080
8080
  }
8081
- const path6 = categoryPath(category, options.market, "history", symbol);
8081
+ const path7 = categoryPath(category, options.market, "history", symbol);
8082
8082
  const query = {
8083
8083
  resolution: options.resolution ?? "D",
8084
8084
  from: String(options.from)
8085
8085
  };
8086
8086
  if (options.to) query.to = String(options.to);
8087
8087
  if (options.session) query.session = options.session;
8088
- const data = await this.getWithPayment(path6, query);
8088
+ const data = await this.getWithPayment(path7, query);
8089
8089
  return {
8090
8090
  symbol: data.symbol ?? symbol.toUpperCase(),
8091
8091
  resolution: data.resolution ?? (options.resolution ?? "D"),
@@ -8097,12 +8097,12 @@ var PriceClient = class {
8097
8097
  };
8098
8098
  }
8099
8099
  async listSymbols(category, options) {
8100
- const path6 = categoryPath(category, options?.market, "list");
8100
+ const path7 = categoryPath(category, options?.market, "list");
8101
8101
  const query = {
8102
8102
  limit: String(options?.limit && options.limit > 0 ? options.limit : 100)
8103
8103
  };
8104
8104
  if (options?.query) query.q = options.query;
8105
- const data = await this.getWithPayment(path6, query);
8105
+ const data = await this.getWithPayment(path7, query);
8106
8106
  if (Array.isArray(data)) {
8107
8107
  return { symbols: data, count: data.length };
8108
8108
  }
@@ -8275,19 +8275,19 @@ var SurfClient = class {
8275
8275
  * `/v1/surf` is tolerated and stripped). Query params are URL-encoded;
8276
8276
  * arrays become repeated keys (`?a=1&a=2`).
8277
8277
  */
8278
- async get(path6, params) {
8279
- const url = this.buildUrl(path6, params);
8278
+ async get(path7, params) {
8279
+ const url = this.buildUrl(path7, params);
8280
8280
  return this.requestWithPayment(url, "GET");
8281
8281
  }
8282
8282
  /**
8283
8283
  * POST a Surf endpoint with a JSON body. Same path normalization as `get`.
8284
8284
  */
8285
- async post(path6, body) {
8286
- const url = this.buildUrl(path6);
8285
+ async post(path7, body) {
8286
+ const url = this.buildUrl(path7);
8287
8287
  return this.requestWithPayment(url, "POST", body);
8288
8288
  }
8289
- buildUrl(path6, params) {
8290
- let normalized = path6.startsWith("/") ? path6 : `/${path6}`;
8289
+ buildUrl(path7, params) {
8290
+ let normalized = path7.startsWith("/") ? path7 : `/${path7}`;
8291
8291
  if (!normalized.startsWith(SURF_PREFIX)) {
8292
8292
  normalized = `${SURF_PREFIX}${normalized}`;
8293
8293
  }
@@ -8745,15 +8745,15 @@ var BlockrunClient = class {
8745
8745
  * `/api` is tolerated and stripped). Query params are URL-encoded; arrays
8746
8746
  * become repeated keys (`?a=1&a=2`); undefined/null are dropped.
8747
8747
  */
8748
- async get(path6, params) {
8749
- const url = this.buildUrl(path6, params);
8748
+ async get(path7, params) {
8749
+ const url = this.buildUrl(path7, params);
8750
8750
  return this.requestWithPayment(url, "GET");
8751
8751
  }
8752
8752
  /**
8753
8753
  * POST a BlockRun endpoint with a JSON body.
8754
8754
  */
8755
- async post(path6, body) {
8756
- const url = this.buildUrl(path6);
8755
+ async post(path7, body) {
8756
+ const url = this.buildUrl(path7);
8757
8757
  return this.requestWithPayment(url, "POST", body);
8758
8758
  }
8759
8759
  /**
@@ -8777,8 +8777,8 @@ var BlockrunClient = class {
8777
8777
  * short-circuits and returns the body. Most long-running endpoints (image,
8778
8778
  * video, music, voice) return 202 with a poll_url.
8779
8779
  */
8780
- async poll(path6, body, options) {
8781
- const submitUrl = this.buildUrl(path6);
8780
+ async poll(path7, body, options) {
8781
+ const submitUrl = this.buildUrl(path7);
8782
8782
  const budgetMs = options?.budgetMs ?? DEFAULT_POLL_BUDGET_MS;
8783
8783
  const intervalMs = options?.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
8784
8784
  const resp402 = await this.fetchWithTimeout(submitUrl, {
@@ -8878,8 +8878,8 @@ var BlockrunClient = class {
8878
8878
  * process.stdout.write(chunk.choices?.[0]?.delta?.content ?? "");
8879
8879
  * }
8880
8880
  */
8881
- async *stream(path6, body) {
8882
- const url = this.buildUrl(path6);
8881
+ async *stream(path7, body) {
8882
+ const url = this.buildUrl(path7);
8883
8883
  const requestBody = JSON.stringify(body ?? {});
8884
8884
  const resp402 = await this.fetchWithTimeout(url, {
8885
8885
  method: "POST",
@@ -8922,8 +8922,8 @@ var BlockrunClient = class {
8922
8922
  // --------------------------------------------------------------------
8923
8923
  // Internal: shared infrastructure
8924
8924
  // --------------------------------------------------------------------
8925
- buildUrl(path6, params) {
8926
- let normalized = path6.startsWith("/") ? path6 : `/${path6}`;
8925
+ buildUrl(path7, params) {
8926
+ let normalized = path7.startsWith("/") ? path7 : `/${path7}`;
8927
8927
  if (normalized.startsWith("/api/")) {
8928
8928
  normalized = normalized.slice(4);
8929
8929
  }
@@ -9569,6 +9569,332 @@ async function getOrCreateSolanaWallet() {
9569
9569
  return { address, privateKey, isNew: true };
9570
9570
  }
9571
9571
 
9572
+ // src/solana-batch.ts
9573
+ var fs6 = __toESM(require("fs"), 1);
9574
+ var path5 = __toESM(require("path"), 1);
9575
+ var BATCH_SCHEME = "batch-settlement";
9576
+ var INSTALL_HINT2 = "npm install @x402/core@~2.28.0 @x402/svm@~2.28.0 @solana/kit";
9577
+ async function load(pkg, importer) {
9578
+ try {
9579
+ return await importer();
9580
+ } catch (err) {
9581
+ throw new Error(
9582
+ `@blockrun/llm: Solana batch-settlement requires the optional peer dependency "${pkg}", which is not installed.
9583
+
9584
+ ${INSTALL_HINT2}
9585
+
9586
+ Original error: ${err instanceof Error ? err.message : String(err)}`,
9587
+ { cause: err }
9588
+ );
9589
+ }
9590
+ }
9591
+ var FileChannelStorage = class {
9592
+ constructor(file) {
9593
+ this.file = file;
9594
+ }
9595
+ file;
9596
+ read() {
9597
+ try {
9598
+ const parsed = JSON.parse(fs6.readFileSync(this.file, "utf8"));
9599
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
9600
+ } catch (err) {
9601
+ if (err.code === "ENOENT") return {};
9602
+ throw err;
9603
+ }
9604
+ }
9605
+ write(all) {
9606
+ fs6.mkdirSync(path5.dirname(this.file), { recursive: true, mode: 448 });
9607
+ const tmp = `${this.file}.${process.pid}.tmp`;
9608
+ fs6.writeFileSync(tmp, JSON.stringify(all, null, 2), { mode: 384 });
9609
+ fs6.renameSync(tmp, this.file);
9610
+ }
9611
+ async get(key) {
9612
+ return this.read()[key];
9613
+ }
9614
+ async set(key, record) {
9615
+ const all = this.read();
9616
+ all[key] = record;
9617
+ this.write(all);
9618
+ }
9619
+ async delete(key) {
9620
+ const all = this.read();
9621
+ if (!(key in all)) return;
9622
+ delete all[key];
9623
+ this.write(all);
9624
+ }
9625
+ /** Forget every channel in this file. */
9626
+ clear() {
9627
+ try {
9628
+ fs6.unlinkSync(this.file);
9629
+ } catch (err) {
9630
+ if (err.code !== "ENOENT") throw err;
9631
+ }
9632
+ }
9633
+ };
9634
+ function processAlive(pid) {
9635
+ try {
9636
+ process.kill(pid, 0);
9637
+ return true;
9638
+ } catch (err) {
9639
+ return err.code === "EPERM";
9640
+ }
9641
+ }
9642
+ var heldLocks = /* @__PURE__ */ new Set();
9643
+ var exitHookInstalled = false;
9644
+ function lockChannelFile(file) {
9645
+ if (heldLocks.has(file)) return void 0;
9646
+ const lock = `${file}.lock`;
9647
+ fs6.mkdirSync(path5.dirname(lock), { recursive: true, mode: 448 });
9648
+ for (let attempt = 0; attempt < 2; attempt += 1) {
9649
+ try {
9650
+ fs6.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
9651
+ heldLocks.add(file);
9652
+ if (!exitHookInstalled) {
9653
+ exitHookInstalled = true;
9654
+ process.once("exit", () => {
9655
+ for (const held of heldLocks) {
9656
+ try {
9657
+ fs6.unlinkSync(`${held}.lock`);
9658
+ } catch {
9659
+ }
9660
+ }
9661
+ });
9662
+ }
9663
+ return void 0;
9664
+ } catch (err) {
9665
+ if (err.code !== "EEXIST") throw err;
9666
+ }
9667
+ const owner = Number(fs6.readFileSync(lock, "utf8").trim());
9668
+ if (Number.isInteger(owner) && owner > 0 && owner !== process.pid && processAlive(owner)) return owner;
9669
+ try {
9670
+ fs6.unlinkSync(lock);
9671
+ } catch {
9672
+ }
9673
+ }
9674
+ return -1;
9675
+ }
9676
+ function pendingDeposit(pending) {
9677
+ const entries = Array.isArray(pending) ? pending : [pending];
9678
+ return entries.some(
9679
+ (entry) => entry?.payment?.payload?.type === "deposit"
9680
+ );
9681
+ }
9682
+ function dropOrphanedPending(storage) {
9683
+ return {
9684
+ get: async (key) => {
9685
+ const record = await storage.get(key);
9686
+ if (!record?.pending || record.channelConfig?.voucherSigner !== "server") return record;
9687
+ if (!record.hasConfirmedState || pendingDeposit(record.pending)) {
9688
+ await storage.delete(key);
9689
+ return void 0;
9690
+ }
9691
+ const { pending: _pending, hasConfirmedState: _confirmed, ...confirmed } = record;
9692
+ await storage.set(key, confirmed);
9693
+ return confirmed;
9694
+ },
9695
+ set: (key, record) => storage.set(key, record),
9696
+ delete: (key) => storage.delete(key)
9697
+ };
9698
+ }
9699
+ function offersBatch(paymentRequired) {
9700
+ return (paymentRequired.accepts ?? []).some((accept) => accept.scheme === BATCH_SCHEME);
9701
+ }
9702
+ function atomicToUsd(amount) {
9703
+ return amount !== void 0 && /^\d+$/.test(amount) ? Number(amount) / 1e6 : void 0;
9704
+ }
9705
+ async function isBatchRefusal(response) {
9706
+ if (response.status === 402) return "payment_required";
9707
+ if (![400, 403, 409, 503].includes(response.status)) return void 0;
9708
+ let body;
9709
+ try {
9710
+ body = await response.clone().json();
9711
+ } catch {
9712
+ return void 0;
9713
+ }
9714
+ const { error, code } = body ?? {};
9715
+ if (code === "PAYMENT_VERIFICATION_UNAVAILABLE") return code;
9716
+ return typeof error === "string" && error.startsWith("batch_") ? error : void 0;
9717
+ }
9718
+ var wallets = /* @__PURE__ */ new Map();
9719
+ var SolanaBatchPayer = class {
9720
+ constructor(init) {
9721
+ this.init = init;
9722
+ const { operators } = init.options;
9723
+ if (!Array.isArray(operators) || operators.length === 0 || operators.some((o) => typeof o !== "string" || !o)) {
9724
+ throw new Error("batch.operators must list at least one base58 operator public key");
9725
+ }
9726
+ }
9727
+ init;
9728
+ state;
9729
+ async storeFile() {
9730
+ const { channelStore } = this.init.options;
9731
+ if (channelStore === false) return void 0;
9732
+ return path5.resolve(channelStore ?? path5.join(paths().dir, "solana-batch", `${await this.init.address()}.json`));
9733
+ }
9734
+ /**
9735
+ * This wallet's shared batch state, or why this client cannot use batch:
9736
+ * another client in this process holds the wallet with a different trust
9737
+ * configuration, or another live process owns its channel file.
9738
+ */
9739
+ wallet() {
9740
+ this.state ??= (async () => {
9741
+ const address = await this.init.address();
9742
+ const file = await this.storeFile();
9743
+ const { operators, maxDeposit } = this.init.options;
9744
+ const config = JSON.stringify([[...operators].sort(), String(maxDeposit ?? ""), file ?? null, this.init.rpcUrl]);
9745
+ const existing = wallets.get(address);
9746
+ if (existing) {
9747
+ return existing.config === config ? existing : "another client in this process uses this wallet with different batch options";
9748
+ }
9749
+ if (file) {
9750
+ const owner = lockChannelFile(file);
9751
+ if (owner !== void 0) return `channel store ${file} is in use by process ${owner}`;
9752
+ }
9753
+ const created = { config, busy: false, warned: /* @__PURE__ */ new Set() };
9754
+ wallets.set(address, created);
9755
+ return created;
9756
+ })();
9757
+ return this.state;
9758
+ }
9759
+ build(wallet) {
9760
+ wallet.client ??= (async () => {
9761
+ const [core, svm, kit] = await Promise.all([
9762
+ load("@x402/core", () => import("@x402/core/client")),
9763
+ load("@x402/svm", () => import("@x402/svm/batch-settlement/client")),
9764
+ load("@solana/kit", () => import("@solana/kit"))
9765
+ ]);
9766
+ const signer = await kit.createKeyPairSignerFromBytes(await this.init.secretKey());
9767
+ const file = await this.storeFile();
9768
+ const storage = file ? dropOrphanedPending(new FileChannelStorage(file)) : void 0;
9769
+ const scheme = new svm.BatchSvmScheme(signer, {
9770
+ rpcUrl: this.init.rpcUrl,
9771
+ serverSignedChannelsPolicy: {
9772
+ allowedOperators: this.init.options.operators,
9773
+ maxDeposit: this.init.options.maxDeposit ?? svm.DEFAULT_SERVER_SIGNED_MAX_DEPOSIT
9774
+ },
9775
+ ...storage ? { channelStorage: storage } : {}
9776
+ });
9777
+ const client = new core.x402Client().register("solana:*", scheme).registerPolicy(scheme.paymentPolicy);
9778
+ client.setSpendControls({ maxAmountPerPayment: false });
9779
+ return {
9780
+ http: new core.x402HTTPClient(client),
9781
+ refund: (url) => scheme.refund(url)
9782
+ };
9783
+ })();
9784
+ wallet.client.catch(() => {
9785
+ wallet.client = void 0;
9786
+ });
9787
+ return wallet.client;
9788
+ }
9789
+ /**
9790
+ * Drop everything this wallet knows about its channel, on disk and in
9791
+ * memory. The next call rebuilds the scheme, which reads the channel's real
9792
+ * state (deposit, open or closed) from the chain.
9793
+ */
9794
+ async forget(wallet) {
9795
+ wallet.client = void 0;
9796
+ const file = await this.storeFile();
9797
+ if (file) new FileChannelStorage(file).clear();
9798
+ }
9799
+ warn(wallet, reason) {
9800
+ if (wallet?.warned.has(reason)) return;
9801
+ wallet?.warned.add(reason);
9802
+ console.error(`[@blockrun/llm] batch-settlement unavailable (${reason}); paying with exact`);
9803
+ }
9804
+ /**
9805
+ * Pay one 402 with batch-settlement, or say why the caller should use exact.
9806
+ *
9807
+ * `send` replays the original request with the payment headers. The
9808
+ * returned response is 2xx; any other answer either becomes a fallback (the
9809
+ * gateway charged nothing) or is returned to the caller's own error path.
9810
+ */
9811
+ async pay(paymentRequired, send) {
9812
+ if (!offersBatch(paymentRequired)) return { kind: "fallback", reason: "not_offered" };
9813
+ const wallet = await this.wallet();
9814
+ if (typeof wallet === "string") {
9815
+ this.warn(void 0, wallet);
9816
+ return { kind: "fallback", reason: wallet };
9817
+ }
9818
+ if (wallet.busy) return { kind: "fallback", reason: "channel_busy" };
9819
+ wallet.busy = true;
9820
+ try {
9821
+ let http;
9822
+ let payload;
9823
+ try {
9824
+ http = (await this.build(wallet)).http;
9825
+ payload = await http.createPaymentPayload(paymentRequired);
9826
+ } catch (err) {
9827
+ const reason = err instanceof Error ? err.message : String(err);
9828
+ this.warn(wallet, reason);
9829
+ return { kind: "fallback", reason };
9830
+ }
9831
+ const deposit = payload.payload?.type === "deposit";
9832
+ let response;
9833
+ try {
9834
+ response = await send(http.encodePaymentSignatureHeader(payload));
9835
+ } catch (err) {
9836
+ if (deposit) await this.forget(wallet);
9837
+ else await this.settle(http, payload, () => null, 0);
9838
+ throw err;
9839
+ }
9840
+ const getHeader = (name) => response.headers.get(name);
9841
+ const settled = await this.settle(http, payload, getHeader, response.status);
9842
+ if (deposit && !(response.ok && settled?.success === true)) await this.forget(wallet);
9843
+ if (response.ok) {
9844
+ const charged = atomicToUsd(settled?.amount) ?? atomicToUsd(payload.accepted?.amount) ?? 0;
9845
+ return { kind: "paid", response, chargedUsd: charged };
9846
+ }
9847
+ const refusal = await isBatchRefusal(response);
9848
+ if (refusal) {
9849
+ this.warn(wallet, refusal);
9850
+ return { kind: "fallback", reason: refusal };
9851
+ }
9852
+ return { kind: "failed", response };
9853
+ } finally {
9854
+ wallet.busy = false;
9855
+ }
9856
+ }
9857
+ /**
9858
+ * Hand the gateway's answer to the scheme, which verifies the operator's
9859
+ * voucher and advances (or rolls back) the channel's local state.
9860
+ *
9861
+ * A malformed receipt must not lose a response the caller has been served,
9862
+ * so a scheme error is logged rather than thrown.
9863
+ */
9864
+ async settle(http, payload, getHeader, status2) {
9865
+ try {
9866
+ return (await http.processPaymentResult(payload, getHeader, status2)).settleResponse;
9867
+ } catch (err) {
9868
+ console.error(
9869
+ `[@blockrun/llm] batch-settlement receipt not reconciled: ${err instanceof Error ? err.message : String(err)}`
9870
+ );
9871
+ return void 0;
9872
+ }
9873
+ }
9874
+ /**
9875
+ * Close the channel and return its unused escrow to the wallet.
9876
+ *
9877
+ * The scheme keeps a closed channel in its records, and would go on paying
9878
+ * into it, so a successful close forgets the channel. The next batch call
9879
+ * finds no open channel on-chain and opens a new one.
9880
+ *
9881
+ * @param url - any batch-enabled route on the gateway the channel was opened with.
9882
+ */
9883
+ async close(url) {
9884
+ const wallet = await this.wallet();
9885
+ if (typeof wallet === "string") throw new Error(`batch-settlement unavailable: ${wallet}`);
9886
+ if (wallet.busy) throw new Error("batch-settlement channel has a request in flight; close it when the call returns");
9887
+ wallet.busy = true;
9888
+ try {
9889
+ const result = await (await this.build(wallet)).refund(url);
9890
+ await this.forget(wallet);
9891
+ return result;
9892
+ } finally {
9893
+ wallet.busy = false;
9894
+ }
9895
+ }
9896
+ };
9897
+
9572
9898
  // src/solana-client.ts
9573
9899
  var SOLANA_API_URL = "https://sol.blockrun.ai/api";
9574
9900
  var DEFAULT_MAX_TOKENS2 = 1024;
@@ -9632,7 +9958,7 @@ async function isSafeStaleBlockhashResponse(response) {
9632
9958
  }
9633
9959
  async function waitForStaleRetry(attempt) {
9634
9960
  await new Promise(
9635
- (resolve) => setTimeout(resolve, STALE_BLOCKHASH_RETRY_BACKOFFS_MS[attempt])
9961
+ (resolve2) => setTimeout(resolve2, STALE_BLOCKHASH_RETRY_BACKOFFS_MS[attempt])
9636
9962
  );
9637
9963
  }
9638
9964
  var DEFAULT_SOLANA_RPC_URL = "https://sol.blockrun.ai/api/v1/solana/rpc";
@@ -9676,6 +10002,7 @@ var SolanaLLMClient = class {
9676
10002
  addressCache = null;
9677
10003
  modelPricingCache = null;
9678
10004
  modelPricingPromise = null;
10005
+ batchPayer;
9679
10006
  constructor(options = {}) {
9680
10007
  this.apiAuth = resolveApiKeyAuth(options);
9681
10008
  if (!this.apiAuth) {
@@ -9695,6 +10022,25 @@ var SolanaLLMClient = class {
9695
10022
  this.rpcUrl = rpc.url;
9696
10023
  this.rpcHeaders = rpc.headers;
9697
10024
  this.timeout = options.timeout || DEFAULT_TIMEOUT14;
10025
+ if (options.batch && !this.apiAuth) {
10026
+ this.batchPayer = new SolanaBatchPayer({
10027
+ options: options.batch,
10028
+ secretKey: () => solanaKeyToBytes(this.privateKey),
10029
+ address: () => this.getWalletAddress(),
10030
+ rpcUrl: this.rpcUrl
10031
+ });
10032
+ }
10033
+ }
10034
+ /**
10035
+ * Close this wallet's batch-settlement channel and return its unused escrow.
10036
+ *
10037
+ * The gateway closes it cooperatively when it can; otherwise this starts a
10038
+ * payer-forced close and the escrow comes back after the channel's grace
10039
+ * period. Only meaningful with the `batch` option.
10040
+ */
10041
+ async closeBatchChannel() {
10042
+ if (!this.batchPayer) throw new Error("closeBatchChannel() requires the `batch` option in wallet mode");
10043
+ return this.batchPayer.close(`${this.apiUrl}/v1/chat/completions`);
9698
10044
  }
9699
10045
  /** Get Solana wallet address (public key in base58). */
9700
10046
  async getWalletAddress() {
@@ -9888,8 +10234,8 @@ var SolanaLLMClient = class {
9888
10234
  * how you end up debugging a request you did not send.
9889
10235
  * @returns each decoded SSE frame, in order.
9890
10236
  */
9891
- async *stream(path6, body) {
9892
- const url = this.buildUrl(path6);
10237
+ async *stream(path7, body) {
10238
+ const url = this.buildUrl(path7);
9893
10239
  const response = await this.openPaidStream(url, JSON.stringify(body ?? {}));
9894
10240
  yield* readSseFrames(
9895
10241
  response,
@@ -9961,8 +10307,8 @@ var SolanaLLMClient = class {
9961
10307
  * @param path - endpoint path, with or without a leading slash.
9962
10308
  * @returns the absolute URL to call.
9963
10309
  */
9964
- buildUrl(path6) {
9965
- let normalized = path6.startsWith("/") ? path6 : `/${path6}`;
10310
+ buildUrl(path7) {
10311
+ let normalized = path7.startsWith("/") ? path7 : `/${path7}`;
9966
10312
  if (normalized.startsWith("/api/")) normalized = normalized.slice(4);
9967
10313
  return `${this.apiUrl}${normalized}`;
9968
10314
  }
@@ -10041,11 +10387,11 @@ var SolanaLLMClient = class {
10041
10387
  return data;
10042
10388
  }
10043
10389
  // ── Prediction Markets (Powered by Predexon) ────────────────────────────
10044
- async pm(path6, params) {
10045
- return this.getWithPaymentRaw(`/v1/pm/${path6}`, params);
10390
+ async pm(path7, params) {
10391
+ return this.getWithPaymentRaw(`/v1/pm/${path7}`, params);
10046
10392
  }
10047
- async pmQuery(path6, query) {
10048
- return this.requestWithPaymentRaw(`/v1/pm/${path6}`, query);
10393
+ async pmQuery(path7, query) {
10394
+ return this.requestWithPaymentRaw(`/v1/pm/${path7}`, query);
10049
10395
  }
10050
10396
  // ── Exa Web Search (Powered by Exa) ──────────────────────────────────────
10051
10397
  /**
@@ -10057,8 +10403,8 @@ var SolanaLLMClient = class {
10057
10403
  * @example
10058
10404
  * const results = await client.exa("search", { query: "latest AI research", numResults: 5 });
10059
10405
  */
10060
- async exa(path6, body) {
10061
- return this.requestWithPaymentRaw(`/v1/exa/${path6}`, body);
10406
+ async exa(path7, body) {
10407
+ return this.requestWithPaymentRaw(`/v1/exa/${path7}`, body);
10062
10408
  }
10063
10409
  /**
10064
10410
  * Neural and keyword web search via Exa (Solana payment).
@@ -10108,8 +10454,8 @@ var SolanaLLMClient = class {
10108
10454
  * "prices/coingecko:bitcoin,base:0x..."
10109
10455
  * @param params - Query parameters passed through to DefiLlama
10110
10456
  */
10111
- async defi(path6, params) {
10112
- return this.getWithPaymentRaw(`/v1/defillama/${path6}`, params);
10457
+ async defi(path7, params) {
10458
+ return this.getWithPaymentRaw(`/v1/defillama/${path7}`, params);
10113
10459
  }
10114
10460
  /** All DeFi protocols with TVL. */
10115
10461
  async defiProtocols() {
@@ -10145,9 +10491,9 @@ var SolanaLLMClient = class {
10145
10491
  * @param params - Query params (chainId, sellToken, buyToken, sellAmount, taker, ...)
10146
10492
  * @param body - JSON body — pass to switch to POST (gasless/submit only)
10147
10493
  */
10148
- async dex(path6, params, body) {
10149
- if (body) return this.requestWithPaymentRaw(`/v1/zerox/${path6}`, body);
10150
- return this.getWithPaymentRaw(`/v1/zerox/${path6}`, params);
10494
+ async dex(path7, params, body) {
10495
+ if (body) return this.requestWithPaymentRaw(`/v1/zerox/${path7}`, body);
10496
+ return this.getWithPaymentRaw(`/v1/zerox/${path7}`, params);
10151
10497
  }
10152
10498
  /** Indicative Permit2 swap price — no commitment (free). */
10153
10499
  async dexPrice(params) {
@@ -10191,8 +10537,8 @@ var SolanaLLMClient = class {
10191
10537
  * "sandbox/status", "sandbox/terminate"
10192
10538
  * @param body - JSON body for the endpoint
10193
10539
  */
10194
- async modal(path6, body) {
10195
- return this.requestWithPaymentRaw(`/v1/modal/${path6}`, body ?? {});
10540
+ async modal(path7, body) {
10541
+ return this.requestWithPaymentRaw(`/v1/modal/${path7}`, body ?? {});
10196
10542
  }
10197
10543
  /** Create a sandboxed compute environment (CPU / GPU tiers). */
10198
10544
  async modalSandboxCreate(body) {
@@ -10229,7 +10575,31 @@ var SolanaLLMClient = class {
10229
10575
  });
10230
10576
  if (response.status === 402) {
10231
10577
  try {
10232
- return await this.handlePaymentAndRetry(url, body, response, staleRetries > 0);
10578
+ const paymentRequired = await this.readPaymentRequired(response);
10579
+ if (this.batchPayer && staleRetries === 0) {
10580
+ const batch = await this.batchPayer.pay(
10581
+ paymentRequired,
10582
+ (paymentHeaders) => this.fetchWithTimeout(url, {
10583
+ method: "POST",
10584
+ headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT, ...paymentHeaders },
10585
+ body: JSON.stringify(body)
10586
+ })
10587
+ );
10588
+ if (batch.kind === "paid") {
10589
+ this.recordSettlement(batch.chargedUsd);
10590
+ return batch.response.json();
10591
+ }
10592
+ if (batch.kind === "failed") {
10593
+ let errorBody;
10594
+ try {
10595
+ errorBody = await batch.response.json();
10596
+ } catch {
10597
+ errorBody = { error: "Request failed" };
10598
+ }
10599
+ throw new APIError(`API error after payment: ${batch.response.status}`, batch.response.status, sanitizeErrorResponse(errorBody));
10600
+ }
10601
+ }
10602
+ return await this.handlePaymentAndRetry(url, body, paymentRequired, staleRetries > 0);
10233
10603
  } catch (error) {
10234
10604
  if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
10235
10605
  await waitForStaleRetry(staleRetries++);
@@ -10350,6 +10720,15 @@ var SolanaLLMClient = class {
10350
10720
  );
10351
10721
  }
10352
10722
  async signPaymentFrom402(url, response, forceFreshBlockhash, resourceFallback = url) {
10723
+ return this.signExactPayment(
10724
+ url,
10725
+ await this.readPaymentRequired(response),
10726
+ forceFreshBlockhash,
10727
+ resourceFallback
10728
+ );
10729
+ }
10730
+ /** Read a `402`'s payment requirements from its header, or its JSON body. */
10731
+ async readPaymentRequired(response) {
10353
10732
  let paymentHeader = response.headers.get("payment-required");
10354
10733
  if (!paymentHeader) {
10355
10734
  try {
@@ -10363,8 +10742,14 @@ var SolanaLLMClient = class {
10363
10742
  if (!paymentHeader) {
10364
10743
  throw new PaymentError("402 response but no payment requirements found");
10365
10744
  }
10366
- const paymentRequired = parsePaymentRequired(paymentHeader);
10367
- const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
10745
+ return parsePaymentRequired(paymentHeader);
10746
+ }
10747
+ /** Sign the `exact` accept of a 402 as an SPL TransferChecked payment. */
10748
+ async signExactPayment(url, paymentRequired, forceFreshBlockhash, resourceFallback = url) {
10749
+ const details = extractPaymentDetails(
10750
+ { ...paymentRequired, accepts: paymentRequired.accepts.filter((accept) => accept.scheme !== BATCH_SCHEME) },
10751
+ SOLANA_NETWORK
10752
+ );
10368
10753
  if (!details.network?.startsWith("solana:")) {
10369
10754
  throw new PaymentError(
10370
10755
  `Expected Solana payment network, got: ${details.network}. Use LLMClient for Base payments.`
@@ -10397,10 +10782,10 @@ var SolanaLLMClient = class {
10397
10782
  );
10398
10783
  return { paymentPayload, costUsd: parseFloat(details.amount) / 1e6 };
10399
10784
  }
10400
- async handlePaymentAndRetry(url, body, response, forceFreshBlockhash = false) {
10401
- const { paymentPayload, costUsd } = await this.signPaymentFrom402(
10785
+ async handlePaymentAndRetry(url, body, paymentRequired, forceFreshBlockhash = false) {
10786
+ const { paymentPayload, costUsd } = await this.signExactPayment(
10402
10787
  url,
10403
- response,
10788
+ paymentRequired,
10404
10789
  forceFreshBlockhash,
10405
10790
  `${this.apiUrl}/v1/chat/completions`
10406
10791
  );
@@ -10561,13 +10946,13 @@ function solanaClient(options = {}) {
10561
10946
  }
10562
10947
 
10563
10948
  // src/cache.ts
10564
- var fs6 = __toESM(require("fs"), 1);
10565
- var path5 = __toESM(require("path"), 1);
10949
+ var fs7 = __toESM(require("fs"), 1);
10950
+ var path6 = __toESM(require("path"), 1);
10566
10951
  var os4 = __toESM(require("os"), 1);
10567
10952
  var crypto2 = __toESM(require("crypto"), 1);
10568
- var CACHE_DIR = path5.join(os4.homedir(), ".blockrun", "cache");
10569
- var DATA_DIR = path5.join(os4.homedir(), ".blockrun", "data");
10570
- var COST_LOG_FILE2 = path5.join(os4.homedir(), ".blockrun", "cost_log.jsonl");
10953
+ var CACHE_DIR = path6.join(os4.homedir(), ".blockrun", "cache");
10954
+ var DATA_DIR = path6.join(os4.homedir(), ".blockrun", "data");
10955
+ var COST_LOG_FILE2 = path6.join(os4.homedir(), ".blockrun", "cost_log.jsonl");
10571
10956
  var DEFAULT_TTL = {
10572
10957
  "/v1/partner/": 3600 * 1e3,
10573
10958
  "/v1/pm/": 1800 * 1e3,
@@ -10587,19 +10972,19 @@ function cacheKey(endpoint, body) {
10587
10972
  return crypto2.createHash("sha256").update(keyData).digest("hex").slice(0, 16);
10588
10973
  }
10589
10974
  function cachePath(key) {
10590
- return path5.join(CACHE_DIR, `${key}.json`);
10975
+ return path6.join(CACHE_DIR, `${key}.json`);
10591
10976
  }
10592
10977
  function getCached(key) {
10593
10978
  const filePath = cachePath(key);
10594
- if (!fs6.existsSync(filePath)) return null;
10979
+ if (!fs7.existsSync(filePath)) return null;
10595
10980
  try {
10596
- const raw = fs6.readFileSync(filePath, "utf-8");
10981
+ const raw = fs7.readFileSync(filePath, "utf-8");
10597
10982
  const entry = JSON.parse(raw);
10598
10983
  const ttl = entry.ttlMs ?? getTtl(entry.endpoint ?? "");
10599
10984
  if (ttl <= 0) return null;
10600
10985
  if (Date.now() - entry.cachedAt > ttl) {
10601
10986
  try {
10602
- fs6.unlinkSync(filePath);
10987
+ fs7.unlinkSync(filePath);
10603
10988
  } catch {
10604
10989
  }
10605
10990
  return null;
@@ -10618,7 +11003,7 @@ function getCachedByRequest(endpoint, body) {
10618
11003
  function setCache(key, data, ttlMs) {
10619
11004
  if (ttlMs <= 0) return;
10620
11005
  try {
10621
- fs6.mkdirSync(CACHE_DIR, { recursive: true });
11006
+ fs7.mkdirSync(CACHE_DIR, { recursive: true });
10622
11007
  } catch {
10623
11008
  }
10624
11009
  const entry = {
@@ -10627,7 +11012,7 @@ function setCache(key, data, ttlMs) {
10627
11012
  ttlMs
10628
11013
  };
10629
11014
  try {
10630
- fs6.writeFileSync(cachePath(key), JSON.stringify(entry));
11015
+ fs7.writeFileSync(cachePath(key), JSON.stringify(entry));
10631
11016
  } catch {
10632
11017
  }
10633
11018
  }
@@ -10648,7 +11033,7 @@ function readableFilename(endpoint, body) {
10648
11033
  }
10649
11034
  function saveReadable(endpoint, body, response, costUsd) {
10650
11035
  try {
10651
- fs6.mkdirSync(DATA_DIR, { recursive: true });
11036
+ fs7.mkdirSync(DATA_DIR, { recursive: true });
10652
11037
  } catch {
10653
11038
  }
10654
11039
  const filename = readableFilename(endpoint, body);
@@ -10660,14 +11045,14 @@ function saveReadable(endpoint, body, response, costUsd) {
10660
11045
  response
10661
11046
  };
10662
11047
  try {
10663
- fs6.writeFileSync(path5.join(DATA_DIR, filename), JSON.stringify(entry, null, 2));
11048
+ fs7.writeFileSync(path6.join(DATA_DIR, filename), JSON.stringify(entry, null, 2));
10664
11049
  } catch {
10665
11050
  }
10666
11051
  }
10667
11052
  function appendCostLog(endpoint, costUsd) {
10668
11053
  if (costUsd <= 0) return;
10669
11054
  try {
10670
- fs6.mkdirSync(path5.dirname(COST_LOG_FILE2), { recursive: true });
11055
+ fs7.mkdirSync(path6.dirname(COST_LOG_FILE2), { recursive: true });
10671
11056
  } catch {
10672
11057
  }
10673
11058
  const entry = {
@@ -10676,7 +11061,7 @@ function appendCostLog(endpoint, costUsd) {
10676
11061
  cost_usd: costUsd
10677
11062
  };
10678
11063
  try {
10679
- fs6.appendFileSync(COST_LOG_FILE2, JSON.stringify(entry) + "\n");
11064
+ fs7.appendFileSync(COST_LOG_FILE2, JSON.stringify(entry) + "\n");
10680
11065
  } catch {
10681
11066
  }
10682
11067
  }
@@ -10684,7 +11069,7 @@ function saveToCache(endpoint, body, response, costUsd = 0) {
10684
11069
  const ttl = getTtl(endpoint);
10685
11070
  if (ttl > 0) {
10686
11071
  try {
10687
- fs6.mkdirSync(CACHE_DIR, { recursive: true });
11072
+ fs7.mkdirSync(CACHE_DIR, { recursive: true });
10688
11073
  } catch {
10689
11074
  }
10690
11075
  const key = cacheKey(endpoint, body);
@@ -10696,7 +11081,7 @@ function saveToCache(endpoint, body, response, costUsd = 0) {
10696
11081
  costUsd
10697
11082
  };
10698
11083
  try {
10699
- fs6.writeFileSync(cachePath(key), JSON.stringify(entry));
11084
+ fs7.writeFileSync(cachePath(key), JSON.stringify(entry));
10700
11085
  } catch {
10701
11086
  }
10702
11087
  }
@@ -10704,14 +11089,14 @@ function saveToCache(endpoint, body, response, costUsd = 0) {
10704
11089
  appendCostLog(endpoint, costUsd);
10705
11090
  }
10706
11091
  function clearCache() {
10707
- if (!fs6.existsSync(CACHE_DIR)) return 0;
11092
+ if (!fs7.existsSync(CACHE_DIR)) return 0;
10708
11093
  let count = 0;
10709
11094
  try {
10710
- const files = fs6.readdirSync(CACHE_DIR);
11095
+ const files = fs7.readdirSync(CACHE_DIR);
10711
11096
  for (const file of files) {
10712
11097
  if (file.endsWith(".json")) {
10713
11098
  try {
10714
- fs6.unlinkSync(path5.join(CACHE_DIR, file));
11099
+ fs7.unlinkSync(path6.join(CACHE_DIR, file));
10715
11100
  count++;
10716
11101
  } catch {
10717
11102
  }
@@ -10722,14 +11107,14 @@ function clearCache() {
10722
11107
  return count;
10723
11108
  }
10724
11109
  function getCostLogSummary() {
10725
- if (!fs6.existsSync(COST_LOG_FILE2)) {
11110
+ if (!fs7.existsSync(COST_LOG_FILE2)) {
10726
11111
  return { totalUsd: 0, calls: 0, byEndpoint: {} };
10727
11112
  }
10728
11113
  let totalUsd = 0;
10729
11114
  let calls = 0;
10730
11115
  const byEndpoint = {};
10731
11116
  try {
10732
- const content = fs6.readFileSync(COST_LOG_FILE2, "utf-8").trim();
11117
+ const content = fs7.readFileSync(COST_LOG_FILE2, "utf-8").trim();
10733
11118
  if (!content) return { totalUsd: 0, calls: 0, byEndpoint: {} };
10734
11119
  for (const line of content.split("\n")) {
10735
11120
  if (!line) continue;