@blockrun/llm 3.24.9 → 3.25.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
@@ -635,7 +635,10 @@ console.log(res.costUsd, res.settlement); // what you paid, and the tx
635
635
  ```
636
636
 
637
637
  `costUsd`, `settlement` and the per-call `timeout` work the same on `LLMClient`
638
- (Base / Arc). `costUsd` is what the call added to `getSpending()`: the signed
638
+ (Base / Arc). Both clients default to a 600 s request timeout
639
+ (`BLOCKRUN_CHAT_TIMEOUT`, in seconds, overrides it; the `timeout` option, in
640
+ ms, overrides both): a call aborted after its payment was sent may already be
641
+ charged, so a batch payment ends unresolved rather than paid again. `costUsd` is what the call added to `getSpending()`: the signed
639
642
  price under `exact`, the metered `extra.chargedAmount` under batch settlement,
640
643
  `0` for a free model; it is undefined in API-key mode (billed to the account).
641
644
  When the gateway answers a paid request from a free fallback model it skips
@@ -720,6 +723,7 @@ await client.closeBatchChannel();
720
723
  - **Never pays twice.** Anything that goes wrong before a batch payment is sent falls back to `exact`. Once one is sent, a call whose payment may have been charged throws `BatchPaymentUnresolvedError` and is never paid again (no new payment, no `exact`, no fallback model).
721
724
  - **Non-streaming chat only.** `chat`, `chatCompletion`, `smartChat` and `smartChatCompletion` use batch; `stream()` and media jobs pay `exact`.
722
725
  - **One channel per wallet.** All clients for a wallet share one channel, saved to `~/.blockrun/solana-batch/<wallet>.json` (mode `0600`) so a restart reuses it.
726
+ - **One process per wallet.** The process that opens the channel holds its store's lock until it exits. Other processes on that wallet (worker pools, PM2/cluster workers, parallel runs) pay `exact`, reported as `channel_store_locked`. Give each worker its own wallet, or route calls through one process. `batch.onLocked: "wait"` waits up to `batch.lockWaitMs` (default 60 s) for the lock, and `"throw"` raises `BatchChannelLockedError` (unpaid, nothing signed) instead of paying `exact`.
723
727
  - **Observable.** Every fallback, backoff, recovery and re-read writes one stderr line; `batch.onEvent` and `client.getBatchStats()` expose the same events and counters.
724
728
 
725
729
  The full runtime contract (the answer-by-answer table, payment-identifier
@@ -1695,7 +1699,7 @@ const configured = new LLMClient({
1695
1699
  | `SOLANA_RPC_URL` | Solana RPC for payments and balance (also `rpcUrl`) |
1696
1700
  | `SOLANA_RPC_API_KEY` / `SOLANA_RPC_HEADERS` | Header auth for that RPC (also `rpcHeaders`) |
1697
1701
  | `BASE_RPC_URL` | Base RPC tried first by `LLMClient.getBalance()` |
1698
- | `BLOCKRUN_CHAT_TIMEOUT` | `LLMClient` default timeout, in **seconds** (default 600) |
1702
+ | `BLOCKRUN_CHAT_TIMEOUT` | `LLMClient` and `SolanaLLMClient` default timeout, in **seconds** (default 600) |
1699
1703
  | `BLOCKRUN_HOME` | Base directory for the Base wallet files (security-sensitive, see [Wallets](#one-wallet-across-every-blockrun-product)) |
1700
1704
 
1701
1705
  `BLOCKRUN_SOL_OPERATOR` is **not** an environment variable or a secret. It is
package/dist/index.cjs CHANGED
@@ -36,11 +36,13 @@ __export(index_exports, {
36
36
  AnthropicClient: () => AnthropicClient,
37
37
  BASE_CHAIN_ID: () => BASE_CHAIN_ID,
38
38
  BLOCKRUN_SOL_OPERATOR: () => BLOCKRUN_SOL_OPERATOR,
39
+ BatchChannelLockedError: () => BatchChannelLockedError,
39
40
  BatchCloseDeferredError: () => BatchCloseDeferredError,
40
41
  BatchPaymentUnresolvedError: () => BatchPaymentUnresolvedError,
41
42
  BlockrunClient: () => BlockrunClient,
42
43
  BlockrunError: () => BlockrunError,
43
44
  DEFAULT_BATCH_IN_DOUBT: () => DEFAULT_BATCH_IN_DOUBT,
45
+ DEFAULT_BATCH_LOCK_WAIT_MS: () => DEFAULT_BATCH_LOCK_WAIT_MS,
44
46
  DEFAULT_BATCH_RATE_LIMIT: () => DEFAULT_BATCH_RATE_LIMIT,
45
47
  EVM_NETWORKS: () => EVM_NETWORKS,
46
48
  ImageClient: () => ImageClient,
@@ -5120,7 +5122,7 @@ function getCostSummary() {
5120
5122
  }
5121
5123
 
5122
5124
  // src/version.ts
5123
- var SDK_VERSION = "3.24.9";
5125
+ var SDK_VERSION = "3.25.0";
5124
5126
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
5125
5127
 
5126
5128
  // src/client.ts
@@ -10222,6 +10224,8 @@ var BATCH_SCHEME = "batch-settlement";
10222
10224
  var BLOCKRUN_SOL_OPERATOR = "5YKPQUFjw5WQqhSUkEGKNNfYYVqnRRNbpYyL71qQ1vm3";
10223
10225
  var DEFAULT_BATCH_RATE_LIMIT = { maxAttempts: 3, maxWaitMs: 6e4 };
10224
10226
  var DEFAULT_BATCH_IN_DOUBT = { maxWaitMs: 3e5 };
10227
+ var DEFAULT_BATCH_LOCK_WAIT_MS = 6e4;
10228
+ var LOCK_POLL_MS = 500;
10225
10229
  var BatchPaymentUnresolvedError = class extends PaymentError {
10226
10230
  /** The {@link SolanaBatchEvent.reason} of the `unresolved` event reported with it. */
10227
10231
  reason;
@@ -10267,6 +10271,22 @@ var BatchCloseDeferredError = class extends BlockrunError {
10267
10271
  this.channelIds = init.channelIds ?? [];
10268
10272
  }
10269
10273
  };
10274
+ var BatchChannelLockedError = class extends BlockrunError {
10275
+ /** The paying wallet's address. */
10276
+ wallet;
10277
+ /** The live owner's pid, when the SDK knows it. */
10278
+ ownerPid;
10279
+ /** How long this call waited for the lock, in ms (0 unless `onLocked` is `"wait"`). */
10280
+ waitedMs;
10281
+ constructor(init) {
10282
+ super(`batch-settlement unavailable for wallet ${init.wallet}: ${init.detail}`);
10283
+ this.name = "BatchChannelLockedError";
10284
+ this.wallet = init.wallet;
10285
+ if (init.ownerPid !== void 0) this.ownerPid = init.ownerPid;
10286
+ this.waitedMs = init.waitedMs;
10287
+ withDisposition(this, "unpaid");
10288
+ }
10289
+ };
10270
10290
  var INSTALL_HINT2 = "npm install @x402/core@~2.28.0 @x402/svm@~2.28.0 @solana/kit";
10271
10291
  async function load(pkg, importer) {
10272
10292
  try {
@@ -10902,6 +10922,38 @@ function withoutClientSignedBatch(paymentRequired) {
10902
10922
  const accepts = paymentRequired.accepts ?? [];
10903
10923
  return accepts.some(clientSignedBatch) ? { ...paymentRequired, accepts: accepts.filter((accept) => !clientSignedBatch(accept)) } : paymentRequired;
10904
10924
  }
10925
+ var DEPOSIT_BLOCKHASH_TTL_MS = 1e4;
10926
+ var MAX_DEPOSIT_BLOCKHASH_ENDPOINTS = 8;
10927
+ var depositBlockhashes = /* @__PURE__ */ new Map();
10928
+ async function finalizedBlockhash(rpcUrl, rpcHeaders) {
10929
+ const cached = depositBlockhashes.get(rpcUrl);
10930
+ if (cached && Date.now() - cached.fetchedAt < DEPOSIT_BLOCKHASH_TTL_MS) return cached;
10931
+ const result = await rpcRequest(rpcUrl, "getLatestBlockhash", [{ commitment: "finalized" }], rpcHeaders);
10932
+ const blockhash = result.value?.blockhash;
10933
+ if (typeof blockhash !== "string" || !blockhash) throw new Error("getLatestBlockhash returned no blockhash");
10934
+ const entry = {
10935
+ blockhash,
10936
+ lastValidBlockHeight: rpcInteger(result.value?.lastValidBlockHeight, "getLatestBlockhash lastValidBlockHeight"),
10937
+ fetchedAt: Date.now()
10938
+ };
10939
+ depositBlockhashes.delete(rpcUrl);
10940
+ depositBlockhashes.set(rpcUrl, entry);
10941
+ while (depositBlockhashes.size > MAX_DEPOSIT_BLOCKHASH_ENDPOINTS) {
10942
+ depositBlockhashes.delete(depositBlockhashes.keys().next().value);
10943
+ }
10944
+ return entry;
10945
+ }
10946
+ function withDepositBlockhash(paymentRequired, blockhash) {
10947
+ const accepts = paymentRequired.accepts ?? [];
10948
+ const lacking = (accept) => accept.scheme === BATCH_SCHEME && !accept.extra?.recentBlockhash;
10949
+ if (!accepts.some(lacking)) return paymentRequired;
10950
+ return {
10951
+ ...paymentRequired,
10952
+ accepts: accepts.map(
10953
+ (accept) => lacking(accept) ? { ...accept, extra: { ...accept.extra, recentBlockhash: blockhash.blockhash, lastValidBlockHeight: blockhash.lastValidBlockHeight } } : accept
10954
+ )
10955
+ };
10956
+ }
10905
10957
  var PAYMENT_IDENTIFIER = "payment-identifier";
10906
10958
  function withPaymentIdentifier(paymentRequired) {
10907
10959
  const declared = paymentRequired.extensions?.[PAYMENT_IDENTIFIER];
@@ -11299,12 +11351,22 @@ var SolanaBatchPayer = class {
11299
11351
  if (!Number.isFinite(this.inDoubtMaxWaitMs) || this.inDoubtMaxWaitMs < 0) {
11300
11352
  throw new Error("batch.inDoubt.maxWaitMs must be a finite number of ms >= 0");
11301
11353
  }
11354
+ this.onLocked = init.options.onLocked ?? "exact";
11355
+ if (this.onLocked !== "exact" && this.onLocked !== "wait" && this.onLocked !== "throw") {
11356
+ throw new Error('batch.onLocked must be "exact", "wait" or "throw"');
11357
+ }
11358
+ this.lockWaitMs = init.options.lockWaitMs ?? DEFAULT_BATCH_LOCK_WAIT_MS;
11359
+ if (!Number.isFinite(this.lockWaitMs) || this.lockWaitMs < 0) {
11360
+ throw new Error("batch.lockWaitMs must be a finite number of ms >= 0");
11361
+ }
11302
11362
  }
11303
11363
  init;
11304
11364
  state;
11305
11365
  maxAttempts;
11306
11366
  maxWaitMs;
11307
11367
  inDoubtMaxWaitMs;
11368
+ onLocked;
11369
+ lockWaitMs;
11308
11370
  counters = {
11309
11371
  fallbacks: 0,
11310
11372
  fallbacksByReason: {},
@@ -11353,6 +11415,7 @@ var SolanaBatchPayer = class {
11353
11415
  if (owner !== void 0) {
11354
11416
  return {
11355
11417
  reason: "channel_store_locked",
11418
+ ownerPid: owner,
11356
11419
  detail: owner === process.pid ? `channel store ${file} is in use by another copy of @blockrun/llm in this process (or by a process that had this pid and died: remove ${file}.lock if nothing uses it)` : `channel store ${file} is in use by process ${owner}`
11357
11420
  };
11358
11421
  }
@@ -11698,12 +11761,23 @@ var SolanaBatchPayer = class {
11698
11761
  const unavailable = batchUnavailable(paymentRequired);
11699
11762
  if (unavailable) return this.fallback(unavailable);
11700
11763
  let wallet;
11764
+ let waitedMs = 0;
11701
11765
  try {
11702
11766
  wallet = await this.wallet();
11767
+ if ("reason" in wallet && wallet.reason === "channel_store_locked" && this.onLocked === "wait") {
11768
+ const started = Date.now();
11769
+ wallet = await this.waitForLock(wallet);
11770
+ waitedMs = Date.now() - started;
11771
+ }
11703
11772
  } catch (err) {
11704
11773
  return this.fallback(creationFailure(err));
11705
11774
  }
11706
- if ("reason" in wallet) return this.fallback({ reason: wallet.reason, detail: wallet.detail });
11775
+ if ("reason" in wallet) {
11776
+ if (wallet.reason === "channel_store_locked" && this.onLocked !== "exact") {
11777
+ return this.lockedOut(wallet.detail, wallet.ownerPid, waitedMs);
11778
+ }
11779
+ return this.fallback({ reason: wallet.reason, detail: wallet.detail });
11780
+ }
11707
11781
  if (closesAtStart !== void 0 && wallet.closes !== closesAtStart) {
11708
11782
  return this.fallback({
11709
11783
  reason: "closed_during_call",
@@ -11720,6 +11794,40 @@ var SolanaBatchPayer = class {
11720
11794
  wallet.active -= 1;
11721
11795
  }
11722
11796
  }
11797
+ /**
11798
+ * `onLocked: "wait"`: try the wallet's channel store again every
11799
+ * {@link LOCK_POLL_MS} until its owner lets go of it or `lockWaitMs` runs
11800
+ * out. Reported once as a `backoff` event (`channel_store_locked`).
11801
+ */
11802
+ async waitForLock(refusal) {
11803
+ const deadline = Date.now() + this.lockWaitMs;
11804
+ let current = refusal;
11805
+ if (this.lockWaitMs > 0) {
11806
+ await this.report({
11807
+ type: "backoff",
11808
+ reason: "channel_store_locked",
11809
+ retryAfterMs: this.lockWaitMs,
11810
+ detail: refusal.detail
11811
+ });
11812
+ }
11813
+ while ("reason" in current && current.reason === "channel_store_locked") {
11814
+ const left = deadline - Date.now();
11815
+ if (left <= 0) break;
11816
+ await sleep2(Math.min(LOCK_POLL_MS, left));
11817
+ current = await this.wallet();
11818
+ }
11819
+ return current;
11820
+ }
11821
+ /** `onLocked` other than `"exact"`: the call's `"unpaid"` error, nothing signed. */
11822
+ async lockedOut(detail, ownerPid, waitedMs) {
11823
+ const error = new BatchChannelLockedError({
11824
+ wallet: await this.init.address(),
11825
+ detail,
11826
+ waitedMs,
11827
+ ...ownerPid !== void 0 ? { ownerPid } : {}
11828
+ });
11829
+ return { kind: "failed", error };
11830
+ }
11723
11831
  /**
11724
11832
  * The close fence for a call about to send its first request: how many
11725
11833
  * closes this wallet has completed so far, to hand to {@link pay}.
@@ -11810,11 +11918,9 @@ var SolanaBatchPayer = class {
11810
11918
  }
11811
11919
  if (wallet.busy) return fallback({ reason: "channel_busy", attempt: budget.attempt });
11812
11920
  if (wallet.file && !holdsChannelFile(wallet.file)) {
11813
- return fallback({
11814
- reason: "channel_store_locked",
11815
- attempt: budget.attempt,
11816
- detail: `this process no longer holds the lock on ${wallet.file}; another owner may be paying into its channels`
11817
- });
11921
+ const detail = `this process no longer holds the lock on ${wallet.file}; another owner may be paying into its channels`;
11922
+ if (this.onLocked !== "exact") return this.lockedOut(detail, void 0, 0);
11923
+ return fallback({ reason: "channel_store_locked", attempt: budget.attempt, detail });
11818
11924
  }
11819
11925
  wallet.busy = true;
11820
11926
  holding = true;
@@ -12391,7 +12497,8 @@ var SolanaBatchPayer = class {
12391
12497
  const http = (await this.build(wallet)).http;
12392
12498
  try {
12393
12499
  await this.prepareTopUp(wallet, paymentRequired);
12394
- return { http, payload: await http.createPaymentPayload(withPaymentIdentifier(withoutClientSignedBatch(paymentRequired))) };
12500
+ const required = await this.withDepositBlockhash(wallet, withoutClientSignedBatch(paymentRequired));
12501
+ return { http, payload: await http.createPaymentPayload(withPaymentIdentifier(required)) };
12395
12502
  } catch (err) {
12396
12503
  if (pass > 0 || !isResyncRequired(err) || wallet.resyncs.size === 0) throw err;
12397
12504
  } finally {
@@ -12399,6 +12506,34 @@ var SolanaBatchPayer = class {
12399
12506
  }
12400
12507
  }
12401
12508
  }
12509
+ /**
12510
+ * The 402 to build this payment from: with a finalized blockhash
12511
+ * ({@link withDepositBlockhash}) when the payment will likely carry a
12512
+ * deposit (no usable record for the trusted accept's channel, so an open,
12513
+ * or a top-up {@link prepareTopUp} just sized). An authorization signs no
12514
+ * transaction and costs no RPC read. Best effort: a failed read leaves the
12515
+ * 402 as it is, and `@x402/svm` fetches its own blockhash.
12516
+ */
12517
+ async withDepositBlockhash(wallet, paymentRequired) {
12518
+ const accept = trustedBatchAccept(paymentRequired, this.init.options.operators);
12519
+ const key = accept ? channelKeyOf(accept) : void 0;
12520
+ if (!accept || accept.extra?.recentBlockhash) return paymentRequired;
12521
+ let deposit = wallet.topUp !== void 0;
12522
+ if (!deposit) {
12523
+ try {
12524
+ const record = key ? await wallet.book.base.get(key) : void 0;
12525
+ deposit = !record || typeof record.channelId !== "string" || record.pending !== void 0;
12526
+ } catch {
12527
+ deposit = true;
12528
+ }
12529
+ }
12530
+ if (!deposit) return paymentRequired;
12531
+ try {
12532
+ return withDepositBlockhash(paymentRequired, await finalizedBlockhash(this.init.rpcUrl, this.init.rpcHeaders));
12533
+ } catch {
12534
+ return paymentRequired;
12535
+ }
12536
+ }
12402
12537
  /**
12403
12538
  * Queue a re-read of a stored channel whose account is gone at finalized
12404
12539
  * and at confirmed commitment (closed, its account reclaimed), anchored at
@@ -12705,7 +12840,6 @@ var SolanaBatchPayer = class {
12705
12840
  // src/solana-client.ts
12706
12841
  var SOLANA_API_URL = "https://sol.blockrun.ai/api";
12707
12842
  var DEFAULT_MAX_TOKENS2 = 1024;
12708
- var DEFAULT_TIMEOUT14 = 6e4;
12709
12843
  var STALE_BLOCKHASH_RETRY_BACKOFFS_MS = [500, 2e3];
12710
12844
  var MAX_PAYMENT_FAILURE_BYTES = 64 * 1024;
12711
12845
  var DEFAULT_VIDEO_MODEL = "xai/grok-imagine-video";
@@ -12873,7 +13007,7 @@ var SolanaLLMClient = class {
12873
13007
  const rpc = resolveRpcConfig(options.rpcUrl, options.rpcHeaders);
12874
13008
  this.rpcUrl = rpc.url;
12875
13009
  this.rpcHeaders = rpc.headers;
12876
- this.timeout = options.timeout || DEFAULT_TIMEOUT14;
13010
+ this.timeout = options.timeout || DEFAULT_TIMEOUT;
12877
13011
  if (options.batch && !this.apiAuth) {
12878
13012
  this.batchPayer = new SolanaBatchPayer({
12879
13013
  options: options.batch,
@@ -14641,11 +14775,13 @@ var AnthropicClient = class {
14641
14775
  AnthropicClient,
14642
14776
  BASE_CHAIN_ID,
14643
14777
  BLOCKRUN_SOL_OPERATOR,
14778
+ BatchChannelLockedError,
14644
14779
  BatchCloseDeferredError,
14645
14780
  BatchPaymentUnresolvedError,
14646
14781
  BlockrunClient,
14647
14782
  BlockrunError,
14648
14783
  DEFAULT_BATCH_IN_DOUBT,
14784
+ DEFAULT_BATCH_LOCK_WAIT_MS,
14649
14785
  DEFAULT_BATCH_RATE_LIMIT,
14650
14786
  EVM_NETWORKS,
14651
14787
  ImageClient,
package/dist/index.d.cts CHANGED
@@ -2923,6 +2923,27 @@ interface SolanaBatchOptions {
2923
2923
  * by scanning the chain for it.
2924
2924
  */
2925
2925
  channelStore?: string | false;
2926
+ /**
2927
+ * What a call does when another live process owns this wallet's channel
2928
+ * store (`channel_store_locked`). One process per wallet pays with batch:
2929
+ * it holds the store's lock until it exits, so worker pools, PM2/cluster
2930
+ * workers or parallel runs on one wallet must each use their own wallet
2931
+ * (or route their calls through one process).
2932
+ * - `"exact"` (default): the call pays with `exact`, as before.
2933
+ * - `"wait"`: the call waits up to {@link lockWaitMs} for the lock (the
2934
+ * owner has to exit), then pays with batch, or raises
2935
+ * {@link BatchChannelLockedError} once the wait runs out.
2936
+ * - `"throw"`: the call raises {@link BatchChannelLockedError} at once.
2937
+ *
2938
+ * The error is raised before anything is signed, so it is `"unpaid"`:
2939
+ * `fallbackModels` (and `smartChat()`'s chain) may move on, and will meet
2940
+ * the same lock. A process that loses its lock to another owner raises it
2941
+ * too unless `onLocked` is `"exact"` (waiting cannot give it back).
2942
+ * Locks between clients of one process do not apply: they share a channel.
2943
+ */
2944
+ onLocked?: "exact" | "wait" | "throw";
2945
+ /** Most time, in ms, one call waits for the lock under `onLocked: "wait"`. Default 60000. */
2946
+ lockWaitMs?: number;
2926
2947
  /**
2927
2948
  * How a call rides out an HTTP 429 (rate limited, or channel capacity
2928
2949
  * exhausted) on a batch payment before it pays with `exact`. The call waits
@@ -2982,6 +3003,8 @@ declare const DEFAULT_BATCH_RATE_LIMIT: {
2982
3003
  declare const DEFAULT_BATCH_IN_DOUBT: {
2983
3004
  readonly maxWaitMs: 300000;
2984
3005
  };
3006
+ /** Default {@link SolanaBatchOptions.lockWaitMs}. */
3007
+ declare const DEFAULT_BATCH_LOCK_WAIT_MS = 60000;
2985
3008
  /**
2986
3009
  * One observable batch-settlement event.
2987
3010
  *
@@ -2996,7 +3019,10 @@ declare const DEFAULT_BATCH_IN_DOUBT: {
2996
3019
  * (`reason: "rate_limited"`) or because another call for the wallet hit one
2997
3020
  * (`reason: "cooldown"`), or before each replay of a payment in doubt that
2998
3021
  * carries a payment identifier (`reason: "in_doubt"`, or `rate_limited`
2999
- * after a 429), with the answer's `status` and the wait in `retryAfterMs`.
3022
+ * after a 429), with the answer's `status` and the wait in `retryAfterMs`;
3023
+ * or, once, when a call starts waiting up to `lockWaitMs` for another
3024
+ * process's channel store lock (`reason: "channel_store_locked"`,
3025
+ * `onLocked: "wait"`).
3000
3026
  * - `recovered`: a call that backed off was then paid with batch (`reason:
