@blockrun/llm 3.24.8 → 3.24.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -554,13 +554,44 @@ function readSettlement(response, quotedUsd, scheme) {
554
554
  };
555
555
  return { costUsd, settlement };
556
556
  }
557
+ function settlementSkipReason(response) {
558
+ const headers = response.headers;
559
+ if (typeof headers?.get !== "function") return void 0;
560
+ const reason = [headers.get("X-Settlement-Skipped"), headers.get("X-Payment-Waived")].map((value) => value?.trim()).find((value) => !!value && !NOT_A_SKIP.test(value));
561
+ if (reason === void 0) return void 0;
562
+ const evidence = settlementEvidence(response);
563
+ if (evidence !== void 0) {
564
+ console.error(
565
+ `[@blockrun/llm] settlement: the gateway named a skipped settlement (${JSON.stringify(reason)}) on a response that also carries ${evidence}; booked as charged`
566
+ );
567
+ return void 0;
568
+ }
569
+ return reason;
570
+ }
571
+ var NOT_A_SKIP = /^(false|0|no|none)$/i;
572
+ function settlementEvidence(response) {
573
+ const header = response.headers.get("PAYMENT-RESPONSE") ?? response.headers.get("X-PAYMENT-RESPONSE");
574
+ const receipt = header ? decode(header) : void 0;
575
+ if (receipt && receipt.success !== false) {
576
+ if (typeof receipt.transaction === "string" && receipt.transaction) return "a settlement receipt with a transaction";
577
+ const extra = receipt.extra;
578
+ const charged = receiptChargedUsd({
579
+ amount: receipt.amount,
580
+ extra: extra && typeof extra === "object" ? extra : void 0
581
+ });
582
+ if (charged !== void 0 && charged > 0) return "a settlement receipt with a charge";
583
+ }
584
+ if (response.headers.get("X-Payment-Receipt")?.trim()) return "an X-Payment-Receipt transaction";
585
+ return void 0;
586
+ }
557
587
  function settlementSkipped(response) {
558
- return response.headers.get("X-Settlement-Skipped") === "free-fallback";
588
+ return settlementSkipReason(response) !== void 0;
559
589
  }
