@dvmkit/sdk 0.1.5-rc.9 → 0.2.1-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 (67) hide show
  1. package/README.md +14 -0
  2. package/dist/{chunk-MLRCSJYX.js → chunk-2D75MNNG.js} +178 -550
  3. package/dist/{chunk-BIFLRKMO.js → chunk-2KGG5ORW.js} +1 -1
  4. package/dist/{chunk-27V2ILSR.js → chunk-4A2RAKCW.js} +2 -2
  5. package/dist/{chunk-5URG56JJ.js → chunk-A4CM2BD3.js} +18 -2
  6. package/dist/{chunk-L4OYF4DQ.js → chunk-AJNFXR7J.js} +2 -2
  7. package/dist/{chunk-S3XAHZQY.js → chunk-COYKJRMM.js} +24 -3
  8. package/dist/{chunk-UP2F5RRT.js → chunk-D7YQYLLM.js} +1 -4
  9. package/dist/{chunk-CEOAHV2I.js → chunk-FDKRXOZO.js} +0 -5
  10. package/dist/{chunk-TQWGQCNV.js → chunk-FPOLJRBF.js} +5 -5
  11. package/dist/{chunk-BTZY7VPH.js → chunk-GAIPXGM3.js} +1 -1
  12. package/dist/{chunk-SSSZUVWM.js → chunk-JAHRF5X3.js} +89 -39
  13. package/dist/{chunk-5PBOA25N.js → chunk-JNKQZVZX.js} +472 -617
  14. package/dist/{chunk-U6M3ATSG.js → chunk-MD3H3BF7.js} +40 -6
  15. package/dist/{chunk-CGKZDODG.js → chunk-MG67KXU7.js} +0 -5
  16. package/dist/{chunk-KSQEFVFJ.js → chunk-MMCE7XSR.js} +1 -1
  17. package/dist/{chunk-JZWELPFH.js → chunk-N5B66UOS.js} +4 -16
  18. package/dist/{chunk-BIP6G74V.js → chunk-O6D72KYA.js} +10 -10
  19. package/dist/{chunk-KXCKFIJI.js → chunk-QX52F6IB.js} +1 -1
  20. package/dist/{chunk-RW5LP57K.js → chunk-SDK6KDJN.js} +0 -1
  21. package/dist/{chunk-KVEHHC7W.js → chunk-U5VNEALI.js} +24 -13
  22. package/dist/{chunk-FROTD5XQ.js → chunk-V5ZRATJH.js} +2 -3
  23. package/dist/{chunk-EVBK675R.js → chunk-VA7GT6C6.js} +31 -37
  24. package/dist/{chunk-2UUXIIOC.js → chunk-VIVXB4CD.js} +3 -3
  25. package/dist/{chunk-6BQM7TOW.js → chunk-XMDQOXHO.js} +11 -38
  26. package/dist/{chunk-DMNLFNTW.js → chunk-XRLKGCAW.js} +2 -2
  27. package/dist/{chunk-M7LHFJ5K.js → chunk-XZ7LW4S5.js} +2 -3
  28. package/dist/{chunk-E4EVGPDX.js → chunk-YO56VCTQ.js} +0 -6
  29. package/dist/{credit-ledger-2DFQHNLB.js → credit-ledger-MHA652TV.js} +2 -2
  30. package/dist/{credit-menu-enwMbn55.d.ts → credit-menu-kDA9RVzD.d.ts} +524 -1077
  31. package/dist/{fx-D860pZvP.d.ts → fx-DhyT1Cjh.d.ts} +38 -82
  32. package/dist/index.d.ts +11 -26
  33. package/dist/index.js +2 -2
  34. package/dist/internal/caller.d.ts +644 -2140
  35. package/dist/internal/caller.js +32 -66
  36. package/dist/internal/server.d.ts +40 -90
  37. package/dist/internal/server.js +16 -16
  38. package/dist/{job-store-BUGqvCfL.d.ts → job-store-D3CcZp_g.d.ts} +73 -66
  39. package/dist/{lightning-backend-BozcevPZ.d.ts → lightning-backend-BhsyrBLa.d.ts} +19 -41
  40. package/dist/{memory-credit-ledger-MNUOTQO5.js → memory-credit-ledger-BYBATRN3.js} +3 -3
  41. package/dist/{mpp-secret-state-WNAQQ6K4.js → mpp-secret-state-B5GCILVH.js} +1 -1
  42. package/dist/{mpp-setup-4FJD6ZHV.js → mpp-setup-MPP3ST5X.js} +1 -1
  43. package/dist/{payout-reporter-RG6XNGPI.js → payout-reporter-KEMUXEUO.js} +2 -2
  44. package/dist/{postgres-consumed-credential-store-VHBT4KEA.js → postgres-consumed-credential-store-6N2V5MJN.js} +2 -2
  45. package/dist/postgres-job-store-MZZZMS2B.js +7 -0
  46. package/dist/postgres-kv-store-DXGTKXLU.js +7 -0
  47. package/dist/{postgres-replay-store-UJXRT6VO.js → postgres-replay-store-ZF4JNZOW.js} +2 -2
  48. package/dist/{pricing-4CEB34RM.js → pricing-MU5GNUJZ.js} +1 -1
  49. package/dist/{processed-payment-store-HAA4SFNK.js → processed-payment-store-GKMAWOM3.js} +2 -2
  50. package/dist/revenue-reporter-O75BGYFK.js +7 -0
  51. package/dist/server/index.d.ts +47 -159
  52. package/dist/server/index.js +67 -138
  53. package/dist/{ssrf-DbFkpDv0.d.ts → ssrf-dMooihtY.d.ts} +1 -2
  54. package/dist/{step-cache-5dljDqrQ.d.ts → step-cache-v2yhyhSZ.d.ts} +617 -1532
  55. package/dist/{tempo-charge-store-RIFTALZK.js → tempo-charge-store-SDTUNTKA.js} +2 -2
  56. package/dist/{tempo-lifecycle-DFIXQ54Q.js → tempo-lifecycle-G5SB4HFC.js} +5 -5
  57. package/dist/{tempo-session-store-2JNOKJGX.js → tempo-session-store-WF3VZXNF.js} +2 -2
  58. package/dist/{tempo-wallet-4QKSV65O.js → tempo-wallet-ZFNUDFVO.js} +2 -2
  59. package/dist/testing/index.d.ts +5 -15
  60. package/dist/testing/index.js +4 -11
  61. package/dist/{usd-DoRuAckA.d.ts → usd-Cu9G6xv2.d.ts} +14 -16
  62. package/dist/{wallet-CJC8lwxx.d.ts → wallet-Dwjs5n_M.d.ts} +1 -1
  63. package/dist/{x402-5H27DCBE.js → x402-CI6PSHNO.js} +5 -5
  64. package/package.json +3 -2
  65. package/dist/postgres-job-store-3RAXMNSY.js +0 -7
  66. package/dist/postgres-kv-store-JFBDP5IP.js +0 -7
  67. package/dist/revenue-reporter-ASZ7SHHH.js +0 -7
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  withSdkInitLock
3
- } from "./chunk-S3XAHZQY.js";
3
+ } from "./chunk-COYKJRMM.js";
4
4
  import {
5
5
  applyRetryToPool
6
6
  } from "./chunk-C3MTFLC6.js";