3001
3027
  * "in_doubt"` when the replay of a payment in doubt resolved it), or its
3002
3028
  * fresh challenge after the wait was served without a payment (`reason:
@@ -3167,6 +3193,26 @@ declare class BatchCloseDeferredError extends BlockrunError {
3167
3193
  detail: string;
3168
3194
  });
3169
3195
  }
3196
+ /**
3197
+ * Batch is on, another owner holds this wallet's channel store, and the
3198
+ * client's `batch.onLocked` is `"wait"` (the wait ran out) or `"throw"`.
3199
+ * Raised before anything is signed: nothing was paid for the call, and its
3200
+ * {@link RetryDisposition} is `"unpaid"`.
3201
+ */
3202
+ declare class BatchChannelLockedError extends BlockrunError {
3203
+ /** The paying wallet's address. */
3204
+ readonly wallet: string;
3205
+ /** The live owner's pid, when the SDK knows it. */
3206
+ readonly ownerPid?: number;
3207
+ /** How long this call waited for the lock, in ms (0 unless `onLocked` is `"wait"`). */
3208
+ readonly waitedMs: number;
3209
+ constructor(init: {
3210
+ wallet: string;
3211
+ detail: string;
3212
+ ownerPid?: number;
3213
+ waitedMs: number;
3214
+ });
3215
+ }
3170
3216
  /**
3171
3217
  * The outcome of one batch attempt.
3172
3218
  *
@@ -3270,6 +3316,8 @@ declare class SolanaBatchPayer {
3270
3316
  private readonly maxAttempts;
3271
3317
  private readonly maxWaitMs;
3272
3318
  private readonly inDoubtMaxWaitMs;
3319
+ private readonly onLocked;
3320
+ private readonly lockWaitMs;
3273
3321
  private readonly counters;
3274
3322
  private readonly hooks;
3275
3323
  constructor(init: SolanaBatchPayerInit);
@@ -3387,6 +3435,14 @@ declare class SolanaBatchPayer {
3387
3435
  * open a channel the caller believes is closed.
3388
3436
  */