560
590
  function withCost(body, response, bookedUsd, scheme) {
561
- if (settlementSkipped(response)) {
591
+ const skipped = settlementSkipReason(response);
592
+ if (skipped !== void 0) {
562
593
  body.costUsd = 0;
563
- body.settlement = { ...readSettlement(response, bookedUsd, scheme).settlement, skipped: "free-fallback" };
594
+ body.settlement = { ...readSettlement(response, bookedUsd, scheme).settlement, skipped };
564
595
  return body;
565
596
  }
566
597
  body.costUsd = bookedUsd;
@@ -5089,7 +5120,7 @@ function getCostSummary() {
5089
5120
  }
5090
5121
 
5091
5122
  // src/version.ts
5092
- var SDK_VERSION = "3.24.8";
5123
+ var SDK_VERSION = "3.24.9";
5093
5124
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
5094
5125
 
5095
5126
  // src/client.ts
@@ -5685,9 +5716,9 @@ var LLMClient = class _LLMClient {
5685
5716
  return this.bookPaid(url, body, details, retryResponse);
5686
5717
  }
5687
5718
  /**
5688
- * Book a served, paid chat call and attach its cost. A response the gateway
5689
- * served from a free fallback with settlement skipped
5690
- * (`X-Settlement-Skipped: free-fallback`) moved nothing on chain, so it is
5719
+ * Book a served, paid chat call and attach its cost. A response whose
5720
+ * settlement the gateway skipped ({@link settlementSkipped}: e.g.
5721
+ * `X-Settlement-Skipped: free-fallback`) moved nothing on chain, so it is
5691
5722
  * not added to `getSpending()` or the cost log, and reports `costUsd: 0`.
5692
5723
  */
5693
5724
  async bookPaid(url, body, details, response) {
@@ -5699,6 +5730,21 @@ var LLMClient = class _LLMClient {
5699
5730
  }
5700
5731
  return withCost(await this.parseChatResponse(response), response, costUsd, "exact");
5701
5732
  }
5733
+ /**
5734
+ * Book a served, paid raw (non-chat) call: the signed amount goes to
5735
+ * `getSpending()` and the cost log, unless the gateway skipped settlement
5736
+ * ({@link settlementSkipped}: e.g. `X-Settlement-Skipped: empty-result` on an
5737
+ * empty prediction-market search, `unexecuted` on a JSON-RPC call that never
5738
+ * ran, or `X-Payment-Waived`), in which case nothing moved and nothing is
5739
+ * booked. `X-Payment-Settled: false` alone is still booked: it is owed.
5740
+ */
5741
+ bookPaidRaw(url, details, response, body) {
5742
+ if (settlementSkipped(response)) return;
5743
+ const costUsd = parseFloat(details.amount) / 1e6;
5744
+ this.sessionCalls += 1;
5745
+ this.sessionTotalUsd += costUsd;
5746
+ this.recordCost(url, costUsd, body ? { body, network: details.network } : { network: details.network });
5747
+ }
5702
5748
  /**
5703
5749
  * Sign a payment header and return the PAYMENT-SIGNATURE value.
5704
5750
  * Extracted to share logic between streaming and non-streaming flows.
@@ -5799,8 +5845,10 @@ var LLMClient = class _LLMClient {
5799
5845
  throw withDisposition(err, "paid-or-in-doubt");
5800
5846
  }
5801
5847
  if (preAuthResp.ok) {
5802
- this.sessionCalls += 1;
5803
- this.sessionTotalUsd += signed.costUsd;
5848
+ if (!settlementSkipped(preAuthResp)) {
5849
+ this.sessionCalls += 1;
5850
+ this.sessionTotalUsd += signed.costUsd;
5851
+ }
5804
5852
  return preAuthResp;
5805
5853
  }
5806
5854
  this.preAuthCache.delete(cacheKey2);
@@ -5865,8 +5913,10 @@ var LLMClient = class _LLMClient {
5865
5913
  }
5866
5914
  throw new APIError(`API error after payment: ${streamResp.status}`, streamResp.status, sanitizeErrorResponse(errorBody));
5867
5915
  }
5868
- this.sessionCalls += 1;
5869
- this.sessionTotalUsd += costUsd;
5916
+ if (!settlementSkipped(streamResp)) {
5917
+ this.sessionCalls += 1;
5918
+ this.sessionTotalUsd += costUsd;
5919
+ }
5870
5920
  return streamResp;
5871
5921
  }
5872
5922
  /**
@@ -5990,10 +6040,7 @@ var LLMClient = class _LLMClient {
5990
6040
  }
5991
6041
  throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
5992
6042
  }
5993
- const costUsd2 = parseFloat(details.amount) / 1e6;
5994
- this.sessionCalls += 1;
5995
- this.sessionTotalUsd += costUsd2;
5996
- this.recordCost(url, costUsd2, { body, network: details.network });
6043
+ this.bookPaidRaw(url, details, retryResp2, body);
5997
6044
  return retryResp2.json();
5998
6045
  }
5999
6046
  }
@@ -6013,10 +6060,7 @@ var LLMClient = class _LLMClient {
6013
6060
  sanitizeErrorResponse(errorBody)
6014
6061
  );
6015
6062
  }
6016
- const costUsd = parseFloat(details.amount) / 1e6;
6017
- this.sessionCalls += 1;
6018
- this.sessionTotalUsd += costUsd;
6019
- this.recordCost(url, costUsd, { body, network: details.network });
6063
+ this.bookPaidRaw(url, details, retryResponse, body);
6020
6064
  return retryResponse.json();
6021
6065
  }
6022
6066
  /**
@@ -6136,10 +6180,7 @@ var LLMClient = class _LLMClient {
6136
6180
  }
6137
6181
  throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
6138
6182
  }
6139
- const costUsd2 = parseFloat(details.amount) / 1e6;
6140
- this.sessionCalls += 1;
6141
- this.sessionTotalUsd += costUsd2;
6142
- this.recordCost(url, costUsd2, { network: details.network });
6183
+ this.bookPaidRaw(url, details, retryResp2);
6143
6184
  return retryResp2.json();
6144
6185
  }
6145
6186
  }
@@ -6159,10 +6200,7 @@ var LLMClient = class _LLMClient {
6159
6200
  sanitizeErrorResponse(errorBody)
6160
6201
  );
6161
6202
  }
6162
- const costUsd = parseFloat(details.amount) / 1e6;
6163
- this.sessionCalls += 1;
6164
- this.sessionTotalUsd += costUsd;
6165
- this.recordCost(url, costUsd, { network: details.network });
6203
+ this.bookPaidRaw(url, details, retryResponse);
6166
6204
  return retryResponse.json();
6167
6205
  }
6168
6206
  /**
@@ -7175,8 +7213,10 @@ var MusicClient = class {
7175
7213
  throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
7176
7214
  }
7177
7215
  const data = await retryResponse.json();
7178
- this.sessionCalls++;
7179
- this.sessionTotalUsd += 0.1575;
7216
+ if (!settlementSkipped(retryResponse)) {
7217
+ this.sessionCalls++;
7218
+ this.sessionTotalUsd += 0.1575;
7219
+ }
7180
7220
  const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
7181
7221
  if (txHash) data.txHash = txHash;
7182
7222
  return data;
@@ -7388,9 +7428,11 @@ var SpeechClient = class {
7388
7428
  throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
7389
7429
  }
7390
7430
  const data = await retryResponse.json();
7391
- this.sessionCalls++;
7392
- const paidUsd = Number(details.amount) / 1e6;
7393
- if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
7431
+ if (!settlementSkipped(retryResponse)) {
7432
+ this.sessionCalls++;
7433
+ const paidUsd = Number(details.amount) / 1e6;
7434
+ if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
7435
+ }
7394
7436
  const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
7395
7437
  if (txHash) data.txHash = txHash;
7396
7438
  return data;
@@ -7687,8 +7729,10 @@ var VideoClient = class {
7687
7729
  }
7688
7730
  if (lastStatus === "completed") {
7689
7731
  const data = pollData;
7690
- this.sessionCalls++;
7691
- this.sessionTotalUsd += Number(details.amount) / 1e6;
7732
+ if (!settlementSkipped(pollResp)) {
7733
+ this.sessionCalls++;
7734
+ this.sessionTotalUsd += Number(details.amount) / 1e6;
7735
+ }
7692
7736
  const txHash = pollResp.headers.get("x-payment-receipt") || pollResp.headers.get("X-Payment-Receipt");
7693
7737
  if (txHash) data.txHash = txHash;
7694
7738
  return data;
@@ -7983,8 +8027,10 @@ var VoiceClient = class {
7983
8027
  throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
7984
8028
  }
7985
8029
  const data = await retryResponse.json();
7986
- this.sessionCalls++;
7987
- this.sessionTotalUsd += CALL_PRICE_USD;
8030
+ if (!settlementSkipped(retryResponse)) {
8031
+ this.sessionCalls++;
8032
+ this.sessionTotalUsd += CALL_PRICE_USD;
8033
+ }
7988
8034
  const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
7989
8035
  if (txHash) data.txHash = txHash;
7990
8036
  return data;
@@ -8204,9 +8250,10 @@ var PhoneClient = class {
8204
8250
  throw new PaymentError("Payment was rejected. Check your wallet balance.");
8205
8251
  }
8206
8252
  const data = await this.unwrap(retry, true);
8207
- const costUsd = Number.parseFloat(details.amount) / 1e6;
8208
- this.sessionCalls += 1;
8209
- this.sessionTotalUsd += costUsd;
8253
+ if (!settlementSkipped(retry)) {
8254
+ this.sessionCalls += 1;
8255
+ this.sessionTotalUsd += Number.parseFloat(details.amount) / 1e6;
8256
+ }
8210
8257
  const txHash = retry.headers.get("x-payment-receipt") || retry.headers.get("X-Payment-Receipt");
8211
8258
  if (txHash && data && typeof data === "object" && !("txHash" in data)) {
8212
8259
  data.txHash = txHash;
@@ -8371,9 +8418,10 @@ var PortraitClient = class {
8371
8418
  throw new PaymentError("Payment was rejected. Check your wallet balance.");
8372
8419
  }
8373
8420
  const data = await this.unwrap(retry, true);
8374
- const costUsd = Number.parseFloat(details.amount) / 1e6;
8375
- this.sessionCalls += 1;
8376
- this.sessionTotalUsd += costUsd;
8421
+ if (!settlementSkipped(retry)) {
8422
+ this.sessionCalls += 1;
8423
+ this.sessionTotalUsd += Number.parseFloat(details.amount) / 1e6;
8424
+ }
8377
8425
  if (data && typeof data === "object" && !("txHash" in data)) {
8378
8426
  const settlement = data.settlement;
8379
8427
  if (settlement?.tx_hash) {
@@ -9190,9 +9238,11 @@ var RpcClient = class {
9190
9238
  }
9191
9239
  throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
9192
9240
  }
9193
- this.sessionCalls++;
9194
- const paidUsd = Number(details.amount) / 1e6;
9195
- if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
9241
+ if (!settlementSkipped(retryResponse)) {
9242
+ this.sessionCalls++;
9243
+ const paidUsd = Number(details.amount) / 1e6;
9244
+ if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
9245
+ }
9196
9246
  return { data: await retryResponse.json(), headers: retryResponse.headers };
9197
9247
  }
9198
9248
  async fetchWithTimeout(url, options) {
@@ -9339,7 +9389,7 @@ var BlockrunClient = class {
9339
9389
  if (resp402.status !== 402) {
9340
9390
  await this.throwApiError(resp402, `poll submit failed (${submitUrl})`);
9341
9391
  }
9342
- const paymentPayload = await this.signFrom402(resp402, submitUrl, {
9392
+ const { paymentPayload, costUsd } = await this.signFrom402(resp402, submitUrl, {
9343
9393
  description: "BlockRun async job",
9344
9394
  maxTimeoutSeconds: MAX_SIGNED_AUTH_SECONDS
9345
9395
  });
@@ -9358,7 +9408,7 @@ var BlockrunClient = class {
9358
9408
  await this.throwApiError(submitResp, `poll submit failed (${submitUrl})`);
9359
9409
  }
9360
9410
  if (submitResp.status === 200) {
9361
- this.recordSpending();
9411
+ this.recordSpending(submitResp, costUsd);
9362
9412
  return submitResp.json();
9363
9413
  }
9364
9414
  const submitData = await submitResp.json();
@@ -9395,7 +9445,7 @@ var BlockrunClient = class {
9395
9445
  );
9396
9446
  }
9397
9447
  if (pollResp.status === 200 && lastStatus === "completed") {
9398
- this.recordSpending();
9448
+ this.recordSpending(pollResp, costUsd);
9399
9449
  return pollData;
9400
9450
  }
9401
9451
  if (pollResp.status !== 200 && pollResp.status !== 202 && pollResp.status !== 504) {
@@ -9436,7 +9486,7 @@ var BlockrunClient = class {
9436
9486
  if (resp402.status === 200) {
9437
9487
  streamResp = resp402;
9438
9488
  } else if (resp402.status === 402) {
9439
- const paymentPayload = await this.signFrom402(resp402, url, {
9489
+ const { paymentPayload, costUsd } = await this.signFrom402(resp402, url, {
9440
9490
  description: "BlockRun stream"
9441
9491
  });
9442
9492
  streamResp = await this.fetchWithTimeout(url, {
@@ -9455,7 +9505,7 @@ var BlockrunClient = class {
9455
9505
  if (!streamResp.ok) {
9456
9506
  await this.throwApiError(streamResp, `stream failed after payment (${url})`);
9457
9507
  }
9458
- this.recordSpending();
9508
+ this.recordSpending(streamResp, costUsd);
9459
9509
  } else {
9460
9510
  await this.throwApiError(resp402, `stream failed (${url})`);
9461
9511
  return;
@@ -9511,7 +9561,7 @@ var BlockrunClient = class {
9511
9561
  return response.json();
9512
9562
  }
9513
9563
  async handlePaymentAndRetry(url, method, body, response) {
9514
- const paymentPayload = await this.signFrom402(response, url, {
9564
+ const { paymentPayload, costUsd } = await this.signFrom402(response, url, {
9515
9565
  description: "BlockRun"
9516
9566
  });
9517
9567
  const retryInit = {
@@ -9534,13 +9584,14 @@ var BlockrunClient = class {
9534
9584
  if (!retry.ok) {
9535
9585
  await this.throwApiError(retry, `${method} ${url} failed after payment`);
9536
9586
  }
9537
- this.recordSpending();
9587
+ this.recordSpending(retry, costUsd);
9538
9588
  return retry.json();
9539
9589
  }
9540
9590
  /**
9541
9591
  * Read a 402 response's payment requirements (header or body), then sign and
9542
- * return the base64 PAYMENT-SIGNATURE payload. Also records the cost-to-be
9543
- * onto the response context (settled on `recordSpending`).
9592
+ * return the base64 PAYMENT-SIGNATURE payload with what it pays, in USD.
9593
+ * The cost stays with the call that signed it (booked by `recordSpending`):
9594
+ * concurrent calls on one client never share it.
9544
9595
  */
9545
9596
  async signFrom402(response, url, opts) {
9546
9597
  let paymentHeader = response.headers.get("payment-required");
@@ -9558,8 +9609,8 @@ var BlockrunClient = class {
9558
9609
  }
9559
9610
  const paymentRequired = parsePaymentRequired(paymentHeader);
9560
9611
  const details = extractPaymentDetails(paymentRequired);
9561
- this.pendingCostUsd = parseFloat(details.amount) / 1e6;
9562
- return createPaymentPayload(
9612
+ const costUsd = parseFloat(details.amount) / 1e6;
9613
+ const paymentPayload = await createPaymentPayload(
9563
9614
  this.privateKey,
9564
9615
  this.account.address,
9565
9616
  details.recipient,
@@ -9576,14 +9627,20 @@ var BlockrunClient = class {
9576
9627
  asset: details.asset
9577
9628
  }
9578
9629
  );
9630
+ return { paymentPayload, costUsd };
9579
9631
  }
9580
- /** Accumulates the most-recent pending cost; settled by recordSpending. */
9581
- pendingCostUsd = 0;
9582
- recordSpending() {
9583
- if (this.pendingCostUsd > 0) {
9632
+ /**
9633
+ * Book `costUsd`, what the call that `response` answered signed. A response
9634
+ * whose settlement the gateway skipped ({@link settlementSkipped}:
9635
+ * `X-Settlement-Skipped: <reason>` or `X-Payment-Waived`) moved nothing, so
9636
+ * nothing is booked. `X-Payment-Settled: false` alone is still booked: the
9637
+ * payment is owed.
9638
+ */
9639
+ recordSpending(response, costUsd) {
9640
+ if (settlementSkipped(response)) return;
9641
+ if (costUsd > 0) {
9584
9642
  this.sessionCalls += 1;
9585
- this.sessionTotalUsd += this.pendingCostUsd;
9586
- this.pendingCostUsd = 0;
9643
+ this.sessionTotalUsd += costUsd;
9587
9644
  }
9588
9645
  }
9589
9646
  async throwApiError(resp, prefix) {
@@ -11802,6 +11859,8 @@ var SolanaBatchPayer = class {
11802
11859
  wallet.refusedFor = outcome.errorReason;
11803
11860
  }
11804
11861
  }
11862
+ } else if (built.key && outcome.reason === "payment_required" && outcome.refusedForMs === void 0) {
11863
+ await this.recheckRefusedChannel(wallet, built.key, built.reclaimAnchor);
11805
11864
  }
11806
11865
  sent = void 0;
11807
11866
  release();
@@ -12069,6 +12128,13 @@ var SolanaBatchPayer = class {
12069
12128
  }
12070
12129
  /** Build a new batch payment for a 402: the payload, its headers and its kind. */
12071
12130
  async buildPayment(wallet, paymentRequired) {
12131
+ const acceptKey = channelKeyOf(trustedBatchAccept(paymentRequired, this.init.options.operators));
12132
+ let reclaimAnchor;
12133
+ try {
12134
+ const stored = acceptKey ? await wallet.book.base.get(acceptKey) : void 0;
12135
+ reclaimAnchor = stored ? savedReclaimAnchor(stored) : void 0;
12136
+ } catch {
12137
+ }
12072
12138
  const { http, payload } = await this.createPayload(wallet, paymentRequired);
12073
12139
  const scope = payScope.getStore();
12074
12140
  if (scope?.afterError) {
@@ -12090,7 +12156,16 @@ var SolanaBatchPayer = class {
12090
12156
  const record = known?.record;
12091
12157
  kind = record && (record.pending === void 0 || record.hasConfirmedState) ? "top-up" : "open";
12092
12158
  }
12093
- return { http, payload, headers, kind, key: known?.key, sentAt: Date.now(), sends: 0 };
12159
+ return {
12160
+ http,
12161
+ payload,
12162
+ headers,
12163
+ kind,
12164
+ key: known?.key,
12165
+ ...reclaimAnchor !== void 0 && known?.key === acceptKey ? { reclaimAnchor } : {},
12166
+ sentAt: Date.now(),
12167
+ sends: 0
12168
+ };
12094
12169
  }
12095
12170
  /**
12096
12171
  * Write a deposit's intent to the wallet's journal before it is sent
@@ -12324,6 +12399,84 @@ var SolanaBatchPayer = class {
12324
12399
  }
12325
12400
  }
12326
12401
  }
12402
+ /**
12403
+ * Queue a re-read of a stored channel whose account is gone at finalized
12404
+ * and at confirmed commitment (closed, its account reclaimed), anchored at
12405
+ * the SDK's own `getBlockHeight` at confirmed. The anchor is saved in the
12406
+ * record (`reclaimAnchorHeight`), so a short-lived process finishes the
12407
+ * wait instead of starting it over; a failed read leaves resync to take it,
12408
+ * and a failed save only costs that. resync pays exact until finality is
12409
+ * LANDING_MARGIN_BLOCKS past the anchor, then drops the record if the
12410
+ * finalized chain still has no account (a lagging read finds the channel
12411
+ * and rewrites the record, without the anchor).
12412
+ */
12413
+ async queueReclaimed(wallet, key, record, channelId, savedAnchor) {
12414
+ let anchorHeight = savedReclaimAnchor(record) ?? savedAnchor;
12415
+ if (anchorHeight === void 0) {
12416
+ try {
12417
+ anchorHeight = rpcInteger(
12418
+ await rpcRequest(this.init.rpcUrl, "getBlockHeight", [{ commitment: "confirmed" }], this.init.rpcHeaders),
12419
+ "getBlockHeight"
12420
+ );
12421
+ } catch {
12422
+ }
12423
+ }
12424
+ if (anchorHeight !== void 0 && savedReclaimAnchor(record) !== anchorHeight) {
12425
+ try {
12426
+ await wallet.book.set(key, { ...record, reclaimAnchorHeight: anchorHeight });
12427
+ } catch {
12428
+ }
12429
+ }
12430
+ addResyncTarget(wallet, {
12431
+ key,
12432
+ channelId,
12433
+ channelConfig: record.channelConfig,
12434
+ cumulative: parseAtomic(record.chargedCumulativeAmount),
12435
+ knownDeposit: parseAtomic(record.deposit),
12436
+ ...anchorHeight !== void 0 ? { anchorHeight } : {},
12437
+ reason: "channel_unusable"
12438
+ });
12439
+ }
12440
+ /**
12441
+ * After the gateway refused an authorization with a plain 402 (no receipt,
12442
+ * not an admission refusal), check the channel it paid into. That is how
12443
+ * the gateway answers an authorization into a channel the facilitator
12444
+ * closed (its verify rejects it), and a channel with deposit to spare is
12445
+ * otherwise never read again: every call would sign another authorization
12446
+ * into it and pay exact. A channel the finalized chain shows closing,
12447
+ * closed or not this wallet's is re-read through resync (record dropped,
12448
+ * or kept and paid exact); one gone at every commitment is queued as
12449
+ * {@link queueReclaimed}; an open one of this wallet's, or one only at
12450
+ * confirmed (not finalized yet), is left as it is. A queued re-read is run
12451
+ * at once. Best effort: a failed read changes nothing, and the next
12452
+ * refusal checks again.
12453
+ */
12454
+ async recheckRefusedChannel(wallet, key, savedAnchor) {
12455
+ try {
12456
+ const record = await wallet.book.get(key);
12457
+ if (!record || record.pending || typeof record.channelId !== "string" || wallet.resyncs.has(key)) return;
12458
+ const channelId = record.channelId;
12459
+ const channel = await readChannelAccount(this.init.rpcUrl, channelId, this.init.rpcHeaders);
12460
+ if (channel !== void 0) {
12461
+ if (channel.open && channel.payer === await this.init.address() && channel.authorizedSigner === record.channelConfig.payerAuthorizer && channel.mint === record.channelConfig.token) {
12462
+ return;
12463
+ }
12464
+ addResyncTarget(wallet, {
12465
+ key,
12466
+ channelId,
12467
+ channelConfig: record.channelConfig,
12468
+ cumulative: parseAtomic(record.chargedCumulativeAmount),
12469
+ knownDeposit: parseAtomic(record.deposit),
12470
+ reason: "channel_unusable"
12471
+ });
12472
+ } else {
12473
+ if (await readChannelAccount(this.init.rpcUrl, channelId, this.init.rpcHeaders, { commitment: "confirmed" }) !== void 0) return;
12474
+ await this.queueReclaimed(wallet, key, record, channelId, savedAnchor);
12475
+ }
12476
+ await this.resync(wallet);
12477
+ } catch {
12478
+ }
12479
+ }
12327
12480
  /**
12328
12481
  * When this payment will top up the wallet's channel, read how much of its
12329
12482
  * deposit is already settled on-chain (at `finalized` commitment, so never
@@ -12377,31 +12530,7 @@ var SolanaBatchPayer = class {
12377
12530
  }
12378
12531
  if (channel === void 0 && unfinalized) return;
12379
12532
  if (channel === void 0) {
12380
- let anchorHeight = savedReclaimAnchor(record);
12381
- if (anchorHeight === void 0) {
12382
- try {
12383
- anchorHeight = rpcInteger(
12384
- await rpcRequest(this.init.rpcUrl, "getBlockHeight", [{ commitment: "confirmed" }], this.init.rpcHeaders),
12385
- "getBlockHeight"
12386
- );
12387
- } catch {
12388
- }
12389
- if (anchorHeight !== void 0) {
12390
- try {
12391
- await wallet.book.set(key, { ...record, reclaimAnchorHeight: anchorHeight });
12392
- } catch {
12393
- }
12394
- }
12395
- }
12396
- addResyncTarget(wallet, {
12397
- key,
12398
- channelId: record.channelId,
12399
- channelConfig: record.channelConfig,
12400
- cumulative: parseAtomic(record.chargedCumulativeAmount),
12401
- knownDeposit: deposit,
12402
- ...anchorHeight !== void 0 ? { anchorHeight } : {},
12403
- reason: "channel_unusable"
12404
- });
12533
+ await this.queueReclaimed(wallet, key, record, record.channelId);
12405
12534
  throw new ChannelResyncRequiredError(record.channelId);
12406
12535
  }
12407
12536
  if (!channel.open || channel.payer !== await this.init.address() || channel.authorizedSigner !== record.channelConfig.payerAuthorizer || channel.mint !== record.channelConfig.token) {
@@ -13038,7 +13167,7 @@ var SolanaLLMClient = class {
13038
13167
  body: requestBody
13039
13168
  });
13040
13169
  await this.assertPaid(paid);
13041
- this.recordSettlement(costUsd);
13170
+ this.recordSettlement(costUsd, paid);
13042
13171
  return paid;
13043
13172
  } catch (error) {
13044
13173
  if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
@@ -13425,7 +13554,7 @@ var SolanaLLMClient = class {
13425
13554
  closesAtStart
13426
13555
  );
13427
13556
  if (batch.kind === "paid") {
13428
- this.recordSettlement(batch.chargedUsd);
13557
+ this.recordSettlement(batch.chargedUsd, batch.response);
13429
13558
  const paid = await this.readPaidJson(batch.response);
13430
13559
  return withCost(paid, batch.response, batch.chargedUsd, "batch");
13431
13560
  }
@@ -13542,7 +13671,7 @@ var SolanaLLMClient = class {
13542
13671
  };
13543
13672
  const deliver = (data, response) => {
13544
13673
  if (kind === "video") {
13545
- if (videoCostUsd !== void 0) this.recordSettlement(videoCostUsd);
13674
+ if (videoCostUsd !== void 0) this.recordSettlement(videoCostUsd, response);
13546
13675
  const txHash = response.headers.get("x-payment-receipt");
13547
13676
  if (txHash) data.txHash = txHash;
13548
13677
  }
@@ -13789,7 +13918,7 @@ var SolanaLLMClient = class {
13789
13918
  throw withDisposition(error, "paid-or-in-doubt");
13790
13919
  }
13791
13920
  await this.assertPaid(retryResponse);
13792
- if (!settlementSkipped(retryResponse)) this.recordSettlement(costUsd);
13921
+ this.recordSettlement(costUsd, retryResponse);
13793
13922
  const paid = await this.readPaidJson(retryResponse);
13794
13923
  return withCost(paid, retryResponse, costUsd, "exact");
13795
13924
  }
@@ -13849,11 +13978,11 @@ var SolanaLLMClient = class {
13849
13978
  await this.assertPaid(retryResponse);
13850
13979
  if (retryResponse.status === 202) {
13851
13980
  const kind = pollKindFor(url);
13852
- if (kind !== "video") this.recordSettlement(costUsd);
13981
+ if (kind !== "video") this.recordSettlement(costUsd, retryResponse);
13853
13982
  const submitted = await retryResponse.json();
13854
13983
  return this.followSolanaJob(submitted, media.pollBudgetMs ?? this.timeout, kind);
13855
13984
  }
13856
- this.recordSettlement(costUsd);
13985
+ this.recordSettlement(costUsd, retryResponse);
13857
13986
  return retryResponse.json();
13858
13987
  }
13859
13988
  async getWithPaymentRaw(endpoint, params) {
@@ -13897,7 +14026,7 @@ var SolanaLLMClient = class {
13897
14026
  }
13898
14027
  });
13899
14028
  await this.assertPaid(retryResponse);
13900
- this.recordSettlement(costUsd);
14029
+ this.recordSettlement(costUsd, retryResponse);
13901
14030
  return retryResponse.json();
13902
14031
  }
13903
14032
  /**
@@ -13935,8 +14064,17 @@ var SolanaLLMClient = class {
13935
14064
  );
13936
14065
  }
13937
14066
  }
13938
- /** Count one settled x402 payment against the session total. */
13939
- recordSettlement(costUsd) {
14067
+ /**
14068
+ * Count one settled x402 payment against the session total.
14069
+ *
14070
+ * Takes the paid response so no booking site can forget the check: a
14071
+ * response whose settlement the gateway skipped ({@link settlementSkipped}:
14072
+ * `X-Settlement-Skipped: free-fallback | empty-result | unexecuted | …`, or
14073
+ * `X-Payment-Waived`) moved nothing on chain and books nothing.
14074
+ * `X-Payment-Settled: false` alone is still booked: the payment is owed.
14075
+ */
14076
+ recordSettlement(costUsd, response) {
14077
+ if (settlementSkipped(response)) return;
13940
14078
  this.sessionCalls += 1;
13941
14079
  this.sessionTotalUsd += costUsd;
13942
14080
  }