@blockrun/llm 3.24.8 → 3.24.10

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.js CHANGED
@@ -423,13 +423,44 @@ function readSettlement(response, quotedUsd, scheme) {
423
423
  };
424
424
  return { costUsd, settlement };
425
425
  }
426
+ function settlementSkipReason(response) {
427
+ const headers = response.headers;
428
+ if (typeof headers?.get !== "function") return void 0;
429
+ const reason = [headers.get("X-Settlement-Skipped"), headers.get("X-Payment-Waived")].map((value) => value?.trim()).find((value) => !!value && !NOT_A_SKIP.test(value));
430
+ if (reason === void 0) return void 0;
431
+ const evidence = settlementEvidence(response);
432
+ if (evidence !== void 0) {
433
+ console.error(
434
+ `[@blockrun/llm] settlement: the gateway named a skipped settlement (${JSON.stringify(reason)}) on a response that also carries ${evidence}; booked as charged`
435
+ );
436
+ return void 0;
437
+ }
438
+ return reason;
439
+ }
440
+ var NOT_A_SKIP = /^(false|0|no|none)$/i;
441
+ function settlementEvidence(response) {
442
+ const header = response.headers.get("PAYMENT-RESPONSE") ?? response.headers.get("X-PAYMENT-RESPONSE");
443
+ const receipt = header ? decode(header) : void 0;
444
+ if (receipt && receipt.success !== false) {
445
+ if (typeof receipt.transaction === "string" && receipt.transaction) return "a settlement receipt with a transaction";
446
+ const extra = receipt.extra;
447
+ const charged = receiptChargedUsd({
448
+ amount: receipt.amount,
449
+ extra: extra && typeof extra === "object" ? extra : void 0
450
+ });
451
+ if (charged !== void 0 && charged > 0) return "a settlement receipt with a charge";
452
+ }
453
+ if (response.headers.get("X-Payment-Receipt")?.trim()) return "an X-Payment-Receipt transaction";
454
+ return void 0;
455
+ }
426
456
  function settlementSkipped(response) {
427
- return response.headers.get("X-Settlement-Skipped") === "free-fallback";
457
+ return settlementSkipReason(response) !== void 0;
428
458
  }