3389
3437
  pay(paymentRequired: PaymentRequired, send: (headers: Record<string, string>) => Promise<Response>, rechallenge: () => Promise<Rechallenge>, closesAtStart?: number): Promise<BatchAttempt>;
3438
+ /**
3439
+ * `onLocked: "wait"`: try the wallet's channel store again every
3440
+ * {@link LOCK_POLL_MS} until its owner lets go of it or `lockWaitMs` runs
3441
+ * out. Reported once as a `backoff` event (`channel_store_locked`).
3442
+ */
3443
+ private waitForLock;
3444
+ /** `onLocked` other than `"exact"`: the call's `"unpaid"` error, nothing signed. */
3445
+ private lockedOut;
3390
3446
  /**
3391
3447
  * The close fence for a call about to send its first request: how many
3392
3448
  * closes this wallet has completed so far, to hand to {@link pay}.
@@ -3508,6 +3564,15 @@ declare class SolanaBatchPayer {
3508
3564
  * once.
3509
3565
  */
3510
3566
  private createPayload;
3567
+ /**
3568
+ * The 402 to build this payment from: with a finalized blockhash
3569
+ * ({@link withDepositBlockhash}) when the payment will likely carry a
3570
+ * deposit (no usable record for the trusted accept's channel, so an open,
3571
+ * or a top-up {@link prepareTopUp} just sized). An authorization signs no
3572
+ * transaction and costs no RPC read. Best effort: a failed read leaves the
3573
+ * 402 as it is, and `@x402/svm` fetches its own blockhash.
3574
+ */
3575
+ private withDepositBlockhash;
3511
3576
  /**
3512
3577
  * Queue a re-read of a stored channel whose account is gone at finalized
3513
3578
  * and at confirmed commitment (closed, its account reclaimed), anchored at
@@ -3647,7 +3712,12 @@ interface SolanaLLMClientOptions extends ApiKeyOptions {
3647
3712
  * (shortcut for `{ "x-api-key": "..." }`) env vars when omitted.
3648
3713
  */
3649
3714
  rpcHeaders?: Record<string, string>;