@@ -108,7 +108,6 @@ var PostgresX402ChannelStorage = class {
108
108
  fleetLockQueueDepth = 0;
109
109
  relayLockTimeoutMs;
110
110
  terminalizeUnbackedCredit;
111
- /** Create the durable channel table under the SDK-wide DDL lock. */
112
111
  async init() {
113
112
  this.settlementPool ??= createSettlementPool(this.pool);
114
113
  await withSdkInitLock(this.pool, async () => {
@@ -208,7 +207,7 @@ var PostgresX402ChannelStorage = class {
208
207
  *
209
208
  * Both accessors run on the settlement pool: every caller reads or writes the
210
209
  * marker while {@link withFleetSettlementLock} holds a host-pool client, which
211
- * is the nesting the internal-review split exists to keep off a non-terminal pool.
210
+ * is the nesting the split exists to keep off a non-terminal pool.
212
211
  */
213
212
  async isSettlePending(scope) {
214
213
  const reader = await this.settlementWriter();
@@ -235,9 +234,9 @@ var PostgresX402ChannelStorage = class {
235
234
  *
236
235
  * On the dedicated settlement pool like every other marker, and for a sharper
237
236
  * reason than they had: this and {@link prepareSettlement} now run *inside*
238
- * the channel transaction (internal-review), which holds a host-pool client for its
237
+ * the channel transaction, which holds a host-pool client for its
239
238
  * whole duration. Taking a second client from that same pool is the
240
- * self-deadlock internal-review fixed — at pool max every client is a holder and a
239
+ * self-deadlock fixed — at pool max every client is a holder and a
241
240
  * waiter — and moving these calls under the transaction is exactly what would
242
241
  * reintroduce it.
243
242
  */
@@ -250,7 +249,7 @@ var PostgresX402ChannelStorage = class {
250
249
  }
251
250
  /**
252
251
  * The refund on this channel that is neither finished nor abandoned, if there
253
- * is one — the credit ledger's spend gate (internal-review).
252
+ * is one — the credit ledger's spend gate.
254
253
  *
255
254
  * `statuses` is the caller's, because the two gates disagree by design: a
256
255
  * draw is blocked by {@link X402_SPEND_BLOCKING_SETTLEMENT_STATUSES}, a fresh
@@ -258,12 +257,12 @@ var PostgresX402ChannelStorage = class {
258
257
  *
259
258
  * On the dedicated settlement pool, and that is what makes the gate callable
260
259
  * at all: a draw routinely runs inside an open channel transaction holding a
261
- * host-pool client, and taking a second one from that pool is the internal-review
260
+ * host-pool client, and taking a second one from that pool is the
262
261
  * self-deadlock. The oldest blocking row wins, so a channel that somehow
263
262
  * accumulated two names the one an operator should reach for first.
264
263
  *
265
264
  * Projects three columns rather than decoding a whole {@link
266
- * X402SettlementIntent}: the decoder refuses a pre-internal-review row that carries
265
+ * X402SettlementIntent}: the decoder refuses a legacy row that carries
267
266
  * no reconciliation snapshot, and a gate that throws on one would take every
268
267
  * draw on that channel down with it — when the row is exactly the kind that
269
268
  * ought to block them.
@@ -282,7 +281,7 @@ var PostgresX402ChannelStorage = class {
282
281
  }
283
282
  /**
284
283
  * Read one settlement by its own identifier — the handle an operator holds
285
- * (internal-review). Same dedicated pool as {@link getSettlement}, for the same
284
+ * Same dedicated pool as {@link getSettlement}, for the same
286
285
  * reason: the repair reads this while a channel transaction may be open.
287
286
  */
288
287
  async getSettlementById(settlementId) {
@@ -308,7 +307,7 @@ var PostgresX402ChannelStorage = class {
308
307
  }
309
308
  /**
310
309
  * The operator's repair queue: settlements stuck between "chain paid" and
311
- * "ledger booked", oldest write first (internal-review).
310
+ * "ledger booked", oldest write first.
312
311
  *
313
312
  * Keyset-paginated on `(created_at, settlement_id)` for the reason the
314
313
  * blocked-invoice queue is: a wedged row never self-clears, so a fixed first
@@ -350,7 +349,7 @@ var PostgresX402ChannelStorage = class {
350
349
  };
351
350
  }
352
351
  /**
353
- * Close a wedged settlement without booking anything (internal-review).
352
+ * Close a wedged settlement without booking anything.
354
353
  *
355
354
  * A single-statement CAS on the wedged statuses, on the settlement pool —
356
355
  * there is no ledger effect to be atomic with, which is the whole point:
@@ -460,7 +459,7 @@ var PostgresX402ChannelStorage = class {
460
459
  * Mark that the next operation is the ambiguous external-call boundary.
461
460
  *
462
461
  * `channelAtSubmission` is the live channel the facilitator is about to size
463
- * and sign the settlement against (internal-review) — bookmarked here, with the
462
+ * and sign the settlement against — bookmarked here, with the
464
463
  * block height, because these two are the only description of that instant
465
464
  * that survives the call.
466
465
  */
@@ -521,7 +520,7 @@ var PostgresX402ChannelStorage = class {
521
520
  /**
522
521
  * Mark the external settlement and ledger effect complete in the active transaction.
523
522
  *
524
- * `resolvedAtMs` is set only by the operator repair (internal-review), so an audit
523
+ * `resolvedAtMs` is set only by the operator repair, so an audit
525
524
  * read can tell a settlement that completed on its own from one a human had
526
525
  * to finish.
527
526
  */
@@ -541,7 +540,6 @@ var PostgresX402ChannelStorage = class {
541
540
  );
542
541
  }
543
542
  }
544
- /** Load one channel snapshot. */
545
543
  async get(channelId) {
546
544
  const q = this.transaction.getStore() ?? this.pool;
547
545
  const { rows } = await q.query(
@@ -591,7 +589,7 @@ var PostgresX402ChannelStorage = class {
591
589
  * this is how the settlement layer says "this exact decrease is the one the
592
590
  * payer's verified refund voucher authorises". Outside this scope a decrease
593
591
  * of `chargedCumulativeAmount` is refused flat, which is what keeps the
594
- * arbitrary-decrease guard meaningful (internal-review).
592
+ * arbitrary-decrease guard meaningful.
595
593
  */
596
594
  withSanctionedRefundBase(base, operation) {
597
595
  return this.refundBase.run(base, operation);
@@ -617,7 +615,7 @@ var PostgresX402ChannelStorage = class {
617
615
  * The lock is session-scoped, so its client stays checked out for the whole
618
616
  * operation — and `operation()` (upstream's claim/settle) re-enters the pool
619
617
  * through {@link updateChannel}. Two bounds keep that from wedging the pool
620
- * (internal-review): local callers are serialized in-process, so this holds at most
618
+ * local callers are serialized in-process, so this holds at most
621
619
  * one client per process no matter how many requests arrive, and a waiting
622
620
  * acquire carries a `lock_timeout` so a wedged fleet-mate can't pin that
623
621
  * client — and the caller's request — indefinitely. A timeout reports the
@@ -640,7 +638,7 @@ var PostgresX402ChannelStorage = class {
640
638
  }
641
639
  }
642
640
  /**
643
- * Hold the fleet-wide self-relay submission lock for one chain submission (internal-review).
641
+ * Hold the fleet-wide self-relay submission lock for one chain submission.
644
642
  *
645
643
  * The session lock keeps one per-DVM gas EOA to one in-flight submission
646
644
  * across the whole fleet — two machines submitting at once collide on the
@@ -997,46 +995,29 @@ var CreditLedger = class {
997
995
  tempoSessionStore;
998
996
  /**
999
997
  * Postgres-backed: balances survive a restart and are shared across the
1000
- * fleet, so this DVM may advertise credit (internal-review).
998
+ * fleet, so this DVM may advertise credit.
1001
999
  */
1002
1000
  durable = true;
1003
1001
  x402Settlements;
1004
1002
  expiryReleaseOutbox;
1005
1003
  /**
1006
- * Bind the durable x402 settlement state this ledger gates spending on
1007
- * (internal-review).
1008
- *
1009
- * Late, because the settlement store is built with the batch-settlement
1010
- * server, which is built *after* the ledger it reads earned draws from. An
1011
- * unbound ledger simply does not gate — correct for a DVM with no x402
1012
- * channel storage, where no settlement row can exist to wedge.
1004
+ * Bound after construction because the settlement store shares this ledger and is
1005
+ * initialized separately. Ledger effects and route preflights use the same refund gate.
1013
1006
  */
1014
1007
  useX402SettlementGate(gate) {
1015
1008
  this.x402Settlements = gate;
1016
1009
  }
1017
1010
  /**
1018
- * Bind the durable outbox that reports credit-expiry releases and their
1019
- * revivals to the platform (internal-review).
1020
- *
1021
- * Held on the ledger rather than threaded through {@link fund} for the same
1022
- * reason as the gate above: the revival half fires from *every* funding path
1023
- * — Cashu commit, x402 exact and channel, Tempo, the Lightning invoice
1024
- * settle, the implicit N=1 per-call payment — and a seam each of those has to
1025
- * remember to pass is a seam one of them will eventually forget.
1026
- *
1027
- * Unbound (a `dvmctl dev` server, a self-hosted builder, a test host) the
1028
- * sweep and the reversal still run and still write the ledger's own rows;
1029
- * only the platform report is skipped. Reporting is bookkeeping and must
1030
- * never gate the ledger.
1011
+ * Expiry release and revival reports join the ledger transaction. A caller-owned funding
1012
+ * transaction must not commit a reversal independently of its outbox row. Without this
1013
+ * wiring, ledger transitions still run.
1031
1014
  */
1032
1015
  useCreditExpiryReleaseOutbox(outbox) {
1033
1016
  this.expiryReleaseOutbox = outbox;
1034
1017
  }
1035
- /** Create the `credits` / `credit_draws` tables if absent. Call once at SDK boot. */
1036
1018
  async init() {
1037
1019
  await withSdkInitLock(this.pool, () => this.createTables());
1038
1020
  }
1039
- /** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
1040
1021
  async createTables() {
1041
1022
  await this.pool.query(`
1042
1023
  CREATE TABLE IF NOT EXISTS credits (
@@ -1314,25 +1295,11 @@ var CreditLedger = class {
1314
1295
  );
1315
1296
  }
1316
1297
  /**
1317
- * Give every open non-channel Bitcoin credit a funding lot (internal-review).
1318
- *
1319
- * Runs inside the boot DDL, is guarded per credit by `NOT EXISTS`, and is
1320
- * therefore both the one-time migration and a standing self-heal for a
1321
- * credit that somehow reaches a positive balance with no lot behind it.
1322
- *
1323
- * The lot is derived from `credits.msats_remaining`, not replayed out of
1324
- * `credit_fundings`. That is not a shortcut: `credit_fundings` records only
1325
- * `{amount_micro, rail}` — no instrument amount at all — and is not written
1326
- * on every funding path, whereas `msats_remaining` **is** the credit's
1327
- * current unspent rail value, maintained pro rata on every draw since
1328
- * internal-review. One synthetic lot pairing it with `balance_micro` therefore
1329
- * reproduces today's in-kind position exactly, and FIFO over a single lot is
1330
- * trivially correct. Historic per-funding rates are unrecoverable and would
1331
- * change nothing: only the remaining position is owed.
1332
- *
1333
- * The semantics change is deliberately retroactive (internal-review) — every
1334
- * current holder is first-party, so there is one regime and no legacy
1335
- * branch.
1298
+ * Backfill an open non-channel Bitcoin credit only when it has no funding lot. Pair
1299
+ * balance_micro with current msats_remaining; credit_fundings lacks historical instrument
1300
+ * amounts and does not cover every funding path. Historical per-funding rates cannot be
1301
+ * recovered. Existing lots are left untouched, so this does not repair partial coverage
1302
+ * gaps.
1336
1303
  */
1337
1304
  async backfillFundingLots() {
1338
1305
  const { rowCount } = await this.pool.query(
@@ -1368,60 +1335,19 @@ var CreditLedger = class {
1368
1335
  );
1369
1336
  }
1370
1337
  /**
1371
- * Create a credit, or top up an existing one (same `creditId`). Top-ups add
1372
- * to the balance and overwrite `expiry_ms` with the provided value — funding
1373
- * an expired credit revives it (spec §5 carry-forward: the credit is a
1374
- * rolling buffer, not per-job escrow). The existing row's `caller_pubkey`
1375
- * and `currency` must match or the fund is refused with a typed error —
1376
- * without that check a top-up against someone else's `credit_id` would
1377
- * silently merge two callers' money.
1378
- *
1379
- * Pass `tx` (a client inside a caller-owned `BEGIN`) to commit the rail
1380
- * receive and the ledger credit atomically (spec condition 3 — the internal-review
1381
- * verifier does this). The ledger issues **no** transaction control on `tx`.
1382
- *
1383
- * Without `tx` it opens one of its own, because a funding is no longer a
1384
- * single statement: it upserts the credit, records its funding lot
1385
- * (internal-review), and reverses any standing expiry release (internal-review) — and that
1386
- * last leg restores balance and queues a report. A crash between the upsert
1387
- * and the reversal would leave a revived credit whose release still stands,
1388
- * which the sweep's own exclusion then makes permanent: `balance_micro > 0`
1389
- * but a standing release means it is neither drainable nor re-releasable.
1390
- *
1391
- * `basis` records the rail value behind the fiat (internal-review) so each draw can
1392
- * be allocated its share of the rail-native amount actually received. A
1393
- * top-up on a different rail than the credit's is refused with
1394
- * `rail_mismatch`: sats and USDC microunits aren't summable, so a blended
1395
- * credit would have no coherent native basis to allocate from.
1396
- *
1397
- * It is **required** (internal-review), in the type and again at runtime via
1398
- * {@link assertFundingBasis}. A basis-less fund wrote `rail = NULL`, and a
1399
- * draw against such a credit settles — a real debit — while
1400
- * `railAsFundingMethod` correctly declines to guess a rail, so `bookRevenue`
1401
- * books nothing and outstanding liability (`deposits − draw revenue`) stays
1402
- * overstated by the drawn amount forever. `credits.rail` stays nullable
1403
- * because rows written before this change still exist in live databases:
1404
- * **write is closed, read is not.** A legacy row adopts a rail from its first
1405
- * typed top-up via the `COALESCE` below.
1406
- *
1407
- * The rail-native **instrument** is pinned at first funding too, and both
1408
- * channel guards are symmetric for the same reason (internal-review). A reusable
1409
- * channel — Tempo session or x402 batch-settlement — may only ever top up
1410
- * the credit it opened, and a credit opened by a one-shot (a Tempo charge, an
1411
- * x402 exact authorization) may never adopt one. The x402 half used to admit
1412
- * `credits.x402_channel_id IS NULL`, so a channel deposit bound itself to a
1413
- * credit already holding exact-funded value: `earnedNativeForX402Channel`
1414
- * then read that credit's whole settled-draw history as the channel's
1415
- * earnings, and the first claim tick swept a deposit nobody had consumed,
1416
- * while the unspent balance lost every exit (the drain's refund base goes
1417
- * below `totalClaimed`, and a channel-bound credit refuses every non-x402
1418
- * drain). `credits_x402_channel_uidx` does not cover this: it guards
1419
- * one-credit-per-channel, not one-source-per-credit.
1420
- *
1421
- * An x402 channel deposit is additionally refused `settlement_pending` while
1422
- * that channel carries an unresolved refund (internal-review) — see the gate read
1423
- * below. This method is the authority on that rule, as it is on the binding
1424
- * rules above; every door preflights it where a refusal is still free.
1338
+ * Funding adds balance and overwrites expiry, reviving an expired credit. It is additive,
1339
+ * not idempotent: the caller’s rail marker or funding record must prevent replay. Owner and
1340
+ * currency must match; rail and reusable-channel bindings cannot be mixed with other
1341
+ * instruments. A legacy NULL rail adopts its first typed top-up.
1342
+ *
1343
+ * With tx, all writes join the caller’s transaction without transaction control. Otherwise
1344
+ * the ledger owns a transaction covering balance, funding lot, reversal of standing expiry
1345
+ * releases and their reports. A partial revival would leave restored balance with a release
1346
+ * still counted.
1347
+ *
1348
+ * An incoming x402 channel with an unresolved refund is refused before the upsert. That
1349
+ * preflight is not atomic with a concurrent cooperative drain; see the incoming-channel
1350
+ * gate below.
1425
1351
  */
1426
1352
  async fund(args) {
1427
1353
  assertAmount(args.amountMicro, { min: 1 });
@@ -1449,11 +1375,6 @@ var CreditLedger = class {
1449
1375
  client.release();
1450
1376
  }
1451
1377
  }
1452
- /**
1453
- * The funding itself, on whichever handle {@link fund} chose. Every refusal
1454
- * it raises is the caller's to see unchanged; a self-opened transaction rolls
1455
- * back around it.
1456
- */
1457
1378
  async fundOn(q, args, b, nowMs, creditId) {
1458
1379
  const wedged = await this.blockingX402Refund(
1459
1380
  b.x402Channel?.channelId,
@@ -1591,33 +1512,12 @@ var CreditLedger = class {
1591
1512
  return this.snapshotFromRow(revived ?? rows[0], q, nowMs);
1592
1513
  }
1593
1514
  /**
1594
- * Undo any expiry release this credit still carries, because a funding just
1595
- * landed on it (internal-review — the revival rule), restoring the balance the
1596
- * release took.
1515
+ * Funding reverses standing expiry releases, restores their released micro and queues
1516
+ * reversal reports on the funding transaction. Do this even when the new expiry is already
1517
+ * past: the next sweep must release the enlarged remainder as a new episode.
1597
1518
  *
1598
- * A release records the credit's **final** undrawn remainder and zeroes it
1599
- * (see {@link releaseExpiredCreditLocked}). Funding adds to the balance and
1600
- * overwrites `expiry_ms` (spec §5 carry-forward: the credit is a rolling
1601
- * buffer), so the moment money arrives the recorded remainder is no longer
1602
- * final: the release stops counting and its micro come back.
1603
- *
1604
- * Deliberately unconditional on the incoming expiry rather than gated on
1605
- * `expiryMs > nowMs`. A release exists only for a credit that had already
1606
- * lapsed, so any funding that reaches one is money the release did not
1607
- * account for — and a funding that arrives with an already-past expiry leaves
1608
- * a credit the very next sweep re-releases at its new, larger remainder. One
1609
- * rule, self-healing in both directions, and it never needs to read the
1610
- * pre-funding expiry the upsert has already overwritten.
1611
- *
1612
- * Runs on the funding's own handle — which {@link fund} now guarantees is a
1613
- * transaction — so the reversal, the restored balance and the report commit
1614
- * with the money or not at all. A reversal that committed without its report
1615
- * would be unrecoverable: `WHERE reversed_at IS NULL` means no later pass
1616
- * re-derives it.
1617
- *
1618
- * @returns the credit row as the restore left it, or undefined when there was
1619
- * nothing to reverse. The caller reads its snapshot off this rather than off
1620
- * the funding upsert's `RETURNING`, which predates the restore.
1519
+ * Returns the row after restoration, or undefined when no release stood. The upsert’s
1520
+ * RETURNING row predates the restoration and is not the final balance.
1621
1521
  */
1622
1522
  async reverseExpiryReleases(q, creditId, nowMs) {
1623
1523
  const { rows } = await q.query(
@@ -1642,37 +1542,15 @@ var CreditLedger = class {
1642
1542
  return restored[0];
1643
1543
  }
1644
1544
  /**
1645
- * Release the undrawn remainder of every credit whose TTL has run out
1646
- * (internal-review). Returns the releases this pass recorded.
1647
- *
1648
- * **What is released.** The credit's whole `balance_micro`. Expiry ends
1649
- * spending but never ownership of the record, so the balance stays readable;
1650
- * what ends is the caller's claim on it, and on the prepaid rails the value
1651
- * behind it is already in builder custody.
1652
- *
1653
- * **Tempo is excluded**, at both of its markers (`tempo_channel_id` and a
1654
- * `tempo` rail). A Tempo credit is channel-backed and its undrawn value
1655
- * returns to the *caller* at channel exit, so releasing it would book the
1656
- * builder money the chain is about to hand back.
1657
- *
1658
- * **A credit with a pending hold is skipped, not partially released.** A draw
1659
- * placed before expiry stays settleable afterwards (`draw`'s replay lookup
1660
- * runs above the expiry check, so a lost response is still recoverable), so
1661
- * the remainder is not final while a hold is outstanding. Skipping costs one
1662
- * sweep interval and keeps the released figure exactly "what nothing bought";
1663
- * holds do resolve — the orphan-draw watchdog (internal-review) is what guarantees
1664
- * a stranded one still reaches a terminal state.
1665
- *
1666
- * **Idempotent** two ways. `release_id` is derived from the expiry instant,
1667
- * so a re-sweep after a crash between the insert and its report collides on
1668
- * the same primary key instead of writing a second row; and the candidate
1669
- * scan excludes any credit that already has an un-reversed release.
1670
- *
1671
- * Each candidate is re-checked under the credit row lock every
1672
- * `credit_draws` write is taken under, so a draw racing the sweep either
1673
- * loses the race (its credit is already released and it would have been
1674
- * refused `credit_expired` anyway) or wins it and leaves a pending hold the
1675
- * locked re-check sees.
1545
+ * Release expired undrawn balances, retaining readable credit records. Tempo rail or
1546
+ * channel-backed credits are excluded because their collateral returns to the caller
1547
+ * through the channel lifecycle.
1548
+ *
1549
+ * Pending holds prevent release: jobs accepted before expiry may still settle or release.
1550
+ * Candidates are rechecked under the same parent-credit lock used by draw writes. The
1551
+ * balance debit, episode record and report commit together; standing releases exclude
1552
+ * repeat work. A later revival may reuse the expiry instant but receives a new release
1553
+ * sequence.
1676
1554
  */
1677
1555
  async sweepExpiredCredits(args) {
1678
1556
  const nowMs = args?.nowMs ?? Date.now();
@@ -1708,30 +1586,12 @@ var CreditLedger = class {
1708
1586
  return { released };
1709
1587
  }
1710
1588
  /**
1711
- * Write one credit's expiry release under its row lock, zero the balance it
1712
- * released, and re-assert every condition the unlocked candidate scan tested.
1713
- * Returns undefined when the credit no longer qualifies — funded, drawn
1714
- * against, or already released between the scan and the lock.
1715
- *
1716
- * **Zeroing the balance is what makes the release real**, and it is the whole
1717
- * reason this runs under the lock rather than as a bare INSERT. The row alone
1718
- * records that the caller's claim ended; it does not *end* it. `requestDrain`
1719
- * has no expiry check by design (expiry gates new draws, not reclaims), so a
1720
- * credit whose remainder had been released and reported was still fully
1721
- * drainable: the DVM would pay out money the platform had already booked as
1722
- * the builder's, and `deposited - drawn - drained - released` would go
1723
- * negative on the same micro. With the balance at zero, `requestDrain`'s
1724
- * existing `availableMicro <= 0` guard refuses with `nothing_to_drain` and no
1725
- * new check is needed anywhere. {@link reverseExpiryReleases} puts the
1726
- * balance back when a funding revives the credit.
1589
+ * Recheck eligibility under the credit lock and zero the released balance in the same
1590
+ * transaction as its episode/report. Otherwise requestDrain, which intentionally ignores
1591
+ * expiry, could reclaim value already released to the builder.
1727
1592
  *
1728
- * `status` deliberately stays `active`: a released credit must remain
1729
- * fundable, or the revival rule has nothing to revive.
1730
- *
1731
- * The zeroed balance also takes the credit out of `BITCOIN_CREDIT_PREDICATE`
1732
- * (`balance_micro > 0`), so the in-kind sweep floor stops counting sats the
1733
- * builder no longer owes back — and counts them again after a revival, with
1734
- * the credit's funding lots untouched throughout.
1593
+ * Keep status active so future funding can revive it. Funding lots remain intact: balance
1594
+ * zero excludes this credit from Bitcoin liability, and reversal restores that liability.
1735
1595
  */
1736
1596
  async releaseExpiredCreditLocked(client, credit, nowMs) {
1737
1597
  const expiryMs = toSafeInt(credit.expiry_ms);
@@ -1779,34 +1639,12 @@ var CreditLedger = class {
1779
1639
  return row;
1780
1640
  }
1781
1641
  /**
1782
- * Record this funding's in-kind basis as a lot (internal-review).
1783
- *
1784
- * Called from inside {@link fund}, on the same handle, so the lot shares
1785
- * whatever transaction the rail opened — `withCommitTx` for Cashu,
1786
- * `ProcessedPaymentStore.withTx` for x402/Tempo, `settleInvoice`'s own
1787
- * `BEGIN` for Lightning. The funding-atomicity invariant therefore extends
1788
- * to it structurally rather than by every call site remembering: a lot
1789
- * cannot exist without its ledger credit, or the credit without its lot.
1790
- *
1791
- * Only a **channel-backed** credit writes no lot: it is in-kind already —
1792
- * the escrow itself retains the unused value — so a lot on one would be
1793
- * counted as Bitcoin deposit liability the hub does not hold.
1794
- *
1795
- * Every other non-channel Bitcoin funding writes one, the implicit N=1
1796
- * per-call payment included. That one funds and draws the same amount in
1797
- * this transaction, so it looks like a lot born depleted — but the draw is a
1798
- * *pending hold*, not a debit, and only a `completed` job settles it
1799
- * (`JobManager.settlesDraw`). A failed, cancelled or stale-swept job
1800
- * releases the hold and the whole funded amount becomes reclaimable balance,
1801
- * so suppressing the lot here would leave routine traffic reclaiming off a
1802
- * live rate and missing from the sweep floor. One small insert on the hot
1803
- * path buys the invariant the liability read depends on: open lots cover
1804
- * `balance_micro` for every Bitcoin credit, always.
1642
+ * Record non-channel Bitcoin funding lots on the funding transaction. An implicit payment
1643
+ * also needs a lot: its draw is initially only a hold, and failure releases the whole
1644
+ * balance for reclaim. Channel collateral is excluded from Bitcoin lots.
1805
1645
  *
1806
- * A zero-sats basis still writes a lot. It is honest — the credit exists and
1807
- * we know its rail — and it is what makes "covered but unbacked" a
1808
- * distinguishable state from "no lot at all" for the reclaim and the
1809
- * liability rollup, instead of both reading as a gap.
1646
+ * A zero-sats basis still records a lot, distinguishing covered but basisless credit from a
1647
+ * missing-lot coverage gap.
1810
1648
  */
1811
1649
  async recordFundingLot(q, creditId, amountMicro, basis, nowMs) {
1812
1650
  if (!isNonChannelBitcoinRail(basis.rail)) return;
@@ -1827,25 +1665,13 @@ var CreditLedger = class {
1827
1665
  );
1828
1666
  }
1829
1667
  /**
1830
- * Place a two-phase `pending` hold of `amountMicro` against the credit and
1831
- * assign the next `ledger_seq`. Idempotent on `(creditId, drawId)`: a replay
1832
- * returns the original `{balanceAfterMicro, ledgerSeq}` with
1833
- * `replayed: true` and places no second hold — this is how a caller recovers
1834
- * a lost response (spec condition 1). A replay whose `amountMicro` or
1835
- * `jobId` differs from the recorded draw is not a replay but a money bug in
1836
- * the caller, refused with `draw_conflict`.
1668
+ * Place a pending hold with the next shared per-credit sequence. A matching (creditId,
1669
+ * drawId) replay returns its recorded tuple without another hold; conflicting amount or
1670
+ * jobId is draw_conflict.
1837
1671
  *
1838
- * The replay lookup runs **before** the expiry check, deliberately: a draw
1839
- * placed pre-expiry must stay recoverable after the credit expires, or the
1840
- * DVM holds a debit the caller can never reconcile. Fresh draws on an
1841
- * expired credit are refused with `credit_expired`; the balance stays
1842
- * intact and readable (spec §5: expiry ends spending, never ownership).
1843
- *
1844
- * Pass `tx` (a client inside a caller-owned `BEGIN`) to place the hold in
1845
- * the same transaction as the rail commit and the `fund` upsert (spec
1846
- * condition 3 — the internal-review implicit N=1 path). The ledger issues no
1847
- * transaction control on `tx`; the `SELECT … FOR UPDATE` row lock is still
1848
- * taken on the caller's transaction, so the locking invariant holds.
1672
+ * Replay precedes expiry and settlement gates so a lost response stays recoverable. Fresh
1673
+ * draws require spendable credit. With tx, take the parent-credit lock on the caller’s
1674
+ * transaction without issuing transaction control.
1849
1675
  */
1850
1676
  async draw(args) {
1851
1677
  assertAmount(args.amountMicro, { min: 0 });
@@ -1961,66 +1787,26 @@ var CreditLedger = class {
1961
1787
  return this.withCreditLock(args.creditId, (client, credit) => run(client, credit));
1962
1788
  }
1963
1789
  /**
1964
- * The unresolved refund on `channelId` that must stop this ledger effect, if
1965
- * any (internal-review).
1966
- *
1967
- * `undefined` whenever there is nothing to ask — an unbound credit, or a DVM
1968
- * with no durable settlement store, where a settlement row cannot exist.
1969
- *
1970
- * Public since internal-review, so `/v1/credit`'s pre-payment preflight can ask the
1971
- * same question `fund` will ask from inside the rail transaction — one read,
1972
- * one answer, rather than a second copy of the rule in the routes.
1790
+ * Return the unresolved refund that blocks an effect on this channel, or undefined. Route
1791
+ * preflights use the same rule before moving money; draw and drain also read it under the
1792
+ * credit lock.
1973
1793
  */
1974
1794
  async blockingX402Refund(channelId, statuses) {
1975
1795
  if (!channelId || !this.x402Settlements) return void 0;
1976
1796
  return this.x402Settlements.pendingRefundSettlement(channelId, statuses);
1977
1797
  }
1978
1798
  /**
1979
- * Grow a pending draw by `addAmountMicro` (internal-review) — the ledger half of a
1980
- * mid-job `requestPayment` top-up. One job keeps **one** draw: the mid-job
1981
- * money funds the same credit and enlarges the hold the upfront leg placed,
1982
- * so the receipt's `ReceiptCredit` block countersigns the job's full cost
1983
- * and `revenue_events` still books exactly one row per job.
1984
- *
1985
- * `draw()` cannot express this: a same-`draw_id` call with a different
1986
- * amount is `draw_conflict` by design (a replayed draw must never re-price).
1987
- *
1988
- * The arithmetic is `draw()`'s, unchanged: `addAmountMicro` is checked
1989
- * against — and `balance_after_micro` recomputed from — the balance net of
1990
- * **every** pending hold, this draw's own included. That is deliberately the
1991
- * conservative reading (a fresh draw of `addAmountMicro` would see exactly
1992
- * the same figure), and it is what keeps the total held after growth inside
1993
- * the credit's balance.
1994
- *
1995
- * The increment's rail value comes from {@link growthRailValue}: earmarked
1996
- * to the funding that backs it when the caller names one (the mid-job case),
1997
- * pro-rata otherwise. Either way the draws of a credit keep summing to
1998
- * precisely what the rails paid (internal-review).
1999
- *
2000
- * **`ledger_seq` is not re-taken.** The per-credit sequence is gap-free
2001
- * (`credit_draws_seq_uidx`), so moving this draw forward would strand its
2002
- * original number. The consequence is deliberate and worth knowing: the
2003
- * recorded `balance_after_micro` is the post-growth figure stamped at the
2004
- * *original* seq, so a credit with interleaved sibling activity has a
2005
- * trajectory that is truthful per draw rather than monotonic across seqs.
2006
- *
2007
- * **No expiry check**, matching `settle`/`release`: the job was accepted
2008
- * pre-expiry and is still running. Refusing here would strand the handler
2009
- * mid-flight over a clock the caller can't influence.
1799
+ * Grow an accepted job’s pending hold while preserving its original ledger_seq. The balance
1800
+ * tuple is updated at that sequence, so interleaved draws need not form a monotonic balance
1801
+ * trajectory. No expiry check applies to an already accepted job.
2010
1802
  *
2011
- * **Not idempotent by itself.** Growth carries no key of its own — it
2012
- * inherits the rail's (the accumulator's `(dvm_id, request_id)` UNIQUE rolls
2013
- * the whole transaction back on replay; x402/mpp collide on the
2014
- * `processed_payments` marker). Never call it outside a rail commit.
1803
+ * Available balance excludes every pending hold, including this one. An earmark allocates
1804
+ * the incoming funding’s rail value; absent one, growth uses the pool.
2015
1805
  *
2016
- * **`capMicro` bounds cumulative growth at what the job cumulatively asked
2017
- * (internal-review)**, and it is the only ceiling that can refuse a top-up: the
2018
- * available-balance check cannot, because the `fund` a moment earlier in
2019
- * this same transaction raised the balance by exactly the amount being
2020
- * drawn. Excess is **granted partially or not at all rather than thrown** —
2021
- * this runs inside the rail commit, so a throw would roll back cashu proofs
2022
- * the caller already sent. The refused remainder stays funded balance the
2023
- * caller owns and can drain.
1806
+ * Growth has no independent replay key: compose it with the rail’s replay protection in one
1807
+ * transaction. capMicro bounds cumulative growth at the job’s cumulative ask. Excess
1808
+ * remains caller-owned funded balance; partial or zero growth returns that excess rather
1809
+ * than throwing and rolling back accepted proofs.
2024
1810
  */
2025
1811
  async growDraw(args) {
2026
1812
  assertAmount(args.addAmountMicro, { min: 1 });
@@ -2204,7 +1990,7 @@ var CreditLedger = class {
2204
1990
  }
2205
1991
  /**
2206
1992
  * Release a pending draw: the hold evaporates, the balance is untouched —
2207
- * this is how "no debit on job failure" is mechanically real (spec §1).
1993
+ * this is how "no debit on job failure" is mechanically real (the credit lifecycle contract).
2208
1994
  * Idempotent: releasing a released draw returns the original tuple. A
2209
1995
  * settled draw cannot be released (`invalid_draw_state`) — un-settling
2210
1996
  * booked revenue is a reconciliation problem, not a ledger verb.
@@ -2213,18 +1999,9 @@ var CreditLedger = class {
2213
1999
  return this.resolveDraw({ ...args, to: "released" });
2214
2000
  }
2215
2001
  /**
2216
- * Record a fund-only top-up under its client-generated `fundId` (internal-review).
2217
- * The deposit half of the evidence chain, and the top-up path's idempotency
2218
- * key: `PRIMARY KEY (credit_id, fund_id)` means a concurrent duplicate
2219
- * loses with a typed `funding_replayed` rather than crediting twice.
2220
- *
2221
- * Call it inside the same `tx` as {@link fund} — the rail commit, the
2222
- * funding record, and the balance increment must land together or not at
2223
- * all (spec §2 condition 3). Like `fund`, this issues no transaction
2224
- * control on `tx`; it is a single statement.
2225
- *
2226
- * This does **not** move money on its own. `fund` still does the crediting;
2227
- * this row is what lets a retry be answered with the original outcome.
2002
+ * Record the fund-only replay key without moving money. A duplicate (credit_id, fund_id)
2003
+ * raises funding_replayed. Compose this insert, fund and rail acceptance on the same
2004
+ * caller-owned transaction; a retry reads the recorded funding outcome.
2228
2005
  */
2229
2006
  async recordFunding(args) {
2230
2007
  assertAmount(args.amountMicro, { min: 1 });
@@ -2295,7 +2072,7 @@ var CreditLedger = class {
2295
2072
  return rows[0].receipt;
2296
2073
  }
2297
2074
  /**
2298
- * Record the bolt11 issued for a `(creditId, fundId)` top-up (internal-review), or
2075
+ * Record the bolt11 issued for a `(creditId, fundId)` top-up, or
2299
2076
  * return the one already issued for it.
2300
2077
  *
2301
2078
  * **Returning the existing row is the point.** A caller re-polling an unpaid
@@ -2349,7 +2126,6 @@ var CreditLedger = class {
2349
2126
  }
2350
2127
  return existing;
2351
2128
  }
2352
- /** Read one invoice (no lock). */
2353
2129
  async getInvoice(args) {
2354
2130
  const { rows } = await this.pool.query(
2355
2131
  `SELECT * FROM credit_invoices WHERE credit_id = $1 AND fund_id = $2`,
@@ -2374,22 +2150,10 @@ var CreditLedger = class {
2374
2150
  return rows.map(invoiceRecordFromRow);
2375
2151
  }
2376
2152
  /**
2377
- * Apply an observed Lightning settlement to the ledger — **the exactly-once
2378
- * boundary** (internal-review; spec §2 condition 3 for a rail whose commit happens
2379
- * off-box).
2380
- *
2381
- * One transaction covers the funding record, the balance, and the invoice's
2382
- * own status flip, so the three can never disagree. A crash anywhere inside
2383
- * leaves the invoice `pending` and the balance untouched: the next
2384
- * `lookup_invoice` observes the same settled payment and credits it, once.
2385
- * A replayed check after the commit sees `settled` and moves nothing.
2386
- *
2387
- * The lock is on the **invoice** row, not the credit row: the first
2388
- * Lightning top-up is what opens the credit, so there is frequently no
2389
- * `credits` row to take `FOR UPDATE` yet. That does not weaken the ledger's
2390
- * concurrency contract — no `credit_draws` write happens here, and `fund`'s
2391
- * upsert is a single statement — while still serialising two settlement
2392
- * checks racing on the same invoice.
2153
+ * Serialize settlement on the invoice row, which exists before the first credit does.
2154
+ * Funding record, balance, invoice status and optional deposit outbox commit together. An
2155
+ * interrupted transaction leaves the prior invoice status intact for retry; a settled replay
2156
+ * moves no money.
2393
2157
  */
2394
2158
  async settleInvoice(args) {
2395
2159
  assertFundingBasis(args.basis);
@@ -2483,19 +2247,9 @@ var CreditLedger = class {
2483
2247
  return this.getInvoice(args);
2484
2248
  }
2485
2249
  /**
2486
- * Retire an invoice that **was paid** and can never be credited (internal-review).
2487
- *
2488
- * Distinct from `expired` because the money is the opposite way round: an
2489
- * expired invoice was never paid and owes nobody anything, whereas a blocked
2490
- * one has sats sitting in the builder's wallet with no ledger row to put them
2491
- * on. Only an operator can resolve that, so the row records `reason` and
2492
- * stops being swept — leaving it `pending` would spend one of the sweep's few
2493
- * slots re-deriving the same verdict on every request the caller makes, and
2494
- * three of them would wedge that caller's sweep so a genuinely payable
2495
- * invoice behind them never credits.
2496
- *
2497
- * A CAS on `pending`, like the expiry retirement: a settlement observed
2498
- * concurrently must win.
2250
+ * Mark a paid invoice blocked when its payment cannot be credited. This removes it from the
2251
+ * bounded pending sweep and leaves its reason for operator repair. The pending-only CAS
2252
+ * cannot overwrite a committed settlement.
2499
2253
  */
2500
2254
  async markInvoiceBlocked(args) {
2501
2255
  await this.pool.query(
@@ -2505,7 +2259,6 @@ var CreditLedger = class {
2505
2259
  );
2506
2260
  return this.getInvoice(args);
2507
2261
  }
2508
- /** Read one invoice by the wallet's own identifier for the payment (no lock). */
2509
2262
  async getInvoiceByPaymentHash(paymentHash) {
2510
2263
  const { rows } = await this.pool.query(
2511
2264
  `SELECT * FROM credit_invoices WHERE payment_hash = $1`,
@@ -2514,17 +2267,9 @@ var CreditLedger = class {
2514
2267
  return rows.length > 0 ? invoiceRecordFromRow(rows[0]) : void 0;
2515
2268
  }
2516
2269
  /**
2517
- * The operator's queue: invoices the sweep gave up on (internal-review).
2518
- *
2519
- * **Keyset-paginated, not offset.** Blocked rows are terminal and never
2520
- * self-clear, so a row the operator declines to act on sits at the head of
2521
- * the age ordering forever; a fixed first page would starve everything
2522
- * behind it on every run, which is the internal-review shape one page up.
2523
- *
2524
- * `includeResolved` widens to the operator-resolved statuses so a run can be
2525
- * audited after the fact — the acceptance criterion this verb exists for.
2526
- * Host-wide, like `listPendingDrains`: the ledger has no `dvm_id`, so on a
2527
- * multi-mount host one builder's admin credential reads every mount's rows.
2270
+ * Host-wide operator queue, keyset-paginated so an untouched blocked row does not starve
2271
+ * later rows. includeResolved also exposes reconciled and written-off history; the ledger
2272
+ * has no per-mount dvm_id.
2528
2273
  */
2529
2274
  async listBlockedInvoices(args) {
2530
2275
  const statuses = args.includeResolved ? ["blocked", "reconciled", "written_off"] : ["blocked"];
@@ -2546,37 +2291,15 @@ var CreditLedger = class {
2546
2291
  return rows.map(invoiceRecordFromRow);
2547
2292
  }
2548
2293
  /**
2549
- * Apply a blocked invoice's payment to a credit an operator named — the
2550
- * repair for the one Lightning outcome the DVM cannot fix itself (internal-review).
2551
- *
2552
- * Shaped statement-for-statement on {@link settleInvoice}, because it is the
2553
- * same money doing the same thing a different way: one transaction covering
2554
- * the funding record, the balance, the invoice's status flip and the deposit
2555
- * outbox row, so the four can never disagree. Three deliberate differences:
2556
- *
2557
- * - **The lock is taken on `payment_hash`**, which is UNIQUE and is the
2558
- * identifier the operator actually holds (it is what the
2559
- * `lightning_settlement_blocked` log carries and what names the payment in
2560
- * their wallet). Invoice row first, credit row second via `fund`'s upsert —
2561
- * the same order `settleInvoice` takes, which is what keeps a reconcile and
2562
- * a concurrent settlement check from deadlocking against each other.
2563
- * - **The funding lands at `(targetCreditId, paymentHash)`, not the invoice's
2564
- * own `(credit_id, fund_id)`.** That key is frequently the reason the row
2565
- * is blocked at all — `funding_replayed_on_cashu` means something else
2566
- * already holds it — and the operator's most natural target is that very
2567
- * credit. The payment hash cannot collide with it, and it makes all three
2568
- * references to this payment agree: `basis.fundingRef`, the deposit's
2569
- * `funding_id`, and the funding row's `fund_id`.
2570
- * - **`written_off` is an accepted input status.** A write-off unwound
2571
- * nothing, so an operator who closed a row by mistake must not need raw SQL
2572
- * against a money table to reopen it.
2573
- *
2574
- * The invoice row's status — never the funding row — is the idempotency
2575
- * source of truth. `fund_id` is caller-chosen and the payment hash is
2576
- * disclosed to them, so a caller *can* squat `(theirCredit, thatHash)` with a
2577
- * top-up on another rail; when they have, `recordFunding` throws
2578
- * `funding_replayed` and that surfaces as its own loud failure rather than
2579
- * being absorbed as "already reconciled".
2294
+ * Apply a blocked or written-off invoice to an operator-named credit. Lock the invoice by
2295
+ * its unique payment_hash before funding the credit, matching settlement’s
2296
+ * invoice-to-credit lock order. Funding, balance, resolution and optional outbox share one
2297
+ * transaction.
2298
+ *
2299
+ * The repair funding key is (targetCreditId, paymentHash), distinct from the original
2300
+ * caller-chosen fundId. Invoice status determines replay. A caller can still occupy the
2301
+ * repair key with another payment; funding_replayed must remain a refusal, not be absorbed
2302
+ * as already reconciled. Preserve blocked_reason and the wallet’s settlement instant.
2580
2303
  */
2581
2304
  async reconcileBlockedInvoice(args) {
2582
2305
  assertFundingBasis(args.basis);
@@ -2676,17 +2399,9 @@ var CreditLedger = class {
2676
2399
  }
2677
2400
  }
2678
2401
  /**
2679
- * Record that an operator reviewed a blocked invoice and chose not to credit
2680
- * it (internal-review) — no ledger effect, purely a queue transition.
2681
- *
2682
- * A CAS on `blocked`, and idempotent: re-running returns the recorded row
2683
- * rather than overwriting the first note. It cannot capture a `reconciled`
2684
- * row, because that one moved money.
2685
- *
2686
- * `written` reports whether *this* call was the transition. The caller has
2687
- * no other way to tell — the row reads identically either way — and the
2688
- * write-off's audit log is the only record of the amount, so a re-emitted
2689
- * one reads as a second decision about the same sats.
2402
+ * Write off an invoice without crediting or moving money. Only the first transition records
2403
+ * the note; its written bit controls the audit event. An operator can later reconcile a
2404
+ * written-off invoice, because write-off did not return its payment.
2690
2405
  */
2691
2406
  async writeOffBlockedInvoice(args) {
2692
2407
  const { rowCount } = await this.pool.query(
@@ -2720,7 +2435,6 @@ var CreditLedger = class {
2720
2435
  }
2721
2436
  return out;
2722
2437
  }
2723
- /** Read one draw (no lock), with its credit's rail basis joined in. */
2724
2438
  async getDraw(args) {
2725
2439
  const { rows } = await this.pool.query(
2726
2440
  `${DRAW_WITH_BASIS_SELECT} WHERE d.credit_id = $1 AND d.draw_id = $2`,
@@ -2736,7 +2450,6 @@ var CreditLedger = class {
2736
2450
  );
2737
2451
  return rows.length === 1 ? drawRecordFromRow(rows[0], rows[0]) : void 0;
2738
2452
  }
2739
- /** Read every draw linked to a job, including already-resolved rows. */
2740
2453
  async listDrawsByJobId(jobId) {
2741
2454
  const { rows } = await this.pool.query(
2742
2455
  `${DRAW_WITH_BASIS_SELECT} WHERE d.job_id = $1 ORDER BY d.created_at, d.credit_id, d.ledger_seq`,
@@ -2746,7 +2459,7 @@ var CreditLedger = class {
2746
2459
  }
2747
2460
  /**
2748
2461
  * All pending holds on a credit, in `ledger_seq` order (no lock). Read
2749
- * surface for the internal-review sweeper wiring — a worker that dies mid-job
2462
+ * surface for the sweeper wiring — a worker that dies mid-job
2750
2463
  * leaves its hold `pending` until something releases it.
2751
2464
  */
2752
2465
  async listPendingDraws(creditId) {
@@ -2757,22 +2470,10 @@ var CreditLedger = class {
2757
2470
  return rows.map((row) => drawRecordFromRow(row, row));
2758
2471
  }
2759
2472
  /**
2760
- * One page of pending holds host-wide placed before `createdBeforeMs`,
2761
- * oldest first (no lock). The orphan sweeper's read surface (internal-review): a
2762
- * draw commits with its `job_id` before the job row is persisted, so a crash
2763
- * or a fail-closed refusal in that window strands a hold nothing can ever
2764
- * resolve — the only release path keys off the `credit_id`/`draw_id` written
2765
- * on the job row.
2766
- *
2767
- * Host-wide like {@link listPendingDrains}, not per-credit: the sweeper has
2768
- * no candidate credit to start from. Age-bounded so it never sees a draw
2769
- * whose job row is merely still in flight.
2770
- *
2771
- * `limit` is required and `after` keyset-paginates: an aged hold whose job
2772
- * row *does* exist is legitimately un-sweepable and stays `pending`
2773
- * indefinitely, so a caller that re-issues the same first page would never
2774
- * see past a `limit`-sized wall of them. Pass the last row's
2775
- * {@link StalePendingDrawCursor} to advance.
2473
+ * Page host-wide pending holds older than createdBeforeMs by (created_at, credit_id,
2474
+ * ledger_seq). Use the same compound key for ordering and continuation, preserving
2475
+ * same-millisecond rows. Terminal resolution still rechecks state under the parent-credit
2476
+ * lock.
2776
2477
  */
2777
2478
  async listStalePendingDraws(args) {
2778
2479
  const after = args.after;
@@ -2794,21 +2495,9 @@ var CreditLedger = class {
2794
2495
  return rows.map((row) => drawRecordFromRow(row, row));
2795
2496
  }
2796
2497
  /**
2797
- * Rail-native value the credit an x402 settlement channel funded has
2798
- * actually earned — the sum of its **settled** draws, in the credit's native
2799
- * atomic units (USDC micro on this rail). The batch-settlement claim job's
2800
- * ceiling (internal-review): a channel is claimable up to what its credit's draws
2801
- * have earned, never up to the deposit that funded it.
2802
- *
2803
- * `undefined` when no credit is bound to the channel, which the claim job
2804
- * treats as "cannot be resolved" and claims nothing for — distinct from a
2805
- * resolved credit that has earned `0`.
2806
- *
2807
- * Deliberately a sum over settled draws rather than `funded − remaining`:
2808
- * a drain lowers `native_remaining` without earning anything, so the
2809
- * subtraction would read the caller's own reclaim as revenue. Pending holds
2810
- * are excluded for the mirror reason — a hold is work in flight, and
2811
- * claiming against it would take money a release still owes back.
2498
+ * Sum settled draws’ native value for the credit bound to this x402 channel.
2499
+ * Pending/released holds and drains earn nothing. Return undefined for an unbound channel,
2500
+ * distinct from earned zero, so settlement cannot guess a claim limit.
2812
2501
  */
2813
2502
  async earnedNativeForX402Channel(channelId) {
2814
2503
  const { rows } = await this.pool.query(
@@ -2828,14 +2517,8 @@ var CreditLedger = class {
2828
2517
  return toSafeInt(rows[0].earned);
2829
2518
  }
2830
2519
  /**
2831
- * The credit an x402 settlement channel funded, if one is bound to it — the
2832
- * binding is UNIQUE, so at most one row can answer (internal-review).
2833
- *
2834
- * The operator repair's entry point: a wedged settlement row carries the
2835
- * channel and an `effect_id`, and this is what turns them back into the
2836
- * credit id, the caller pubkey, and the drain id the effect encodes. Deriving
2837
- * the credit id by splitting `effect_id` instead would be wrong — credit ids
2838
- * carry their own colons (`imp:`, `fnd:` and the rail segment inside them).
2520
+ * Resolve the unique credit bound to the channel. The ID is opaque and can contain colons;
2521
+ * do not reconstruct it by splitting settlement effect IDs.
2839
2522
  */
2840
2523
  async getCreditByX402Channel(channelId) {
2841
2524
  const { rows } = await this.pool.query(
@@ -2844,7 +2527,6 @@ var CreditLedger = class {
2844
2527
  );
2845
2528
  return rows[0] ? this.snapshotFromRow(rows[0], this.pool, Date.now()) : void 0;
2846
2529
  }
2847
- /** Retire every active credit backed by a chain-proven lost Tempo channel. */
2848
2530
  async terminalizeTempoCredits(evidence, tx) {
2849
2531
  const channelId = evidence.channelId.toLowerCase();
2850
2532
  const observedAt = evidence.observedAt ?? Date.now();
@@ -2910,7 +2592,6 @@ var CreditLedger = class {
2910
2592
  }
2911
2593
  return this.readTempoCreditLosses(q, { channelId });
2912
2594
  }
2913
- /** List terminal Tempo credit losses, newest observation first. */
2914
2595
  async listTempoCreditLosses(args) {
2915
2596
  return this.readTempoCreditLosses(this.pool, { limit: args?.limit ?? 50 });
2916
2597
  }
@@ -3004,33 +2685,18 @@ var CreditLedger = class {
3004
2685
  }
3005
2686
  return this.readX402CreditLosses(q, { channelId });
3006
2687
  }
3007
- /** List terminal x402 credit losses, newest observation first. */
3008
2688
  async listX402CreditLosses(args) {
3009
2689
  return this.readX402CreditLosses(this.pool, { limit: args?.limit ?? 50 });
3010
2690
  }
3011
- // ── Drains (internal-review) ─────────────────────────────────────────────────
3012
2691
  /**
3013
- * Debit the caller's entire available balance into a drain liability
3014
- * (spec §5: expiry ends spending, never ownership — this is the
3015
- * builder-honored reclaim floor). Runs under the credit row lock: the
3016
- * amount is `balance − pending holds` read under `FOR UPDATE`, the balance
3017
- * is decremented in the same transaction, and the drain takes the next
3018
- * gap-free `ledger_seq` — so the balance zeroes exactly once however many
3019
- * machines race the request.
3020
- *
3021
- * Idempotent on `(creditId, drainId)`: a replay returns the recorded drain
3022
- * (this is also the poll/pickup read). A replay whose `method` or `payout`
3023
- * differs from the recorded drain is refused with `drain_conflict` —
3024
- * silently honouring a changed payout would ship the caller's money to a
3025
- * destination they never signed alongside this `drain_id`.
3026
- *
3027
- * Pending holds are NOT drained — in-flight jobs keep their money until
3028
- * they settle or release. A release after a drain restores available
3029
- * balance the caller reclaims with a **new** `drain_id`; drains are ledger
3030
- * arithmetic, not a terminal credit state, so `status` stays `active`.
3031
- *
3032
- * Deliberately **no expiry check**: draining works before and after
3033
- * expiry. The one thing expiry gates is new draws.
2692
+ * Move all available balance to a drain liability under the credit lock; pending holds stay
2693
+ * reserved. Expiry alone does not prevent reclaim before a sweep releases the balance.
2694
+ * Repeating (creditId, drainId) returns the recorded liability; owner, method and payout
2695
+ * conflicts refuse.
2696
+ *
2697
+ * An unresolved channel refund blocks fresh drain IDs but admits its own effect ID for
2698
+ * repair. Caller-supplied payout validation belongs to the route. With tx, join the channel
2699
+ * transaction without controlling it.
3034
2700
  */
3035
2701
  async requestDrain(args) {
3036
2702
  const nowMs = args.nowMs ?? Date.now();
@@ -3140,7 +2806,6 @@ var CreditLedger = class {
3140
2806
  }
3141
2807
  return this.withCreditLock(args.creditId, run);
3142
2808
  }
3143
- /** Read one drain (no lock), joined with its credit for currency + owner. */
3144
2809
  async getDrain(args) {
3145
2810
  const { rows } = await this.pool.query(
3146
2811
  `SELECT d.*, ${DRAIN_CREDIT_COLUMNS} FROM credit_drains d
@@ -3151,7 +2816,7 @@ var CreditLedger = class {
3151
2816
  return rows.length > 0 ? drainRecordFromJoinRow(rows[0]) : void 0;
3152
2817
  }
3153
2818
  /**
3154
- * One credit's undepleted funding lots, oldest first (internal-review) — the
2819
+ * One credit's undepleted funding lots, oldest first — the
3155
2820
  * public read behind the reclaim's own arithmetic, for anything that needs
3156
2821
  * to show its work.
3157
2822
  */
@@ -3159,25 +2824,11 @@ var CreditLedger = class {
3159
2824
  return this.openLots(this.pool, creditId);
3160
2825
  }
3161
2826
  /**
3162
- * What this DVM owes back in satoshis if every open non-channel Bitcoin
3163
- * credit reclaimed right now (internal-review) — the deposit half of the hub
3164
- * balance, and the floor a payout sweep must never go below.
3165
- *
3166
- * Read entirely off the funding lots, at their own funding rates, with no
3167
- * exchange-rate lookup anywhere: the obligation is in kind, so a rate has
3168
- * nothing to say about it. Pending job holds are deliberately **not**
3169
- * subtracted — a hold either settles into revenue or releases back to the
3170
- * caller, and until it does, treating it as already earned would understate
3171
- * the floor. The figure is conservative by exactly that amount.
3172
- *
3173
- * The two coverage holes ride along rather than being folded in silently, so
3174
- * a gap reads as a gap: `uncovered` is a credit whose lots fall short of its
3175
- * balance, `unbacked` is one holding any open micro with no sats basis. A
3176
- * reclaim on either prices the **whole** balance off a live rate — the
3177
- * depletion refuses a mixture rather than blending two bases — so both are
3178
- * reported as the whole balance, and neither contributes to `depositSats`.
3179
- * A figure that counted the backed half of a mixed credit would be a floor
3180
- * the payout never matches.
2827
+ * Host-wide in-kind Bitcoin deposit liability, excluding channel collateral. Credits with
2828
+ * missing coverage or any basisless lot are reported as gaps and use the live-rate fallback
2829
+ * rather than a guessed sats obligation. Group fiat micro by currency; never sum
2830
+ * denominations.
2831
+ * Pending job holds remain liabilities until settled because they may release to the caller.
3181
2832
  */
3182
2833
  async bitcoinDepositLiability() {
3183
2834
  const { rows } = await this.pool.query(
@@ -3205,21 +2856,43 @@ var CreditLedger = class {
3205
2856
  byRail: [],
3206
2857
  byCurrency: [],
3207
2858
  uncoveredCredits: 0,
3208
- uncoveredMicro: 0,
2859
+ uncoveredByCurrency: [],
3209
2860
  unbackedCredits: 0,
3210
- unbackedMicro: 0
2861
+ unbackedByCurrency: []
3211
2862
  };
3212
2863
  const byRail = /* @__PURE__ */ new Map();
3213
2864
  const byCurrency = /* @__PURE__ */ new Map();
2865
+ const uncoveredByCurrency = /* @__PURE__ */ new Map();
2866
+ const unbackedByCurrency = /* @__PURE__ */ new Map();
3214
2867
  for (const row of rows) {
3215
2868
  const sats = toSafeInt(row.owed_sats);
3216
2869
  const credits = toSafeInt(row.credits);
2870
+ const uncoveredCredits = toSafeInt(row.uncovered_credits);
2871
+ const unbackedCredits = toSafeInt(row.unbacked_credits);
3217
2872
  liability.depositSats += sats;
3218
2873
  liability.credits += credits;
3219
- liability.uncoveredCredits += toSafeInt(row.uncovered_credits);
3220
- liability.uncoveredMicro += toSafeInt(row.uncovered_micro);
3221
- liability.unbackedCredits += toSafeInt(row.unbacked_credits);
3222
- liability.unbackedMicro += toSafeInt(row.unbacked_micro);
2874
+ liability.uncoveredCredits += uncoveredCredits;
2875
+ liability.unbackedCredits += unbackedCredits;
2876
+ if (uncoveredCredits > 0) {
2877
+ const currency2 = uncoveredByCurrency.get(row.currency) ?? {
2878
+ currency: row.currency,
2879
+ micro: 0,
2880
+ credits: 0
2881
+ };
2882
+ currency2.micro += toSafeInt(row.uncovered_micro);
2883
+ currency2.credits += uncoveredCredits;
2884
+ uncoveredByCurrency.set(row.currency, currency2);
2885
+ }
2886
+ if (unbackedCredits > 0) {
2887
+ const currency2 = unbackedByCurrency.get(row.currency) ?? {
2888
+ currency: row.currency,
2889
+ micro: 0,
2890
+ credits: 0
2891
+ };
2892
+ currency2.micro += toSafeInt(row.unbacked_micro);
2893
+ currency2.credits += unbackedCredits;
2894
+ unbackedByCurrency.set(row.currency, currency2);
2895
+ }
3223
2896
  const rail = byRail.get(row.rail) ?? { rail: row.rail, sats: 0, credits: 0 };
3224
2897
  rail.sats += sats;
3225
2898
  rail.credits += credits;
@@ -3235,6 +2908,8 @@ var CreditLedger = class {
3235
2908
  }
3236
2909
  liability.byRail = [...byRail.values()];
3237
2910
  liability.byCurrency = [...byCurrency.values()];
2911
+ liability.uncoveredByCurrency = [...uncoveredByCurrency.values()];
2912
+ liability.unbackedByCurrency = [...unbackedByCurrency.values()];
3238
2913
  return liability;
3239
2914
  }
3240
2915
  /**
@@ -3254,34 +2929,10 @@ var CreditLedger = class {
3254
2929
  return rows.map(drainRecordFromJoinRow);
3255
2930
  }
3256
2931
  /**
3257
- * One page of still-`pending` drains on a channel-backed credit, oldest
3258
- * first (no lock) — the operator's repair queue (internal-review).
3259
- *
3260
- * Keyset-paginated for the x402 settlement queue's reason: a wedged row
3261
- * never self-clears, so a caller that re-issues the same first page would
3262
- * never see past a `limit`-sized wall of them. The keyset is
3263
- * `(created_at, credit_id, drain_id)` — `created_at` is immutable, so no
3264
- * concurrent write can push an unexamined row past the cursor. It is a
3265
- * BIGINT of epoch milliseconds rather than a `TIMESTAMPTZ`, so the
3266
- * microsecond-vs-millisecond cursor trap that bit `listSettlements` cannot
3267
- * arise here: the value round-trips as the integer it already is.
3268
- *
3269
- * `createdBeforeMs` is the staleness floor. A healthy cooperative close
3270
- * moves the drain to `sent` inside the request that opened it, so anything
3271
- * younger is in flight rather than stuck.
3272
- *
3273
- * The method filter is `'tempo'`, and it is the one line here worth a second
3274
- * look: `init()` migrates the pre-internal-review `'mpp'` spelling away and
3275
- * `DrainMethod` no longer carries it, so a query naming it matches nothing a
3276
- * DVM has ever written. It named it anyway until internal-review's rename sweep and
3277
- * internal-review's live rung caught it independently — the whole operator queue
3278
- * read empty in production while every route test stayed green, because
3279
- * those run on `MemoryCreditLedger`, which filters on the typed value. The
3280
- * two implementations of this one predicate must be read together.
3281
- *
3282
- * A written-off row is excluded unless `includeResolved`: the operator has
3283
- * already decided about it, and leaving it on the live queue would re-serve
3284
- * a closed decision on every walk.
2932
+ * Page pending Tempo-channel drains by (created_at, credit_id, drain_id), optionally
2933
+ * age-bounded, excluding written-off rows. Accept the legacy mpp stored method as well as
2934
+ * tempo so pre-migration drains remain repairable. Use the compound key for both ordering
2935
+ * and continuation.
3285
2936
  */
3286
2937
  async listChannelDrains(args) {
3287
2938
  const params = [args.createdBeforeMs, args.limit];
@@ -3307,7 +2958,7 @@ var CreditLedger = class {
3307
2958
  }
3308
2959
  /**
3309
2960
  * Record that an operator reviewed a floor-refused channel drain and is not
3310
- * booking it (internal-review). Books nothing anywhere — no status change, no
2961
+ * booking it. Books nothing anywhere — no status change, no
3311
2962
  * balance movement, no platform report.
3312
2963
  *
3313
2964
  * A single-statement CAS on `written_off_at IS NULL`, so the first note on
@@ -3450,7 +3101,7 @@ var CreditLedger = class {
3450
3101
  * loss, so the caller's re-poll keeps returning the same token either way.
3451
3102
  *
3452
3103
  * Returns `replayed` because this is a **terminal** transition and the money
3453
- * has left at exactly one of these calls (internal-review): the platform drain
3104
+ * has left at exactly one of these calls: the platform drain
3454
3105
  * report must be emitted by that caller and no other. Pass `tx` to write the
3455
3106
  * report through the same transaction as the CAS.
3456
3107
  */
@@ -3497,11 +3148,9 @@ var CreditLedger = class {
3497
3148
  });
3498
3149
  }
3499
3150
  /**
3500
- * Append a countersigned reclaim-event receipt to the drain's evidence
3501
- * trail. A single-statement JSONB append — receipts are emitted after the
3502
- * state transition they attest, and losing one never loses money (the
3503
- * transition is the source of truth; the receipt is the proof the caller
3504
- * can carry away).
3151
+ * Append an opaque supplied receipt observation to the drain’s JSONB evidence array.
3152
+ * Issuance and authentication belong to callers/consumers; this store does not verify
3153
+ * signatures. Preserve append order on replay/poll reads.
3505
3154
  */
3506
3155
  async appendDrainReceipt(args) {
3507
3156
  const q = args.tx ?? this.pool;
@@ -3511,7 +3160,6 @@ var CreditLedger = class {
3511
3160
  [args.creditId, args.drainId, JSON.stringify([args.receipt])]
3512
3161
  );
3513
3162
  }
3514
- // ── Internals ─────────────────────────────────────────────────────────
3515
3163
  async readDrain(q, creditId, drainId) {
3516
3164
  const { rows } = await q.query(
3517
3165
  `SELECT * FROM credit_drains WHERE credit_id = $1 AND drain_id = $2`,
@@ -3519,7 +3167,6 @@ var CreditLedger = class {
3519
3167
  );
3520
3168
  return rows[0];
3521
3169
  }
3522
- /** Re-read the credit join fields for a freshly-updated drain row. */
3523
3170
  async rejoin(q, row) {
3524
3171
  const { rows } = await q.query(
3525
3172
  `SELECT currency, caller_pubkey, tempo_channel_id, x402_channel_id
@@ -3528,7 +3175,6 @@ var CreditLedger = class {
3528
3175
  );
3529
3176
  return drainRecordFromRow(row, rows[0]);
3530
3177
  }
3531
- /** Shared refusal tail for the CAS transitions: idempotent replay or typed error. */
3532
3178
  async refuseTransition(q, creditId, drainId, opts) {
3533
3179
  const row = await this.readDrain(q, creditId, drainId);
3534
3180
  if (!row) {
@@ -3544,26 +3190,10 @@ var CreditLedger = class {
3544
3190
  );
3545
3191
  }
3546
3192
  /**
3547
- * Price a reclaim in kind and say which lots it takes (internal-review).
3548
- *
3549
- * The debits are returned whether or not the reclaim can be priced, and the
3550
- * caller applies them either way: the money is leaving the credit, so the
3551
- * lots have to shrink with it or the deposit-liability rollup would keep
3552
- * counting a balance that is gone.
3553
- *
3554
- * `owedSats` is `null` where {@link isInKindDepletion} refuses — the lots
3555
- * are short of the balance, or the covering lots carry no sats basis (a
3556
- * credit funded before internal-review). That row is priced off a live rate by the
3557
- * admin surface instead, which is what *every* row did before this change,
3558
- * so the fallback is the old behaviour rather than a new failure mode.
3559
- *
3560
- * What it publishes is **net of the delivery reserve** (internal-review): handing a
3561
- * refund over costs a mint fee and, when the notes have to be minted just in
3562
- * time, a Lightning hop. The caller carries that cost per internal-review, and a
3563
- * flat reserve is how they carry it without the figure moving — an
3564
- * actual-fee true-up could only be applied after the promise was made. The
3565
- * debits stay gross because they are denominated in micro and are what
3566
- * `releaseDrain` restores; only the sats figure is netted.
3193
+ * Deplete FIFO lots even when they cannot price the reclaim; stale open lots must not
3194
+ * remain behind a debited balance. Publish owedSats only with complete positive sats
3195
+ * coverage, net of one delivery reserve. Otherwise preserve null for a live-rate fallback.
3196
+ * Returned debits allow an unpaid drain release to restore the exact lots.
3567
3197
  */
3568
3198
  async reclaimInKind(q, creditId, amountMicro) {
3569
3199
  const depletion = depleteLots(await this.openLots(q, creditId), amountMicro);
@@ -3575,7 +3205,7 @@ var CreditLedger = class {
3575
3205
  };
3576
3206
  }
3577
3207
  /**
3578
- * This credit's undepleted funding lots, oldest first (internal-review).
3208
+ * This credit's undepleted funding lots, oldest first.
3579
3209
  *
3580
3210
  * Ordered in SQL on the same `(created_at, lot_id)` key the partial index
3581
3211
  * carries, so FIFO is the index's own order rather than something a reader
@@ -3608,7 +3238,6 @@ var CreditLedger = class {
3608
3238
  );
3609
3239
  }
3610
3240
  }
3611
- /** Shared settle/release state machine — see the public JSDoc on each verb. */
3612
3241
  async resolveDraw(args) {
3613
3242
  if (args.tx) {
3614
3243
  const credit = await this.lockCreditRow(args.tx, args.creditId);
@@ -3718,7 +3347,6 @@ var CreditLedger = class {
3718
3347
  client.release();
3719
3348
  }
3720
3349
  }
3721
- /** `SELECT … FOR UPDATE` the credit row on `q` (which must be inside a transaction). */
3722
3350
  async lockCreditRow(q, creditId) {
3723
3351
  const { rows } = await q.query(
3724
3352
  `SELECT * FROM credits WHERE credit_id = $1 FOR UPDATE`,
@@ -3739,7 +3367,7 @@ var CreditLedger = class {
3739
3367
  /**
3740
3368
  * Held-but-unresolved totals for a credit, in all three units at once — the
3741
3369
  * fiat micro that defines available balance plus the rail-value remainders
3742
- * a new draw allocates against (internal-review). One round-trip, since every
3370
+ * a new draw allocates against. One round-trip, since every
3743
3371
  * caller needs the micro sum anyway.
3744
3372
  */
3745
3373
  async pendingSums(q, creditId) {
@@ -3814,7 +3442,7 @@ function isFundingRail(rail) {
3814
3442
  }
3815
3443
  function assertFundingBasis(basis) {
3816
3444
  if (!basis) {
3817
- throw new CreditLedgerError("invalid_basis", "basis is required (internal-review)");
3445
+ throw new CreditLedgerError("invalid_basis", "basis is required");
3818
3446
  }
3819
3447
  if (!isFundingRail(basis.rail)) {
3820
3448
  throw new CreditLedgerError(