429
459
  function withCost(body, response, bookedUsd, scheme) {
430
- if (settlementSkipped(response)) {
460
+ const skipped = settlementSkipReason(response);
461
+ if (skipped !== void 0) {
431
462
  body.costUsd = 0;
432
- body.settlement = { ...readSettlement(response, bookedUsd, scheme).settlement, skipped: "free-fallback" };
463
+ body.settlement = { ...readSettlement(response, bookedUsd, scheme).settlement, skipped };
433
464
  return body;
434
465
  }
435
466
  body.costUsd = bookedUsd;
@@ -4958,7 +4989,7 @@ function getCostSummary() {
4958
4989
  }
4959
4990
 
4960
4991
  // src/version.ts
4961
- var SDK_VERSION = "3.24.8";
4992
+ var SDK_VERSION = "3.24.10";
4962
4993
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
4963
4994
 
4964
4995
  // src/client.ts
@@ -5554,9 +5585,9 @@ var LLMClient = class _LLMClient {
5554
5585
  return this.bookPaid(url, body, details, retryResponse);
5555
5586
  }
5556
5587
  /**
5557
- * Book a served, paid chat call and attach its cost. A response the gateway
5558
- * served from a free fallback with settlement skipped
5559
- * (`X-Settlement-Skipped: free-fallback`) moved nothing on chain, so it is
5588
+ * Book a served, paid chat call and attach its cost. A response whose
5589
+ * settlement the gateway skipped ({@link settlementSkipped}: e.g.
5590
+ * `X-Settlement-Skipped: free-fallback`) moved nothing on chain, so it is
5560
5591
  * not added to `getSpending()` or the cost log, and reports `costUsd: 0`.
5561
5592
  */
5562
5593
  async bookPaid(url, body, details, response) {
@@ -5568,6 +5599,21 @@ var LLMClient = class _LLMClient {
5568
5599
  }
5569
5600
  return withCost(await this.parseChatResponse(response), response, costUsd, "exact");
5570
5601
  }
5602
+ /**
5603
+ * Book a served, paid raw (non-chat) call: the signed amount goes to
5604
+ * `getSpending()` and the cost log, unless the gateway skipped settlement
5605
+ * ({@link settlementSkipped}: e.g. `X-Settlement-Skipped: empty-result` on an
5606
+ * empty prediction-market search, `unexecuted` on a JSON-RPC call that never
5607
+ * ran, or `X-Payment-Waived`), in which case nothing moved and nothing is
5608
+ * booked. `X-Payment-Settled: false` alone is still booked: it is owed.
5609
+ */
5610
+ bookPaidRaw(url, details, response, body) {
5611
+ if (settlementSkipped(response)) return;
5612
+ const costUsd = parseFloat(details.amount) / 1e6;
5613
+ this.sessionCalls += 1;
5614
+ this.sessionTotalUsd += costUsd;
5615
+ this.recordCost(url, costUsd, body ? { body, network: details.network } : { network: details.network });
5616
+ }
5571
5617
  /**
5572
5618
  * Sign a payment header and return the PAYMENT-SIGNATURE value.
5573
5619
  * Extracted to share logic between streaming and non-streaming flows.
@@ -5668,8 +5714,10 @@ var LLMClient = class _LLMClient {
5668
5714
  throw withDisposition(err, "paid-or-in-doubt");
5669
5715
  }
5670
5716
  if (preAuthResp.ok) {
5671
- this.sessionCalls += 1;
5672
- this.sessionTotalUsd += signed.costUsd;
5717
+ if (!settlementSkipped(preAuthResp)) {
5718
+ this.sessionCalls += 1;
5719
+ this.sessionTotalUsd += signed.costUsd;
5720
+ }
5673
5721
  return preAuthResp;
5674
5722
  }
5675
5723
  this.preAuthCache.delete(cacheKey2);
@@ -5734,8 +5782,10 @@ var LLMClient = class _LLMClient {
5734
5782
  }
5735
5783
  throw new APIError(`API error after payment: ${streamResp.status}`, streamResp.status, sanitizeErrorResponse(errorBody));
5736
5784
  }
5737
- this.sessionCalls += 1;
5738
- this.sessionTotalUsd += costUsd;
5785
+ if (!settlementSkipped(streamResp)) {
5786
+ this.sessionCalls += 1;
5787
+ this.sessionTotalUsd += costUsd;
5788
+ }
5739
5789
  return streamResp;
5740
5790
  }
5741
5791
  /**
@@ -5859,10 +5909,7 @@ var LLMClient = class _LLMClient {
5859
5909
  }
5860
5910
  throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
5861
5911
  }
5862
- const costUsd2 = parseFloat(details.amount) / 1e6;
5863
- this.sessionCalls += 1;
5864
- this.sessionTotalUsd += costUsd2;
5865
- this.recordCost(url, costUsd2, { body, network: details.network });
5912
+ this.bookPaidRaw(url, details, retryResp2, body);
5866
5913
  return retryResp2.json();
5867
5914
  }
5868
5915
  }
@@ -5882,10 +5929,7 @@ var LLMClient = class _LLMClient {
5882
5929
  sanitizeErrorResponse(errorBody)
5883
5930
  );
5884
5931
  }
5885
- const costUsd = parseFloat(details.amount) / 1e6;
5886
- this.sessionCalls += 1;
5887
- this.sessionTotalUsd += costUsd;
5888
- this.recordCost(url, costUsd, { body, network: details.network });
5932
+ this.bookPaidRaw(url, details, retryResponse, body);
5889
5933
  return retryResponse.json();
5890
5934
  }
5891
5935
  /**
@@ -6005,10 +6049,7 @@ var LLMClient = class _LLMClient {
6005
6049
  }
6006
6050
  throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
6007
6051
  }
6008
- const costUsd2 = parseFloat(details.amount) / 1e6;
6009
- this.sessionCalls += 1;
6010
- this.sessionTotalUsd += costUsd2;
6011
- this.recordCost(url, costUsd2, { network: details.network });
6052
+ this.bookPaidRaw(url, details, retryResp2);
6012
6053
  return retryResp2.json();
6013
6054
  }
6014
6055
  }
@@ -6028,10 +6069,7 @@ var LLMClient = class _LLMClient {
6028
6069
  sanitizeErrorResponse(errorBody)
6029
6070
  );
6030
6071
  }
6031
- const costUsd = parseFloat(details.amount) / 1e6;
6032
- this.sessionCalls += 1;
6033
- this.sessionTotalUsd += costUsd;
6034
- this.recordCost(url, costUsd, { network: details.network });
6072
+ this.bookPaidRaw(url, details, retryResponse);
6035
6073
  return retryResponse.json();
6036
6074
  }
6037
6075
  /**
@@ -7044,8 +7082,10 @@ var MusicClient = class {
7044
7082
  throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
7045
7083
  }
7046
7084
  const data = await retryResponse.json();
7047
- this.sessionCalls++;
7048
- this.sessionTotalUsd += 0.1575;
7085
+ if (!settlementSkipped(retryResponse)) {
7086
+ this.sessionCalls++;
7087
+ this.sessionTotalUsd += 0.1575;
7088
+ }
7049
7089
  const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
7050
7090
  if (txHash) data.txHash = txHash;
7051
7091
  return data;
@@ -7257,9 +7297,11 @@ var SpeechClient = class {
7257
7297
  throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
7258
7298
  }
7259
7299
  const data = await retryResponse.json();
7260
- this.sessionCalls++;
7261
- const paidUsd = Number(details.amount) / 1e6;
7262
- if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
7300
+ if (!settlementSkipped(retryResponse)) {
7301
+ this.sessionCalls++;
7302
+ const paidUsd = Number(details.amount) / 1e6;
7303
+ if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
7304
+ }
7263
7305
  const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
7264
7306
  if (txHash) data.txHash = txHash;
7265
7307
  return data;
@@ -7556,8 +7598,10 @@ var VideoClient = class {
7556
7598
  }
7557
7599
  if (lastStatus === "completed") {
7558
7600
  const data = pollData;
7559
- this.sessionCalls++;
7560
- this.sessionTotalUsd += Number(details.amount) / 1e6;
7601
+ if (!settlementSkipped(pollResp)) {
7602
+ this.sessionCalls++;
7603
+ this.sessionTotalUsd += Number(details.amount) / 1e6;
7604
+ }
7561
7605
  const txHash = pollResp.headers.get("x-payment-receipt") || pollResp.headers.get("X-Payment-Receipt");
7562
7606
  if (txHash) data.txHash = txHash;
7563
7607
  return data;
@@ -7852,8 +7896,10 @@ var VoiceClient = class {
7852
7896
  throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
7853
7897
  }
7854
7898
  const data = await retryResponse.json();
7855
- this.sessionCalls++;
7856
- this.sessionTotalUsd += CALL_PRICE_USD;
7899
+ if (!settlementSkipped(retryResponse)) {
7900
+ this.sessionCalls++;
7901
+ this.sessionTotalUsd += CALL_PRICE_USD;
7902
+ }
7857
7903
  const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
7858
7904
  if (txHash) data.txHash = txHash;
7859
7905
  return data;
@@ -8073,9 +8119,10 @@ var PhoneClient = class {
8073
8119
  throw new PaymentError("Payment was rejected. Check your wallet balance.");
8074
8120
  }
8075
8121
  const data = await this.unwrap(retry, true);
8076
- const costUsd = Number.parseFloat(details.amount) / 1e6;
8077
- this.sessionCalls += 1;
8078
- this.sessionTotalUsd += costUsd;
8122
+ if (!settlementSkipped(retry)) {
8123
+ this.sessionCalls += 1;
8124
+ this.sessionTotalUsd += Number.parseFloat(details.amount) / 1e6;
8125
+ }
8079
8126
  const txHash = retry.headers.get("x-payment-receipt") || retry.headers.get("X-Payment-Receipt");
8080
8127
  if (txHash && data && typeof data === "object" && !("txHash" in data)) {
8081
8128
  data.txHash = txHash;
@@ -8240,9 +8287,10 @@ var PortraitClient = class {
8240
8287
  throw new PaymentError("Payment was rejected. Check your wallet balance.");
8241
8288
  }
8242
8289
  const data = await this.unwrap(retry, true);
8243
- const costUsd = Number.parseFloat(details.amount) / 1e6;
8244
- this.sessionCalls += 1;
8245
- this.sessionTotalUsd += costUsd;
8290
+ if (!settlementSkipped(retry)) {
8291
+ this.sessionCalls += 1;
8292
+ this.sessionTotalUsd += Number.parseFloat(details.amount) / 1e6;
8293
+ }
8246
8294
  if (data && typeof data === "object" && !("txHash" in data)) {
8247
8295
  const settlement = data.settlement;
8248
8296
  if (settlement?.tx_hash) {
@@ -9059,9 +9107,11 @@ var RpcClient = class {
9059
9107
  }
9060
9108
  throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
9061
9109
  }
9062
- this.sessionCalls++;
9063
- const paidUsd = Number(details.amount) / 1e6;
9064
- if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
9110
+ if (!settlementSkipped(retryResponse)) {
9111
+ this.sessionCalls++;
9112
+ const paidUsd = Number(details.amount) / 1e6;
9113
+ if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
9114
+ }
9065
9115
  return { data: await retryResponse.json(), headers: retryResponse.headers };
9066
9116
  }
9067
9117
  async fetchWithTimeout(url, options) {
@@ -9208,7 +9258,7 @@ var BlockrunClient = class {
9208
9258
  if (resp402.status !== 402) {
9209
9259
  await this.throwApiError(resp402, `poll submit failed (${submitUrl})`);
9210
9260
  }
9211
- const paymentPayload = await this.signFrom402(resp402, submitUrl, {
9261
+ const { paymentPayload, costUsd } = await this.signFrom402(resp402, submitUrl, {
9212
9262
  description: "BlockRun async job",
9213
9263
  maxTimeoutSeconds: MAX_SIGNED_AUTH_SECONDS
9214
9264
  });
@@ -9227,7 +9277,7 @@ var BlockrunClient = class {
9227
9277
  await this.throwApiError(submitResp, `poll submit failed (${submitUrl})`);
9228
9278
  }
9229
9279
  if (submitResp.status === 200) {
9230
- this.recordSpending();
9280
+ this.recordSpending(submitResp, costUsd);
9231
9281
  return submitResp.json();
9232
9282
  }
9233
9283
  const submitData = await submitResp.json();
@@ -9264,7 +9314,7 @@ var BlockrunClient = class {
9264
9314
  );
9265
9315
  }
9266
9316
  if (pollResp.status === 200 && lastStatus === "completed") {
9267
- this.recordSpending();
9317
+ this.recordSpending(pollResp, costUsd);
9268
9318
  return pollData;
9269
9319
  }
9270
9320
  if (pollResp.status !== 200 && pollResp.status !== 202 && pollResp.status !== 504) {
@@ -9305,7 +9355,7 @@ var BlockrunClient = class {
9305
9355
  if (resp402.status === 200) {
9306
9356
  streamResp = resp402;
9307
9357
  } else if (resp402.status === 402) {
9308
- const paymentPayload = await this.signFrom402(resp402, url, {
9358
+ const { paymentPayload, costUsd } = await this.signFrom402(resp402, url, {
9309
9359
  description: "BlockRun stream"
9310
9360
  });
9311
9361
  streamResp = await this.fetchWithTimeout(url, {
@@ -9324,7 +9374,7 @@ var BlockrunClient = class {
9324
9374
  if (!streamResp.ok) {
9325
9375
  await this.throwApiError(streamResp, `stream failed after payment (${url})`);
9326
9376
  }
9327
- this.recordSpending();
9377
+ this.recordSpending(streamResp, costUsd);
9328
9378
  } else {
9329
9379
  await this.throwApiError(resp402, `stream failed (${url})`);
9330
9380
  return;
@@ -9380,7 +9430,7 @@ var BlockrunClient = class {
9380
9430
  return response.json();
9381
9431
  }
9382
9432
  async handlePaymentAndRetry(url, method, body, response) {
9383
- const paymentPayload = await this.signFrom402(response, url, {
9433
+ const { paymentPayload, costUsd } = await this.signFrom402(response, url, {
9384
9434
  description: "BlockRun"
9385
9435
  });
9386
9436
  const retryInit = {
@@ -9403,13 +9453,14 @@ var BlockrunClient = class {
9403
9453
  if (!retry.ok) {
9404
9454
  await this.throwApiError(retry, `${method} ${url} failed after payment`);
9405
9455
  }
9406
- this.recordSpending();
9456
+ this.recordSpending(retry, costUsd);
9407
9457
  return retry.json();
9408
9458
  }
9409
9459
  /**
9410
9460
  * Read a 402 response's payment requirements (header or body), then sign and
9411
- * return the base64 PAYMENT-SIGNATURE payload. Also records the cost-to-be
9412
- * onto the response context (settled on `recordSpending`).
9461
+ * return the base64 PAYMENT-SIGNATURE payload with what it pays, in USD.
9462
+ * The cost stays with the call that signed it (booked by `recordSpending`):
9463
+ * concurrent calls on one client never share it.
9413
9464
  */
9414
9465
  async signFrom402(response, url, opts) {
9415
9466
  let paymentHeader = response.headers.get("payment-required");
@@ -9427,8 +9478,8 @@ var BlockrunClient = class {
9427
9478
  }
9428
9479
  const paymentRequired = parsePaymentRequired(paymentHeader);
9429
9480
  const details = extractPaymentDetails(paymentRequired);
9430
- this.pendingCostUsd = parseFloat(details.amount) / 1e6;
9431
- return createPaymentPayload(
9481
+ const costUsd = parseFloat(details.amount) / 1e6;
9482
+ const paymentPayload = await createPaymentPayload(
9432
9483
  this.privateKey,
9433
9484
  this.account.address,
9434
9485
  details.recipient,
@@ -9445,14 +9496,20 @@ var BlockrunClient = class {
9445
9496
  asset: details.asset
9446
9497
  }
9447
9498
  );
9499
+ return { paymentPayload, costUsd };
9448
9500
  }
9449
- /** Accumulates the most-recent pending cost; settled by recordSpending. */
9450
- pendingCostUsd = 0;
9451
- recordSpending() {
9452
- if (this.pendingCostUsd > 0) {
9501
+ /**
9502
+ * Book `costUsd`, what the call that `response` answered signed. A response
9503
+ * whose settlement the gateway skipped ({@link settlementSkipped}:
9504
+ * `X-Settlement-Skipped: <reason>` or `X-Payment-Waived`) moved nothing, so
9505
+ * nothing is booked. `X-Payment-Settled: false` alone is still booked: the
9506
+ * payment is owed.
9507
+ */
9508
+ recordSpending(response, costUsd) {
9509
+ if (settlementSkipped(response)) return;
9510
+ if (costUsd > 0) {
9453
9511
  this.sessionCalls += 1;
9454
- this.sessionTotalUsd += this.pendingCostUsd;
9455
- this.pendingCostUsd = 0;
9512
+ this.sessionTotalUsd += costUsd;
9456
9513
  }
9457
9514
  }
9458
9515
  async throwApiError(resp, prefix) {
@@ -10714,6 +10771,38 @@ function withoutClientSignedBatch(paymentRequired) {
10714
10771
  const accepts = paymentRequired.accepts ?? [];
10715
10772
  return accepts.some(clientSignedBatch) ? { ...paymentRequired, accepts: accepts.filter((accept) => !clientSignedBatch(accept)) } : paymentRequired;
10716
10773
  }
10774
+ var DEPOSIT_BLOCKHASH_TTL_MS = 1e4;
10775
+ var MAX_DEPOSIT_BLOCKHASH_ENDPOINTS = 8;
10776
+ var depositBlockhashes = /* @__PURE__ */ new Map();
10777
+ async function finalizedBlockhash(rpcUrl, rpcHeaders) {
10778
+ const cached = depositBlockhashes.get(rpcUrl);
10779
+ if (cached && Date.now() - cached.fetchedAt < DEPOSIT_BLOCKHASH_TTL_MS) return cached;
10780
+ const result = await rpcRequest(rpcUrl, "getLatestBlockhash", [{ commitment: "finalized" }], rpcHeaders);
10781
+ const blockhash = result.value?.blockhash;
10782
+ if (typeof blockhash !== "string" || !blockhash) throw new Error("getLatestBlockhash returned no blockhash");
10783
+ const entry = {
10784
+ blockhash,
10785
+ lastValidBlockHeight: rpcInteger(result.value?.lastValidBlockHeight, "getLatestBlockhash lastValidBlockHeight"),
10786
+ fetchedAt: Date.now()
10787
+ };
10788
+ depositBlockhashes.delete(rpcUrl);
10789
+ depositBlockhashes.set(rpcUrl, entry);
10790
+ while (depositBlockhashes.size > MAX_DEPOSIT_BLOCKHASH_ENDPOINTS) {
10791
+ depositBlockhashes.delete(depositBlockhashes.keys().next().value);
10792
+ }
10793
+ return entry;
10794
+ }
10795
+ function withDepositBlockhash(paymentRequired, blockhash) {
10796
+ const accepts = paymentRequired.accepts ?? [];
10797
+ const lacking = (accept) => accept.scheme === BATCH_SCHEME && !accept.extra?.recentBlockhash;
10798
+ if (!accepts.some(lacking)) return paymentRequired;
10799
+ return {
10800
+ ...paymentRequired,
10801
+ accepts: accepts.map(
10802
+ (accept) => lacking(accept) ? { ...accept, extra: { ...accept.extra, recentBlockhash: blockhash.blockhash, lastValidBlockHeight: blockhash.lastValidBlockHeight } } : accept
10803
+ )
10804
+ };
10805
+ }
10717
10806
  var PAYMENT_IDENTIFIER = "payment-identifier";
10718
10807
  function withPaymentIdentifier(paymentRequired) {
10719
10808
  const declared = paymentRequired.extensions?.[PAYMENT_IDENTIFIER];
@@ -11671,6 +11760,8 @@ var SolanaBatchPayer = class {
11671
11760
  wallet.refusedFor = outcome.errorReason;
11672
11761
  }
11673
11762
  }
11763
+ } else if (built.key && outcome.reason === "payment_required" && outcome.refusedForMs === void 0) {
11764
+ await this.recheckRefusedChannel(wallet, built.key, built.reclaimAnchor);
11674
11765
  }
11675
11766
  sent = void 0;
11676
11767
  release();
@@ -11938,6 +12029,13 @@ var SolanaBatchPayer = class {
11938
12029
  }
11939
12030
  /** Build a new batch payment for a 402: the payload, its headers and its kind. */
11940
12031
  async buildPayment(wallet, paymentRequired) {
12032
+ const acceptKey = channelKeyOf(trustedBatchAccept(paymentRequired, this.init.options.operators));
12033
+ let reclaimAnchor;
12034
+ try {
12035
+ const stored = acceptKey ? await wallet.book.base.get(acceptKey) : void 0;
12036
+ reclaimAnchor = stored ? savedReclaimAnchor(stored) : void 0;
12037
+ } catch {
12038
+ }
11941
12039
  const { http, payload } = await this.createPayload(wallet, paymentRequired);
11942
12040
  const scope = payScope.getStore();
11943
12041
  if (scope?.afterError) {
@@ -11959,7 +12057,16 @@ var SolanaBatchPayer = class {
11959
12057
  const record = known?.record;
11960
12058
  kind = record && (record.pending === void 0 || record.hasConfirmedState) ? "top-up" : "open";
11961
12059
  }
11962
- return { http, payload, headers, kind, key: known?.key, sentAt: Date.now(), sends: 0 };
12060
+ return {
12061
+ http,
12062
+ payload,
12063
+ headers,
12064
+ kind,
12065
+ key: known?.key,
12066
+ ...reclaimAnchor !== void 0 && known?.key === acceptKey ? { reclaimAnchor } : {},
12067
+ sentAt: Date.now(),
12068
+ sends: 0
12069
+ };
11963
12070
  }
11964
12071
  /**
11965
12072
  * Write a deposit's intent to the wallet's journal before it is sent
@@ -12185,7 +12292,8 @@ var SolanaBatchPayer = class {
12185
12292
  const http = (await this.build(wallet)).http;
12186
12293
  try {
12187
12294
  await this.prepareTopUp(wallet, paymentRequired);
12188
- return { http, payload: await http.createPaymentPayload(withPaymentIdentifier(withoutClientSignedBatch(paymentRequired))) };
12295
+ const required = await this.withDepositBlockhash(wallet, withoutClientSignedBatch(paymentRequired));
12296
+ return { http, payload: await http.createPaymentPayload(withPaymentIdentifier(required)) };
12189
12297
  } catch (err) {
12190
12298
  if (pass > 0 || !isResyncRequired(err) || wallet.resyncs.size === 0) throw err;
12191
12299
  } finally {
@@ -12193,6 +12301,112 @@ var SolanaBatchPayer = class {
12193
12301
  }
12194
12302
  }
12195
12303
  }
12304
+ /**
12305
+ * The 402 to build this payment from: with a finalized blockhash
12306
+ * ({@link withDepositBlockhash}) when the payment will likely carry a
12307
+ * deposit (no usable record for the trusted accept's channel, so an open,
12308
+ * or a top-up {@link prepareTopUp} just sized). An authorization signs no
12309
+ * transaction and costs no RPC read. Best effort: a failed read leaves the
12310
+ * 402 as it is, and `@x402/svm` fetches its own blockhash.
12311
+ */
12312
+ async withDepositBlockhash(wallet, paymentRequired) {
12313
+ const accept = trustedBatchAccept(paymentRequired, this.init.options.operators);
12314
+ const key = accept ? channelKeyOf(accept) : void 0;
12315
+ if (!accept || accept.extra?.recentBlockhash) return paymentRequired;
12316
+ let deposit = wallet.topUp !== void 0;
12317
+ if (!deposit) {
12318
+ try {
12319
+ const record = key ? await wallet.book.base.get(key) : void 0;
12320
+ deposit = !record || typeof record.channelId !== "string" || record.pending !== void 0;
12321
+ } catch {
12322
+ deposit = true;
12323
+ }
12324
+ }
12325
+ if (!deposit) return paymentRequired;
12326
+ try {
12327
+ return withDepositBlockhash(paymentRequired, await finalizedBlockhash(this.init.rpcUrl, this.init.rpcHeaders));
12328
+ } catch {
12329
+ return paymentRequired;
12330
+ }
12331
+ }
12332
+ /**
12333
+ * Queue a re-read of a stored channel whose account is gone at finalized
12334
+ * and at confirmed commitment (closed, its account reclaimed), anchored at
12335
+ * the SDK's own `getBlockHeight` at confirmed. The anchor is saved in the
12336
+ * record (`reclaimAnchorHeight`), so a short-lived process finishes the
12337
+ * wait instead of starting it over; a failed read leaves resync to take it,
12338
+ * and a failed save only costs that. resync pays exact until finality is
12339
+ * LANDING_MARGIN_BLOCKS past the anchor, then drops the record if the
12340
+ * finalized chain still has no account (a lagging read finds the channel
12341
+ * and rewrites the record, without the anchor).
12342
+ */
12343
+ async queueReclaimed(wallet, key, record, channelId, savedAnchor) {
12344
+ let anchorHeight = savedReclaimAnchor(record) ?? savedAnchor;
12345
+ if (anchorHeight === void 0) {
12346
+ try {
12347
+ anchorHeight = rpcInteger(
12348
+ await rpcRequest(this.init.rpcUrl, "getBlockHeight", [{ commitment: "confirmed" }], this.init.rpcHeaders),
12349
+ "getBlockHeight"
12350
+ );
12351
+ } catch {
12352
+ }
12353
+ }
12354
+ if (anchorHeight !== void 0 && savedReclaimAnchor(record) !== anchorHeight) {
12355
+ try {
12356
+ await wallet.book.set(key, { ...record, reclaimAnchorHeight: anchorHeight });
12357
+ } catch {
12358
+ }
12359
+ }
12360
+ addResyncTarget(wallet, {
12361
+ key,
12362
+ channelId,
12363
+ channelConfig: record.channelConfig,
12364
+ cumulative: parseAtomic(record.chargedCumulativeAmount),
12365
+ knownDeposit: parseAtomic(record.deposit),
12366
+ ...anchorHeight !== void 0 ? { anchorHeight } : {},
12367
+ reason: "channel_unusable"
12368
+ });
12369
+ }
12370
+ /**
12371
+ * After the gateway refused an authorization with a plain 402 (no receipt,
12372
+ * not an admission refusal), check the channel it paid into. That is how
12373
+ * the gateway answers an authorization into a channel the facilitator
12374
+ * closed (its verify rejects it), and a channel with deposit to spare is
12375
+ * otherwise never read again: every call would sign another authorization
12376
+ * into it and pay exact. A channel the finalized chain shows closing,
12377
+ * closed or not this wallet's is re-read through resync (record dropped,
12378
+ * or kept and paid exact); one gone at every commitment is queued as
12379
+ * {@link queueReclaimed}; an open one of this wallet's, or one only at
12380
+ * confirmed (not finalized yet), is left as it is. A queued re-read is run
12381
+ * at once. Best effort: a failed read changes nothing, and the next
12382
+ * refusal checks again.
12383
+ */
12384
+ async recheckRefusedChannel(wallet, key, savedAnchor) {
12385
+ try {
12386
+ const record = await wallet.book.get(key);
12387
+ if (!record || record.pending || typeof record.channelId !== "string" || wallet.resyncs.has(key)) return;
12388
+ const channelId = record.channelId;
12389
+ const channel = await readChannelAccount(this.init.rpcUrl, channelId, this.init.rpcHeaders);
12390
+ if (channel !== void 0) {
12391
+ if (channel.open && channel.payer === await this.init.address() && channel.authorizedSigner === record.channelConfig.payerAuthorizer && channel.mint === record.channelConfig.token) {
12392
+ return;
12393
+ }
12394
+ addResyncTarget(wallet, {
12395
+ key,
12396
+ channelId,
12397
+ channelConfig: record.channelConfig,
12398
+ cumulative: parseAtomic(record.chargedCumulativeAmount),
12399
+ knownDeposit: parseAtomic(record.deposit),
12400
+ reason: "channel_unusable"
12401
+ });
12402
+ } else {
12403
+ if (await readChannelAccount(this.init.rpcUrl, channelId, this.init.rpcHeaders, { commitment: "confirmed" }) !== void 0) return;
12404
+ await this.queueReclaimed(wallet, key, record, channelId, savedAnchor);
12405
+ }
12406
+ await this.resync(wallet);
12407
+ } catch {
12408
+ }
12409
+ }
12196
12410
  /**
12197
12411
  * When this payment will top up the wallet's channel, read how much of its
12198
12412
  * deposit is already settled on-chain (at `finalized` commitment, so never
@@ -12246,31 +12460,7 @@ var SolanaBatchPayer = class {
12246
12460
  }
12247
12461
  if (channel === void 0 && unfinalized) return;
12248
12462
  if (channel === void 0) {
12249
- let anchorHeight = savedReclaimAnchor(record);
12250
- if (anchorHeight === void 0) {
12251
- try {
12252
- anchorHeight = rpcInteger(
12253
- await rpcRequest(this.init.rpcUrl, "getBlockHeight", [{ commitment: "confirmed" }], this.init.rpcHeaders),
12254
- "getBlockHeight"
12255
- );
12256
- } catch {
12257
- }
12258
- if (anchorHeight !== void 0) {
12259
- try {
12260
- await wallet.book.set(key, { ...record, reclaimAnchorHeight: anchorHeight });
12261
- } catch {
12262
- }
12263
- }
12264
- }
12265
- addResyncTarget(wallet, {
12266
- key,
12267
- channelId: record.channelId,
12268
- channelConfig: record.channelConfig,
12269
- cumulative: parseAtomic(record.chargedCumulativeAmount),
12270
- knownDeposit: deposit,
12271
- ...anchorHeight !== void 0 ? { anchorHeight } : {},
12272
- reason: "channel_unusable"
12273
- });
12463
+ await this.queueReclaimed(wallet, key, record, record.channelId);
12274
12464
  throw new ChannelResyncRequiredError(record.channelId);
12275
12465
  }
12276
12466
  if (!channel.open || channel.payer !== await this.init.address() || channel.authorizedSigner !== record.channelConfig.payerAuthorizer || channel.mint !== record.channelConfig.token) {
@@ -12445,7 +12635,6 @@ var SolanaBatchPayer = class {
12445
12635
  // src/solana-client.ts
12446
12636
  var SOLANA_API_URL = "https://sol.blockrun.ai/api";
12447
12637
  var DEFAULT_MAX_TOKENS2 = 1024;
12448
- var DEFAULT_TIMEOUT14 = 6e4;
12449
12638
  var STALE_BLOCKHASH_RETRY_BACKOFFS_MS = [500, 2e3];
12450
12639
  var MAX_PAYMENT_FAILURE_BYTES = 64 * 1024;
12451
12640
  var DEFAULT_VIDEO_MODEL = "xai/grok-imagine-video";
@@ -12613,7 +12802,7 @@ var SolanaLLMClient = class {
12613
12802
  const rpc = resolveRpcConfig(options.rpcUrl, options.rpcHeaders);
12614
12803
  this.rpcUrl = rpc.url;
12615
12804
  this.rpcHeaders = rpc.headers;
12616
- this.timeout = options.timeout || DEFAULT_TIMEOUT14;
12805
+ this.timeout = options.timeout || DEFAULT_TIMEOUT;
12617
12806
  if (options.batch && !this.apiAuth) {
12618
12807
  this.batchPayer = new SolanaBatchPayer({
12619
12808
  options: options.batch,
@@ -12907,7 +13096,7 @@ var SolanaLLMClient = class {
12907
13096
  body: requestBody
12908
13097
  });
12909
13098
  await this.assertPaid(paid);
12910
- this.recordSettlement(costUsd);
13099
+ this.recordSettlement(costUsd, paid);
12911
13100
  return paid;
12912
13101
  } catch (error) {
12913
13102
  if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
@@ -13294,7 +13483,7 @@ var SolanaLLMClient = class {
13294
13483
  closesAtStart
13295
13484
  );
13296
13485
  if (batch.kind === "paid") {
13297
- this.recordSettlement(batch.chargedUsd);
13486
+ this.recordSettlement(batch.chargedUsd, batch.response);
13298
13487
  const paid = await this.readPaidJson(batch.response);
13299
13488
  return withCost(paid, batch.response, batch.chargedUsd, "batch");
13300
13489
  }
@@ -13411,7 +13600,7 @@ var SolanaLLMClient = class {
13411
13600
  };
13412
13601
  const deliver = (data, response) => {
13413
13602
  if (kind === "video") {
13414
- if (videoCostUsd !== void 0) this.recordSettlement(videoCostUsd);
13603
+ if (videoCostUsd !== void 0) this.recordSettlement(videoCostUsd, response);
13415
13604
  const txHash = response.headers.get("x-payment-receipt");
13416
13605
  if (txHash) data.txHash = txHash;
13417
13606
  }
@@ -13658,7 +13847,7 @@ var SolanaLLMClient = class {
13658
13847
  throw withDisposition(error, "paid-or-in-doubt");
13659
13848
  }
13660
13849
  await this.assertPaid(retryResponse);
13661
- if (!settlementSkipped(retryResponse)) this.recordSettlement(costUsd);
13850
+ this.recordSettlement(costUsd, retryResponse);
13662
13851
  const paid = await this.readPaidJson(retryResponse);
13663
13852
  return withCost(paid, retryResponse, costUsd, "exact");
13664
13853
  }
@@ -13718,11 +13907,11 @@ var SolanaLLMClient = class {
13718
13907
  await this.assertPaid(retryResponse);
13719
13908
  if (retryResponse.status === 202) {
13720
13909
  const kind = pollKindFor(url);
13721
- if (kind !== "video") this.recordSettlement(costUsd);
13910
+ if (kind !== "video") this.recordSettlement(costUsd, retryResponse);
13722
13911
  const submitted = await retryResponse.json();
13723
13912
  return this.followSolanaJob(submitted, media.pollBudgetMs ?? this.timeout, kind);
13724
13913
  }
13725
- this.recordSettlement(costUsd);
13914
+ this.recordSettlement(costUsd, retryResponse);
13726
13915
  return retryResponse.json();
13727
13916
  }
13728
13917
  async getWithPaymentRaw(endpoint, params) {
@@ -13766,7 +13955,7 @@ var SolanaLLMClient = class {
13766
13955
  }
13767
13956
  });
13768
13957
  await this.assertPaid(retryResponse);
13769
- this.recordSettlement(costUsd);
13958
+ this.recordSettlement(costUsd, retryResponse);
13770
13959
  return retryResponse.json();
13771
13960
  }
13772
13961
  /**
@@ -13804,8 +13993,17 @@ var SolanaLLMClient = class {
13804
13993
  );
13805
13994
  }
13806
13995
  }
13807
- /** Count one settled x402 payment against the session total. */
13808
- recordSettlement(costUsd) {
13996
+ /**
13997
+ * Count one settled x402 payment against the session total.
13998
+ *
13999
+ * Takes the paid response so no booking site can forget the check: a
14000
+ * response whose settlement the gateway skipped ({@link settlementSkipped}:
14001
+ * `X-Settlement-Skipped: free-fallback | empty-result | unexecuted | …`, or
14002
+ * `X-Payment-Waived`) moved nothing on chain and books nothing.
14003
+ * `X-Payment-Settled: false` alone is still booked: the payment is owed.
14004
+ */
14005
+ recordSettlement(costUsd, response) {
14006
+ if (settlementSkipped(response)) return;
13809
14007
  this.sessionCalls += 1;
13810
14008
  this.sessionTotalUsd += costUsd;
13811
14009
  }