3650
- /** Request timeout in milliseconds (default: 60000) */
3715
+ /**
3716
+ * Request timeout in milliseconds. Default: `BLOCKRUN_CHAT_TIMEOUT` (in
3717
+ * seconds) when set, else 600000, the same as `LLMClient`: a reasoning
3718
+ * model's long turn can take minutes, and a batch payment whose call is
3719
+ * aborted after it was sent ends unresolved.
3720
+ */
3651
3721
  timeout?: number;
3652
3722
  /**
3653
3723
  * Opt in to x402 batch-settlement (metered billing) for chat completions.
@@ -4476,4 +4546,4 @@ declare function readSettlement(response: Response, quotedUsd: number, scheme: S
4476
4546
  settlement: Settlement;
4477
4547
  };
4478
4548
 
4479
- export { APIError, API_KEY_URL, ARC_CHAIN_ID, AnthropicClient, type AnyTool, type ApiKeyOptions, type AudioModel, type AudioTrack, BASE_CHAIN_ID, BLOCKRUN_SOL_OPERATOR, type BarResolution, type BatchAttempt, BatchCloseDeferredError, type BatchPayloadKind, BatchPaymentUnresolvedError, type BatchUnresolvedReason, type BlockRunAnthropicOptions, BlockrunClient, type BlockrunClientOptions, BlockrunError, type CallInitiatedResponse, type CallModel, type CallOptions, type CallStatusResponse, type ChatChoice, type ChatCompletionOptions, type ChatMessage, type ChatOptions, type ChatResponse, type ChatResponseWithCost, type ChatUsage, type CostEntry, type CostEstimate, type CreatePaymentOptions, DEFAULT_BATCH_IN_DOUBT, DEFAULT_BATCH_RATE_LIMIT, EVM_NETWORKS, type EvmNetwork, type FunctionCall, type FunctionDefinition, type HistoryOptions, ImageClient, type ImageClientOptions, type ImageData, type ImageEditOptions, type ImageGenerateOptions, type ImageModel, type ImageResponse, KNOWN_PROVIDERS, LLMClient, type LLMClientOptions, type ListOptions, type MarketSession, type Model, MusicClient, type MusicClientOptions, type MusicGenerateOptions, type MusicResponse, NETWORK_ALIASES, type NewsSearchSource, OpenAI, type OpenAIChatCompletionChoice, type OpenAIChatCompletionChunk, type OpenAIChatCompletionParams, type OpenAIChatCompletionResponse, type OpenAIClientOptions, PHONE_PRICES, PORTAL_URL, PORTRAIT_ENROLLMENT_PRICE_USD, PaymentError, type PaymentLinks, type PhoneBuyOptions, type PhoneBuyResponse, PhoneClient, type PhoneClientOptions, type PhoneListResponse, type PhoneLookupResponse, type PhoneNumberRecord, type PhoneReleaseResponse, type PhoneRenewResponse, type PollOptions, PortraitClient, type PortraitClientOptions, type PortraitEnrollOptions, type PortraitEnrollResponse, type PriceBar, type PriceCategory, PriceClient, type PriceClientOptions, type PriceHistoryResponse, type PriceOptions, type PricePoint, RPC_PRICE_USD, type Rechallenge, type ResponseFormat, RetiredEndpointError, type RetryDisposition, type RoutingDecision, type RoutingProfile, type RoutingTaskType, type RoutingTier, type RoutingTierConfig, type RpcBatchRequest, RpcClient, type RpcClientOptions, type RpcError, type RpcNetwork, type RpcResponse, type RssSearchSource, SOLANA_NETWORK, SOLANA_WALLET_FILE as SOLANA_WALLET_FILE_PATH, SUPPORTED_NETWORKS, SearchClient, type SearchClientOptions, type SearchOptions, type SearchParameters, type SearchResult, type SearchSource, type SearchUsage, type Settlement, type SmartChatCompletionOptions, type SmartChatCompletionResponse, type SmartChatOptions, type SmartChatResponse, type SolanaBatchEvent, type SolanaBatchOptions, SolanaBatchPayer, type SolanaBatchPayerInit, type SolanaBatchStats, SolanaLLMClient, type SolanaLLMClientOptions, type SolanaWalletInfo, type SoundEffectOptions, type SpeechAudio, SpeechClient, type SpeechClientOptions, type SpeechGenerateOptions, type SpeechModel, type SpeechResponse, type SpeechVoice, type Spending, type SpendingReport, type StockMarket, SurfClient, type SurfClientOptions, type SymbolListResponse, type Tool, type ToolCall, type ToolChoice, USDC_ARC, USDC_BASE, USDC_BASE_CONTRACT, USDC_SOLANA, type UrlCitation, VideoClient, type VideoClientOptions, type VideoClip, type VideoGenerateOptions, type VideoModel, type VideoResponse, VoiceClient, type VoiceClientOptions, type VoiceInfo, type VoicePreset, WALLET_DIR_PATH, WALLET_FILE_PATH, type WalletInfo, type WebSearchSource, type WebSearchTool, type X402ClientLike, type X402HookContext, type XSearchSource, type XSearchTool, clearCache, createPaymentPayload, createSolanaPaymentPayload, createSolanaWallet, createWallet, LLMClient as default, evmNetwork, extractPaymentDetails, formatFundingMessageCompact, formatNeedsFundingMessage, formatSolanaWalletMigrationNotice, formatWalletCreatedMessage, formatWalletMigrationNotice, getCached, getCachedByRequest, getCostLogSummary, getCostSummary, getEip681Uri, getOrCreateSolanaWallet, getOrCreateWallet, getPaymentLinks, getWalletAddress, importSolanaWallet, importWallet, listDiscoveredSolanaWallets, listDiscoveredWallets, loadSolanaWallet, loadWallet, logCost, parsePaymentRequired, readSettlement, retryDisposition, saveSolanaWallet, saveToCache, saveWallet, scanSolanaWallets, scanWallets, setCache, setupAgentClient, setupAgentSolanaWallet, setupAgentWallet, solanaClient, solanaKeyToBytes, solanaPublicKey, status, toWireSearchParameters, toWireTools, validateMaxTokens, validateModel, validateTemperature, validateTopP };
4549
+ export { APIError, API_KEY_URL, ARC_CHAIN_ID, AnthropicClient, type AnyTool, type ApiKeyOptions, type AudioModel, type AudioTrack, BASE_CHAIN_ID, BLOCKRUN_SOL_OPERATOR, type BarResolution, type BatchAttempt, BatchChannelLockedError, BatchCloseDeferredError, type BatchPayloadKind, BatchPaymentUnresolvedError, type BatchUnresolvedReason, type BlockRunAnthropicOptions, BlockrunClient, type BlockrunClientOptions, BlockrunError, type CallInitiatedResponse, type CallModel, type CallOptions, type CallStatusResponse, type ChatChoice, type ChatCompletionOptions, type ChatMessage, type ChatOptions, type ChatResponse, type ChatResponseWithCost, type ChatUsage, type CostEntry, type CostEstimate, type CreatePaymentOptions, DEFAULT_BATCH_IN_DOUBT, DEFAULT_BATCH_LOCK_WAIT_MS, DEFAULT_BATCH_RATE_LIMIT, EVM_NETWORKS, type EvmNetwork, type FunctionCall, type FunctionDefinition, type HistoryOptions, ImageClient, type ImageClientOptions, type ImageData, type ImageEditOptions, type ImageGenerateOptions, type ImageModel, type ImageResponse, KNOWN_PROVIDERS, LLMClient, type LLMClientOptions, type ListOptions, type MarketSession, type Model, MusicClient, type MusicClientOptions, type MusicGenerateOptions, type MusicResponse, NETWORK_ALIASES, type NewsSearchSource, OpenAI, type OpenAIChatCompletionChoice, type OpenAIChatCompletionChunk, type OpenAIChatCompletionParams, type OpenAIChatCompletionResponse, type OpenAIClientOptions, PHONE_PRICES, PORTAL_URL, PORTRAIT_ENROLLMENT_PRICE_USD, PaymentError, type PaymentLinks, type PhoneBuyOptions, type PhoneBuyResponse, PhoneClient, type PhoneClientOptions, type PhoneListResponse, type PhoneLookupResponse, type PhoneNumberRecord, type PhoneReleaseResponse, type PhoneRenewResponse, type PollOptions, PortraitClient, type PortraitClientOptions, type PortraitEnrollOptions, type PortraitEnrollResponse, type PriceBar, type PriceCategory, PriceClient, type PriceClientOptions, type PriceHistoryResponse, type PriceOptions, type PricePoint, RPC_PRICE_USD, type Rechallenge, type ResponseFormat, RetiredEndpointError, type RetryDisposition, type RoutingDecision, type RoutingProfile, type RoutingTaskType, type RoutingTier, type RoutingTierConfig, type RpcBatchRequest, RpcClient, type RpcClientOptions, type RpcError, type RpcNetwork, type RpcResponse, type RssSearchSource, SOLANA_NETWORK, SOLANA_WALLET_FILE as SOLANA_WALLET_FILE_PATH, SUPPORTED_NETWORKS, SearchClient, type SearchClientOptions, type SearchOptions, type SearchParameters, type SearchResult, type SearchSource, type SearchUsage, type Settlement, type SmartChatCompletionOptions, type SmartChatCompletionResponse, type SmartChatOptions, type SmartChatResponse, type SolanaBatchEvent, type SolanaBatchOptions, SolanaBatchPayer, type SolanaBatchPayerInit, type SolanaBatchStats, SolanaLLMClient, type SolanaLLMClientOptions, type SolanaWalletInfo, type SoundEffectOptions, type SpeechAudio, SpeechClient, type SpeechClientOptions, type SpeechGenerateOptions, type SpeechModel, type SpeechResponse, type SpeechVoice, type Spending, type SpendingReport, type StockMarket, SurfClient, type SurfClientOptions, type SymbolListResponse, type Tool, type ToolCall, type ToolChoice, USDC_ARC, USDC_BASE, USDC_BASE_CONTRACT, USDC_SOLANA, type UrlCitation, VideoClient, type VideoClientOptions, type VideoClip, type VideoGenerateOptions, type VideoModel, type VideoResponse, VoiceClient, type VoiceClientOptions, type VoiceInfo, type VoicePreset, WALLET_DIR_PATH, WALLET_FILE_PATH, type WalletInfo, type WebSearchSource, type WebSearchTool, type X402ClientLike, type X402HookContext, type XSearchSource, type XSearchTool, clearCache, createPaymentPayload, createSolanaPaymentPayload, createSolanaWallet, createWallet, LLMClient as default, evmNetwork, extractPaymentDetails, formatFundingMessageCompact, formatNeedsFundingMessage, formatSolanaWalletMigrationNotice, formatWalletCreatedMessage, formatWalletMigrationNotice, getCached, getCachedByRequest, getCostLogSummary, getCostSummary, getEip681Uri, getOrCreateSolanaWallet, getOrCreateWallet, getPaymentLinks, getWalletAddress, importSolanaWallet, importWallet, listDiscoveredSolanaWallets, listDiscoveredWallets, loadSolanaWallet, loadWallet, logCost, parsePaymentRequired, readSettlement, retryDisposition, saveSolanaWallet, saveToCache, saveWallet, scanSolanaWallets, scanWallets, setCache, setupAgentClient, setupAgentSolanaWallet, setupAgentWallet, solanaClient, solanaKeyToBytes, solanaPublicKey, status, toWireSearchParameters, toWireTools, validateMaxTokens, validateModel, validateTemperature, validateTopP };
package/dist/index.d.ts CHANGED
@@ -2923,6 +2923,27 @@ interface SolanaBatchOptions {
2923
2923
  * by scanning the chain for it.
2924
2924
  */
