@dvmkit/sdk 0.2.0-rc.9 → 0.3.0-rc.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.
Files changed (58) hide show
  1. package/README.md +2 -0
  2. package/dist/{chunk-V7EVFLAK.js → chunk-2D75MNNG.js} +154 -523
  3. package/dist/{chunk-BIFLRKMO.js → chunk-2KGG5ORW.js} +1 -1
  4. package/dist/{chunk-5URG56JJ.js → chunk-A4CM2BD3.js} +18 -2
  5. package/dist/{chunk-FT6HTUM4.js → chunk-AJNFXR7J.js} +1 -1
  6. package/dist/{chunk-S3XAHZQY.js → chunk-COYKJRMM.js} +24 -3
  7. package/dist/{chunk-UP2F5RRT.js → chunk-D7YQYLLM.js} +1 -4
  8. package/dist/{chunk-OMIQMMME.js → chunk-FPOLJRBF.js} +3 -3
  9. package/dist/{chunk-QP53RWAD.js → chunk-HHTL3ZFT.js} +9 -6
  10. package/dist/{chunk-PCUQZDZA.js → chunk-LTAIBLS2.js} +537 -321
  11. package/dist/{chunk-JDT5LCJC.js → chunk-MD3H3BF7.js} +1 -1
  12. package/dist/{chunk-KSQEFVFJ.js → chunk-MMCE7XSR.js} +1 -1
  13. package/dist/{chunk-MRAGS5VP.js → chunk-N5B66UOS.js} +4 -16
  14. package/dist/{chunk-2ATUAUAO.js → chunk-O6D72KYA.js} +2 -2
  15. package/dist/{chunk-KXCKFIJI.js → chunk-QX52F6IB.js} +1 -1
  16. package/dist/{chunk-PHHAYRQV.js → chunk-U5VNEALI.js} +23 -10
  17. package/dist/{chunk-JLXYOV4Y.js → chunk-V5ZRATJH.js} +1 -1
  18. package/dist/{chunk-6GRIKOFB.js → chunk-VA7GT6C6.js} +5 -16
  19. package/dist/{chunk-O2X2CCKH.js → chunk-VIVXB4CD.js} +1 -1
  20. package/dist/{chunk-L67WTZX2.js → chunk-XMDQOXHO.js} +10 -33
  21. package/dist/{chunk-QT4ONTST.js → chunk-XRLKGCAW.js} +1 -1
  22. package/dist/{chunk-KMZXTBLA.js → chunk-XZ7LW4S5.js} +0 -1
  23. package/dist/{chunk-XQXJKJ3P.js → chunk-YO56VCTQ.js} +0 -4
  24. package/dist/{credit-ledger-5ZEJRI46.js → credit-ledger-MHA652TV.js} +2 -2
  25. package/dist/{credit-menu-D4Gcdgc4.d.ts → credit-menu-BlCL7gZx.d.ts} +175 -533
  26. package/dist/{fx-B0SLBe5x.d.ts → fx-DhyT1Cjh.d.ts} +1 -1
  27. package/dist/index.d.ts +5 -17
  28. package/dist/internal/caller.d.ts +109 -698
  29. package/dist/internal/caller.js +10 -12
  30. package/dist/internal/server.d.ts +17 -42
  31. package/dist/internal/server.js +16 -15
  32. package/dist/{job-store-B2uZvga4.d.ts → job-store-D3CcZp_g.d.ts} +6 -10
  33. package/dist/{lightning-backend-CQBnQgsT.d.ts → lightning-backend-BhsyrBLa.d.ts} +1 -15
  34. package/dist/{memory-credit-ledger-ZOH6C3N4.js → memory-credit-ledger-BYBATRN3.js} +3 -3
  35. package/dist/{mpp-secret-state-WNAQQ6K4.js → mpp-secret-state-B5GCILVH.js} +1 -1
  36. package/dist/{mpp-setup-IOJBF7DB.js → mpp-setup-MPP3ST5X.js} +1 -1
  37. package/dist/{payout-reporter-5PIRYFVQ.js → payout-reporter-KEMUXEUO.js} +2 -2
  38. package/dist/{postgres-consumed-credential-store-ISRHBMOU.js → postgres-consumed-credential-store-6N2V5MJN.js} +1 -1
  39. package/dist/postgres-job-store-VE2JRUEC.js +7 -0
  40. package/dist/postgres-kv-store-DXGTKXLU.js +7 -0
  41. package/dist/{postgres-replay-store-IZFLTTAC.js → postgres-replay-store-ZF4JNZOW.js} +2 -2
  42. package/dist/{processed-payment-store-FIDI3RNH.js → processed-payment-store-GKMAWOM3.js} +2 -2
  43. package/dist/revenue-reporter-O75BGYFK.js +7 -0
  44. package/dist/server/index.d.ts +37 -140
  45. package/dist/server/index.js +64 -127
  46. package/dist/{step-cache-CXg7ziML.d.ts → step-cache-v2yhyhSZ.d.ts} +359 -1112
  47. package/dist/{tempo-charge-store-76TDAF34.js → tempo-charge-store-SDTUNTKA.js} +2 -2
  48. package/dist/{tempo-lifecycle-DXM7QXJQ.js → tempo-lifecycle-G5SB4HFC.js} +4 -4
  49. package/dist/{tempo-session-store-2JNOKJGX.js → tempo-session-store-WF3VZXNF.js} +2 -2
  50. package/dist/{tempo-wallet-O67H5M4N.js → tempo-wallet-ZFNUDFVO.js} +1 -1
  51. package/dist/testing/index.d.ts +2 -2
  52. package/dist/testing/index.js +2 -2
  53. package/dist/{usd-BNDg1715.d.ts → usd-Cu9G6xv2.d.ts} +1 -1
  54. package/dist/{x402-7S2EFINY.js → x402-CI6PSHNO.js} +4 -4
  55. package/package.json +2 -2
  56. package/dist/postgres-job-store-OGQ6IT4U.js +0 -7
  57. package/dist/postgres-kv-store-D5E2EZ24.js +0 -7
  58. package/dist/revenue-reporter-NNCNRY4C.js +0 -7
@@ -170,20 +170,7 @@ interface X402Receipt {
170
170
  invalidReason?: string;
171
171
  }
172
172
 
173
- /**
174
- * dvmkit's x402 wire-format implementation — the v1/v2 `PaymentRequirements` /
175
- * `PaymentPayload` envelope shapes, payment-header encode/decode, and the
176
- * facilitator verify/settle calls that a deployed DVM's 402 response and
177
- * payment parsing are built from. A wire-format change lands here.
178
- *
179
- * **On a change here, or an x402 reference-client upgrade, run the conformance
180
- * check** — `public compatibility guide`. Our own SDK round-trips
181
- * against itself (the vitest tiers, `money-loop:x402`), so only a client
182
- * that isn't dvmkit can catch a *mutual* misreading of the spec. The runbook
183
- * names the out-of-tree v1 and v2 clients plus the funded Base Sepolia key
184
- * (`public compatibility guide`); the runbook carries a run log of
185
- * what it last ran against.
186
- */
173
+ /** Wire compatibility requires independent v1 and v2 reference-client checks. */
187
174
 
188
175
  /** Current x402 protocol version preferred by dvmkit clients and servers. */
189
176
  declare const X402_VERSION = 2;
@@ -341,11 +328,8 @@ interface BuildPaymentRequirementsOpts {
341
328
  version?: X402Version;
342
329
  }
343
330
  /**
344
- * Build a single x402 `PaymentRequirements` entry from the DVM's `X402Config`,
345
- * the resolved per-call USDC amount, and the live request's resource URL.
346
- * The resource string is bound into the EIP-3009 `extra` field on signing in
347
- * the reference clients, so it must match exactly between the 402 response
348
- * and the retry payload — pass the request URL the client will retry against.
331
+ * V1 carries the resource URL in each requirement; v2 carries it in the outer
332
+ * response. This metadata is not part of the EIP-3009 authorization signature.
349
333
  */