2925
2925
  channelStore?: string | false;
2926
+ /**
2927
+ * What a call does when another live process owns this wallet's channel
2928
+ * store (`channel_store_locked`). One process per wallet pays with batch:
2929
+ * it holds the store's lock until it exits, so worker pools, PM2/cluster
2930
+ * workers or parallel runs on one wallet must each use their own wallet
2931
+ * (or route their calls through one process).
2932
+ * - `"exact"` (default): the call pays with `exact`, as before.
2933
+ * - `"wait"`: the call waits up to {@link lockWaitMs} for the lock (the
2934
+ * owner has to exit), then pays with batch, or raises
2935
+ * {@link BatchChannelLockedError} once the wait runs out.
2936
+ * - `"throw"`: the call raises {@link BatchChannelLockedError} at once.
2937
+ *
2938
+ * The error is raised before anything is signed, so it is `"unpaid"`:
2939
+ * `fallbackModels` (and `smartChat()`'s chain) may move on, and will meet
2940
+ * the same lock. A process that loses its lock to another owner raises it
2941
+ * too unless `onLocked` is `"exact"` (waiting cannot give it back).
2942
+ * Locks between clients of one process do not apply: they share a channel.
2943
+ */
2944
+ onLocked?: "exact" | "wait" | "throw";
2945
+ /** Most time, in ms, one call waits for the lock under `onLocked: "wait"`. Default 60000. */
2946
+ lockWaitMs?: number;
2926
2947
  /**
2927
2948
  * How a call rides out an HTTP 429 (rate limited, or channel capacity
2928
2949
  * exhausted) on a batch payment before it pays with `exact`. The call waits
@@ -2982,6 +3003,8 @@ declare const DEFAULT_BATCH_RATE_LIMIT: {
2982
3003
  declare const DEFAULT_BATCH_IN_DOUBT: {
2983
3004
  readonly maxWaitMs: 300000;
2984
3005
  };
3006
+ /** Default {@link SolanaBatchOptions.lockWaitMs}. */
3007
+ declare const DEFAULT_BATCH_LOCK_WAIT_MS = 60000;
2985
3008
  /**
2986
3009
  * One observable batch-settlement event.
2987
3010
  *
@@ -2996,7 +3019,10 @@ declare const DEFAULT_BATCH_IN_DOUBT: {
2996
3019
  * (`reason: "rate_limited"`) or because another call for the wallet hit one
2997
3020
  * (`reason: "cooldown"`), or before each replay of a payment in doubt that
2998
3021
  * carries a payment identifier (`reason: "in_doubt"`, or `rate_limited`
2999
- * after a 429), with the answer's `status` and the wait in `retryAfterMs`.
3022
+ * after a 429), with the answer's `status` and the wait in `retryAfterMs`;
3023
+ * or, once, when a call starts waiting up to `lockWaitMs` for another
3024
+ * process's channel store lock (`reason: "channel_store_locked"`,
3025
+ * `onLocked: "wait"`).
3000
3026
  * - `recovered`: a call that backed off was then paid with batch (`reason:
3001
3027
  * "in_doubt"` when the replay of a payment in doubt resolved it), or its
3002
3028
  * fresh challenge after the wait was served without a payment (`reason:
@@ -3167,6 +3193,26 @@ declare class BatchCloseDeferredError extends BlockrunError {
3167
3193
  detail: string;
3168
3194
  });
3169
3195
  }
3196
+ /**
3197
+ * Batch is on, another owner holds this wallet's channel store, and the
3198
+ * client's `batch.onLocked` is `"wait"` (the wait ran out) or `"throw"`.
3199
+ * Raised before anything is signed: nothing was paid for the call, and its
3200
+ * {@link RetryDisposition} is `"unpaid"`.
3201
+ */
3202
+ declare class BatchChannelLockedError extends BlockrunError {
3203
+ /** The paying wallet's address. */
3204
+ readonly wallet: string;
3205
+ /** The live owner's pid, when the SDK knows it. */
3206
+ readonly ownerPid?: number;
3207
+ /** How long this call waited for the lock, in ms (0 unless `onLocked` is `"wait"`). */
3208
+ readonly waitedMs: number;
3209
+ constructor(init: {
3210
+ wallet: string;
3211
+ detail: string;
3212
+ ownerPid?: number;
3213
+ waitedMs: number;
3214
+ });
3215
+ }
3170
3216
  /**
3171
3217
  * The outcome of one batch attempt.
3172
3218
  *
@@ -3270,6 +3316,8 @@ declare class SolanaBatchPayer {
3270
3316
  private readonly maxAttempts;
3271
3317
  private readonly maxWaitMs;
3272
3318
  private readonly inDoubtMaxWaitMs;
3319
+ private readonly onLocked;
3320
+ private readonly lockWaitMs;
3273
3321
  private readonly counters;
3274
3322
  private readonly hooks;
3275
3323
  constructor(init: SolanaBatchPayerInit);
@@ -3387,6 +3435,14 @@ declare class SolanaBatchPayer {
3387
3435
  * open a channel the caller believes is closed.
3388
3436
  */
3389
3437
  pay(paymentRequired: PaymentRequired, send: (headers: Record<string, string>) => Promise<Response>, rechallenge: () => Promise<Rechallenge>, closesAtStart?: number): Promise<BatchAttempt>;
3438
+ /**
3439
+ * `onLocked: "wait"`: try the wallet's channel store again every
3440
+ * {@link LOCK_POLL_MS} until its owner lets go of it or `lockWaitMs` runs
3441
+ * out. Reported once as a `backoff` event (`channel_store_locked`).
3442
+ */
3443
+ private waitForLock;
3444
+ /** `onLocked` other than `"exact"`: the call's `"unpaid"` error, nothing signed. */
3445
+ private lockedOut;
3390
3446
  /**
3391
3447
  * The close fence for a call about to send its first request: how many
3392
3448
  * closes this wallet has completed so far, to hand to {@link pay}.
@@ -3508,6 +3564,15 @@ declare class SolanaBatchPayer {
3508
3564
  * once.
3509
3565
  */
3510
3566
  private createPayload;
3567
+ /**
3568
+ * The 402 to build this payment from: with a finalized blockhash
3569
+ * ({@link withDepositBlockhash}) when the payment will likely carry a
3570
+ * deposit (no usable record for the trusted accept's channel, so an open,
3571
+ * or a top-up {@link prepareTopUp} just sized). An authorization signs no
3572
+ * transaction and costs no RPC read. Best effort: a failed read leaves the
3573
+ * 402 as it is, and `@x402/svm` fetches its own blockhash.
3574
+ */
3575
+ private withDepositBlockhash;
3511
3576
  /**
3512
3577
  * Queue a re-read of a stored channel whose account is gone at finalized
3513
3578
  * and at confirmed commitment (closed, its account reclaimed), anchored at
@@ -3647,7 +3712,12 @@ interface SolanaLLMClientOptions extends ApiKeyOptions {
3647
3712
  * (shortcut for `{ "x-api-key": "..." }`) env vars when omitted.
3648
3713
  */
3649
3714
  rpcHeaders?: Record<string, string>;
3650
- /** Request timeout in milliseconds (default: 60000) */
3715
+ /**
3716
+ * Request timeout in milliseconds. Default: `BLOCKRUN_CHAT_TIMEOUT` (in
3717
+ * seconds) when set, else 600000, the same as `LLMClient`: a reasoning
3718
+ * model's long turn can take minutes, and a batch payment whose call is
3719
+ * aborted after it was sent ends unresolved.
3720
+ */
3651
3721
  timeout?: number;
3652
3722
  /**
3653
3723
  * Opt in to x402 batch-settlement (metered billing) for chat completions.
@@ -4476,4 +4546,4 @@ declare function readSettlement(response: Response, quotedUsd: number, scheme: S
4476
4546
  settlement: Settlement;
4477
4547
  };
4478
4548
 
4479
- export { APIError, API_KEY_URL, ARC_CHAIN_ID, AnthropicClient, type AnyTool, type ApiKeyOptions, type AudioModel, type AudioTrack, BASE_CHAIN_ID, BLOCKRUN_SOL_OPERATOR, type BarResolution, type BatchAttempt, BatchCloseDeferredError, type BatchPayloadKind, BatchPaymentUnresolvedError, type BatchUnresolvedReason, type BlockRunAnthropicOptions, BlockrunClient, type BlockrunClientOptions, BlockrunError, type CallInitiatedResponse, type CallModel, type CallOptions, type CallStatusResponse, type ChatChoice, type ChatCompletionOptions, type ChatMessage, type ChatOptions, type ChatResponse, type ChatResponseWithCost, type ChatUsage, type CostEntry, type CostEstimate, type CreatePaymentOptions, DEFAULT_BATCH_IN_DOUBT, DEFAULT_BATCH_RATE_LIMIT, EVM_NETWORKS, type EvmNetwork, type FunctionCall, type FunctionDefinition, type HistoryOptions, ImageClient, type ImageClientOptions, type ImageData, type ImageEditOptions, type ImageGenerateOptions, type ImageModel, type ImageResponse, KNOWN_PROVIDERS, LLMClient, type LLMClientOptions, type ListOptions, type MarketSession, type Model, MusicClient, type MusicClientOptions, type MusicGenerateOptions, type MusicResponse, NETWORK_ALIASES, type NewsSearchSource, OpenAI, type OpenAIChatCompletionChoice, type OpenAIChatCompletionChunk, type OpenAIChatCompletionParams, type OpenAIChatCompletionResponse, type OpenAIClientOptions, PHONE_PRICES, PORTAL_URL, PORTRAIT_ENROLLMENT_PRICE_USD, PaymentError, type PaymentLinks, type PhoneBuyOptions, type PhoneBuyResponse, PhoneClient, type PhoneClientOptions, type PhoneListResponse, type PhoneLookupResponse, type PhoneNumberRecord, type PhoneReleaseResponse, type PhoneRenewResponse, type PollOptions, PortraitClient, type PortraitClientOptions, type PortraitEnrollOptions, type PortraitEnrollResponse, type PriceBar, type PriceCategory, PriceClient, type PriceClientOptions, type PriceHistoryResponse, type PriceOptions, type PricePoint, RPC_PRICE_USD, type Rechallenge, type ResponseFormat, RetiredEndpointError, type RetryDisposition, type RoutingDecision, type RoutingProfile, type RoutingTaskType, type RoutingTier, type RoutingTierConfig, type RpcBatchRequest, RpcClient, type RpcClientOptions, type RpcError, type RpcNetwork, type RpcResponse, type RssSearchSource, SOLANA_NETWORK, SOLANA_WALLET_FILE as SOLANA_WALLET_FILE_PATH, SUPPORTED_NETWORKS, SearchClient, type SearchClientOptions, type SearchOptions, type SearchParameters, type SearchResult, type SearchSource, type SearchUsage, type Settlement, type SmartChatCompletionOptions, type SmartChatCompletionResponse, type SmartChatOptions, type SmartChatResponse, type SolanaBatchEvent, type SolanaBatchOptions, SolanaBatchPayer, type SolanaBatchPayerInit, type SolanaBatchStats, SolanaLLMClient, type SolanaLLMClientOptions, type SolanaWalletInfo, type SoundEffectOptions, type SpeechAudio, SpeechClient, type SpeechClientOptions, type SpeechGenerateOptions, type SpeechModel, type SpeechResponse, type SpeechVoice, type Spending, type SpendingReport, type StockMarket, SurfClient, type SurfClientOptions, type SymbolListResponse, type Tool, type ToolCall, type ToolChoice, USDC_ARC, USDC_BASE, USDC_BASE_CONTRACT, USDC_SOLANA, type UrlCitation, VideoClient, type VideoClientOptions, type VideoClip, type VideoGenerateOptions, type VideoModel, type VideoResponse, VoiceClient, type VoiceClientOptions, type VoiceInfo, type VoicePreset, WALLET_DIR_PATH, WALLET_FILE_PATH, type WalletInfo, type WebSearchSource, type WebSearchTool, type X402ClientLike, type X402HookContext, type XSearchSource, type XSearchTool, clearCache, createPaymentPayload, createSolanaPaymentPayload, createSolanaWallet, createWallet, LLMClient as default, evmNetwork, extractPaymentDetails, formatFundingMessageCompact, formatNeedsFundingMessage, formatSolanaWalletMigrationNotice, formatWalletCreatedMessage, formatWalletMigrationNotice, getCached, getCachedByRequest, getCostLogSummary, getCostSummary, getEip681Uri, getOrCreateSolanaWallet, getOrCreateWallet, getPaymentLinks, getWalletAddress, importSolanaWallet, importWallet, listDiscoveredSolanaWallets, listDiscoveredWallets, loadSolanaWallet, loadWallet, logCost, parsePaymentRequired, readSettlement, retryDisposition, saveSolanaWallet, saveToCache, saveWallet, scanSolanaWallets, scanWallets, setCache, setupAgentClient, setupAgentSolanaWallet, setupAgentWallet, solanaClient, solanaKeyToBytes, solanaPublicKey, status, toWireSearchParameters, toWireTools, validateMaxTokens, validateModel, validateTemperature, validateTopP };
4549
+ export { APIError, API_KEY_URL, ARC_CHAIN_ID, AnthropicClient, type AnyTool, type ApiKeyOptions, type AudioModel, type AudioTrack, BASE_CHAIN_ID, BLOCKRUN_SOL_OPERATOR, type BarResolution, type BatchAttempt, BatchChannelLockedError, BatchCloseDeferredError, type BatchPayloadKind, BatchPaymentUnresolvedError, type BatchUnresolvedReason, type BlockRunAnthropicOptions, BlockrunClient, type BlockrunClientOptions, BlockrunError, type CallInitiatedResponse, type CallModel, type CallOptions, type CallStatusResponse, type ChatChoice, type ChatCompletionOptions, type ChatMessage, type ChatOptions, type ChatResponse, type ChatResponseWithCost, type ChatUsage, type CostEntry, type CostEstimate, type CreatePaymentOptions, DEFAULT_BATCH_IN_DOUBT, DEFAULT_BATCH_LOCK_WAIT_MS, DEFAULT_BATCH_RATE_LIMIT, EVM_NETWORKS, type EvmNetwork, type FunctionCall, type FunctionDefinition, type HistoryOptions, ImageClient, type ImageClientOptions, type ImageData, type ImageEditOptions, type ImageGenerateOptions, type ImageModel, type ImageResponse, KNOWN_PROVIDERS, LLMClient, type LLMClientOptions, type ListOptions, type MarketSession, type Model, MusicClient, type MusicClientOptions, type MusicGenerateOptions, type MusicResponse, NETWORK_ALIASES, type NewsSearchSource, OpenAI, type OpenAIChatCompletionChoice, type OpenAIChatCompletionChunk, type OpenAIChatCompletionParams, type OpenAIChatCompletionResponse, type OpenAIClientOptions, PHONE_PRICES, PORTAL_URL, PORTRAIT_ENROLLMENT_PRICE_USD, PaymentError, type PaymentLinks, type PhoneBuyOptions, type PhoneBuyResponse, PhoneClient, type PhoneClientOptions, type PhoneListResponse, type PhoneLookupResponse, type PhoneNumberRecord, type PhoneReleaseResponse, type PhoneRenewResponse, type PollOptions, PortraitClient, type PortraitClientOptions, type PortraitEnrollOptions, type PortraitEnrollResponse, type PriceBar, type PriceCategory, PriceClient, type PriceClientOptions, type PriceHistoryResponse, type PriceOptions, type PricePoint, RPC_PRICE_USD, type Rechallenge, type ResponseFormat, RetiredEndpointError, type RetryDisposition, type RoutingDecision, type RoutingProfile, type RoutingTaskType, type RoutingTier, type RoutingTierConfig, type RpcBatchRequest, RpcClient, type RpcClientOptions, type RpcError, type RpcNetwork, type RpcResponse, type RssSearchSource, SOLANA_NETWORK, SOLANA_WALLET_FILE as SOLANA_WALLET_FILE_PATH, SUPPORTED_NETWORKS, SearchClient, type SearchClientOptions, type SearchOptions, type SearchParameters, type SearchResult, type SearchSource, type SearchUsage, type Settlement, type SmartChatCompletionOptions, type SmartChatCompletionResponse, type SmartChatOptions, type SmartChatResponse, type SolanaBatchEvent, type SolanaBatchOptions, SolanaBatchPayer, type SolanaBatchPayerInit, type SolanaBatchStats, SolanaLLMClient, type SolanaLLMClientOptions, type SolanaWalletInfo, type SoundEffectOptions, type SpeechAudio, SpeechClient, type SpeechClientOptions, type SpeechGenerateOptions, type SpeechModel, type SpeechResponse, type SpeechVoice, type Spending, type SpendingReport, type StockMarket, SurfClient, type SurfClientOptions, type SymbolListResponse, type Tool, type ToolCall, type ToolChoice, USDC_ARC, USDC_BASE, USDC_BASE_CONTRACT, USDC_SOLANA, type UrlCitation, VideoClient, type VideoClientOptions, type VideoClip, type VideoGenerateOptions, type VideoModel, type VideoResponse, VoiceClient, type VoiceClientOptions, type VoiceInfo, type VoicePreset, WALLET_DIR_PATH, WALLET_FILE_PATH, type WalletInfo, type WebSearchSource, type WebSearchTool, type X402ClientLike, type X402HookContext, type XSearchSource, type XSearchTool, clearCache, createPaymentPayload, createSolanaPaymentPayload, createSolanaWallet, createWallet, LLMClient as default, evmNetwork, extractPaymentDetails, formatFundingMessageCompact, formatNeedsFundingMessage, formatSolanaWalletMigrationNotice, formatWalletCreatedMessage, formatWalletMigrationNotice, getCached, getCachedByRequest, getCostLogSummary, getCostSummary, getEip681Uri, getOrCreateSolanaWallet, getOrCreateWallet, getPaymentLinks, getWalletAddress, importSolanaWallet, importWallet, listDiscoveredSolanaWallets, listDiscoveredWallets, loadSolanaWallet, loadWallet, logCost, parsePaymentRequired, readSettlement, retryDisposition, saveSolanaWallet, saveToCache, saveWallet, scanSolanaWallets, scanWallets, setCache, setupAgentClient, setupAgentSolanaWallet, setupAgentWallet, solanaClient, solanaKeyToBytes, solanaPublicKey, status, toWireSearchParameters, toWireTools, validateMaxTokens, validateModel, validateTemperature, validateTopP };
package/dist/index.js CHANGED
@@ -4989,7 +4989,7 @@ function getCostSummary() {
4989
4989
  }
4990
4990
 
4991
4991
  // src/version.ts
4992
- var SDK_VERSION = "3.24.9";
4992
+ var SDK_VERSION = "3.25.0";
4993
4993
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
4994
4994
 
4995
4995
  // src/client.ts
@@ -10091,6 +10091,8 @@ var BATCH_SCHEME = "batch-settlement";
10091
10091
  var BLOCKRUN_SOL_OPERATOR = "5YKPQUFjw5WQqhSUkEGKNNfYYVqnRRNbpYyL71qQ1vm3";
10092
10092
  var DEFAULT_BATCH_RATE_LIMIT = { maxAttempts: 3, maxWaitMs: 6e4 };
10093
10093
  var DEFAULT_BATCH_IN_DOUBT = { maxWaitMs: 3e5 };
10094
+ var DEFAULT_BATCH_LOCK_WAIT_MS = 6e4;
10095
+ var LOCK_POLL_MS = 500;
10094
10096
  var BatchPaymentUnresolvedError = class extends PaymentError {
10095
10097
  /** The {@link SolanaBatchEvent.reason} of the `unresolved` event reported with it. */