350
334
  declare function buildPaymentRequirements(opts: BuildPaymentRequirementsOpts & {
351
335
  version: typeof X402_V1_VERSION;
@@ -361,13 +345,7 @@ declare function buildPaymentRequiredV2(opts: Omit<BuildPaymentRequirementsOpts,
361
345
  }): PaymentRequiredV2;
362
346
  /** Convert the server's legacy body requirements into the parallel v2 declaration. */
363
347
  declare function paymentRequiredV2FromV1(requirements: PaymentRequirementsV1[], error?: string): PaymentRequiredV2;
364
- /**
365
- * Translate a CAIP-2 chain id (`eip155:8453`) to the x402-spec network slug
366
- * (`base`). Operators configure DVMs in CAIP-2 because it's the lingua-franca
367
- * across our other rails; the x402 facilitator API only accepts named slugs.
368
- * Unknown CAIP-2 ids fall through to the original string so non-EVM rails
369
- * (Solana mainnet, etc.) can also be plumbed once supported.
370
- */
348
+ /** Legacy v1 network slugs; unknown identifiers pass through unchanged. */
371
349
  declare function caip2ToX402Network(network: string): string;
372
350
  /** Translate a legacy x402 network slug back to its CAIP-2 chain id. */
373
351
  declare function x402NetworkToCaip2(network: string): string;
@@ -375,12 +353,6 @@ declare function x402NetworkToCaip2(network: string): string;
375
353
  declare function chainIdFromCaip2(network: string): number;
376
354
  /** USDC contract address for a network slug (post-translation). */
377
355
  declare function usdcContractFor(networkSlug: string): `0x${string}` | undefined;
378
- /**
379
- * USDC contract address for a CAIP-2 chain id (e.g. `eip155:8453`). Resolves
380
- * via {@link caip2ToX402Network} so the one network table is the source of
381
- * truth — keeps callers that hold the CAIP-2 form (CLI wallet) in lockstep
382
- * with the SDK 402-builder which already speaks x402 slugs.
383
- */
384
356
  declare function usdcContractByCaip2(caip2: string): `0x${string}` | undefined;
385
357
  /** USDC EIP-712 domain `name` for a network slug. */
386
358
  declare function usdcDomainNameFor(networkSlug: string): string | undefined;
@@ -575,7 +547,6 @@ interface ProgressContent {
575
547
  /** Optional human-readable hint about what's happening right now. */
576
548
  hint?: string;
577
549
  }
578
- /** A single message in a job conversation — discriminated union on `type`. */
579
550
  type Message = (MessageBase & {
580
551
  type: "text";
581
552
  content: TextContent;
@@ -610,37 +581,30 @@ type Message = (MessageBase & {
610
581
  type: "progress";
611
582
  content: ProgressContent;
612
583
  });
613
- /** Type guard: message is a prompt from the provider. */
614
584
  declare function isPromptMessage(msg: Message): msg is MessageBase & {
615
585
  type: "prompt";
616
586
  content: PromptContent;
617
587
  };
618
- /** Type guard: message contains an artifact. */
619
588
  declare function isArtifactMessage(msg: Message): msg is MessageBase & {
620
589
  type: "artifact";
621
590
  content: ArtifactContent;
622
591
  };
623
- /** Type guard: message is a payment request. */
624
592
  declare function isPaymentRequestMessage(msg: Message): msg is MessageBase & {
625
593
  type: "payment-request";
626
594
  content: PaymentRequestContent;
627
595
  };
628
- /** Type guard: message is a text message. */
629
596
  declare function isTextMessage(msg: Message): msg is MessageBase & {
630
597
  type: "text";
631
598
  content: TextContent;
632
599
  };
633
- /** Type guard: message signals completion. */
634
600
  declare function isCompleteMessage(msg: Message): msg is MessageBase & {
635
601
  type: "complete";
636
602
  content: CompleteContent;
637
603
  };
638
- /** Type guard: message is a cancellation. */
639
604
  declare function isCancelMessage(msg: Message): msg is MessageBase & {
640
605
  type: "cancel";
641
606
  content: CancelContent;
642
607
  };
643
- /** Type guard: message is a working signal. */
644
608
  declare function isWorkingMessage(msg: Message): msg is MessageBase & {
645
609
  type: "working";
646
610
  content: WorkingContent;
@@ -686,7 +650,6 @@ declare const CLIENT_COMPATIBILITY_HEADERS: {
686
650
  readonly protocolVersion: "DVM-Protocol-Version";
687
651
  readonly capabilities: "DVM-Client-Capabilities";
688
652
  };
689
- /** A parsed SemVer value carried by `DVM-Client`. */
690
653
  interface ClientSemVer {
691
654
  readonly version: string;
692
655
  readonly major: number;
@@ -694,7 +657,6 @@ interface ClientSemVer {
694
657
  readonly patch: number;
695
658
  readonly prerelease: readonly (string | number)[];
696
659
  }
697
- /** Validated compatibility metadata from one HTTP request. */
698
660
  interface ClientCompatibility {
699
661
  readonly clientVersion?: ClientSemVer;
700
662
  readonly protocolVersion?: number;
@@ -710,16 +672,13 @@ interface ClientCompatibility {
710
672
  readonly capabilities: boolean;
711
673
  };
712
674
  }
713
- /** Compatibility requirement attached to one endpoint. */
714
675
  interface ClientCompatibilityRequirement {
715
676
  /** Additive capability tokens the caller must advertise. */
716
677
  readonly requiredCapabilities?: readonly string[];
717
678
  /** Lowest `dvm` SemVer accepted by this endpoint. */
718
679
  readonly minimumClientVersion?: string;
719
680
  }
720
- /** Factory for compatibility middleware attached to one custom route. */
721
681
  type ClientCompatibilityGate = (requirement: ClientCompatibilityRequirement) => MiddlewareHandler<ClientCompatibilityEnv>;
722
- /** Hono variables installed by the SDK compatibility middleware. */
723
682
  interface ClientCompatibilityEnv {
724
683
  Variables: {
725
684
  clientCompatibility?: ClientCompatibility;
@@ -737,35 +696,20 @@ declare function parseClientCapabilities(value: string): readonly string[] | und
737
696
  declare function clientCompatibilityAttributes(compatibility: ClientCompatibility): Record<string, unknown>;
738
697
  /** Parse request metadata and advertise the server protocol version on every response. */
739
698
  declare function clientCompatibilityMiddleware(): MiddlewareHandler<ClientCompatibilityEnv>;
740
- /** Build pre-handler enforcement middleware for one endpoint declaration. */
741
699
  declare const requireClientCompatibility: ClientCompatibilityGate;
742
- /** Render the normal structured HTTP 426 compatibility error. */
743
700
  declare function clientUpgradeRequired(c: Context, compatibility: ClientCompatibility, requirement: ClientCompatibilityRequirement): Response;
744
701
 
745
- /**
746
- * Algorithm: recursively sort object keys lexicographically (UTF-16 code-unit
747
- * order — JavaScript's default), then JSON.stringify with no whitespace.
748
- * Arrays preserve order.
749
- */
750
- /** Any JSON-serialisable value accepted by `canonicalize`. */
751
702
  type JsonValue = string | number | boolean | null | JsonValue[] | {
752
703
  [key: string]: JsonValue;
753
704
  };
754
705
  /**
755
- * Serialise `value` to its canonical JSON form (sorted keys, no whitespace).
756
- *
757
- * `undefined` properties on objects are silently omitted (mirroring
758
- * `JSON.stringify`), so callers don't need to explicitly delete optional fields
759
- * that weren't set. Throws `TypeError` for unsupported scalar types (functions,
760
- * symbols, non-finite numbers). The caller is responsible for stripping fields
761
- * that shouldn't appear in the signed payload (e.g. `signature` itself).
706
+ * Object keys sort recursively in UTF-16 code-unit order; arrays preserve order.
707
+ * Undefined object properties are omitted. Unsupported scalars, including
708
+ * non-finite numbers, throw TypeError. Callers strip unsigned fields themselves.
762
709
  */
763
710
  declare function canonicalize(value: JsonValue): string;
764
711
  /**
765
- * Canonicalise `value` and return the UTF-8 bytes the signer should produce a
766
- * signature over. Both the cast client signer (`signAddEpisode`) and the
767
- * server verifier (`verifyCanonicalSignedRequest`) call this — sharing the function
768
- * guarantees byte-identical input on both sides.
712
+ * UTF-8 encoding of the canonical JSON form.
769
713
  */
770
714
  declare function canonicaliseForSigning(value: JsonValue): Uint8Array;
771
715
 
@@ -895,9 +839,8 @@ interface DrainReceipt {
895
839
  /** Amount reclaimed, micro-units of the credit's currency. */
896
840
  amount_micro: number;
897
841
  /**
898
- * Credit balance immediately after the drain debit (micro-units) — the
899
- * pending job holds at request time, fixed when the drain was recorded.
900
- * Every event's receipt attests this same figure.
842
+ * Credit balance left after the drain debit, in micro-units. It includes
843
+ * pending job holds at request time and is fixed across this drain's event receipts.
901
844
  */
902
845
  balance_after_micro: number;
903
846
  /** Per-credit monotonic sequence the drain consumed, shared with draws. */
@@ -965,13 +908,6 @@ type UnsignedFundingReceipt = Omit<FundingReceipt, "signature">;
965
908
  * issuer does the same (`resultHashFor` in `sdk/server/receipt-issuer.ts`).
966
909
  */
967
910
  declare function computeResultHash(summary: string | undefined, artifactContents: JsonValue[]): string;
968
- /**
969
- * Sign an unsigned receipt with the DVM's receipt secret, returning the
970
- * complete receipt. Signs `canonicaliseForSigning(receipt)` — the same
971
- * sorted-key canonical JSON the caller-auth envelope and the builder
972
- * attestation use, so {@link verifyReceipt} is symmetric with
973
- * `verifyCanonicalSignedRequest`. There is no second canonicalisation.
974
- */
975
911
  declare function signReceipt(unsigned: UnsignedJobReceipt, secretHex: string): JobReceipt;
976
912
  /**
977
913
  * Verify a receipt's self-consistency: the BIP-340 signature over the
@@ -1155,43 +1091,41 @@ declare function isInKindDepletion(depletion: LotDepletion, amountMicro: number)
1155
1091
  /** The rails a payout lands on. Lightning never pays out: credits fund straight into the receive wallet. */
1156
1092
  type PayoutRail = "cashu" | "x402" | "tempo";
1157
1093
  /**
1158
- * What gathered the payments into one transfer: a cashu accumulator melt, an
1159
- * x402 batch settle to `pay_to`, a Tempo channel's cooperative close, or a
1160
- * Tempo channel's scheduled / manual settlement to the recipient.
1094
+ * Transfers aggregate payments: a Cashu melt, x402 batch to pay_to, or Tempo recipient
1095
+ * settlement/close.
1161
1096
  */
1162
1097
  type PayoutKind = "melt" | "batch" | "close" | "settle";
1163
- /** Which repair verb completed a stuck movement. Only `tempo_close_reconciled` is produced here today. */
1164
1098
  type PayoutRepairKind = "x402_settlement_reconciled" | "lightning_invoice_reconciled" | "tempo_close_reconciled";
1165
1099
  /**
1166
- * JSON wire shape POSTed to the platform's `/_internal/payout` endpoint —
1167
- * one landed movement into the builder's custody. Idempotent platform-side
1168
- * on `(dvmId, payoutId)`, so the durable retry loop can redeliver freely; a
1169
- * `repaired` redelivery of an id that already landed upgrades that row rather
1170
- * than adding a second.
1100
+ * One landed movement into builder custody. Platform retries deduplicate by (dvmId,
1101
+ * payoutId); repaired redelivery upgrades that movement rather than adding another.
1171
1102
  */
1172
1103
  interface PayoutReportPayload {
1173
- /** Required. Platform DVM record ID (`dvms.id`); must match the bearer token's DVM. */
1104
+ /**
1105
+ * Platform record ID; must match the bearer token’s DVM.
1106
+ */
1174
1107
  dvmId: string;
1175
1108
  /** Required. The DVM's own id for the movement — `cashu:melt:<quote>`, `x402:batch:<uuid>`, `tempo:<tx>`. */
1176
1109
  payoutId: string;
1177
1110
  rail: PayoutRail;
1178
1111
  kind: PayoutKind;
1179
- /** Required. Rail-native atomic units: sats, or USDC micro. Always > 0. */
1112
+ /**
1113
+ * Positive rail-native atomic units: sats or USDC micro.
1114
+ */
1180
1115
  nativeAmount: number;
1181
- /** Required. `sats` for cashu, `usdc` for the stablecoin rails. */
1182
1116
  nativeAsset: "sats" | "usdc";
1183
1117
  /** Where it landed; `null` for a melt, whose Lightning destination the platform never records. */
1184
1118
  recipient: string | null;
1185
- /** Mechanism references — mint, melt quote, tx hash, batch id, voucher count, channel id. Description material, never arithmetic. */
1119
+ /**
1120
+ * Descriptive mechanism references, never an arithmetic basis.
1121
+ */
1186
1122
  refs: Record<string, unknown>;
1187
1123
  /** Required. When the money landed (epoch ms). */
1188
1124
  landedAt: number;
1189
- /** Set when a repair verb completed this movement. */
1190
1125
  repaired?: boolean;
1191
1126
  /** Required iff `repaired`. */
1192
1127
  repairKind?: PayoutRepairKind;
1193
1128
  }
1194
- /** A stuck movement inside a pending snapshot. */
1195
1129
  interface PayoutWedge {
1196
1130
  id: string;
1197
1131
  /**
@@ -1211,17 +1145,14 @@ interface PayoutWedge {
1211
1145
  /** Rail-native amount held up, when known. */
1212
1146
  native?: number;
1213
1147
  }
1214
- /** One pool inside a rail's pending figure — a mint, a batch, a channel. */
1215
1148
  interface PayoutPool {
1216
1149
  id: string;
1217
1150
  native: number;
1218
1151
  label?: string;
1219
1152
  }
1220
1153
  /**
1221
- * JSON wire shape POSTed to the platform's `/_internal/payout-pending`
1222
- * endpoint — one rail's snapshot of money still moving toward the builder,
1223
- * replaced on each report. Best-effort: a lost one is superseded by the next
1224
- * tick, so it rides no durable queue.
1154
+ * A replaceable snapshot of one rail’s pending builder funds. Best-effort delivery uses no
1155
+ * durable queue; a later tick supersedes a lost report.
1225
1156
  */
1226
1157
  interface PayoutPendingPayload {
1227
1158
  dvmId: string;
@@ -1229,14 +1160,12 @@ interface PayoutPendingPayload {
1229
1160
  /** Everything on this rail still moving toward the builder, in atomic units. */
1230
1161
  poolNative: number;
1231
1162
  nativeAsset: "sats" | "usdc";
1232
- /** How many pools the figure is made of. */
1233
1163
  items: number;
1234
1164
  pools: PayoutPool[];
1235
1165
  wedged: PayoutWedge[];
1236
1166
  /** This DVM's clock when the snapshot was taken (epoch ms). */
1237
1167
  snapshotAt: number;
1238
1168
  }
1239
- /** The delivery seam — implemented by `RevenueReporter`, stubbed in tests. */
1240
1169
  interface PayoutTransport {
1241
1170
  /**
1242
1171
  * Queue a payout through the caller's open transaction, so the row and the
@@ -1253,19 +1182,13 @@ interface PayoutTransport {
1253
1182
  drainPending?(): Promise<void>;
1254
1183
  }
1255
1184
  /**
1256
- * A money-path hook whose report must commit with the fact it describes.
1257
- * `enqueue` runs inside the caller's transaction, and a throw rolls the fact
1258
- * back with it — the drain report's own discipline: a call the
1259
- * caller has to repeat costs a round trip, where a landed movement with no
1260
- * row understates paid-out forever. `committed` runs once that transaction
1261
- * has committed, for the delivery and the snapshot that must not read
1262
- * uncommitted state.
1185
+ * enqueue joins the money transaction and must throw on failure so its fact rolls back.
1186
+ * committed runs after commit, when delivery and snapshots can safely read the new state.
1263
1187
  */
1264
1188
  interface TransactionalPayoutHook<E> {
1265
1189
  enqueue(event: E, tx: RevenueReporterQuerier): Promise<void>;
1266
1190
  committed(): void;
1267
1191
  }
1268
- /** What `/admin/cashu/mark-melted` knows once the rows are marked. */
1269
1192
  interface CashuMeltCompleted {
1270
1193
  rows: {
1271
1194
  id: string;
@@ -1275,7 +1198,6 @@ interface CashuMeltCompleted {
1275
1198
  meltQuoteId: string;
1276
1199
  paymentPreimage: string;
1277
1200
  }
1278
- /** What mppx reports after a Tempo channel settlement or close confirmed on chain. */
1279
1201
  interface TempoSessionSettled {
1280
1202
  txHash: string;
1281
1203
  channelId: string;
@@ -1285,27 +1207,23 @@ interface TempoSessionSettled {
1285
1207
  /** Newly settled to the payee by this transaction, atomic units. */
1286
1208
  delta: bigint;
1287
1209
  }
1288
- /** What `reconcile-tempo-drain` re-read from the close receipt it booked against. */
1289
1210
  interface TempoCloseReconciled {
1290
1211
  channelId: string;
1291
1212
  txHash: string;
1292
1213
  /** Captured by the payee — this DVM — per the receipt, atomic units as a decimal string. */
1293
1214
  settledToPayee: string;
1294
1215
  }
1295
- /** The channel view the tracker sums claims over. */
1296
1216
  interface X402TrackedChannel {
1297
1217
  channelId: string;
1298
1218
  chargedCumulativeAmount: string;
1299
1219
  totalClaimed: string;
1300
1220
  }
1301
- /** A refund settlement wedged between chain and ledger, for the wedge list. */
1302
1221
  interface X402WedgedRefund {
1303
1222
  settlementId: string;
1304
1223
  /** Epoch ms the settlement was first prepared. */
1305
1224
  createdAt: number;
1306
1225
  native?: number;
1307
1226
  }
1308
- /** What the batch-settlement server hands the tracker once it knows its scope. */
1309
1227
  interface X402PayoutContext {
1310
1228
  /** `${network}|${payTo}|${token}` — the scope the settle-pending marker is keyed on. */
1311
1229
  scope: string;
@@ -1318,7 +1236,6 @@ interface X402PayoutContext {
1318
1236
  settleIntervalMs: number;
1319
1237
  listWedgedRefunds?: () => Promise<X402WedgedRefund[]>;
1320
1238
  }
1321
- /** The two manager verbs the tracker wraps. */
1322
1239
  interface X402TrackedManager {
1323
1240
  claim(...args: never[]): Promise<{
1324
1241
  vouchers: number;
@@ -1341,9 +1258,7 @@ interface X402PayoutObserver {
1341
1258
  /** A settle attempt that failed outside the tracked manager. One call per attempt a builder would count as one. */
1342
1259
  recordSettleFailure(error: unknown): Promise<void>;
1343
1260
  }
1344
- /** The Tempo readers the host attaches after mounting. */
1345
1261
  interface TempoPayoutReader {
1346
- /** The recipient address every Tempo payout names. */
1347
1262
  recipient?: string;
1348
1263
  /** CAIP-2 network for the refs, when known. */
1349
1264
  network?: string;
@@ -1363,11 +1278,8 @@ interface TempoPayoutReader {
1363
1278
  }) => Promise<CreditDrainRecord[]>;
1364
1279
  }
1365
1280
  /**
1366
- * The claims gathered since the last settle — the batch the next settle pays
1367
- * out. Durable across machines because claims run under the fleet lock on
1368
- * whichever machine won the tick, and the settle that finally moves the money
1369
- * may run on another: an in-process figure would report a partial amount
1370
- * after any restart in between.
1281
+ * Claims accumulated since the last settle. The batch is durable across machines: claims and
1282
+ * eventual settlement can run on different hosts under the fleet lock.
1371
1283
  */
1372
1284
  interface X402OpenBatch {
1373
1285
  scope: string;
@@ -1381,55 +1293,39 @@ interface X402OpenBatch {
1381
1293
  lastError: string | null;
1382
1294
  nextRetryAt: number | null;
1383
1295
  }
1384
- /** Where the open batch lives — Postgres beside the settlement rows, or memory in dev. */
1385
1296
  interface X402BatchStore {
1386
1297
  get(scope: string): Promise<X402OpenBatch | undefined>;
1387
1298
  put(batch: X402OpenBatch): Promise<void>;
1388
1299
  delete(scope: string): Promise<void>;
1389
1300
  /**
1390
- * Close the open batch and run `enqueue` in the same transaction, so the
1391
- * payout row is queued exactly when the batch is gone and never otherwise:
1392
- * a settle can never lose its payout, because a failed enqueue leaves the
1393
- * batch open for the next settle to report in full. `false` when the batch
1394
- * under `scope` is no longer `batchId` — a sibling settled it first — in
1395
- * which case nothing is queued.
1301
+ * Atomically delete the matching batch and enqueue its payout. Failed enqueue preserves the
1302
+ * batch for retry; false means the scope no longer holds batchId and nothing was queued.
1396
1303
  */
1397
1304
  settle(scope: string, batchId: string, enqueue: (tx?: RevenueReporterQuerier) => Promise<void>): Promise<boolean>;
1398
1305
  }
1399
- /** Construction options for {@link PayoutReporter}. */
1400
1306
  interface PayoutReporterOpts {
1401
- /** Platform DVM record ID, bound into every report. */
1402
1307
  dvmId: string;
1403
1308
  transport: PayoutTransport;
1404
1309
  /**
1405
- * The SDK's Postgres. Reads the cashu accumulator for the pending pool and
1406
- * holds the open x402 batch; without it the batch lives in memory (dev) and
1407
- * no cashu snapshot is taken.
1310
+ * Postgres holds durable x402 batches and supplies Cashu snapshots. Without it, the default
1311
+ * batch store is memory and Cashu snapshots are omitted.
1408
1312
  */
1409
1313
  db?: Pool;
1410
1314
  /**
1411
- * Whether this DVM runs the cashu accumulator. The cashu snapshot reads
1412
- * `wallet_accumulator`, which only that mode creates — on any other DVM the
1413
- * read would fail every tick and warn about a rail it does not carry.
1315
+ * Only accumulator hosts create wallet_accumulator; enabling this on another host makes
1316
+ * each snapshot query fail.
1414
1317
  */
1415
1318
  cashu?: boolean;
1416
1319
  /** Override the batch store (tests); defaults to Postgres on `db`, memory without. */
1417
1320
  batchStore?: X402BatchStore;
1418
1321
  /** Snapshot cadence, ms. Default 5 minutes. */
1419
1322
  snapshotIntervalMs?: number;
1420
- /** Override for the current time (tests). */
1421
1323
  now?: () => number;
1422
1324
  }
1423
1325
  /**
1424
- * Builds the payout and pending reports from the SDK's money paths and hands
1425
- * them to the transport.
1426
- *
1427
- * Two entry points are transactional: `cashuMelted` and `tempoCloseReconciled`
1428
- * queue their row inside the caller's transaction and throw when they cannot,
1429
- * so the melt or the reconciled close rolls back with its missing report rather
1430
- * than landing without one. The x402 settle and the pending snapshots are
1431
- * fail-safe instead: a report that cannot be built or queued there is logged and
1432
- * picked up on the next tick, never thrown into the settle that produced it.
1326
+ * Cashu melts and repaired Tempo closes enqueue inside their caller’s transaction and
1327
+ * propagate failures. x402 reporting and pending snapshots instead log failures; failed x402
1328
+ * enqueue preserves the open batch for retry.
1433
1329
  */
1434
1330
  declare class PayoutReporter implements PayoutTransport {
1435
1331
  private readonly dvmId;
@@ -1446,7 +1342,6 @@ declare class PayoutReporter implements PayoutTransport {
1446
1342
  private timer;
1447
1343
  private warmup;
1448
1344
  constructor(opts: PayoutReporterOpts);
1449
- /** Boot DDL for the durable batch store, when this reporter owns one. */
1450
1345
  init(): Promise<void>;
1451
1346
  /**
1452
1347
  * Start the snapshot loop. Unref'd, so it never keeps a scale-to-zero
@@ -1460,23 +1355,13 @@ declare class PayoutReporter implements PayoutTransport {
1460
1355
  reportPayoutPending(payload: PayoutPendingPayload): Promise<void>;
1461
1356
  /** Deliver what is queued now; a transport without the seam waits for its retry loop. */
1462
1357
  flush(): Promise<void>;
1463
- /** The hook `mark-melted` runs inside its transaction: the mark and its payout commit together. */
1464
1358
  meltHook(): TransactionalPayoutHook<CashuMeltCompleted>;
1465
1359
  /**
1466
- * A melt completed: the accumulator rows are SPENT at the mint and the
1467
- * Lightning payment reached the builder's destination. One payout per mint
1468
- * in the call — a `mark-melted` normally names one, since a melt quote
1469
- * belongs to one mint. The amount is the face value melted; the mint's fee
1470
- * and the Lightning amount received are builder-machine facts this DVM does
1471
- * not see.
1472
- *
1473
- * Queued through `tx` when the caller is inside the transaction that marks
1474
- * the rows, and a failure is thrown rather than swallowed so that mark rolls
1475
- * back with it — see {@link TransactionalPayoutHook}. Without `tx` the rows
1476
- * are queued on their own and delivered at once.
1360
+ * Report melted face value per mint, not the Lightning amount received after fees. Hash the
1361
+ * payment preimage; do not publish it. With tx, propagate enqueue failures and defer
1362
+ * delivery until commit. Without tx, enqueue independently and start delivery.
1477
1363
  */
1478
1364
  cashuMelted(event: CashuMeltCompleted, tx?: RevenueReporterQuerier): Promise<void>;
1479
- /** The host attaches the session store and ledger readers once they exist. */
1480
1365
  attachTempo(reader: TempoPayoutReader): void;
1481
1366
  /**
1482
1367
  * A Tempo channel settled or closed on chain. `delta` is what this
@@ -1484,29 +1369,18 @@ declare class PayoutReporter implements PayoutTransport {
1484
1369
  * (a close of a fully settled channel) is no payout.
1485
1370
  */
1486
1371
  tempoSettled(event: TempoSessionSettled): Promise<void>;
1487
- /** The hook `reconcile-tempo-drain` runs inside its transaction: the booking and its payout commit together. */
1488
1372
  tempoRepairHook(): TransactionalPayoutHook<TempoCloseReconciled>;
1489
1373
  /**
1490
- * The operator repaired a wedged cooperative close. The close's
1491
- * payee side landed on chain when the close did; if the live hook reported
1492
- * it, this upgrades that row to `repair`, otherwise it is the row. The
1493
- * figure is the receipt's payee total for the channel.
1494
- *
1495
- * Queued through `tx` when the caller is inside the transaction that books
1496
- * the drain, and thrown rather than swallowed so that booking rolls back
1497
- * with it — see {@link TransactionalPayoutHook}.
1374
+ * Report the receipt’s payee total under the live close’s transaction ID, flagged repaired.
1375
+ * The platform upgrades a live row if one exists. Join tx and propagate enqueue failures;
1376
+ * this does not report the caller refund.
1498
1377
  */
1499
1378
  tempoCloseReconciled(event: TempoCloseReconciled, tx?: RevenueReporterQuerier): Promise<void>;
1500
- /** The observer the batch-settlement server attaches to and routes its manager through. */
1501
1379
  x402Observer(): X402PayoutObserver;
1502
1380
  /**
1503
- * Wrap the upstream channel manager so every claim grows the open batch and
1504
- * every settle closes it. The claim delta is read off storage rather than
1505
- * off upstream's result, which carries only a voucher count; under the
1506
- * fleet lock the before/after read is consistent. A channel the claim
1507
- * removed on its way through — a refund's claim-then-delete —
1508
- * moved whatever it still owed before it went, and that value reaches
1509
- * `pay_to` in this batch too, so it counts at its pre-claim figure.
1381
+ * Read claim deltas from storage because upstream returns voucher counts. Under the fleet
1382
+ * lock, before/after reads are consistent. A deleted channel contributes its pre-claim
1383
+ * unclaimed value, including refund claim-then-delete.
1510
1384
  */
1511
1385
  private trackX402Manager;
1512
1386
  private recordX402Claim;
@@ -1540,28 +1414,28 @@ declare class PayoutReporter implements PayoutTransport {
1540
1414
  * and stored in `pending_dvm_reports.payload` for durable retry.
1541
1415
  */
1542
1416
  interface RevenueReportPayload {
1543
- /** Required. Platform DVM record ID (`dvms.id`). Identifies which DVM earned the revenue. */
1417
+ /** Platform DVM record ID (`dvms.id`). Identifies which DVM earned the revenue. */
1544
1418
  dvmId: string;
1545
- /** Required. Job identifier used by the platform to de-duplicate revenue rows and link to the job record. */
1419
+ /** Job identifier used by the platform to de-duplicate revenue rows and link to the job record. */
1546
1420
  jobId: string;
1547
1421
  /**
1548
- * Optional. Capability selected by dispatch for this job. Omitted for
1422
+ * Capability selected by dispatch for this job. Omitted for
1549
1423
  * reports from older SDKs and legacy persisted jobs that predate capability
1550
1424
  * dispatch; absence must not be rewritten as a capability name.
1551
1425
  */
1552
1426
  capability?: string;
1553
- /** Required. Total amount paid by the caller in millisatoshis, across all credits that satisfied the job. */
1427
+ /** Total amount paid by the caller in millisatoshis, across all credits that satisfied the job. */
1554
1428
  paidMsats: number;
1555
- /** Optional. Cashu mint URL from which the payment tokens were issued. Set only for the `cashu` rail; omitted for all other rails. */
1429
+ /** Cashu mint URL from which the payment tokens were issued. Set only for the `cashu` rail; omitted for all other rails. */
1556
1430
  paymentMint?: string;
1557
1431
  /**
1558
- * Required. Payment rail identifier (`"cashu"`, `"tempo"`, `"x402"`, `"stripe"`).
1432
+ * Payment rail identifier (`"cashu"`, `"tempo"`, `"x402"`, `"stripe"`).
1559
1433
  * Mirrors `PaymentMethod` on the platform; carried as a plain string over the wire
1560
1434
  * so the SDK has no platform-type dependency.
1561
1435
  */
1562
1436
  rail: string;
1563
1437
  /**
1564
- * Optional. Settlement reference for the credit: EVM tx hash for `x402`, credential
1438
+ * Settlement reference for the credit: EVM tx hash for `x402`, credential
1565
1439
  * challenge ID for `tempo`, `X-Cashu-Request-Id` UUID for `cashu`. Required by the
1566
1440
  * platform revenue ledger for `tempo`/`x402` rails (recordJobRevenue throws without it);
1567
1441
  * nullable for `cashu`.
@@ -1574,20 +1448,20 @@ interface RevenueReportPayload {
1574
1448
  */
1575
1449
  nativeAmount?: number;
1576
1450
  /**
1577
- * Optional. Asset tag paired with `nativeAmount`: `"sats"`, `"usdc"`, `"usdc.e"`, or
1451
+ * Asset tag paired with `nativeAmount`: `"sats"`, `"usdc"`, `"usdc.e"`, or
1578
1452
  * `"usd-cents"`. Both fields must be present for the platform to write a non-cashu
1579
1453
  * revenue row.
1580
1454
  */
1581
1455
  nativeAsset?: string;
1582
1456
  /**
1583
- * Optional. Cashu flow discriminator. Set to `"p2pk_accumulator"` when the cashu
1457
+ * Cashu flow discriminator. Set to `"p2pk_accumulator"` when the cashu
1584
1458
  * accumulator path satisfied the job; omitted for legacy cashu. Written into
1585
1459
  * `revenue_events.metadata.cashu_flow` by the platform so ops can split per-call cashu
1586
1460
  * rows by source.
1587
1461
  */
1588
1462
  cashuFlow?: string;
1589
1463
  /**
1590
- * Optional. What the job cost the **builder** to serve, as declared by the
1464
+ * What the job cost the **builder** to serve, as declared by the
1591
1465
  * handler through `ctx.cost()` — the other half of the
1592
1466
  * margin the dashboard states. `amountMicro` is 1e-6 of `currency`, which
1593
1467
  * is the currency the builder incurred the cost in and need not be the
@@ -1609,28 +1483,28 @@ interface RevenueReportPayload {
1609
1483
  currency: string;
1610
1484
  };
1611
1485
  /**
1612
- * Optional. Credit this job's payment drew against. Present on
1486
+ * Credit this job's payment drew against. Present on
1613
1487
  * every job a ledger-backed payment satisfied; its presence is what tells
1614
1488
  * the platform the row is a **draw-keyed** revenue event rather than a
1615
1489
  * pre-credits per-call one.
1616
1490
  */
1617
1491
  creditId?: string;
1618
- /** Optional. The draw this job settled — one draw, one revenue row. */
1492
+ /** The draw this job settled — one draw, one revenue row. */
1619
1493
  drawId?: string;
1620
- /** Optional. The draw's fiat amount, 1e-6 of `creditCurrency`. The exact liability offset. */
1494
+ /** The draw's fiat amount, 1e-6 of `creditCurrency`. The exact liability offset. */
1621
1495
  drawAmountMicro?: number;
1622
- /** Optional. Currency the credit is denominated in (lowercase ISO, e.g. `usd`). */
1496
+ /** Currency the credit is denominated in (lowercase ISO, e.g. `usd`). */
1623
1497
  creditCurrency?: string;
1624
1498
  /**
1625
- * Optional. Settlement instant (epoch ms) — draws are revenue *when they
1499
+ * Settlement instant (epoch ms) — draws are revenue *when they
1626
1500
  * settle*, so this dates the row. Omitted for pre-credits rows, which the
1627
1501
  * platform dates at record time as before.
1628
1502
  */
1629
1503
  settledAt?: number;
1630
- /** Optional. The funding's own rail reference, for reconciling a draw back to its deposit. */
1504
+ /** The funding's own rail reference, for reconciling a draw back to its deposit. */
1631
1505
  fundingRef?: string;
1632
1506
  /**
1633
- * Optional. Revenue class when this row is not ordinary service revenue.
1507
+ * Revenue class when this row is not ordinary service revenue.
1634
1508
  * `short_pay_forfeit` marks money kept from an underpayment that bought no
1635
1509
  * job so analytics can exclude it.
1636
1510
  */
@@ -1663,16 +1537,16 @@ interface JobTerminalReportPayload {
1663
1537
  * delivery order.
1664
1538
  */
1665
1539
  interface JobCostReportPayload {
1666
- /** Required. Platform DVM record ID (`dvms.id`); bound to the authenticating token. */
1540
+ /** Platform DVM record ID (`dvms.id`); bound to the authenticating token. */
1667
1541
  dvmId: string;
1668
- /** Required. Job identifier and the report's platform-side idempotency key. */
1542
+ /** Job identifier and the report's platform-side idempotency key. */
1669
1543
  jobId: string;
1670
- /** Optional. Capability selected by dispatch; absent only on legacy persisted jobs. */
1544
+ /** Capability selected by dispatch; absent only on legacy persisted jobs. */
1671
1545
  capability?: string;
1672
- /** Required. Terminal outcome that explains why this event carries no revenue. */
1546
+ /** Terminal outcome that explains why this event carries no revenue. */
1673
1547
  outcome: "completed" | "failed" | "cancelled";
1674
1548
  /**
1675
- * Required. Number of declarations incorporated into `cost`; the
1549
+ * Number of declarations incorporated into `cost`; the
1676
1550
  * platform keeps the payload with the highest revision for this job.
1677
1551
  */
1678
1552
  costRevision: number;
@@ -1695,34 +1569,34 @@ interface JobCostReportPayload {
1695
1569
  * redeliver freely.
1696
1570
  */
1697
1571
  interface CreditDepositPayload {
1698
- /** Required. Platform DVM record ID (`dvms.id`); must match the bearer token's DVM. */
1572
+ /** Platform DVM record ID (`dvms.id`); must match the bearer token's DVM. */
1699
1573
  dvmId: string;
1700
1574
  creditId: string;
1701
- /** Required. Canonical owner of the funded credit. */
1575
+ /** Canonical owner of the funded credit. */
1702
1576
  callerPubkey: string;
1703
- /** Required. The rail's settlement reference — the deposit's idempotency key. */
1577
+ /** The rail's settlement reference — the deposit's idempotency key. */
1704
1578
  fundingId: string;
1705
1579
  /** Payment rail (`"cashu"`, `"tempo"`, `"x402"`). */
1706
1580
  rail: string;
1707
- /** Required. Rail value received, in millisatoshis. */
1581
+ /** Rail value received, in millisatoshis. */
1708
1582
  paidMsats: number;
1709
1583
  /** Cashu only. Full received proof face before its receiver input fee. */
1710
1584
  grossPaidMsats?: number;
1711
1585
  /** Cashu only. Standalone NUT-02 fee reserved by the received proofs. */
1712
1586
  cashuInputFeeMsats?: number;
1713
- /** Optional. Rail-native atomic units; omitted for cashu (derived from `paidMsats`). */
1587
+ /** Rail-native atomic units; omitted for cashu (derived from `paidMsats`). */
1714
1588
  nativeAmount?: number;
1715
- /** Optional. Asset tag paired with `nativeAmount`. */
1589
+ /** Asset tag paired with `nativeAmount`. */
1716
1590
  nativeAsset?: string;
1717
- /** Optional. Cashu mint URL the funding proofs were issued from. */
1591
+ /** Cashu mint URL the funding proofs were issued from. */
1718
1592
  paymentMint?: string;
1719
- /** Required. Fiat micro credited to the ledger — the liability this deposit creates. */
1593
+ /** Fiat micro credited to the ledger — the liability this deposit creates. */
1720
1594
  amountMicro: number;
1721
- /** Required. Currency `amountMicro` is 1e-6 of. */
1595
+ /** Currency `amountMicro` is 1e-6 of. */
1722
1596
  creditCurrency: string;
1723
- /** Required. When the funding committed (epoch ms). */
1597
+ /** When the funding committed (epoch ms). */
1724
1598
  fundedAt: number;
1725
- /** Required. Current credit expiry after this funding committed (epoch ms). */
1599
+ /** Current credit expiry after this funding committed (epoch ms). */
1726
1600
  expiryMs: number;
1727
1601
  /**
1728
1602
  * Tempo session funding only: the TIP-1034 channel this deposit
@@ -1783,59 +1657,45 @@ interface CreditDepositPayload {
1783
1657
  * redeliver freely.
1784
1658
  */
1785
1659
  interface CreditDrawReleasePayload {
1786
- /** Required. Platform DVM record ID (`dvms.id`); must match the bearer token's DVM. */
1660
+ /** Platform DVM record ID (`dvms.id`); must match the bearer token's DVM. */
1787
1661
  dvmId: string;
1788
1662
  creditId: string;
1789
- /** Required. Released draw; the platform idempotency key with DVM and credit. */
1663
+ /** Released draw; the platform idempotency key with DVM and credit. */
1790
1664
  drawId: string;
1791
- /** Required. Fiat micro restored to availability. */
1665
+ /** Fiat micro restored to availability. */
1792
1666
  amountMicro: number;
1793
- /** Required. Currency `amountMicro` is 1e-6 of. */
1667
+ /** Currency `amountMicro` is 1e-6 of. */
1794
1668
  creditCurrency: string;
1795
- /** Required. Per-credit gap-free sequence assigned when the draw was placed. */
1669
+ /** Per-credit gap-free sequence assigned when the draw was placed. */
1796
1670
  ledgerSeq: number;
1797
- /** Required. When the hold reached `released` (epoch ms). */
1671
+ /** When the hold reached `released` (epoch ms). */
1798
1672
  releasedAt: number;
1799
1673
  }
1800
1674
  type CreditDrainRepairKind = "x402_settlement_reconciled" | "tempo_close_reconciled";
1801
1675
  /**
1802
- * JSON wire shape POSTed to the platform's `/_internal/credit-drain`
1803
- * endpoint. A drain is a **reclaim**: deposited money paid back to
1804
- * the caller, which extinguishes the liability the funding created. Reported
1805
- * so outstanding liability nets out to `deposited − drawn − drained`; never
1806
- * summed into revenue, and never a revenue row — no job was sold.
1807
- *
1808
- * Reported only on the drain's **terminal** transition (`picked_up` for cashu,
1809
- * `sent` for the other rails), because that is where the money has verifiably
1810
- * left. `requested` and `parked` have not paid the caller yet.
1811
- *
1812
- * Idempotent on `(dvmId, creditId, drainId)`, so the shared retry loop can
1813
- * redeliver freely.
1814
- *
1815
- * Fiat micro only, with no `paidMsats` sibling: `credit_drains` holds no rail
1816
- * value (the sats figure is derived at read time from a live rate), so there
1817
- * would be nothing truthful to put in one — and the liability arithmetic this
1818
- * feeds is exact fiat micro on all three of its terms.
1676
+ * Terminal picked_up/sent drains report reclaimed fiat micro-units, reducing
1677
+ * outstanding liability rather than adding revenue. Requested or parked drains
1678
+ * have not paid the caller. Idempotency key: (dvmId, creditId, drainId).
1819
1679
  */
1820
1680
  interface CreditDrainPayload {
1821
- /** Required. Platform DVM record ID (`dvms.id`); must match the bearer token's DVM. */
1681
+ /** Platform DVM record ID (`dvms.id`); must match the bearer token's DVM. */
1822
1682
  dvmId: string;
1823
1683
  creditId: string;
1824
- /** Required. Canonical owner of the reclaimed credit. */
1684
+ /** Canonical owner of the reclaimed credit. */
1825
1685
  callerPubkey: string;
1826
- /** Required. The drain's own idempotency key. */
1686
+ /** The drain's own idempotency key. */
1827
1687
  drainId: string;
1828
1688
  /** Payout rail (`"cashu"`, `"x402"`, `"tempo"`). */
1829
1689
  method: string;
1830
- /** Required. Fiat micro reclaimed — the liability this drain extinguishes. */
1690
+ /** Fiat micro reclaimed — the liability this drain extinguishes. */
1831
1691
  amountMicro: number;
1832
- /** Required. Currency `amountMicro` is 1e-6 of. */
1692
+ /** Currency `amountMicro` is 1e-6 of. */
1833
1693
  creditCurrency: string;
1834
1694
  /** Terminal state: `"picked_up"` for cashu, `"sent"` for other rails. */
1835
1695
  status: string;
1836
- /** Required. When the money left (epoch ms). */
1696
+ /** When the money left (epoch ms). */
1837
1697
  drainedAt: number;
1838
- /** Optional. Per-credit gap-free ledger sequence, for reconciling against the DVM's ledger. */
1698
+ /** Per-credit gap-free ledger sequence, for reconciling against the DVM's ledger. */
1839
1699
  ledgerSeq?: number;
1840
1700
  /** Set when an operator repair verb completed this reclaim. */
1841
1701
  repaired?: boolean;
@@ -1893,21 +1753,21 @@ type CreditDrainEnqueue = (tx: RevenueReporterQuerier, payload: CreditDrainPaylo
1893
1753
  * redeliver freely.
1894
1754
  */
1895
1755
  interface CreditExpiryReleasePayload {
1896
- /** Required. Platform DVM record ID (`dvms.id`); must match the bearer token's DVM. */
1756
+ /** Platform DVM record ID (`dvms.id`); must match the bearer token's DVM. */
1897
1757
  dvmId: string;
1898
1758
  creditId: string;
1899
- /** Required. Per-expiry-episode key; the platform idempotency key with DVM and credit. */
1759
+ /** Per-expiry-episode key; the platform idempotency key with DVM and credit. */
1900
1760
  releaseId: string;
1901
1761
  callerPubkey: string;
1902
1762
  /** Funding rail. `null` on a credit funded before the basis became mandatory. */
1903
1763
  rail: string | null;
1904
- /** Required. Fiat micro released — the liability this expiry extinguishes. */
1764
+ /** Fiat micro released — the liability this expiry extinguishes. */
1905
1765
  amountMicro: number;
1906
- /** Required. Currency `amountMicro` is 1e-6 of. */
1766
+ /** Currency `amountMicro` is 1e-6 of. */
1907
1767
  creditCurrency: string;
1908
- /** Required. When the credit's TTL ran out (epoch ms) — when the liability ended. */
1768
+ /** When the credit's TTL ran out (epoch ms) — when the liability ended. */
1909
1769
  expiryMs: number;
1910
- /** Required. When the sweep observed the expiry (epoch ms). */
1770
+ /** When the sweep observed the expiry (epoch ms). */
1911
1771
  releasedAt: number;
1912
1772
  /** Set only on the reversal report: when the revival funding landed (epoch ms). */
1913
1773
  reversedAt?: number | null;
@@ -1933,21 +1793,21 @@ type CreditExpiryReleaseEnqueue = (tx: RevenueReporterQuerier, payload: CreditEx
1933
1793
  * report degrades to log-only rather than blocking the sweep.
1934
1794
  */
1935
1795
  interface PaidJobDeathPayload {
1936
- /** Required. Platform DVM record ID (`dvms.id`). Bound to the authenticating token. */
1796
+ /** Platform DVM record ID (`dvms.id`). Bound to the authenticating token. */
1937
1797
  dvmId: string;
1938
1798
  jobId: string;
1939
1799
  capability: string;
1940
- /** Required. Amount of the released credit draw in millisatoshis. */
1800
+ /** Amount of the released credit draw in millisatoshis. */
1941
1801
  paidMsats: number;
1942
1802
  /** Payment rail of the released draw (`"cashu"`, `"tempo"`, `"x402"`). */
1943
1803
  rail: string;
1944
1804
  /** Reaper reason: dead worker or never-terminal idle job. */
1945
1805
  reason: string;
1946
- /** Optional. Rail-native released amount, paired with `nativeAsset`. */
1806
+ /** Rail-native released amount, paired with `nativeAsset`. */
1947
1807
  nativeAmount?: number;
1948
- /** Optional. Asset tag for `nativeAmount` (`"sats"` / `"usdc"` / …). */
1808
+ /** Asset tag for `nativeAmount` (`"sats"` / `"usdc"` / …). */
1949
1809
  nativeAsset?: string;
1950
- /** Optional. Cashu mint URL the credit draw's tokens were issued from. */
1810
+ /** Cashu mint URL the credit draw's tokens were issued from. */
1951
1811
  paymentMint?: string;
1952
1812
  }
1953
1813
  type RevenueSkippedNoRailReason = "credit_rail_null" | "credit_rail_unrecognized" | "job_rail_missing" | "ledger_not_configured";
@@ -1959,16 +1819,16 @@ type RevenueSkippedNoRailReason = "credit_rail_null" | "credit_rail_unrecognized
1959
1819
  * and a failed alert must never block terminal handling or reconciliation.
1960
1820
  */
1961
1821
  interface RevenueSkippedNoRailPayload {
1962
- /** Required. Platform DVM record ID (`dvms.id`). Bound to the authenticating token. */
1822
+ /** Platform DVM record ID (`dvms.id`). Bound to the authenticating token. */
1963
1823
  dvmId: string;
1964
1824
  jobId: string;
1965
1825
  creditId: string;
1966
1826
  drawId: string;
1967
- /** Required. Caller payment captured for the job, in millisatoshis. */
1827
+ /** Caller payment captured for the job, in millisatoshis. */
1968
1828
  paidMsats: number;
1969
- /** Optional. Settled draw amount in 1e-6 of `creditCurrency`. */
1829
+ /** Settled draw amount in 1e-6 of `creditCurrency`. */
1970
1830
  drawAmountMicro?: number;
1971
- /** Optional. Currency the draw amount is denominated in. */
1831
+ /** Currency the draw amount is denominated in. */
1972
1832
  creditCurrency?: string;
1973
1833
  /** Credit funding rail as stored on the draw, or null for legacy rows. */
1974
1834
  creditRail: string | null;
@@ -2084,13 +1944,8 @@ declare class RevenueReporter {
2084
1944
  /** Join a released draw to the transaction that made the hold terminal. */
2085
1945
  enqueueCreditDrawRelease(tx: RevenueReporterQuerier, payload: CreditDrawReleasePayload): Promise<void>;
2086
1946
  /**
2087
- * Join a credit drain to the transaction that made it terminal.
2088
- *
2089
- * Same discipline as {@link enqueueDeposit} and the same reason: no network
2090
- * I/O here, only the durable queue row, written through `tx` so the report
2091
- * and the status CAS commit or roll back together. A drain whose row was
2092
- * lost between the two would overstate outstanding liability forever —
2093
- * the mirror image of a lost deposit.
1947
+ * Await durable queue insertion through tx so the terminal drain and its
1948
+ * report commit or roll back together; no network I/O here.
2094
1949
  */
2095
1950
  enqueueCreditDrain(tx: RevenueReporterQuerier, payload: CreditDrainPayload): Promise<void>;
2096
1951
  /**
@@ -2120,14 +1975,8 @@ declare class RevenueReporter {
2120
1975
  */
2121
1976
  reportPayoutPending(payload: PayoutPendingPayload): Promise<void>;
2122
1977
  /**
2123
- * Join a credit-expiry release — or the revival that reverses one — to the
2124
- * transaction that made it true.
2125
- *
2126
- * Same discipline as {@link enqueueCreditDrain}: no network I/O, only the
2127
- * durable queue row written through `tx`. A release whose row was lost after
2128
- * the sweep committed would understate committed value forever; a lost
2129
- * reversal would overstate it, which is the worse direction — the builder
2130
- * would see money as theirs that a revived credit can still buy work with.
1978
+ * Await durable queue insertion through tx so expiry release or reversal and
1979
+ * its report commit together. A missing reversal would overstate builder value.
2131
1980
  */
2132
1981
  enqueueCreditExpiryRelease(tx: RevenueReporterQuerier, payload: CreditExpiryReleasePayload): Promise<void>;
2133
1982
  /**
@@ -2259,7 +2108,6 @@ interface X402SettlementPage {
2259
2108
  interface X402RelaySubmissionLock {
2260
2109
  withRelaySubmissionLock<T>(key: string, operation: () => Promise<T>): Promise<T>;
2261
2110
  }
2262
- /** Construction options for {@link PostgresX402ChannelStorage}. */
2263
2111
  interface X402ChannelStorageOpts {
2264
2112
  /**
2265
2113
  * Override the relay submission lock's wait ceiling, in milliseconds.
@@ -2360,7 +2208,6 @@ declare class PostgresX402ChannelStorage implements ChannelStorage {
2360
2208
  private readonly relayLockTimeoutMs;
2361
2209
  private readonly terminalizeUnbackedCredit?;
2362
2210
  constructor(pool: Pool, opts?: X402ChannelStorageOpts);
2363
- /** Create the durable channel table under the SDK-wide DDL lock. */
2364
2211
  init(): Promise<void>;
2365
2212
  /**
2366
2213
  * Read the fleet's durable "claimed funds are awaiting transfer" marker.
@@ -2492,7 +2339,6 @@ declare class PostgresX402ChannelStorage implements ChannelStorage {
2492
2339
  * to finish.
2493
2340
  */
2494
2341
  completeSettlement(settlementId: string, resolvedAtMs?: number): Promise<void>;
2495
- /** Load one channel snapshot. */
2496
2342
  get(channelId: string): Promise<Channel | undefined>;
2497
2343
  /** List all channel snapshots in stable channel-id order. */
2498
2344
  list(): Promise<Channel[]>;
@@ -2568,7 +2414,6 @@ declare class PostgresX402ChannelStorage implements ChannelStorage {
2568
2414
  private updateInTransaction;
2569
2415
  }
2570
2416
 
2571
- /** Durable channel seam used to compose a settled draw with consumed Tempo value. */
2572
2417
  interface TempoSessionSettlementStore {
2573
2418
  withDrawPlacement<value>(channelId: string, operation: (tx: CreditLedgerQuerier, lifecycle: TempoSessionLifecycle$1) => Promise<value>, tx?: CreditLedgerQuerier): Promise<value>;
2574
2419
  withTerminalConsumption<value>(channelId: string, operation: (tx: CreditLedgerQuerier, lifecycle: TempoSessionLifecycle$1) => Promise<{
@@ -2577,7 +2422,6 @@ interface TempoSessionSettlementStore {
2577
2422
  consume: boolean;
2578
2423
  }>): Promise<value>;
2579
2424
  }
2580
- /** Tempo channel state read while holding its durable row lock. */
2581
2425
  interface TempoSessionLifecycle$1 {
2582
2426
  finalized: boolean;
2583
2427
  closeRequestedAt: bigint;
@@ -2593,7 +2437,6 @@ interface TempoCreditLossEvidence {
2593
2437
  consumedNative: bigint;
2594
2438
  observedAt?: number;
2595
2439
  }
2596
- /** Immutable audit record for one terminal, unbacked Tempo credit. */
2597
2440
  interface TempoCreditLoss {
2598
2441
  creditId: string;
2599
2442
  channelId: string;
@@ -2613,7 +2456,6 @@ interface X402CreditLossEvidence {
2613
2456
  totalClaimedNative: bigint;
2614
2457
  observedAt?: number;
2615
2458
  }
2616
- /** Immutable audit record for one terminal, unbacked x402 credit. */
2617
2459
  interface X402CreditLoss {
2618
2460
  creditId: string;
2619
2461
  channelId: string;
@@ -2626,25 +2468,13 @@ interface X402CreditLoss {
2626
2468
  observedAt: number;
2627
2469
  }
2628
2470
  /**
2629
- * Money-safe credit ledger — the SDK-owned settlement primitive required for
2630
- * prepaid balances in production DVMs.
2631
- * Two tables in the DVM's own Postgres (`credits`, `credit_draws`), two-phase
2632
- * draws (`pending` at job-accept → `settled` on success / `released` on
2633
- * failure), and a per-credit gap-free `ledger_seq` for the receipt evidence
2634
- * chain (`ReceiptCredit` in `lib/receipt.ts`).
2635
- *
2636
- * Concurrency contract: **every write to `credit_draws` happens inside a
2637
- * transaction that first takes `SELECT … FOR UPDATE` on the parent `credits`
2638
- * row.** The pending-hold SUM that defines available balance is only stable
2639
- * because of that lock — never add a `credit_draws` write path that skips it.
2640
- * `ctx.store` / KV is prohibited for credit state: last-write-wins KV on a
2641
- * multi-machine fleet loses money under concurrency (spec condition 2).
2471
+ * Every write to credit_draws takes SELECT … FOR UPDATE on its parent credit within the same
2472
+ * transaction. That lock keeps the pending-hold sum and available balance stable across
2473
+ * machines; KV storage cannot provide this money boundary.
2642
2474
  *
2643
- * Scope note: the ledger is host-wide, keyed only by `credit_id` /
2644
- * `caller_pubkey`. A multi-mount host shares one ledger across its mounts —
2645
- * same operator, same money destination, so not a money hazard; a `dvm_id`
2646
- * column can be added by idempotent migration if per-mount ledgers are ever
2647
- * needed.
2475
+ * The ledger is host-wide: credit_id and caller_pubkey identify rows, without a per-mount
2476
+ * dvm_id. Draws hold funds until settlement or release, and share a gap-free per-credit
2477
+ * ledger_seq with drains.
2648
2478
  */
2649
2479
  declare class CreditLedger {
2650
2480
  private readonly pool;
@@ -2658,267 +2488,95 @@ declare class CreditLedger {
2658
2488
  private expiryReleaseOutbox?;
2659
2489
  constructor(pool: CreditLedgerPool, tempoSessionStore?: TempoSessionSettlementStore | undefined);
2660
2490
  /**
2661
- * Bind the durable x402 settlement state this ledger gates spending on
2662
- *
2663
- * Late, because the settlement store is built with the batch-settlement
2664
- * server, which is built *after* the ledger it reads earned draws from. An
2665
- * unbound ledger simply does not gate — correct for a DVM with no x402
2666
- * channel storage, where no settlement row can exist to wedge.
2491
+ * Bound after construction because the settlement store shares this ledger and is
2492
+ * initialized separately. Ledger effects and route preflights use the same refund gate.
2667
2493
  */
2668
2494
  useX402SettlementGate(gate: X402RefundSettlementGate): void;
2669
2495
  /**
2670
- * Bind the durable outbox that reports credit-expiry releases and their
2671
- * revivals to the platform.
2672
- *
2673
- * Held on the ledger rather than threaded through {@link fund} for the same
2674
- * reason as the gate above: the revival half fires from *every* funding path
2675
- * Cashu commit, x402 exact and channel, Tempo, the Lightning invoice
2676
- * settle, the implicit N=1 per-call payment — and a seam each of those has to
2677
- * remember to pass is a seam one of them will eventually forget.
2678
- *
2679
- * Unbound (a `dvmctl dev` server, a self-hosted builder, a test host) the
2680
- * sweep and the reversal still run and still write the ledger's own rows;
2681
- * only the platform report is skipped. Reporting is bookkeeping and must
2682
- * never gate the ledger.
2496
+ * Expiry release and revival reports join the ledger transaction. A caller-owned funding
2497
+ * transaction must not commit a reversal independently of its outbox row. Without this
2498
+ * wiring, ledger transitions still run.
2683
2499
  */
2684
2500
  useCreditExpiryReleaseOutbox(outbox: CreditExpiryReleaseOutbox): void;
2685
- /** Create the `credits` / `credit_draws` tables if absent. Call once at SDK boot. */
2686
2501
  init(): Promise<void>;
2687
- /** The boot DDL itself — always runs under {@link withSdkInitLock}. */
2688
2502
  private createTables;
2689
2503
  /**
2690
- * Give every open non-channel Bitcoin credit a funding lot.
2691
- *
2692
- * Runs inside the boot DDL, is guarded per credit by `NOT EXISTS`, and is
2693
- * therefore both the one-time migration and a standing self-heal for a
2694
- * credit that somehow reaches a positive balance with no lot behind it.
2695
- *
2696
- * The lot is derived from `credits.msats_remaining`, not replayed out of
2697
- * `credit_fundings`. That is not a shortcut: `credit_fundings` records only
2698
- * `{amount_micro, rail}` — no instrument amount at all — and is not written
2699
- * on every funding path, whereas `msats_remaining` **is** the credit's
2700
- * current unspent rail value, maintained pro rata on every draw since
2701
- * One synthetic lot pairing it with `balance_micro` therefore
2702
- * reproduces today's in-kind position exactly, and FIFO over a single lot is
2703
- * trivially correct. Historic per-funding rates are unrecoverable and would
2704
- * change nothing: only the remaining position is owed.
2705
- *
2706
- * The semantics change is deliberately retroactive — every
2707
- * current holder is first-party, so there is one regime and no legacy
2708
- * branch.
2504
+ * Backfill an open non-channel Bitcoin credit only when it has no funding lot. Pair
2505
+ * balance_micro with current msats_remaining; credit_fundings lacks historical instrument
2506
+ * amounts and does not cover every funding path. Historical per-funding rates cannot be
2507
+ * recovered. Existing lots are left untouched, so this does not repair partial coverage
2508
+ * gaps.
2709
2509
  */
2710
2510
  private backfillFundingLots;
2711
2511
  /**
2712
- * Create a credit, or top up an existing one (same `creditId`). Top-ups add
2713
- * to the balance and overwrite `expiry_ms` with the provided value — funding
2714
- * an expired credit revives it (the caller-ownership contract carry-forward: the credit is a
2715
- * rolling buffer, not per-job escrow). The existing row's `caller_pubkey`
2716
- * and `currency` must match or the fund is refused with a typed error —
2717
- * without that check a top-up against someone else's `credit_id` would
2718
- * silently merge two callers' money.
2719
- *
2720
- * Pass `tx` (a client inside a caller-owned `BEGIN`) to commit the rail
2721
- * receive and the ledger credit atomically (spec condition 3 — the
2722
- * verifier does this). The ledger issues **no** transaction control on `tx`.
2723
- *
2724
- * Without `tx` it opens one of its own, because a funding is no longer a
2725
- * single statement: it upserts the credit, records its funding lot
2726
- * and reverses any standing expiry release — and that
2727
- * last leg restores balance and queues a report. A crash between the upsert
2728
- * and the reversal would leave a revived credit whose release still stands,
2729
- * which the sweep's own exclusion then makes permanent: `balance_micro > 0`
2730
- * but a standing release means it is neither drainable nor re-releasable.
2731
- *
2732
- * `basis` records the rail value behind the fiat so each draw can
2733
- * be allocated its share of the rail-native amount actually received. A
2734
- * top-up on a different rail than the credit's is refused with
2735
- * `rail_mismatch`: sats and USDC microunits aren't summable, so a blended
2736
- * credit would have no coherent native basis to allocate from.
2512
+ * Funding adds balance and overwrites expiry, reviving an expired credit. It is additive,
2513
+ * not idempotent: the caller’s rail marker or funding record must prevent replay. Owner and
2514
+ * currency must match; rail and reusable-channel bindings cannot be mixed with other
2515
+ * instruments. A legacy NULL rail adopts its first typed top-up.
2737
2516
  *
2738
- * It is **required**, in the type and again at runtime via
2739
- * {@link assertFundingBasis}. A basis-less fund wrote `rail = NULL`, and a
2740
- * draw against such a credit settles — a real debit — while
2741
- * `railAsFundingMethod` correctly declines to guess a rail, so `bookRevenue`
2742
- * books nothing and outstanding liability (`deposits − draw revenue`) stays
2743
- * overstated by the drawn amount forever. `credits.rail` stays nullable
2744
- * because rows written before this change still exist in live databases:
2745
- * **write is closed, read is not.** A legacy row adopts a rail from its first
2746
- * typed top-up via the `COALESCE` below.
2517
+ * With tx, all writes join the caller’s transaction without transaction control. Otherwise
2518
+ * the ledger owns a transaction covering balance, funding lot, reversal of standing expiry
2519
+ * releases and their reports. A partial revival would leave restored balance with a release
2520
+ * still counted.
2747
2521
  *
2748
- * The rail-native **instrument** is pinned at first funding too, and both
2749
- * channel guards are symmetric for the same reason. A reusable
2750
- * channel — Tempo session or x402 batch-settlement — may only ever top up
2751
- * the credit it opened, and a credit opened by a one-shot (a Tempo charge, an
2752
- * x402 exact authorization) may never adopt one. The x402 half used to admit
2753
- * `credits.x402_channel_id IS NULL`, so a channel deposit bound itself to a
2754
- * credit already holding exact-funded value: `earnedNativeForX402Channel`
2755
- * then read that credit's whole settled-draw history as the channel's
2756
- * earnings, and the first claim tick swept a deposit nobody had consumed,
2757
- * while the unspent balance lost every exit (the drain's refund base goes
2758
- * below `totalClaimed`, and a channel-bound credit refuses every non-x402
2759
- * drain). `credits_x402_channel_uidx` does not cover this: it guards
2760
- * one-credit-per-channel, not one-source-per-credit.
2761
- *
2762
- * An x402 channel deposit is additionally refused `settlement_pending` while
2763
- * that channel carries an unresolved refund — see the gate read
2764
- * below. This method is the authority on that rule, as it is on the binding
2765
- * rules above; every door preflights it where a refusal is still free.
2522
+ * An incoming x402 channel with an unresolved refund is refused before the upsert. That
2523
+ * preflight is not atomic with a concurrent cooperative drain; see the incoming-channel
2524
+ * gate below.
2766
2525
  */
2767
2526
  fund(args: CreditFundArgs): Promise<CreditSnapshot>;
2768
- /**
2769
- * The funding itself, on whichever handle {@link fund} chose. Every refusal
2770
- * it raises is the caller's to see unchanged; a self-opened transaction rolls
2771
- * back around it.
2772
- */
2773
2527
  private fundOn;
2774
2528
  /**
2775
- * Undo any expiry release this credit still carries, because a funding just
2776
- * landed on it (the revival rule), restoring the balance the
2777
- * release took.
2778
- *
2779
- * A release records the credit's **final** undrawn remainder and zeroes it
2780
- * (see {@link releaseExpiredCreditLocked}). Funding adds to the balance and
2781
- * overwrites `expiry_ms` (the caller-ownership contract carry-forward: the credit is a rolling
2782
- * buffer), so the moment money arrives the recorded remainder is no longer
2783
- * final: the release stops counting and its micro come back.
2784
- *
2785
- * Deliberately unconditional on the incoming expiry rather than gated on
2786
- * `expiryMs > nowMs`. A release exists only for a credit that had already
2787
- * lapsed, so any funding that reaches one is money the release did not
2788
- * account for — and a funding that arrives with an already-past expiry leaves
2789
- * a credit the very next sweep re-releases at its new, larger remainder. One
2790
- * rule, self-healing in both directions, and it never needs to read the
2791
- * pre-funding expiry the upsert has already overwritten.
2529
+ * Funding reverses standing expiry releases, restores their released micro and queues
2530
+ * reversal reports on the funding transaction. Do this even when the new expiry is already
2531
+ * past: the next sweep must release the enlarged remainder as a new episode.
2792
2532
  *
2793
- * Runs on the funding's own handle — which {@link fund} now guarantees is a
2794
- * transaction — so the reversal, the restored balance and the report commit
2795
- * with the money or not at all. A reversal that committed without its report
2796
- * would be unrecoverable: `WHERE reversed_at IS NULL` means no later pass
2797
- * re-derives it.
2798
- *
2799
- * @returns the credit row as the restore left it, or undefined when there was
2800
- * nothing to reverse. The caller reads its snapshot off this rather than off
2801
- * the funding upsert's `RETURNING`, which predates the restore.
2533
+ * Returns the row after restoration, or undefined when no release stood. The upsert’s
2534
+ * RETURNING row predates the restoration and is not the final balance.
2802
2535
  */
2803
2536
  private reverseExpiryReleases;
2804
2537
  /**
2805
- * Release the undrawn remainder of every credit whose TTL has run out
2806
- * Returns the releases this pass recorded.
2807
- *
2808
- * **What is released.** The credit's whole `balance_micro`. Expiry ends
2809
- * spending but never ownership of the record, so the balance stays readable;
2810
- * what ends is the caller's claim on it, and on the prepaid rails the value
2811
- * behind it is already in builder custody.
2812
- *
2813
- * **Tempo is excluded**, at both of its markers (`tempo_channel_id` and a
2814
- * `tempo` rail). A Tempo credit is channel-backed and its undrawn value
2815
- * returns to the *caller* at channel exit, so releasing it would book the
2816
- * builder money the chain is about to hand back.
2817
- *
2818
- * **A credit with a pending hold is skipped, not partially released.** A draw
2819
- * placed before expiry stays settleable afterwards (`draw`'s replay lookup
2820
- * runs above the expiry check, so a lost response is still recoverable), so
2821
- * the remainder is not final while a hold is outstanding. Skipping costs one
2822
- * sweep interval and keeps the released figure exactly "what nothing bought";
2823
- * holds do resolve — the orphan-draw watchdog is what guarantees
2824
- * a stranded one still reaches a terminal state.
2825
- *
2826
- * **Idempotent** two ways. `release_id` is derived from the expiry instant,
2827
- * so a re-sweep after a crash between the insert and its report collides on
2828
- * the same primary key instead of writing a second row; and the candidate
2829
- * scan excludes any credit that already has an un-reversed release.
2538
+ * Release expired undrawn balances, retaining readable credit records. Tempo rail or
2539
+ * channel-backed credits are excluded because their collateral returns to the caller
2540
+ * through the channel lifecycle.
2830
2541
  *
2831
- * Each candidate is re-checked under the credit row lock every
2832
- * `credit_draws` write is taken under, so a draw racing the sweep either
2833
- * loses the race (its credit is already released and it would have been
2834
- * refused `credit_expired` anyway) or wins it and leaves a pending hold the
2835
- * locked re-check sees.
2542
+ * Pending holds prevent release: jobs accepted before expiry may still settle or release.
2543
+ * Candidates are rechecked under the same parent-credit lock used by draw writes. The
2544
+ * balance debit, episode record and report commit together; standing releases exclude
2545
+ * repeat work. A later revival may reuse the expiry instant but receives a new release
2546
+ * sequence.
2836
2547
  */
2837
2548
  sweepExpiredCredits(args?: {
2838
2549
  nowMs?: number;
2839
- /** Credits released per pass. Bounds the work a long-idle DVM does at boot. */
2840
2550
  limit?: number;
2841
2551
  }): Promise<{
2842
2552
  released: CreditExpiryRelease[];
2843
2553
  }>;
2844
2554
  /**
2845
- * Write one credit's expiry release under its row lock, zero the balance it
2846
- * released, and re-assert every condition the unlocked candidate scan tested.
2847
- * Returns undefined when the credit no longer qualifies — funded, drawn
2848
- * against, or already released between the scan and the lock.
2555
+ * Recheck eligibility under the credit lock and zero the released balance in the same
2556
+ * transaction as its episode/report. Otherwise requestDrain, which intentionally ignores
2557
+ * expiry, could reclaim value already released to the builder.
2849
2558
  *
2850
- * **Zeroing the balance is what makes the release real**, and it is the whole
2851
- * reason this runs under the lock rather than as a bare INSERT. The row alone
2852
- * records that the caller's claim ended; it does not *end* it. `requestDrain`
2853
- * has no expiry check by design (expiry gates new draws, not reclaims), so a
2854
- * credit whose remainder had been released and reported was still fully
2855
- * drainable: the DVM would pay out money the platform had already booked as
2856
- * the builder's, and `deposited - drawn - drained - released` would go
2857
- * negative on the same micro. With the balance at zero, `requestDrain`'s
2858
- * existing `availableMicro <= 0` guard refuses with `nothing_to_drain` and no
2859
- * new check is needed anywhere. {@link reverseExpiryReleases} puts the
2860
- * balance back when a funding revives the credit.
2861
- *
2862
- * `status` deliberately stays `active`: a released credit must remain
2863
- * fundable, or the revival rule has nothing to revive.
2864
- *
2865
- * The zeroed balance also takes the credit out of `BITCOIN_CREDIT_PREDICATE`
2866
- * (`balance_micro > 0`), so the in-kind sweep floor stops counting sats the
2867
- * builder no longer owes back — and counts them again after a revival, with
2868
- * the credit's funding lots untouched throughout.
2559
+ * Keep status active so future funding can revive it. Funding lots remain intact: balance
2560
+ * zero excludes this credit from Bitcoin liability, and reversal restores that liability.
2869
2561
  */
2870
2562
  private releaseExpiredCreditLocked;
2871
2563
  /**
2872
- * Record this funding's in-kind basis as a lot.
2873
- *
2874
- * Called from inside {@link fund}, on the same handle, so the lot shares
2875
- * whatever transaction the rail opened — `withCommitTx` for Cashu,
2876
- * `ProcessedPaymentStore.withTx` for x402/Tempo, `settleInvoice`'s own
2877
- * `BEGIN` for Lightning. The funding-atomicity invariant therefore extends
2878
- * to it structurally rather than by every call site remembering: a lot
2879
- * cannot exist without its ledger credit, or the credit without its lot.
2880
- *
2881
- * Only a **channel-backed** credit writes no lot: it is in-kind already —
2882
- * the escrow itself retains the unused value — so a lot on one would be
2883
- * counted as Bitcoin deposit liability the hub does not hold.
2884
- *
2885
- * Every other non-channel Bitcoin funding writes one, the implicit N=1
2886
- * per-call payment included. That one funds and draws the same amount in
2887
- * this transaction, so it looks like a lot born depleted — but the draw is a
2888
- * *pending hold*, not a debit, and only a `completed` job settles it
2889
- * (`JobManager.settlesDraw`). A failed, cancelled or stale-swept job
2890
- * releases the hold and the whole funded amount becomes reclaimable balance,
2891
- * so suppressing the lot here would leave routine traffic reclaiming off a
2892
- * live rate and missing from the sweep floor. One small insert on the hot
2893
- * path buys the invariant the liability read depends on: open lots cover
2894
- * `balance_micro` for every Bitcoin credit, always.
2564
+ * Record non-channel Bitcoin funding lots on the funding transaction. An implicit payment
2565
+ * also needs a lot: its draw is initially only a hold, and failure releases the whole
2566
+ * balance for reclaim. Channel collateral is excluded from Bitcoin lots.
2895
2567
  *
2896
- * A zero-sats basis still writes a lot. It is honest — the credit exists and
2897
- * we know its rail — and it is what makes "covered but unbacked" a
2898
- * distinguishable state from "no lot at all" for the reclaim and the
2899
- * liability rollup, instead of both reading as a gap.
2568
+ * A zero-sats basis still records a lot, distinguishing covered but basisless credit from a
2569
+ * missing-lot coverage gap.
2900
2570
  */
2901
2571
  private recordFundingLot;
2902
2572
  /**
2903
- * Place a two-phase `pending` hold of `amountMicro` against the credit and
2904
- * assign the next `ledger_seq`. Idempotent on `(creditId, drawId)`: a replay
2905
- * returns the original `{balanceAfterMicro, ledgerSeq}` with
2906
- * `replayed: true` and places no second hold — this is how a caller recovers
2907
- * a lost response (spec condition 1). A replay whose `amountMicro` or
2908
- * `jobId` differs from the recorded draw is not a replay but a money bug in
2909
- * the caller, refused with `draw_conflict`.
2573
+ * Place a pending hold with the next shared per-credit sequence. A matching (creditId,
2574
+ * drawId) replay returns its recorded tuple without another hold; conflicting amount or
2575
+ * jobId is draw_conflict.
2910
2576
  *
2911
- * The replay lookup runs **before** the expiry check, deliberately: a draw
2912
- * placed pre-expiry must stay recoverable after the credit expires, or the
2913
- * DVM holds a debit the caller can never reconcile. Fresh draws on an
2914
- * expired credit are refused with `credit_expired`; the balance stays
2915
- * intact and readable (the caller-ownership contract: expiry ends spending, never ownership).
2916
- *
2917
- * Pass `tx` (a client inside a caller-owned `BEGIN`) to place the hold in
2918
- * the same transaction as the rail commit and the `fund` upsert (spec
2919
- * condition 3 — the implicit N=1 path). The ledger issues no
2920
- * transaction control on `tx`; the `SELECT … FOR UPDATE` row lock is still
2921
- * taken on the caller's transaction, so the locking invariant holds.
2577
+ * Replay precedes expiry and settlement gates so a lost response stays recoverable. Fresh
2578
+ * draws require spendable credit. With tx, take the parent-credit lock on the caller’s
2579
+ * transaction without issuing transaction control.
2922
2580
  */
2923
2581
  draw(args: {
2924
2582
  creditId: string;
@@ -2931,63 +2589,23 @@ declare class CreditLedger {
2931
2589
  nowMs?: number;
2932
2590
  }): Promise<DrawResult>;
2933
2591
  /**
2934
- * The unresolved refund on `channelId` that must stop this ledger effect, if
2935
- * any.
2936
- *
2937
- * `undefined` whenever there is nothing to ask — an unbound credit, or a DVM
2938
- * with no durable settlement store, where a settlement row cannot exist.
2939
- *
2940
- * Public since the behavior was introduced, so `/v1/credit`'s pre-payment preflight can ask the
2941
- * same question `fund` will ask from inside the rail transaction — one read,
2942
- * one answer, rather than a second copy of the rule in the routes.
2592
+ * Return the unresolved refund that blocks an effect on this channel, or undefined. Route
2593
+ * preflights use the same rule before moving money; draw and drain also read it under the
2594
+ * credit lock.
2943
2595
  */
2944
2596
  blockingX402Refund(channelId: string | null | undefined, statuses: X402SettlementStatus[]): Promise<X402UnresolvedRefund | undefined>;
2945
2597
  /**
2946
- * Grow a pending draw by `addAmountMicro` — the ledger half of a
2947
- * mid-job `requestPayment` top-up. One job keeps **one** draw: the mid-job
2948
- * money funds the same credit and enlarges the hold the upfront leg placed,
2949
- * so the receipt's `ReceiptCredit` block countersigns the job's full cost
2950
- * and `revenue_events` still books exactly one row per job.
2951
- *
2952
- * `draw()` cannot express this: a same-`draw_id` call with a different
2953
- * amount is `draw_conflict` by design (a replayed draw must never re-price).
2598
+ * Grow an accepted job’s pending hold while preserving its original ledger_seq. The balance
2599
+ * tuple is updated at that sequence, so interleaved draws need not form a monotonic balance
2600
+ * trajectory. No expiry check applies to an already accepted job.
2954
2601
  *
2955
- * The arithmetic is `draw()`'s, unchanged: `addAmountMicro` is checked
2956
- * against — and `balance_after_micro` recomputed from — the balance net of
2957
- * **every** pending hold, this draw's own included. That is deliberately the
2958
- * conservative reading (a fresh draw of `addAmountMicro` would see exactly
2959
- * the same figure), and it is what keeps the total held after growth inside
2960
- * the credit's balance.
2602
+ * Available balance excludes every pending hold, including this one. An earmark allocates
2603
+ * the incoming funding’s rail value; absent one, growth uses the pool.
2961
2604
  *
2962
- * The increment's rail value comes from {@link growthRailValue}: earmarked
2963
- * to the funding that backs it when the caller names one (the mid-job case),
2964
- * pro-rata otherwise. Either way the draws of a credit keep summing to
2965
- * precisely what the rails paid.
2966
- *
2967
- * **`ledger_seq` is not re-taken.** The per-credit sequence is gap-free
2968
- * (`credit_draws_seq_uidx`), so moving this draw forward would strand its
2969
- * original number. The consequence is deliberate and worth knowing: the
2970
- * recorded `balance_after_micro` is the post-growth figure stamped at the
2971
- * *original* seq, so a credit with interleaved sibling activity has a
2972
- * trajectory that is truthful per draw rather than monotonic across seqs.
2973
- *
2974
- * **No expiry check**, matching `settle`/`release`: the job was accepted
2975
- * pre-expiry and is still running. Refusing here would strand the handler
2976
- * mid-flight over a clock the caller can't influence.
2977
- *
2978
- * **Not idempotent by itself.** Growth carries no key of its own — it
2979
- * inherits the rail's (the accumulator's `(dvm_id, request_id)` UNIQUE rolls
2980
- * the whole transaction back on replay; x402/mpp collide on the
2981
- * `processed_payments` marker). Never call it outside a rail commit.
2982
- *
2983
- * **`capMicro` bounds cumulative growth at what the job cumulatively asked,**
2984
- * and it is the only ceiling that can refuse a top-up: the
2985
- * available-balance check cannot, because the `fund` a moment earlier in
2986
- * this same transaction raised the balance by exactly the amount being
2987
- * drawn. Excess is **granted partially or not at all rather than thrown** —
2988
- * this runs inside the rail commit, so a throw would roll back cashu proofs
2989
- * the caller already sent. The refused remainder stays funded balance the
2990
- * caller owns and can drain.
2605
+ * Growth has no independent replay key: compose it with the rail’s replay protection in one
2606
+ * transaction. capMicro bounds cumulative growth at the job’s cumulative ask. Excess
2607
+ * remains caller-owned funded balance; partial or zero growth returns that excess rather
2608
+ * than throwing and rolling back accepted proofs.
2991
2609
  */
2992
2610
  growDraw(args: {
2993
2611
  creditId: string;
@@ -3051,18 +2669,9 @@ declare class CreditLedger {
3051
2669
  nowMs?: number;
3052
2670
  }): Promise<DrawResolution>;
3053
2671
  /**
3054
- * Record a fund-only top-up under its client-generated `fundId`.
3055
- * The deposit half of the evidence chain, and the top-up path's idempotency
3056
- * key: `PRIMARY KEY (credit_id, fund_id)` means a concurrent duplicate
3057
- * loses with a typed `funding_replayed` rather than crediting twice.
3058
- *
3059
- * Call it inside the same `tx` as {@link fund} — the rail commit, the
3060
- * funding record, and the balance increment must land together or not at
3061
- * all (the funding-commitment rule). Like `fund`, this issues no transaction
3062
- * control on `tx`; it is a single statement.
3063
- *
3064
- * This does **not** move money on its own. `fund` still does the crediting;
3065
- * this row is what lets a retry be answered with the original outcome.
2672
+ * Record the fund-only replay key without moving money. A duplicate (credit_id, fund_id)
2673
+ * raises funding_replayed. Compose this insert, fund and rail acceptance on the same
2674
+ * caller-owned transaction; a retry reads the recorded funding outcome.
3066
2675
  */
3067
2676
  recordFunding(args: {
3068
2677
  creditId: string;
@@ -3121,7 +2730,6 @@ declare class CreditLedger {
3121
2730
  expiresAt: number;
3122
2731
  nowMs?: number;
3123
2732
  }): Promise<CreditInvoiceRecord>;
3124
- /** Read one invoice (no lock). */
3125
2733
  getInvoice(args: {
3126
2734
  creditId: string;
3127
2735
  fundId: string;
@@ -3137,30 +2745,17 @@ declare class CreditLedger {
3137
2745
  limit: number;
3138
2746
  }): Promise<CreditInvoiceRecord[]>;
3139
2747
  /**
3140
- * Apply an observed Lightning settlement to the ledger — **the exactly-once
3141
- * boundary** (the funding-commitment rule for a rail whose commit happens
3142
- * off-box).
3143
- *
3144
- * One transaction covers the funding record, the balance, and the invoice's
3145
- * own status flip, so the three can never disagree. A crash anywhere inside
3146
- * leaves the invoice `pending` and the balance untouched: the next
3147
- * `lookup_invoice` observes the same settled payment and credits it, once.
3148
- * A replayed check after the commit sees `settled` and moves nothing.
3149
- *
3150
- * The lock is on the **invoice** row, not the credit row: the first
3151
- * Lightning top-up is what opens the credit, so there is frequently no
3152
- * `credits` row to take `FOR UPDATE` yet. That does not weaken the ledger's
3153
- * concurrency contract — no `credit_draws` write happens here, and `fund`'s
3154
- * upsert is a single statement — while still serialising two settlement
3155
- * checks racing on the same invoice.
2748
+ * Serialize settlement on the invoice row, which exists before the first credit does.
2749
+ * Funding record, balance, invoice status and optional deposit outbox commit together. An
2750
+ * interrupted transaction leaves the prior invoice status intact for retry; a settled replay
2751
+ * moves no money.
3156
2752
  */
3157
2753
  settleInvoice(args: {
3158
2754
  creditId: string;
3159
2755
  fundId: string;
3160
2756
  expiryMs: number;
3161
2757
  /**
3162
- * Rail-value basis, pinned when the invoice was issued. Required
3163
- * by — it is what `fund` writes to `credits.rail`.
2758
+ * Funding rail value pinned when this invoice was issued.
3164
2759
  */
3165
2760
  basis: CreditFundingBasis;
3166
2761
  /**
@@ -3184,39 +2779,20 @@ declare class CreditLedger {
3184
2779
  fundId: string;
3185
2780
  }): Promise<CreditInvoiceRecord | undefined>;
3186
2781
  /**
3187
- * Retire an invoice that **was paid** and can never be credited.
3188
- *
3189
- * Distinct from `expired` because the money is the opposite way round: an
3190
- * expired invoice was never paid and owes nobody anything, whereas a blocked
3191
- * one has sats sitting in the builder's wallet with no ledger row to put them
3192
- * on. Only an operator can resolve that, so the row records `reason` and
3193
- * stops being swept — leaving it `pending` would spend one of the sweep's few
3194
- * slots re-deriving the same verdict on every request the caller makes, and
3195
- * three of them would wedge that caller's sweep so a genuinely payable
3196
- * invoice behind them never credits.
3197
- *
3198
- * A CAS on `pending`, like the expiry retirement: a settlement observed
3199
- * concurrently must win.
2782
+ * Mark a paid invoice blocked when its payment cannot be credited. This removes it from the
2783
+ * bounded pending sweep and leaves its reason for operator repair. The pending-only CAS
2784
+ * cannot overwrite a committed settlement.
3200
2785
  */
3201
2786
  markInvoiceBlocked(args: {
3202
2787
  creditId: string;
3203
2788
  fundId: string;
3204
2789
  reason: string;
3205
2790
  }): Promise<CreditInvoiceRecord | undefined>;
3206
- /** Read one invoice by the wallet's own identifier for the payment (no lock). */
3207
2791
  getInvoiceByPaymentHash(paymentHash: string): Promise<CreditInvoiceRecord | undefined>;
3208
2792
  /**
3209
- * The operator's queue: invoices the sweep gave up on.
3210
- *
3211
- * **Keyset-paginated, not offset.** Blocked rows are terminal and never
3212
- * self-clear, so a row the operator declines to act on sits at the head of
3213
- * the age ordering forever; a fixed first page would starve everything
3214
- * behind it on every run, which is the shape one page up.
3215
- *
3216
- * `includeResolved` widens to the operator-resolved statuses so a run can be
3217
- * audited after the fact.
3218
- * Host-wide, like `listPendingDrains`: the ledger has no `dvm_id`, so on a
3219
- * multi-mount host one builder's admin credential reads every mount's rows.
2793
+ * Host-wide operator queue, keyset-paginated so an untouched blocked row does not starve
2794
+ * later rows. includeResolved also exposes reconciled and written-off history; the ledger
2795
+ * has no per-mount dvm_id.
3220
2796
  */
3221
2797
  listBlockedInvoices(args: {
3222
2798
  limit: number;
@@ -3224,37 +2800,15 @@ declare class CreditLedger {
3224
2800
  includeResolved?: boolean;
3225
2801
  }): Promise<CreditInvoiceRecord[]>;
3226
2802
  /**
3227
- * Apply a blocked invoice's payment to a credit an operator named — the
3228
- * repair for the one Lightning outcome the DVM cannot fix itself.
3229
- *
3230
- * Shaped statement-for-statement on {@link settleInvoice}, because it is the
3231
- * same money doing the same thing a different way: one transaction covering
3232
- * the funding record, the balance, the invoice's status flip and the deposit
3233
- * outbox row, so the four can never disagree. Three deliberate differences:
2803
+ * Apply a blocked or written-off invoice to an operator-named credit. Lock the invoice by
2804
+ * its unique payment_hash before funding the credit, matching settlement’s
2805
+ * invoice-to-credit lock order. Funding, balance, resolution and optional outbox share one
2806
+ * transaction.
3234
2807
  *
3235
- * - **The lock is taken on `payment_hash`**, which is UNIQUE and is the
3236
- * identifier the operator actually holds (it is what the
3237
- * `lightning_settlement_blocked` log carries and what names the payment in
3238
- * their wallet). Invoice row first, credit row second via `fund`'s upsert —
3239
- * the same order `settleInvoice` takes, which is what keeps a reconcile and
3240
- * a concurrent settlement check from deadlocking against each other.
3241
- * - **The funding lands at `(targetCreditId, paymentHash)`, not the invoice's
3242
- * own `(credit_id, fund_id)`.** That key is frequently the reason the row
3243
- * is blocked at all — `funding_replayed_on_cashu` means something else
3244
- * already holds it — and the operator's most natural target is that very
3245
- * credit. The payment hash cannot collide with it, and it makes all three
3246
- * references to this payment agree: `basis.fundingRef`, the deposit's
3247
- * `funding_id`, and the funding row's `fund_id`.
3248
- * - **`written_off` is an accepted input status.** A write-off unwound
3249
- * nothing, so an operator who closed a row by mistake must not need raw SQL
3250
- * against a money table to reopen it.
3251
- *
3252
- * The invoice row's status — never the funding row — is the idempotency
3253
- * source of truth. `fund_id` is caller-chosen and the payment hash is
3254
- * disclosed to them, so a caller *can* squat `(theirCredit, thatHash)` with a
3255
- * top-up on another rail; when they have, `recordFunding` throws
3256
- * `funding_replayed` and that surfaces as its own loud failure rather than
3257
- * being absorbed as "already reconciled".
2808
+ * The repair funding key is (targetCreditId, paymentHash), distinct from the original
2809
+ * caller-chosen fundId. Invoice status determines replay. A caller can still occupy the
2810
+ * repair key with another payment; funding_replayed must remain a refusal, not be absorbed
2811
+ * as already reconciled. Preserve blocked_reason and the wallet’s settlement instant.
3258
2812
  */
3259
2813
  reconcileBlockedInvoice(args: {
3260
2814
  paymentHash: string;
@@ -3272,17 +2826,9 @@ declare class CreditLedger {
3272
2826
  nowMs?: number;
3273
2827
  }): Promise<InvoiceReconciliation>;
3274
2828
  /**
3275
- * Record that an operator reviewed a blocked invoice and chose not to credit
3276
- * it — no ledger effect, purely a queue transition.
3277
- *
3278
- * A CAS on `blocked`, and idempotent: re-running returns the recorded row
3279
- * rather than overwriting the first note. It cannot capture a `reconciled`
3280
- * row, because that one moved money.
3281
- *
3282
- * `written` reports whether *this* call was the transition. The caller has
3283
- * no other way to tell — the row reads identically either way — and the
3284
- * write-off's audit log is the only record of the amount, so a re-emitted
3285
- * one reads as a second decision about the same sats.
2829
+ * Write off an invoice without crediting or moving money. Only the first transition records
2830
+ * the note; its written bit controls the audit event. An operator can later reconcile a
2831
+ * written-off invoice, because write-off did not return its payment.
3286
2832
  */
3287
2833
  writeOffBlockedInvoice(args: {
3288
2834
  paymentHash: string;
@@ -3297,14 +2843,12 @@ declare class CreditLedger {
3297
2843
  getForCaller(callerPubkey: string, opts?: {
3298
2844
  nowMs?: number;
3299
2845
  }): Promise<CreditSnapshot[]>;
3300
- /** Read one draw (no lock), with its credit's rail basis joined in. */
3301
2846
  getDraw(args: {
3302
2847
  creditId: string;
3303
2848
  drawId: string;
3304
2849
  }): Promise<DrawRecord | undefined>;
3305
2850
  /** Read the sole draw bound to a pre-allocated job id; ambiguity fails closed. */
3306
2851
  getDrawByJobId(jobId: string): Promise<DrawRecord | undefined>;
3307
- /** Read every draw linked to a job, including already-resolved rows. */
3308
2852
  listDrawsByJobId(jobId: string): Promise<DrawRecord[]>;
3309
2853
  /**
3310
2854
  * All pending holds on a credit, in `ledger_seq` order (no lock). Read
@@ -3313,22 +2857,10 @@ declare class CreditLedger {
3313
2857
  */
3314
2858
  listPendingDraws(creditId: string): Promise<DrawRecord[]>;
3315
2859
  /**
3316
- * One page of pending holds host-wide placed before `createdBeforeMs`,
3317
- * oldest first (no lock). The orphan sweeper's read surface: a
3318
- * draw commits with its `job_id` before the job row is persisted, so a crash
3319
- * or a fail-closed refusal in that window strands a hold nothing can ever
3320
- * resolve — the only release path keys off the `credit_id`/`draw_id` written
3321
- * on the job row.
3322
- *
3323
- * Host-wide like {@link listPendingDrains}, not per-credit: the sweeper has
3324
- * no candidate credit to start from. Age-bounded so it never sees a draw
3325
- * whose job row is merely still in flight.
3326
- *
3327
- * `limit` is required and `after` keyset-paginates: an aged hold whose job
3328
- * row *does* exist is legitimately un-sweepable and stays `pending`
3329
- * indefinitely, so a caller that re-issues the same first page would never
3330
- * see past a `limit`-sized wall of them. Pass the last row's
3331
- * {@link StalePendingDrawCursor} to advance.
2860
+ * Page host-wide pending holds older than createdBeforeMs by (created_at, credit_id,
2861
+ * ledger_seq). Use the same compound key for ordering and continuation, preserving
2862
+ * same-millisecond rows. Terminal resolution still rechecks state under the parent-credit
2863
+ * lock.
3332
2864
  */
3333
2865
  listStalePendingDraws(args: {
3334
2866
  createdBeforeMs: number;
@@ -3336,38 +2868,18 @@ declare class CreditLedger {
3336
2868
  after?: StalePendingDrawCursor;
3337
2869
  }): Promise<DrawRecord[]>;
3338
2870
  /**
3339
- * Rail-native value the credit an x402 settlement channel funded has
3340
- * actually earned — the sum of its **settled** draws, in the credit's native
3341
- * atomic units (USDC micro on this rail). The batch-settlement claim job's
3342
- * ceiling: a channel is claimable up to what its credit's draws
3343
- * have earned, never up to the deposit that funded it.
3344
- *
3345
- * `undefined` when no credit is bound to the channel, which the claim job
3346
- * treats as "cannot be resolved" and claims nothing for — distinct from a
3347
- * resolved credit that has earned `0`.
3348
- *
3349
- * Deliberately a sum over settled draws rather than `funded − remaining`:
3350
- * a drain lowers `native_remaining` without earning anything, so the
3351
- * subtraction would read the caller's own reclaim as revenue. Pending holds
3352
- * are excluded for the mirror reason — a hold is work in flight, and
3353
- * claiming against it would take money a release still owes back.
2871
+ * Sum settled draws’ native value for the credit bound to this x402 channel.
2872
+ * Pending/released holds and drains earn nothing. Return undefined for an unbound channel,
2873
+ * distinct from earned zero, so settlement cannot guess a claim limit.
3354
2874
  */
3355
2875
  earnedNativeForX402Channel(channelId: string): Promise<number | undefined>;
3356
2876
  /**
3357
- * The credit an x402 settlement channel funded, if one is bound to it — the
3358
- * binding is UNIQUE, so at most one row can answer.
3359
- *
3360
- * The operator repair's entry point: a wedged settlement row carries the
3361
- * channel and an `effect_id`, and this is what turns them back into the
3362
- * credit id, the caller pubkey, and the drain id the effect encodes. Deriving
3363
- * the credit id by splitting `effect_id` instead would be wrong — credit ids
3364
- * carry their own colons (`imp:`, `fnd:` and the rail segment inside them).
2877
+ * Resolve the unique credit bound to the channel. The ID is opaque and can contain colons;
2878
+ * do not reconstruct it by splitting settlement effect IDs.
3365
2879
  */
3366
2880
  getCreditByX402Channel(channelId: string): Promise<CreditSnapshot | undefined>;
3367
- /** Retire every active credit backed by a chain-proven lost Tempo channel. */
3368
2881
  terminalizeTempoCredits(evidence: TempoCreditLossEvidence, tx?: CreditLedgerQuerier): Promise<TempoCreditLoss[]>;
3369
2882
  private terminalizeTempoCreditsLocked;
3370
- /** List terminal Tempo credit losses, newest observation first. */
3371
2883
  listTempoCreditLosses(args?: {
3372
2884
  limit?: number;
3373
2885
  }): Promise<TempoCreditLoss[]>;
@@ -3382,36 +2894,21 @@ declare class CreditLedger {
3382
2894
  */
3383
2895
  terminalizeX402Credit(evidence: X402CreditLossEvidence, tx?: CreditLedgerQuerier): Promise<X402CreditLoss[]>;
3384
2896
  private terminalizeX402CreditLocked;
3385
- /** List terminal x402 credit losses, newest observation first. */
3386
2897
  listX402CreditLosses(args?: {
3387
2898
  limit?: number;
3388
2899
  }): Promise<X402CreditLoss[]>;
3389
2900
  /**
3390
- * Debit the caller's entire available balance into a drain liability
3391
- * (the caller-ownership contract: expiry ends spending, never ownership — this is the
3392
- * builder-honored reclaim floor). Runs under the credit row lock: the
3393
- * amount is `balance − pending holds` read under `FOR UPDATE`, the balance
3394
- * is decremented in the same transaction, and the drain takes the next
3395
- * gap-free `ledger_seq` — so the balance zeroes exactly once however many
3396
- * machines race the request.
3397
- *
3398
- * Idempotent on `(creditId, drainId)`: a replay returns the recorded drain
3399
- * (this is also the poll/pickup read). A replay whose `method` or `payout`
3400
- * differs from the recorded drain is refused with `drain_conflict` —
3401
- * silently honouring a changed payout would ship the caller's money to a
3402
- * destination they never signed alongside this `drain_id`.
2901
+ * Move all available balance to a drain liability under the credit lock; pending holds stay
2902
+ * reserved. Expiry alone does not prevent reclaim before a sweep releases the balance.
2903
+ * Repeating (creditId, drainId) returns the recorded liability; owner, method and payout
2904
+ * conflicts refuse.
3403
2905
  *
3404
- * Pending holds are NOT drained — in-flight jobs keep their money until
3405
- * they settle or release. A release after a drain restores available
3406
- * balance the caller reclaims with a **new** `drain_id`; drains are ledger
3407
- * arithmetic, not a terminal credit state, so `status` stays `active`.
3408
- *
3409
- * Deliberately **no expiry check**: draining works before and after
3410
- * expiry. The one thing expiry gates is new draws.
2906
+ * An unresolved channel refund blocks fresh drain IDs but admits its own effect ID for
2907
+ * repair. Caller-supplied payout validation belongs to the route. With tx, join the channel
2908
+ * transaction without controlling it.
3411
2909
  */
3412
2910
  requestDrain(args: {
3413
2911
  creditId: string;
3414
- /** Client-generated drain id from the signed request body. */
3415
2912
  drainId: string;
3416
2913
  /** Verified caller pubkey — a drain may only be requested by the owner. */
3417
2914
  callerPubkey: string;
@@ -3424,7 +2921,6 @@ declare class CreditLedger {
3424
2921
  tx?: CreditLedgerQuerier;
3425
2922
  nowMs?: number;
3426
2923
  }): Promise<DrainRequestResult>;
3427
- /** Read one drain (no lock), joined with its credit for currency + owner. */
3428
2924
  getDrain(args: {
3429
2925
  creditId: string;
3430
2926
  drainId: string;
@@ -3436,25 +2932,11 @@ declare class CreditLedger {
3436
2932
  */
3437
2933
  listFundingLots(creditId: string): Promise<FundingLot[]>;
3438
2934
  /**
3439
- * What this DVM owes back in satoshis if every open non-channel Bitcoin
3440
- * credit reclaimed right now — the deposit half of the hub
3441
- * balance, and the floor a payout sweep must never go below.
3442
- *
3443
- * Read entirely off the funding lots, at their own funding rates, with no
3444
- * exchange-rate lookup anywhere: the obligation is in kind, so a rate has
3445
- * nothing to say about it. Pending job holds are deliberately **not**
3446
- * subtracted — a hold either settles into revenue or releases back to the
3447
- * caller, and until it does, treating it as already earned would understate
3448
- * the floor. The figure is conservative by exactly that amount.
3449
- *
3450
- * The two coverage holes ride along rather than being folded in silently, so
3451
- * a gap reads as a gap: `uncovered` is a credit whose lots fall short of its
3452
- * balance, `unbacked` is one holding any open micro with no sats basis. A
3453
- * reclaim on either prices the **whole** balance off a live rate — the
3454
- * depletion refuses a mixture rather than blending two bases — so both are
3455
- * reported as the whole balance, and neither contributes to `depositSats`.
3456
- * A figure that counted the backed half of a mixed credit would be a floor
3457
- * the payout never matches.
2935
+ * Host-wide in-kind Bitcoin deposit liability, excluding channel collateral. Credits with
2936
+ * missing coverage or any basisless lot are reported as gaps and use the live-rate fallback
2937
+ * rather than a guessed sats obligation. Group fiat micro by currency; never sum
2938
+ * denominations.
2939
+ * Pending job holds remain liabilities until settled because they may release to the caller.
3458
2940
  */
3459
2941
  bitcoinDepositLiability(): Promise<BitcoinDepositLiability>;
3460
2942
  /**
@@ -3467,40 +2949,15 @@ declare class CreditLedger {
3467
2949
  */
3468
2950
  listPendingDrains(): Promise<CreditDrainRecord[]>;
3469
2951
  /**
3470
- * One page of still-`pending` drains on a channel-backed credit, oldest
3471
- * first (no lock) — the operator's repair queue.
3472
- *
3473
- * Keyset-paginated for the x402 settlement queue's reason: a wedged row
3474
- * never self-clears, so a caller that re-issues the same first page would
3475
- * never see past a `limit`-sized wall of them. The keyset is
3476
- * `(created_at, credit_id, drain_id)` — `created_at` is immutable, so no
3477
- * concurrent write can push an unexamined row past the cursor. It is a
3478
- * BIGINT of epoch milliseconds rather than a `TIMESTAMPTZ`, so the
3479
- * microsecond-vs-millisecond cursor trap that bit `listSettlements` cannot
3480
- * arise here: the value round-trips as the integer it already is.
3481
- *
3482
- * `createdBeforeMs` is the staleness floor. A healthy cooperative close
3483
- * moves the drain to `sent` inside the request that opened it, so anything
3484
- * younger is in flight rather than stuck.
3485
- *
3486
- * The method filter is `'tempo'`, and it is the one line here worth a second
3487
- * look: `init()` migrates the legacy `'mpp'` spelling away and
3488
- * `DrainMethod` no longer carries it, so a query naming it matches nothing a
3489
- * DVM has ever written. It named it anyway until rename sweep and
3490
- * live rung caught it independently — the whole operator queue
3491
- * read empty in production while every route test stayed green, because
3492
- * those run on `MemoryCreditLedger`, which filters on the typed value. The
3493
- * two implementations of this one predicate must be read together.
3494
- *
3495
- * A written-off row is excluded unless `includeResolved`: the operator has
3496
- * already decided about it, and leaving it on the live queue would re-serve
3497
- * a closed decision on every walk.
2952
+ * Page pending Tempo-channel drains by (created_at, credit_id, drain_id), optionally
2953
+ * age-bounded, excluding written-off rows. Accept the legacy mpp stored method as well as
2954
+ * tempo so pre-migration drains remain repairable. Use the compound key for both ordering
2955
+ * and continuation.
3498
2956
  */
3499
2957
  listChannelDrains(args: {
3500
2958
  limit: number;
3501
2959
  after?: ChannelDrainCursor;
3502
2960
  createdBeforeMs: number;
3503
- /** Which channel binding to walk. Today only Tempo has a repair. */
3504
2961
  rail: "tempo";
3505
2962
  includeResolved?: boolean;
3506
2963
  }): Promise<CreditDrainRecord[]>;
@@ -3606,11 +3063,9 @@ declare class CreditLedger {
3606
3063
  nowMs?: number;
3607
3064
  }): Promise<DrainTransitionResult>;
3608
3065
  /**
3609
- * Append a countersigned reclaim-event receipt to the drain's evidence
3610
- * trail. A single-statement JSONB append — receipts are emitted after the
3611
- * state transition they attest, and losing one never loses money (the
3612
- * transition is the source of truth; the receipt is the proof the caller
3613
- * can carry away).
3066
+ * Append an opaque supplied receipt observation to the drain’s JSONB evidence array.
3067
+ * Issuance and authentication belong to callers/consumers; this store does not verify
3068
+ * signatures. Preserve append order on replay/poll reads.
3614
3069
  */
3615
3070
  appendDrainReceipt(args: {
3616
3071
  creditId: string;
@@ -3619,31 +3074,13 @@ declare class CreditLedger {
3619
3074
  tx?: CreditLedgerQuerier;
3620
3075
  }): Promise<void>;
3621
3076
  private readDrain;
3622
- /** Re-read the credit join fields for a freshly-updated drain row. */
3623
3077
  private rejoin;
3624
- /** Shared refusal tail for the CAS transitions: idempotent replay or typed error. */
3625
3078
  private refuseTransition;
3626
3079
  /**
3627
- * Price a reclaim in kind and say which lots it takes.
3628
- *
3629
- * The debits are returned whether or not the reclaim can be priced, and the
3630
- * caller applies them either way: the money is leaving the credit, so the
3631
- * lots have to shrink with it or the deposit-liability rollup would keep
3632
- * counting a balance that is gone.
3633
- *
3634
- * `owedSats` is `null` where {@link isInKindDepletion} refuses — the lots
3635
- * are short of the balance, or the covering lots carry no sats basis (a
3636
- * legacy credit whose funding recorded no sats basis. That row is priced off a live rate by the
3637
- * admin surface instead, which is what *every* row did before this change,
3638
- * so the fallback is the old behaviour rather than a new failure mode.
3639
- *
3640
- * What it publishes is **net of the delivery reserve**: handing a
3641
- * refund over costs a mint fee and, when the notes have to be minted just in
3642
- * time, a Lightning hop. The caller carries that cost per and a
3643
- * flat reserve is how they carry it without the figure moving — an
3644
- * actual-fee true-up could only be applied after the promise was made. The
3645
- * debits stay gross because they are denominated in micro and are what
3646
- * `releaseDrain` restores; only the sats figure is netted.
3080
+ * Deplete FIFO lots even when they cannot price the reclaim; stale open lots must not
3081
+ * remain behind a debited balance. Publish owedSats only with complete positive sats
3082
+ * coverage, net of one delivery reserve. Otherwise preserve null for a live-rate fallback.
3083
+ * Returned debits allow an unpaid drain release to restore the exact lots.
3647
3084
  */
3648
3085
  private reclaimInKind;
3649
3086
  /**
@@ -3663,7 +3100,6 @@ declare class CreditLedger {
3663
3100
  * as every other balance move is.
3664
3101
  */
3665
3102
  private applyLotDebits;
3666
- /** Shared settle/release state machine — see the public JSDoc on each verb. */
3667
3103
  private resolveDraw;
3668
3104
  private resolveDrawLocked;
3669
3105
  /**
@@ -3672,7 +3108,6 @@ declare class CreditLedger {
3672
3108
  * success (harmless for read-only replay paths), rolls back on throw.
3673
3109
  */
3674
3110
  private withCreditLock;
3675
- /** `SELECT … FOR UPDATE` the credit row on `q` (which must be inside a transaction). */
3676
3111
  private lockCreditRow;
3677
3112
  private readDraw;
3678
3113
  /**
@@ -3687,13 +3122,9 @@ declare class CreditLedger {
3687
3122
  private readX402CreditLosses;
3688
3123
  }
3689
3124
  /**
3690
- * The ledger surface the SDK server codes against. Implemented by
3691
- * the Postgres {@link CreditLedger} (production — spec condition 2 requires
3692
- * the DVM's own Postgres) and by `MemoryCreditLedger` (devMode / in-process
3693
- * tests / pool-less `createDVMServer` setups, mirroring the `MemoryJobStore`
3694
- * precedent). The `tx` handles on `fund`/`draw` are meaningful only on the
3695
- * Postgres implementation; the memory ledger ignores them — its operations
3696
- * are synchronous-atomic in-process, so there is no rail transaction to join.
3125
+ * Implemented by durable Postgres and per-process memory ledgers. Production funding menus
3126
+ * require durable storage; memory is for explicit development and tests. Mutations accepting
3127
+ * tx join the caller’s transaction; memory implementations ignore that handle.
3697
3128
  */
3698
3129
  interface CreditLedgerLike {
3699
3130
  /**
@@ -3882,9 +3313,7 @@ interface CreditLedgerLike {
3882
3313
  creditId: string;
3883
3314
  drawId: string;
3884
3315
  }): Promise<DrawRecord | undefined>;
3885
- /** Read the sole draw linked to a stable pre-payment job id. */
3886
3316
  getDrawByJobId(jobId: string): Promise<DrawRecord | undefined>;
3887
- /** Read every draw linked to a job so retention can resolve all holds before redaction. */
3888
3317
  listDrawsByJobId(jobId: string): Promise<DrawRecord[]>;
3889
3318
  listPendingDraws(creditId: string): Promise<DrawRecord[]>;
3890
3319
  listStalePendingDraws(args: {
@@ -3919,7 +3348,6 @@ interface CreditLedgerLike {
3919
3348
  listPendingDrains(): Promise<CreditDrainRecord[]>;
3920
3349
  /** See {@link CreditLedger.bitcoinDepositLiability} — the in-kind deposit floor. */
3921
3350
  bitcoinDepositLiability(): Promise<BitcoinDepositLiability>;
3922
- /** One credit's undepleted funding lots, oldest first. */
3923
3351
  listFundingLots(creditId: string): Promise<FundingLot[]>;
3924
3352
  listChannelDrains(args: {
3925
3353
  limit: number;
@@ -3981,14 +3409,12 @@ interface CreditLedgerLike {
3981
3409
  interface CreditLedgerQuerier {
3982
3410
  query: Pool["query"];
3983
3411
  }
3984
- /** Reporter outbox joined to a draw release's own transaction. */
3985
3412
  interface CreditDrawReleaseOutbox {
3986
3413
  /** Platform DVM record ID bound into the report. */
3987
3414
  dvmId: string;
3988
3415
  /** Reporter-owned insert executed through the ledger transaction. */
3989
3416
  enqueue: CreditDrawReleaseEnqueue;
3990
3417
  }
3991
- /** Reporter seam for credit-expiry releases and the revivals that reverse them. */
3992
3418
  interface CreditExpiryReleaseOutbox {
3993
3419
  /** Platform DVM record ID bound into the report. */
3994
3420
  dvmId: string;
@@ -3996,16 +3422,15 @@ interface CreditExpiryReleaseOutbox {
3996
3422
  enqueue: CreditExpiryReleaseEnqueue;
3997
3423
  }
3998
3424
  /**
3999
- * One recorded expiry episode: a credit's undrawn remainder passing to the
4000
- * builder when its TTL ran out.
4001
- *
4002
- * `reversedAt` is set when a later funding revived the credit, at which point
4003
- * the release stops counting against committed value — the row stays as the
4004
- * record that it once did.
3425
+ * One committed expiry episode. The sweep releases the undrawn remainder; later funding sets
3426
+ * reversedAt and restores that release on the same transaction. Multiple episodes may share
3427
+ * an expiry instant.
4005
3428
  */
4006
3429
  interface CreditExpiryRelease {
4007
3430
  creditId: string;
4008
- /** Per-expiry-episode key, derived from {@link CreditExpiryRelease.expiryMs}. */
3431
+ /**
3432
+ * Key derived from expiry instant and the release sequence counted under the credit lock.
3433
+ */
4009
3434
  releaseId: string;
4010
3435
  callerPubkey: string;
4011
3436
  currency: string;
@@ -4013,31 +3438,29 @@ interface CreditExpiryRelease {
4013
3438
  rail: string | null;
4014
3439
  /** Fiat micro released, 1e-6 of `currency`. */
4015
3440
  amountMicro: number;
4016
- /** When the credit's TTL ran out — when the liability ended. */
3441
+ /**
3442
+ * Nominal credit expiry, ms since epoch; the claim is released only when the sweep commits.
3443
+ */
4017
3444
  expiryMs: number;
4018
- /** When the sweep observed the expiry. */
3445
+ /**
3446
+ * When the sweep recorded this release, ms since epoch.
3447
+ */
4019
3448
  releasedAt: number;
4020
3449
  /** When a funding revived the credit and undid this release. */
4021
3450
  reversedAt: number | null;
4022
3451
  }
4023
- /** Pool-shaped handle (`query` + `connect`). Required for the locked write paths. */
4024
3452
  type CreditLedgerPool = Pick<Pool, "query" | "connect">;
4025
3453
  /**
4026
- * Credit lifecycle status. `unbacked` is the terminal audit state for a Tempo
4027
- * channel that finalized below its accepted voucher; its nominal balance is
4028
- * preserved, but it can never be drawn, funded, grown, or drained.
3454
+ * unbacked credit is unavailable for spending or reclaim while channel collateral is gone. An
3455
+ * x402 credit with no remaining liability can be restored to active by channel
3456
+ * reconciliation.
4029
3457
  */
4030
3458
  type CreditStatus = "active" | "unbacked";
4031
3459
  /** Draw lifecycle: two-phase hold → terminal settle/release. */
4032
3460
  type DrawStatus = "pending" | "settled" | "released";
4033
3461
  /**
4034
- * Rails a drain can pay out on (the reclaim contract).
4035
- *
4036
- * Direct Lightning payout is deliberately absent: a
4037
- * first-party deployment holds no send-capable Lightning credential by design
4038
- * (the funding NWC is receive-only), so P2PK-locked Cashu notes are
4039
- * the only non-channel Bitcoin reclaim path. Adding `lightning` back means
4040
- * re-deciding that, not just widening a union.
3462
+ * Drain payout rails. Direct Lightning send is excluded: deployments reclaiming Bitcoin can
3463
+ * use Cashu notes without putting a spending wallet credential in the DVM.
4041
3464
  */
4042
3465
  type DrainMethod = "cashu" | "x402" | "tempo";
4043
3466
  /**
@@ -4076,12 +3499,20 @@ interface BitcoinDepositLiability {
4076
3499
  }[];
4077
3500
  /** Credits holding balance no funding lot backs — a defect, not a liability figure. */
4078
3501
  uncoveredCredits: number;
4079
- /** Their whole balance: a short-covered credit reclaims off a live rate entirely. */
4080
- uncoveredMicro: number;
3502
+ /** Whole balances of short-covered credits, by ledger currency; reclaim uses a live rate. */
3503
+ uncoveredByCurrency: {
3504
+ currency: string;
3505
+ micro: number;
3506
+ credits: number;
3507
+ }[];
4081
3508
  /** Credits holding any open micro with no sats basis, so their reclaim prices off a live rate. */
4082
3509
  unbackedCredits: number;
4083
- /** Their whole balance, for the same all-or-nothing reason. */
4084
- unbackedMicro: number;
3510
+ /** Whole balances of basisless credits, by ledger currency. */
3511
+ unbackedByCurrency: {
3512
+ currency: string;
3513
+ micro: number;
3514
+ credits: number;
3515
+ }[];
4085
3516
  }
4086
3517
  /**
4087
3518
  * What it cost to put a just-in-time-minted refund in the caller's hands
@@ -4119,16 +3550,9 @@ interface CreditDrainRecord {
4119
3550
  currency: string;
4120
3551
  method: DrainMethod;
4121
3552
  /**
4122
- * The channel this drain's credit is bound to (Tempo session or x402
4123
- * settlement channel), `null` on a one-payment or Cashu/Lightning credit.
4124
- *
4125
- * The method alone does not say who settles a reclaim: an `mpp` or `x402`
4126
- * drain on a channel-bound credit is closed by the channel itself, and a
4127
- * `pending` one is routinely mid-reconciliation rather than owed by anybody
4128
- * `completeTempoDrain` leaves every ambiguous close pending on purpose for
4129
- * `recoverTempoSessionClose` to finish on the caller's next poll. Carrying
4130
- * the binding on the record is what lets a reader tell that apart from a
4131
- * one-payment reclaim only a person can settle.
3553
+ * Reusable Tempo or x402 channel backing this drain, or null for one-payment/non-channel
3554
+ * credit. The channel’s cooperative lifecycle owns fulfillment; the operator’s manual sent
3555
+ * route refuses channel-bound drains.
4132
3556
  */
4133
3557
  channelId: string | null;
4134
3558
  /** Method-specific payout target as signed by the caller. */
@@ -4143,12 +3567,11 @@ interface CreditDrainRecord {
4143
3567
  */
4144
3568
  drainedNative: number | null;
4145
3569
  /**
4146
- * What this reclaim owes in satoshis, computed from the credit's funding
4147
- * lots when it was requested and never re-priced: the deposited
4148
- * sats, pro rata to the share of the credit being reclaimed, floored per
4149
- * lot. `null` on a channel rail (in-kind structurally, through the escrow)
4150
- * and on a Bitcoin credit whose lots cannot price it — that one falls back
4151
- * to a live rate on the admin surface.
3570
+ * In-kind sats obligation fixed at request time: the decrease in FIFO lots’ remaining
3571
+ * floored obligations, net of one delivery reserve. A non-null obligation is not repriced.
3572
+ * Null on unpriceable non-channel Bitcoin refunds requires the live-rate Cashu fallback.
3573
+ * Channel and stablecoin refunds also use null: they have no sats obligation and follow
3574
+ * their native/channel payout lifecycle, using `drainedNative` where applicable.
4152
3575
  */
4153
3576
  owedSats: number | null;
4154
3577
  /**
@@ -4174,7 +3597,10 @@ interface CreditDrainRecord {
4174
3597
  * already holding.
4175
3598
  */
4176
3599
  fulfilment: DrainFulfilment | null;
4177
- /** Countersigned reclaim-event receipts, in emission order. */
3600
+ /**
3601
+ * Opaque supplied reclaim-event observations in append order; consumers shape-check and
3602
+ * authenticate them.
3603
+ */
4178
3604
  receipts: Record<string, unknown>[];
4179
3605
  createdAt: number;
4180
3606
  parkedAt: number | null;
@@ -4245,9 +3671,8 @@ interface DrainRequestResult {
4245
3671
  replayed: boolean;
4246
3672
  }
4247
3673
  /**
4248
- * Read-model of one credit. `availableMicro` = `balanceMicro` − pending
4249
- * holds; `expired` is computed from `expiryMs` at read time (expiry ends
4250
- * spending, never ownership — the balance stays readable).
3674
+ * Read model with funded balance and pending holds. Expiry is computed at read time and does
3675
+ * not erase the record; unbacked credit has no available balance.
4251
3676
  */
4252
3677
  interface CreditSnapshot {
4253
3678
  creditId: string;
@@ -4255,7 +3680,9 @@ interface CreditSnapshot {
4255
3680
  currency: string;
4256
3681
  /** Funded balance, 1e-6 currency units. Pending holds are NOT subtracted. */
4257
3682
  balanceMicro: number;
4258
- /** Spendable balance: `balanceMicro` minus pending holds. */
3683
+ /**
3684
+ * Balance minus pending holds, or zero while backing is unbacked.
3685
+ */
4259
3686
  availableMicro: number;
4260
3687
  /**
4261
3688
  * `availableMicro` in the funding rail's own units — the ceiling any
@@ -4266,7 +3693,9 @@ interface CreditSnapshot {
4266
3693
  expiryMs: number;
4267
3694
  expired: boolean;
4268
3695
  status: CreditStatus;
4269
- /** High-water `ledger_seq` — the seq of the most recent draw. */
3696
+ /**
3697
+ * High-water sequence shared by draw placements and drains.
3698
+ */
4270
3699
  lastLedgerSeq: number;
4271
3700
  createdAt: number;
4272
3701
  /**
@@ -4281,19 +3710,17 @@ interface CreditSnapshot {
4281
3710
  /** Reusable x402 channel bound to this credit, if one funded it. */
4282
3711
  x402ChannelId: string | null;
4283
3712
  }
4284
- /**
4285
- * Result of a draw — carries the receipt tuple the countersignature
4286
- * consumes (`ReceiptCredit`: `draw_id`, `amount`, `balance_after`,
4287
- * `ledger_seq`). `balanceAfterMicro` and `ledgerSeq` are the values recorded
4288
- * when the draw was first placed; a replay returns them unchanged.
4289
- */
4290
3713
  interface DrawResult {
4291
3714
  creditId: string;
4292
3715
  drawId: string;
4293
3716
  amountMicro: number;
4294
- /** Available balance immediately after this draw's hold, as first recorded. */
3717
+ /**
3718
+ * Recorded balance after placement or latest growth, at the draw’s original sequence.
3719
+ */
4295
3720
  balanceAfterMicro: number;
4296
- /** Per-credit monotonic, gap-free draw counter. */
3721
+ /**
3722
+ * Per-credit gap-free sequence shared with drains; growth keeps the original value.
3723
+ */
4297
3724
  ledgerSeq: number;
4298
3725
  status: DrawStatus;
4299
3726
  /** True when this call was an idempotent replay of an already-recorded state. */
@@ -4304,14 +3731,9 @@ interface DrawResult {
4304
3731
  resolvedAt: number | null;
4305
3732
  }
4306
3733
  /**
4307
- * The rail-native slice of a credit that one draw carries — what
4308
- * makes a settled draw bookable as a revenue event on the funding's rail.
4309
- *
4310
- * Allocated at draw time as `remaining × amountMicro / availableMicro`, where
4311
- * both remainders are net of pending holds. The draw that consumes a credit's
4312
- * last micro therefore takes ratio 1 and the exact remainder, so the draws of
4313
- * a credit sum to precisely the value funded — no rounding residue, and no
4314
- * last-draw special case.
3734
+ * A draw’s rail value and funding identity. Placement records a pooled forecast; settling
3735
+ * non-channel Bitcoin can replace it with FIFO lot pricing while retaining the original as
3736
+ * allocatedMsats. Native null means no native amount was reported, distinct from zero.
4315
3737
  */
4316
3738
  interface DrawRailValue {
4317
3739
  /** Millisatoshis of rail value allocated to this draw. */
@@ -4364,7 +3786,6 @@ interface DrawResolution extends DrawResult {
4364
3786
  /** Settlement instant — the timestamp a draw-keyed revenue event is dated by. */
4365
3787
  resolvedAt: number;
4366
3788
  }
4367
- /** Full persisted draw row, camel-cased. */
4368
3789
  interface DrawRecord extends Omit<DrawResult, "replayed"> {
4369
3790
  jobId: string | null;
4370
3791
  createdAt: number;
@@ -4434,9 +3855,11 @@ interface CreditFundingBasis {
4434
3855
  maxClaimableAmount: string;
4435
3856
  };
4436
3857
  }
4437
- /** What {@link CreditLedger.fund} takes. Named so the private body can share the shape. */
4438
3858
  interface CreditFundArgs {
4439
- /** Omit to mint a fresh credit id; provide to top up (or idempotently create). */
3859
+ /**
3860
+ * Omit to mint a credit ID; provide to add funding to that credit. Replay prevention is the
3861
+ * caller’s responsibility.
3862
+ */
4440
3863
  creditId?: string;
4441
3864
  callerPubkey: string;
4442
3865
  /** The DVM's pricing currency; `amountMicro` is 1e-6 of it. */
@@ -4444,9 +3867,13 @@ interface CreditFundArgs {
4444
3867
  amountMicro: number;
4445
3868
  /** Absolute expiry, ms since epoch. Overwrites the previous value on top-up. */
4446
3869
  expiryMs: number;
4447
- /** Rail-value basis this funding contributes (required by ). */
3870
+ /**
3871
+ * Rail value contributed by this funding; required on writes.
3872
+ */
4448
3873
  basis: CreditFundingBasis;
4449
- /** External transaction handle — caller owns BEGIN/COMMIT (spec condition 3). */
3874
+ /**
3875
+ * External transaction handle; caller owns BEGIN, COMMIT and ROLLBACK.
3876
+ */
4450
3877
  tx?: CreditLedgerQuerier;
4451
3878
  nowMs?: number;
4452
3879
  }
@@ -4466,7 +3893,6 @@ declare function allocateDrawValue(args: {
4466
3893
  drawMsats: number;
4467
3894
  drawNative: number | null;
4468
3895
  };
4469
- /** Full persisted `credit_fundings` row, camel-cased. */
4470
3896
  interface FundingRecord {
4471
3897
  creditId: string;
4472
3898
  fundId: string;
@@ -4498,7 +3924,6 @@ interface FundingRecord {
4498
3924
  * human had to intervene.
4499
3925
  */
4500
3926
  type CreditInvoiceStatus = "pending" | "settled" | "expired" | "blocked" | "reconciled" | "written_off";
4501
- /** Full persisted `credit_invoices` row, camel-cased. */
4502
3927
  interface CreditInvoiceRecord {
4503
3928
  creditId: string;
4504
3929
  fundId: string;
@@ -4531,7 +3956,6 @@ interface CreditInvoiceRecord {
4531
3956
  /** When the payment landed at the wallet — never the instant an operator acted on it. */
4532
3957
  settledAt: number | null;
4533
3958
  }
4534
- /** Outcome of applying an observed settlement to the ledger. */
4535
3959
  interface InvoiceSettlement {
4536
3960
  invoice: CreditInvoiceRecord;
4537
3961
  /** The `credit_fundings` row; absent only if a legacy row somehow lacks one. */
@@ -4541,10 +3965,8 @@ interface InvoiceSettlement {
4541
3965
  replayed: boolean;
4542
3966
  }
4543
3967
  /**
4544
- * Outcome of an operator crediting a blocked invoice onto a credit they named;
4545
- * it has the same shape as {@link InvoiceSettlement}, but `funding` and
4546
- * `credit` describe the **target** credit, not the one the invoice was minted
4547
- * against.
3968
+ * Reconciliation keeps the original invoice identity; funding and credit describe the
3969
+ * operator-selected target credit, which may differ from the invoice’s original credit.
4548
3970
  */
4549
3971
  interface InvoiceReconciliation {
4550
3972
  invoice: CreditInvoiceRecord;
@@ -4553,7 +3975,6 @@ interface InvoiceReconciliation {
4553
3975
  /** True when this reconcile had already been applied — nothing moved. */
4554
3976
  replayed: boolean;
4555
3977
  }
4556
- /** Outcome of `writeOffBlockedInvoice` — the recorded row, plus whether this call wrote it. */
4557
3978
  interface InvoiceWriteOff {
4558
3979
  invoice: CreditInvoiceRecord;
4559
3980
  /** False when the invoice was already written off, so nothing changed. */
@@ -4565,31 +3986,13 @@ interface BlockedInvoiceCursor {
4565
3986
  creditId: string;
4566
3987
  fundId: string;
4567
3988
  }
4568
- /** Typed failure codes for {@link CreditLedgerError}. */
4569
3989
  type CreditLedgerErrorCode = "credit_not_found" | "credit_expired" | "credit_unbacked" | "tempo_channel_closing" | "tempo_settlement_pending" | "settlement_pending" | "insufficient_credit" | "draw_conflict" | "draw_not_found" | "invalid_draw_state" | "funding_replayed" | "currency_mismatch" | "caller_mismatch" | "rail_mismatch" | "invalid_amount" | "invalid_expiry" | "invalid_basis" | "nothing_to_drain" | "drain_below_dust" | "drain_not_found" | "drain_conflict" | "invalid_drain_state" | "invoice_not_found" | "invoice_conflict" | "invoice_not_blocked" | "below_rail_minimum";
4570
3990
  /**
4571
- * Which conflict a `drain_conflict` is, for the routes that answer one.
4572
- *
4573
- * One code covers refusals whose only common property is that no drain was
4574
- * booked; the copy a caller relays to a human differs entirely between them.
4575
- * A tag rather than a message substring because that is what the copy used to
4576
- * key on, and both of the x402 commit leg's conflicts missed it by a word —
4577
- * silently, since the fallback is a well-formed answer about a different
4578
- * refusal.
4579
- *
4580
- * The three are not one shape with three causes. `pending_draws` and
4581
- * `balance_moved` roll back with nothing on any chain and clear on their own;
4582
- * `refund_floor` is the opposite — the cooperative refund reached the chain and
4583
- * paid short, so the caller's money moved and only the operator repair resolves
4584
- * it. A route's `retryable` and `drain_registered` follow from which
4585
- * one it is, so the tag has to reach it.
4586
- *
4587
- * Absent means the duplicate-payout conflict `PAYMENT_ERROR_COPY.drain_conflict`
4588
- * was written for, which is also the conservative default for a conflict nobody
4589
- * has classified yet.
3991
+ * Structured drain_conflict reasons select caller instructions without matching prose. A
3992
+ * refund_floor shortfall needs operator resolution, unlike duplicate payout; foreign_credit
3993
+ * discloses no liability to the wrong owner; drain_id_retired requires a fresh ID.
4590
3994
  */
4591
3995
  type DrainConflictReason = "pending_draws" | "balance_moved" | "refund_floor";
4592
- /** Structured context on a {@link CreditLedgerError}, per code. */
4593
3996
  interface CreditLedgerErrorDetails {
4594
3997
  /** On `drain_conflict`: which conflict it is, so a route can answer it without matching on prose. */
4595
3998
  drainConflict?: DrainConflictReason;
@@ -4689,7 +4092,8 @@ interface TempoSessionLifecycle {
4689
4092
  *
4690
4093
  * Every mutation takes an advisory transaction lock keyed by the channel
4691
4094
  * before locking its row with `FOR UPDATE`, applies mppx's synchronous
4692
- * transition exactly once, and commits before returning. The advisory lock
4095
+ * transition and commits before returning. Transaction recovery may retry a callback
4096
+ * attempt; a rolled-back attempt is not a second committed transition. The advisory lock
4693
4097
  * covers a channel's first write, when there is no row for `FOR UPDATE` to
4694
4098
  * lock. Together these are the cross-machine linearization point for voucher,
4695
4099
  * top-up, spend, and close state; mppx's process-local fallback is
@@ -4717,13 +4121,10 @@ declare class PostgresTempoSessionStore implements Store.AtomicStore {
4717
4121
  private readonly pool;
4718
4122
  private readonly acceptance;
4719
4123
  constructor(pool: Pool);
4720
- /** Create the channel table under the SDK-wide migration lock. */
4721
4124
  init(): Promise<void>;
4722
- /** Read one channel snapshot. */
4723
4125
  get(key: string): Promise<unknown>;
4724
4126
  /** Replace one channel snapshot. Used by mppx only for non-RMW maintenance. */
4725
4127
  put(key: string, value: unknown): Promise<void>;
4726
- /** Delete one channel snapshot. */
4727
4128
  delete(key: string): Promise<void>;
4728
4129
  /** Atomic read-modify-write using a row lock shared by every SDK replica. */
4729
4130
  update<result>(key: string, fn: (current: unknown) => Store.Change<unknown, result>): Promise<result>;
@@ -5103,7 +4504,6 @@ declare const _testing: {
5103
4504
  attachTempoSessionRuntime: typeof attachTempoSessionRuntime;
5104
4505
  };
5105
4506
 
5106
- /** Envelope fields the verifier validates around the signed payload. */
5107
4507
  interface CanonicalEnvelope {
5108
4508
  pubkey: string;
5109
4509
  signature: string;
@@ -5115,21 +4515,20 @@ interface CreateSignedRequestVerifierOpts {
5115
4515
  /** Fixed audience/operation for a custom signed route or standalone verifier. */
5116
4516
  domain?: SignedRequestDomain;
5117
4517
  /**
5118
- * Permitted clock drift in seconds. Defaults to 300 (5 minutes — matches
5119
- * cast's `SIGNATURE_DRIFT_SECONDS`).
4518
+ * Permitted clock drift in seconds; default 300.
5120
4519
  */
5121
4520
  driftSeconds?: number;
5122
4521
  /**
5123
- * Field names to strip from the parsed input when computing the canonical
5124
- * bytes the signature covers. Defaults to `["signature"]`. The signature
5125
- * itself is always stripped (callers don't need to repeat it).
4522
+ * Fields stripped from raw input when forming signing bytes; default signature,
4523
+ * which is always stripped even when additional fields are configured.
5126
4524
  */
5127
4525
  envelopeFields?: string[];
5128
4526
  /**
5129
4527
  * Field names removed from the input before `schema` validates it — and
5130
4528
  * **kept** in the canonical bytes the signature covers. For protocol-reserved
5131
4529
  * keys the caller signs but the schema never declares: the credit
5132
- * envelope riding a capability schema. Defaults to none.
4530
+ * envelope riding a capability schema. No additional fields are ignored by default;
4531
+ * auth_statement is always excluded from schema validation.
5133
4532
  *
5134
4533
  * This is the one place where "what is validated" and "what is signed"
5135
4534
  * legitimately differ. Don't reach for it to paper over a schema mismatch —
@@ -5144,7 +4543,6 @@ interface CreateSignedRequestVerifierOpts {
5144
4543
  /** Current Unix-seconds source. Defaults to `() => Math.floor(Date.now() / 1000)`. */
5145
4544
  now?: () => number;
5146
4545
  }
5147
- /** Minimal replay-store contract — same shape as cast's `ReplayStore`. */
5148
4546
  interface SignedRequestReplayStore {
5149
4547
  /**
5150
4548
  * True iff (pubkey, timestamp, nonce) was already seen within the retention
@@ -5161,20 +4559,9 @@ interface SignedRequestSignOpts {
5161
4559
  nonce?: string;
5162
4560
  }
5163
4561
  /**
5164
- * Round-trip verifier + signer for secp256k1+BIP-340 Schnorr-signed protocol requests.
5165
- *
5166
- * Authentication is two-step so the replay nonce is committed iff a job is
5167
- * created, not on every request entry:
5168
- *
5169
- * 1. {@link checkAuth} runs schema → drift → signature at request entry on
5170
- * both `/v1/quote` and `/v1/job`. Sync; no replay-store touch.
5171
- * 2. {@link recordReplay} commits `(pubkey, timestamp, nonce)` to the replay
5172
- * store. Called from `/v1/job` only, after upfront payment is verified
5173
- * and immediately before `jobManager.createJob`.
5174
- *
5175
- * The unpaid `/v1/job` warmup probe (price discovery → 402) thus no longer
5176
- * burns the nonce — clients can re-submit the same envelope with a real
5177
- * payment.
4562
+ * Authentication does not touch replay state. Job submission commits replay
4563
+ * only after upfront payment verification, so unpaid discovery cannot burn
4564
+ * the nonce. Reactivation rechecks the persisted signature without drift/replay.
5178
4565
  */
5179
4566
  interface SignedRequestVerifier<T> {
5180
4567
  /**
@@ -5208,20 +4595,11 @@ interface SignedRequestVerifier<T> {
5208
4595
  */
5209
4596
  signRequest(privkey: Uint8Array, body: Record<string, unknown>, opts?: SignedRequestSignOpts, domain?: SignedRequestDomain): T & CanonicalEnvelope;
5210
4597
  }
5211
- /** Failure mode taxonomy returned from `verify()`. */
5212
4598
  type SignedRequestFailure = "schema_invalid" | "timestamp_drift" | "signature_invalid" | "replay_detected";
5213
4599
  /**
5214
- * The four fields every signed-request envelope carries around the payload,
5215
- * with the runtime type each must have once the schema has parsed.
5216
- *
5217
- * The single definition of "which part of a schema failure is the envelope's":
5218
- * the post-parse shape check in `parse()` iterates it, and `authErrorBody`'s
5219
- * classifier reads its keys to decide whether a `schema_invalid` throw
5220
- * is the caller's *input* being wrong (disclosable: the schema is public on
5221
- * `/v1/info`) or their *signing* being wrong (folded into `signature_invalid`,
5222
- * one answer for every key and signature failure). `satisfies` pins it to
5223
- * {@link CanonicalEnvelope}, so a fifth envelope field can't be added to the
5224
- * type without both readers picking it up.
4600
+ * One definition of envelope field types shared by the runtime shape guard
4601
+ * and error disclosure classifier. Envelope failures stay private; violations
4602
+ * of the published builder input schema can be disclosed.
5225
4603
  */
5226
4604
  declare const SIGNED_ENVELOPE_TYPES: {
5227
4605
  readonly pubkey: "string";
@@ -5230,9 +4608,7 @@ declare const SIGNED_ENVELOPE_TYPES: {
5230
4608
  readonly nonce: "string";
5231
4609
  readonly auth_statement: "object";
5232
4610
  };
5233
- /** The envelope field names — {@link SIGNED_ENVELOPE_TYPES}' keys. */
5234
4611
  declare const SIGNED_ENVELOPE_FIELDS: readonly (keyof CanonicalEnvelope)[];
5235
- /** Structured error from `verify()` — `sub_reason` is the failure kind. */
5236
4612
  declare class SignedRequestError extends Error {
5237
4613
  readonly sub_reason: SignedRequestFailure;
5238
4614
  /**
@@ -5259,38 +4635,15 @@ declare class SignedRequestError extends Error {
5259
4635
  issues?: readonly z.core.$ZodIssue[] | undefined);
5260
4636
  }
5261
4637
  /**
5262
- * Build a secp256k1+BIP-340 Schnorr-over-canonical-JSON request verifier.
5263
- *
5264
- * Generalises cast's `canonicalSignedPayload` + `verifyCanonicalSignedRequest`
5265
- * into a reusable primitive — `(schema, opts) -> { verify, canonicalBytes, signRequest }`.
5266
- * Both halves derive their canonical bytes through the same function so any
5267
- * Zod schema change automatically propagates to both signer and verifier.
4638
+ * Signatures cover the raw input, not Zod-parsed output. Schema validation
4639
+ * may apply defaults or transformations without changing the signed bytes.
5268
4640
  */
5269
4641
  declare function createSignedRequestVerifier<T = unknown>(schema: ZodLike<T>, opts?: CreateSignedRequestVerifierOpts): SignedRequestVerifier<T>;
5270
4642
  /**
5271
- * The wire object the caller signed, taken out of a request body.
5272
- *
5273
- * **The single definition of which bytes get verified, and every auth call site
5274
- * reads it**: `/v1/quote`, the `/v1/job` submit, and the reactivation
5275
- * re-verify. They used to disagree — quote checked the raw `body.data`, submit
5276
- * checked `JobManager.parseInput`'s Zod output — so the two agreed only where
5277
- * the parse happened to be an identity on the caller's payload. A nested
5278
- * `.default()`, a `.transform()`, a `z.coerce.*`, or any key the caller sent
5279
- * that the schema doesn't declare made the quote pass and the paid submit 401
5280
- * `signature_invalid`, blaming the signature for a schema shape the caller
5281
- * never saw.
5282
- *
5283
- * Raw is what `public SDK documentation` specifies ("take the body, remove the
5284
- * `signature` field, and canonicalise"), what the CLI signs, and what the SDK
5285
- * persists as `record.input` — so the parsed form has no claim here. The
5286
- * capability schema still validates what the handler receives; it just doesn't
5287
- * get to rewrite the bytes the signature covers. It also means the credit
5288
- * envelope needs no re-merge: it never left.
5289
- *
5290
- * `body.data` wins when present; otherwise the JSON-encoded `body.input` form
5291
- * is parsed. Anything else — no input at all, a non-JSON legacy string — yields
5292
- * `{}`, which carries no envelope and so fails the auth check, exactly as
5293
- * before.
4643
+ * Raw structured input for authentication and durable replay. data wins;
4644
+ * otherwise input is parsed as JSON, with absent/legacy text becoming {}.
4645
+ * Builder validation may transform the handler input but cannot rewrite these
4646
+ * signing bytes, including protocol-owned credit fields.
5294
4647
  */
5295
4648
  declare function signedRequestInput(body: {
5296
4649
  input?: string;
@@ -5315,55 +4668,15 @@ declare function signedRequestInput(body: {
5315
4668
  declare function createDefaultReplayStore(): SignedRequestReplayStore;
5316
4669
 
5317
4670
  /**
5318
- * A DVM-level auth scheme, declared on `DVMDescriptor.auth`. Authentication
5319
- * is two-step:
5320
- *
5321
- * 1. {@link checkAuth} fires at request entry on `/v1/quote` and `/v1/job`,
5322
- * before payment verification. Schema / drift / signature checks only —
5323
- * no replay-store touch. Unsigned / malformed / stale / wrong-sig
5324
- * envelopes reject with HTTP 401 with no Cashu round-trip and no pending
5325
- * revenue row.
5326
- * 2. {@link recordReplay} fires on `/v1/job` only, after upfront payment is
5327
- * verified and immediately before `jobManager.createJob`. Commits the
5328
- * `(pubkey, timestamp, nonce)` tuple to the replay store; throws 401
5329
- * `replay_detected` on duplicate. The unpaid 402-discovery hop does not
5330
- * burn the nonce, so clients can re-submit with payment.
5331
- *
5332
- * On `replay_detected` after payment was already accepted, the SDK responds
5333
- * 401 fail-closed uniformly across cashu / mppx / x402 — no rail-specific
5334
- * refund or compensation logic. The Cashu accept path
5335
- * commits proofs into the accumulator wallet inside `verifyUpfrontPayment`
5336
- * before `recordReplay` runs. Unlocked Cashu is rejected upstream, so the
5337
- * accumulator is the only production path and no proofs are held to
5338
- * refund; mppx / x402 credentials are single-use and already consumed by
5339
- * the facilitator.
5340
- *
5341
- * The SDK picks the active capability's `input` schema and dispatches the
5342
- * `checkAuth` call against it — schemes themselves take no schema parameter,
5343
- * so one declaration covers every capability under a multi-capability DVM.
5344
- *
5345
- * A scheme validates the input it is handed, and on `/v1/job` that input
5346
- * carries the credit envelope (`credit_id` / `draw_id` / `fund`) because the
5347
- * signature covers it. Those keys are the protocol's, not the capability's, so
5348
- * a scheme must not fail a schema that doesn't declare them — `secp256k1Auth`
5349
- * removes them for the schema check only, keeping the signing bytes verbatim
5350
- * A custom scheme owes callers the same tolerance, or every strict
5351
- * schema 401s on an explicit draw.
4671
+ * Boot-summary backend: postgres is shared across machines; memory is per-process.
4672
+ * Explicit user-supplied stores and null (disabled) are never overridden.
5352
4673
  */
4674
+ type ReplayStoreBackend = "postgres" | "memory" | "user-supplied" | "disabled";
5353
4675
  /**
5354
- * The replay-store backend resolved at host-boot time. Surfaced in
5355
- * the boot summary so operators see which protection is active.
5356
- *
5357
- * - `postgres` — SDK auto-wired a `PostgresReplayStore` against the host's
5358
- * pool. Cross-machine safe.
5359
- * - `memory` — SDK fell back to the in-memory FIFO. Per-process; unsafe for
5360
- * multi-machine deploys. `createDVMHost` emits a warning (or throws under
5361
- * `DVMKIT_FAIL_FAST=true`) when this kicks in outside `devMode`.
5362
- * - `user-supplied` — user passed an explicit `replayStore` at factory time.
5363
- * SDK doesn't override.
5364
- * - `disabled` — user passed `replayStore: null`. No replay tracking.
4676
+ * Auth runs before payment; replay commits after accepted upfront payment.
4677
+ * Custom schemes must validate builder input while tolerating signed protocol
4678
+ * fields (credit, recovery and auth statement) outside the builder schema.
5365
4679
  */
5366
- type ReplayStoreBackend = "postgres" | "memory" | "user-supplied" | "disabled";
5367
4680
  interface DVMAuthScheme {
5368
4681
  /** Wire-level identifier surfaced on `/v1/info#auth`. */
5369
4682
  readonly id: typeof SIGNED_REQUEST_AUTH_ID;
@@ -5373,13 +4686,9 @@ interface DVMAuthScheme {
5373
4686
  envelope: T & CanonicalEnvelope;
5374
4687
  };
5375
4688
  /**
5376
- * Re-verify the signature on a persisted envelope at reactivation time
5377
- * Schema + signature only — no drift check, no replay-store
5378
- * touch. Catches DB-layer tampering: persisted `record.input` would
5379
- * otherwise be trusted as authentic, and a compromised row could feed an
5380
- * attacker-supplied pubkey/envelope into the handler. Throws
5381
- * {@link SignedRequestError} with `sub_reason: "signature_invalid"` or
5382
- * `"schema_invalid"` on failure.
4689
+ * Reactivation validates schema and signature without timestamp drift or replay
4690
+ * checks: the original timestamp and already-committed nonce must remain usable.
4691
+ * Throws signature_invalid or schema_invalid on failed validation.
5383
4692
  */
5384
4693
  recheckSignature<T>(schema: ZodLike<T>, input: unknown, domain?: SignedRequestDomain): void;
5385
4694
  /** Commit the envelope's `(pubkey, timestamp, nonce)` to the replay store. Throws on duplicate. */
@@ -5397,17 +4706,9 @@ interface DVMAuthScheme {
5397
4706
  }
5398
4707
  interface Secp256k1AuthOpts {
5399
4708
  /**
5400
- * Override the replay-window store. Most callers should omit this opt:
5401
- * `createDVMHost` auto-wires a cross-machine `PostgresReplayStore` when a
5402
- * pool is available, and falls back to a bounded in-memory FIFO (10-min
5403
- * window, 100k cap) only when no pool exists — emitting a warning (or
5404
- * throwing under `DVMKIT_FAIL_FAST=true`) outside `devMode` so multi-machine
5405
- * deploys can't silently inherit the per-process default.
5406
- *
5407
- * Pass a custom `SignedRequestReplayStore` here only to override that
5408
- * default (e.g. a different cross-machine backend), or pass `null` to
5409
- * disable replay tracking entirely. Any explicit value — including `null` —
5410
- * locks the choice: the host-boot wiring never overrides it.
4709
+ * Omitting this lets the host adopt Postgres, otherwise falling back to a
4710
+ * bounded per-process store with a warning or fail-fast refusal outside dev mode.
4711
+ * Any explicit store, including null to disable replay tracking, is locked.
5411
4712
  */
5412
4713
  replayStore?: SignedRequestReplayStore | null;
5413
4714
  /** Permitted clock drift in seconds. Defaults to 300 (5 minutes). */
@@ -5416,30 +4717,16 @@ interface Secp256k1AuthOpts {
5416
4717
  now?: () => number;
5417
4718
  }
5418
4719
  /**
5419
- * Build a descriptor-level secp256k1+BIP-340 Schnorr auth scheme.
5420
- *
5421
- * Declare on `configureDVM({ auth: secp256k1Auth({...}) })` to gate every
5422
- * capability's `/v1/job` and `/v1/quote` on a signed request envelope
5423
- * (`pubkey` / `signature` / `timestamp` / `nonce`) over the canonical JSON
5424
- * form of the capability's input. The SDK calls {@link DVMAuthScheme.checkAuth}
5425
- * at request entry on both endpoints, and {@link DVMAuthScheme.recordReplay}
5426
- * on `/v1/job` only — after upfront payment is verified, just before the job
5427
- * is created. The 402-discovery hop on `/v1/job` therefore does not consume
5428
- * the replay nonce; the same signed envelope is accepted on the paid resubmit.
5429
- *
5430
- * One scheme instance covers every capability under the DVM — internally
5431
- * the SDK caches a per-schema verifier so each capability's Zod schema is
5432
- * compiled once. The replay store is shared across all capabilities.
4720
+ * One scheme covers all capabilities and shares their replay store. Per-schema
4721
+ * verifiers validate without touching replay state; the host commits replay
4722
+ * only after upfront payment verification, immediately before job creation.
5433
4723
  */
5434
4724
  declare function secp256k1Auth(opts?: Secp256k1AuthOpts): DVMAuthScheme;
5435
4725
 
5436
4726
  /** Structural type matching any Zod schema. Avoids a hard dependency on zod. */
5437
4727
  interface ZodLike<T = unknown> {
5438
- /** Internal Zod metadata (used for duck-type detection). */
5439
4728
  _zod: unknown;
5440
- /** Parse and validate data, throwing on failure. */
5441
4729
  parse(data: unknown): T;
5442
- /** Parse and validate data, returning a result object instead of throwing. */
5443
4730
  safeParse(data: unknown): {
5444
4731
  success: true;
5445
4732
  data: T;
@@ -5448,28 +4735,13 @@ interface ZodLike<T = unknown> {
5448
4735
  error: unknown;
5449
4736
  };
5450
4737
  }
5451
- /** Extract the output type from a ZodLike schema, or default to string. */
5452
4738
  type InputType<T> = T extends ZodLike<infer O> ? O : string;
5453
4739
  /**
5454
- * Narrowed type for capability `example` payloads.
5455
- *
5456
- * Surfaces the Zod schema's output type so a drifted example produces a
5457
- * TypeScript error at the `configureDVM` call site. Falls back to
5458
- * `Record<string, unknown>` when `InputSchema` is undefined or a plain JSON
5459
- * Schema object (which can't validate at type-level; the runtime
5460
- * `validateExampleAgainstInput` in `configure.ts` covers schema-less paths).
5461
- *
5462
- * Approximation: `ZodLike<T>` exposes only one type param. True `z.input`
5463
- * distinguishes pre-coercion input from post-coercion output for schemas
5464
- * that use `.transform()` or `z.coerce.*`. None of the first-party DVMs do
5465
- * that on their input schemas today. Revisit — extend `ZodLike` to a
5466
- * two-param shape `ZodLike<Output, Input = Output>` — if a builder ever
5467
- * hits a real divergence on `example`.
4740
+ * Zod examples use the schema output type, including transforms. Plain JSON Schema examples have
4741
+ * no equivalent static validation.
5468
4742
  */
5469
4743
  type ExampleType<InputSchema> = [InputSchema] extends [ZodLike<infer O>] ? O : Record<string, unknown>;
5470
- /** Runtime check for whether a value is a Zod schema. */
5471
4744
  declare function isZodSchema(value: unknown): value is ZodLike;
5472
- /** Structured logging interface injected via ctx.log. */
5473
4745
  interface Logger {
5474
4746
  debug(message: string, data?: Record<string, unknown>): void;
5475
4747
  info(message: string, data?: Record<string, unknown>): void;
@@ -5495,7 +4767,6 @@ interface PromptOpts {
5495
4767
  schema?: Record<string, unknown>;
5496
4768
  }
5497
4769
  interface SDKPaymentRequestOpts {
5498
- /** Cashu mints accepted for this payment. */
5499
4770
  mints?: string[];
5500
4771
  /** Minimum P2PK locktime in seconds. */
5501
4772
  minLocktimeSeconds?: number;
@@ -5548,11 +4819,8 @@ interface JobCost {
5548
4819
  /** ISO 4217 lowercase, e.g. `"usd"`. */
5549
4820
  currency: Currency;
5550
4821
  }
5551
- /** Extended job context provided by the SDK runtime to DVM handlers. */
5552
4822
  interface SDKJobContext<State = Record<string, unknown>, Input = string> {
5553
- /** Server-assigned job ID. */
5554
4823
  readonly jobId: string;
5555
- /** Tags from the DVM descriptor (what this DVM handles). */
5556
4824
  readonly tags: string[];
5557
4825
  /** Job input — raw string or parsed/validated object when an input schema is defined. */
5558
4826
  readonly input: Input;
@@ -5572,24 +4840,17 @@ interface SDKJobContext<State = Record<string, unknown>, Input = string> {
5572
4840
  pubkey: string;
5573
4841
  envelope: CanonicalEnvelope & Record<string, unknown>;
5574
4842
  };
5575
- /** Send a progress text message to the client. */
5576
4843
  text(message: string): void;
5577
- /** Send an artifact (output data) to the client. */
5578
4844
  artifact(content: ArtifactContent): void;
5579
4845
  /** Send a raw typed message (escape hatch). */
5580
4846
  sendMessage(type: MessageType, content: Record<string, unknown>): void;
5581
4847
  /** Ask the client a question. Resolves when the client responds. */
5582
4848
  prompt(id: string, text: string, opts?: PromptOpts): Promise<ResponseContent>;
5583
4849
  /**
5584
- * Request payment from the client. Resolves when payment is received.
5585
- *
5586
- * - `number` = msats raw (sats-native operators).
5587
- * - `{ amount, currency }` = fiat envelope; the SDK converts to sats at
5588
- * send-time via its centralised fx fetcher. v1 supports `currency: "usd"`
5589
- * only; other currencies throw `UnsupportedCurrencyError`.
5590
- *
5591
- * The legacy `string` form (`"$0.05"`) was removed in under the
5592
- * project's no-shims convention. Migrate callers to the fiat envelope.
4850
+ * Request payment; resolves when the ask is satisfied by the upfront pool or a payment.
4851
+ * Numbers are millisatoshis. Fiat envelopes use major units in the supplied
4852
+ * supported currency; the ledger pin is converted into the job's denomination.
4853
+ * x402 is offered only for USD; other configured rails can use supported FX currencies.
5593
4854
  */
5594
4855
  requestPayment(amount: number | {
5595
4856
  amount: number;
@@ -5673,7 +4934,7 @@ interface SDKJobContext<State = Record<string, unknown>, Input = string> {
5673
4934
  step<T>(id: string, fn: () => Promise<T>): Promise<T>;
5674
4935
  /** Per-job typed state, auto-loaded and auto-persisted. */
5675
4936
  state: State;
5676
- /** Persistent cross-job KV storage. */
4937
+ /** Persistent storage shared across jobs. */
5677
4938
  store: KVStore;
5678
4939
  /**
5679
4940
  * Instrumented fetch — emits `dvm.fetch` spans with redacted URLs, and
@@ -5690,17 +4951,14 @@ interface SDKJobContext<State = Record<string, unknown>, Input = string> {
5690
4951
  * / `ctx.fail` are no-ops: the job stays `cancelled`.
5691
4952
  */
5692
4953
  readonly signal: AbortSignal;
5693
- /** Environment variables configured by the operator. */
5694
4954
  env: Record<string, string>;
5695
4955
  log: Logger;
5696
4956
  }
5697
- /** Context provided to the onQuote handler. */
5698
4957
  interface QuoteContext<QuoteData = Record<string, unknown>> {
5699
- /** Parsed quote data — typed when a Zod schema is provided. */
4958
+ /** Quote data, parsed by the configured Zod schema when present. */
5700
4959
  data: QuoteData;
5701
- /** Raw input string from the client, if provided. */
4960
+ /** The client's raw input string, when supplied. */
5702
4961
  input?: string;
5703
- /** Job parameters from the client, if provided. */
5704
4962
  params?: Record<string, string>;
5705
4963
  /**
5706
4964
  * Operator environment — same object threaded into `JobContext.env`.
@@ -5822,14 +5080,10 @@ interface QuoteResult {
5822
5080
  max_units?: number;
5823
5081
  };
5824
5082
  }
5825
- /** Quote handler configuration with a Zod schema for structured quote data. */
5826
5083
  interface QuoteConfig<QuoteData = Record<string, unknown>> {
5827
- /** Schema describing the data clients must provide for a quote. */
5828
5084
  schema: ZodLike<QuoteData> | Record<string, unknown>;
5829
- /** Compute a price based on the structured quote data. */
5830
5085
  handler(ctx: QuoteContext<QuoteData>): Promise<QuoteResult>;
5831
5086
  }
5832
- /** Incoming message shape for the onMessage catch-all handler. */
5833
5087
  interface IncomingMessage {
5834
5088
  /** Message type (e.g. "response", "payment", "approval", "cancel"). */
5835
5089
  type: MessageType;
@@ -5846,7 +5100,6 @@ interface IncomingMessage {
5846
5100
  * the SDK converts at request time.
5847
5101
  */
5848
5102
  type PriceValue = string;
5849
- /** Accepted payment rail identifier. */
5850
5103
  type PaymentMethod = "cashu" | "x402" | "tempo";
5851
5104
  /**
5852
5105
  * Rails that may fund a prepaid **credit**.
@@ -6263,11 +5516,9 @@ interface DVMDescriptor<State = Record<string, unknown>, InputSchema extends Zod
6263
5516
  readonly auth?: DVMAuthScheme;
6264
5517
  }
6265
5518
 
6266
- /** Serializable record of a completed step. */
6267
5519
  interface StepRecord {
6268
5520
  /** Step identifier (must be unique within a job). */
6269
5521
  id: string;
6270
- /** Cached return value of the step function. */
6271
5522
  value: unknown;
6272
5523
  /** Cost declarations made while the step body ran, replayed on cache hits. */
6273
5524
  costs?: JobCost[];
@@ -6283,14 +5534,10 @@ interface StepRecord {
6283
5534
  declare class StepCache {
6284
5535
  private cache;
6285
5536
  has(id: string): boolean;
6286
- /** Get a cached step result. Throws if not present. */
6287
5537
  get(id: string): unknown;
6288
- /** Get a cached step record, including replay metadata. Throws if not present. */
6289
5538
  getRecord(id: string): StepRecord;
6290
5539
  set(id: string, value: unknown, costs?: readonly JobCost[]): void;
6291
- /** Export all cached steps for persistence. */
6292
5540
  serialize(): StepRecord[];
6293
- /** Restore a StepCache from persisted records. */
6294
5541
  static deserialize(records: StepRecord[]): StepCache;
6295
5542
  }
6296
5543