10096
10098
  reason;
@@ -10136,6 +10138,22 @@ var BatchCloseDeferredError = class extends BlockrunError {
10136
10138
  this.channelIds = init.channelIds ?? [];
10137
10139
  }
10138
10140
  };
10141
+ var BatchChannelLockedError = class extends BlockrunError {
10142
+ /** The paying wallet's address. */
10143
+ wallet;
10144
+ /** The live owner's pid, when the SDK knows it. */
10145
+ ownerPid;
10146
+ /** How long this call waited for the lock, in ms (0 unless `onLocked` is `"wait"`). */
10147
+ waitedMs;
10148
+ constructor(init) {
10149
+ super(`batch-settlement unavailable for wallet ${init.wallet}: ${init.detail}`);
10150
+ this.name = "BatchChannelLockedError";
10151
+ this.wallet = init.wallet;
10152
+ if (init.ownerPid !== void 0) this.ownerPid = init.ownerPid;
10153
+ this.waitedMs = init.waitedMs;
10154
+ withDisposition(this, "unpaid");
10155
+ }
10156
+ };
10139
10157
  var INSTALL_HINT2 = "npm install @x402/core@~2.28.0 @x402/svm@~2.28.0 @solana/kit";
10140
10158
  async function load(pkg, importer) {
10141
10159
  try {
@@ -10771,6 +10789,38 @@ function withoutClientSignedBatch(paymentRequired) {
10771
10789
  const accepts = paymentRequired.accepts ?? [];
10772
10790
  return accepts.some(clientSignedBatch) ? { ...paymentRequired, accepts: accepts.filter((accept) => !clientSignedBatch(accept)) } : paymentRequired;
10773
10791
  }
10792
+ var DEPOSIT_BLOCKHASH_TTL_MS = 1e4;
10793
+ var MAX_DEPOSIT_BLOCKHASH_ENDPOINTS = 8;
10794
+ var depositBlockhashes = /* @__PURE__ */ new Map();
10795
+ async function finalizedBlockhash(rpcUrl, rpcHeaders) {
10796
+ const cached = depositBlockhashes.get(rpcUrl);
10797
+ if (cached && Date.now() - cached.fetchedAt < DEPOSIT_BLOCKHASH_TTL_MS) return cached;
10798
+ const result = await rpcRequest(rpcUrl, "getLatestBlockhash", [{ commitment: "finalized" }], rpcHeaders);
10799
+ const blockhash = result.value?.blockhash;
10800
+ if (typeof blockhash !== "string" || !blockhash) throw new Error("getLatestBlockhash returned no blockhash");
10801
+ const entry = {
10802
+ blockhash,
10803
+ lastValidBlockHeight: rpcInteger(result.value?.lastValidBlockHeight, "getLatestBlockhash lastValidBlockHeight"),
10804
+ fetchedAt: Date.now()
10805
+ };
10806
+ depositBlockhashes.delete(rpcUrl);
10807
+ depositBlockhashes.set(rpcUrl, entry);
10808
+ while (depositBlockhashes.size > MAX_DEPOSIT_BLOCKHASH_ENDPOINTS) {
10809
+ depositBlockhashes.delete(depositBlockhashes.keys().next().value);
10810
+ }
10811
+ return entry;
10812
+ }
10813
+ function withDepositBlockhash(paymentRequired, blockhash) {
10814
+ const accepts = paymentRequired.accepts ?? [];
10815
+ const lacking = (accept) => accept.scheme === BATCH_SCHEME && !accept.extra?.recentBlockhash;
10816
+ if (!accepts.some(lacking)) return paymentRequired;
10817
+ return {
10818
+ ...paymentRequired,
10819
+ accepts: accepts.map(
10820
+ (accept) => lacking(accept) ? { ...accept, extra: { ...accept.extra, recentBlockhash: blockhash.blockhash, lastValidBlockHeight: blockhash.lastValidBlockHeight } } : accept
10821
+ )
10822
+ };
10823
+ }
10774
10824
  var PAYMENT_IDENTIFIER = "payment-identifier";
10775
10825
  function withPaymentIdentifier(paymentRequired) {
10776
10826
  const declared = paymentRequired.extensions?.[PAYMENT_IDENTIFIER];
@@ -11168,12 +11218,22 @@ var SolanaBatchPayer = class {
11168
11218
  if (!Number.isFinite(this.inDoubtMaxWaitMs) || this.inDoubtMaxWaitMs < 0) {
11169
11219
  throw new Error("batch.inDoubt.maxWaitMs must be a finite number of ms >= 0");
11170
11220
  }
11221
+ this.onLocked = init.options.onLocked ?? "exact";
11222
+ if (this.onLocked !== "exact" && this.onLocked !== "wait" && this.onLocked !== "throw") {
11223
+ throw new Error('batch.onLocked must be "exact", "wait" or "throw"');
11224
+ }
11225
+ this.lockWaitMs = init.options.lockWaitMs ?? DEFAULT_BATCH_LOCK_WAIT_MS;
11226
+ if (!Number.isFinite(this.lockWaitMs) || this.lockWaitMs < 0) {
11227
+ throw new Error("batch.lockWaitMs must be a finite number of ms >= 0");
11228
+ }
11171
11229
  }
11172
11230
  init;
11173
11231
  state;
11174
11232
  maxAttempts;
11175
11233
  maxWaitMs;
11176
11234
  inDoubtMaxWaitMs;
11235
+ onLocked;
11236
+ lockWaitMs;
11177
11237
  counters = {
11178
11238
  fallbacks: 0,
11179
11239
  fallbacksByReason: {},
@@ -11222,6 +11282,7 @@ var SolanaBatchPayer = class {
11222
11282
  if (owner !== void 0) {
11223
11283
  return {
11224
11284
  reason: "channel_store_locked",
11285
+ ownerPid: owner,
11225
11286
  detail: owner === process.pid ? `channel store ${file} is in use by another copy of @blockrun/llm in this process (or by a process that had this pid and died: remove ${file}.lock if nothing uses it)` : `channel store ${file} is in use by process ${owner}`
11226
11287
  };
11227
11288
  }
@@ -11567,12 +11628,23 @@ var SolanaBatchPayer = class {
11567
11628
  const unavailable = batchUnavailable(paymentRequired);
11568
11629
  if (unavailable) return this.fallback(unavailable);
11569
11630
  let wallet;
11631
+ let waitedMs = 0;
11570
11632
  try {
11571
11633
  wallet = await this.wallet();
11634
+ if ("reason" in wallet && wallet.reason === "channel_store_locked" && this.onLocked === "wait") {
11635
+ const started = Date.now();
11636
+ wallet = await this.waitForLock(wallet);
11637
+ waitedMs = Date.now() - started;
11638
+ }
11572
11639
  } catch (err) {
11573
11640
  return this.fallback(creationFailure(err));
11574
11641
  }
11575
- if ("reason" in wallet) return this.fallback({ reason: wallet.reason, detail: wallet.detail });
11642
+ if ("reason" in wallet) {
11643
+ if (wallet.reason === "channel_store_locked" && this.onLocked !== "exact") {
11644
+ return this.lockedOut(wallet.detail, wallet.ownerPid, waitedMs);
11645
+ }
11646
+ return this.fallback({ reason: wallet.reason, detail: wallet.detail });
11647
+ }
11576
11648
  if (closesAtStart !== void 0 && wallet.closes !== closesAtStart) {
11577
11649
  return this.fallback({
11578
11650
  reason: "closed_during_call",
@@ -11589,6 +11661,40 @@ var SolanaBatchPayer = class {
11589
11661
  wallet.active -= 1;
11590
11662
  }
11591
11663
  }
11664
+ /**
11665
+ * `onLocked: "wait"`: try the wallet's channel store again every
11666
+ * {@link LOCK_POLL_MS} until its owner lets go of it or `lockWaitMs` runs
11667
+ * out. Reported once as a `backoff` event (`channel_store_locked`).
11668
+ */
11669
+ async waitForLock(refusal) {
11670
+ const deadline = Date.now() + this.lockWaitMs;
11671
+ let current = refusal;
11672
+ if (this.lockWaitMs > 0) {
11673
+ await this.report({
11674
+ type: "backoff",
11675
+ reason: "channel_store_locked",
11676
+ retryAfterMs: this.lockWaitMs,
11677
+ detail: refusal.detail
11678
+ });
11679
+ }
11680
+ while ("reason" in current && current.reason === "channel_store_locked") {
11681
+ const left = deadline - Date.now();
11682
+ if (left <= 0) break;
11683
+ await sleep2(Math.min(LOCK_POLL_MS, left));
11684
+ current = await this.wallet();
11685
+ }
11686
+ return current;
11687
+ }
11688
+ /** `onLocked` other than `"exact"`: the call's `"unpaid"` error, nothing signed. */
11689
+ async lockedOut(detail, ownerPid, waitedMs) {
11690
+ const error = new BatchChannelLockedError({
11691
+ wallet: await this.init.address(),
11692
+ detail,
11693
+ waitedMs,
11694
+ ...ownerPid !== void 0 ? { ownerPid } : {}
11695
+ });
11696
+ return { kind: "failed", error };
11697
+ }
11592
11698
  /**
11593
11699
  * The close fence for a call about to send its first request: how many
11594
11700
  * closes this wallet has completed so far, to hand to {@link pay}.
@@ -11679,11 +11785,9 @@ var SolanaBatchPayer = class {
11679
11785
  }
11680
11786
  if (wallet.busy) return fallback({ reason: "channel_busy", attempt: budget.attempt });
11681
11787
  if (wallet.file && !holdsChannelFile(wallet.file)) {
11682
- return fallback({
11683
- reason: "channel_store_locked",
11684
- attempt: budget.attempt,
11685
- detail: `this process no longer holds the lock on ${wallet.file}; another owner may be paying into its channels`
11686
- });
11788
+ const detail = `this process no longer holds the lock on ${wallet.file}; another owner may be paying into its channels`;
11789
+ if (this.onLocked !== "exact") return this.lockedOut(detail, void 0, 0);
11790
+ return fallback({ reason: "channel_store_locked", attempt: budget.attempt, detail });
11687
11791
  }
11688
11792
  wallet.busy = true;
11689
11793
  holding = true;
@@ -12260,7 +12364,8 @@ var SolanaBatchPayer = class {
12260
12364
  const http = (await this.build(wallet)).http;
12261
12365
  try {
12262
12366
  await this.prepareTopUp(wallet, paymentRequired);
12263
- return { http, payload: await http.createPaymentPayload(withPaymentIdentifier(withoutClientSignedBatch(paymentRequired))) };
12367
+ const required = await this.withDepositBlockhash(wallet, withoutClientSignedBatch(paymentRequired));
12368
+ return { http, payload: await http.createPaymentPayload(withPaymentIdentifier(required)) };
12264
12369
  } catch (err) {
12265
12370
  if (pass > 0 || !isResyncRequired(err) || wallet.resyncs.size === 0) throw err;
12266
12371
  } finally {
@@ -12268,6 +12373,34 @@ var SolanaBatchPayer = class {
12268
12373
  }
12269
12374
  }
12270
12375
  }
12376
+ /**
12377
+ * The 402 to build this payment from: with a finalized blockhash
12378
+ * ({@link withDepositBlockhash}) when the payment will likely carry a
12379
+ * deposit (no usable record for the trusted accept's channel, so an open,
12380
+ * or a top-up {@link prepareTopUp} just sized). An authorization signs no
12381
+ * transaction and costs no RPC read. Best effort: a failed read leaves the
12382
+ * 402 as it is, and `@x402/svm` fetches its own blockhash.
12383
+ */
12384
+ async withDepositBlockhash(wallet, paymentRequired) {
12385
+ const accept = trustedBatchAccept(paymentRequired, this.init.options.operators);
12386
+ const key = accept ? channelKeyOf(accept) : void 0;
12387
+ if (!accept || accept.extra?.recentBlockhash) return paymentRequired;
12388
+ let deposit = wallet.topUp !== void 0;
12389
+ if (!deposit) {
12390
+ try {
12391
+ const record = key ? await wallet.book.base.get(key) : void 0;
12392
+ deposit = !record || typeof record.channelId !== "string" || record.pending !== void 0;
12393
+ } catch {
12394
+ deposit = true;
12395
+ }
12396
+ }
12397
+ if (!deposit) return paymentRequired;
12398
+ try {
12399
+ return withDepositBlockhash(paymentRequired, await finalizedBlockhash(this.init.rpcUrl, this.init.rpcHeaders));
12400
+ } catch {
12401
+ return paymentRequired;
12402
+ }
12403
+ }
12271
12404
  /**
12272
12405
  * Queue a re-read of a stored channel whose account is gone at finalized
12273
12406
  * and at confirmed commitment (closed, its account reclaimed), anchored at
@@ -12574,7 +12707,6 @@ var SolanaBatchPayer = class {
12574
12707
  // src/solana-client.ts
12575
12708
  var SOLANA_API_URL = "https://sol.blockrun.ai/api";
12576
12709
  var DEFAULT_MAX_TOKENS2 = 1024;
12577
- var DEFAULT_TIMEOUT14 = 6e4;
12578
12710
  var STALE_BLOCKHASH_RETRY_BACKOFFS_MS = [500, 2e3];
12579
12711
  var MAX_PAYMENT_FAILURE_BYTES = 64 * 1024;
12580
12712
  var DEFAULT_VIDEO_MODEL = "xai/grok-imagine-video";
@@ -12742,7 +12874,7 @@ var SolanaLLMClient = class {
12742
12874
  const rpc = resolveRpcConfig(options.rpcUrl, options.rpcHeaders);
12743
12875
  this.rpcUrl = rpc.url;
12744
12876
  this.rpcHeaders = rpc.headers;
12745
- this.timeout = options.timeout || DEFAULT_TIMEOUT14;
12877
+ this.timeout = options.timeout || DEFAULT_TIMEOUT;
12746
12878
  if (options.batch && !this.apiAuth) {
12747
12879
  this.batchPayer = new SolanaBatchPayer({
12748
12880
  options: options.batch,
@@ -14509,11 +14641,13 @@ export {
14509
14641
  AnthropicClient,
14510
14642
  BASE_CHAIN_ID,
14511
14643
  BLOCKRUN_SOL_OPERATOR,
14644
+ BatchChannelLockedError,
14512
14645
  BatchCloseDeferredError,
14513
14646
  BatchPaymentUnresolvedError,
14514
14647
  BlockrunClient,
14515
14648
  BlockrunError,
14516
14649
  DEFAULT_BATCH_IN_DOUBT,
14650
+ DEFAULT_BATCH_LOCK_WAIT_MS,
14517
14651
  DEFAULT_BATCH_RATE_LIMIT,
14518
14652
  EVM_NETWORKS,
14519
14653
  ImageClient,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/llm",
3
- "version": "3.24.9",
3
+ "version": "3.25.0",
4
4
  "type": "module",
5
5
  "description": "TypeScript SDK for BlockRun - every frontier model behind one OpenAI-compatible client. Pay with a BlockRun API key or per-call in USDC. Smart routing picks the cheapest capable model. No rate limits.",
6
6
  "main": "dist/index.cjs",