@dvmkit/sdk 0.1.2-rc.7 → 0.1.3-rc.7

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 (45) hide show
  1. package/README.md +14 -0
  2. package/dist/chunk-2K7E3N2D.js +1709 -0
  3. package/dist/{chunk-N4VTG3KH.js → chunk-3ZHMQCYP.js} +645 -3027
  4. package/dist/{chunk-TKA6ZP4M.js → chunk-4B56DEEV.js} +50 -426
  5. package/dist/chunk-BIFLRKMO.js +87 -0
  6. package/dist/chunk-BQ2NMWKE.js +160 -0
  7. package/dist/{chunk-LDTWX7JW.js → chunk-BTZY7VPH.js} +13 -1
  8. package/dist/{chunk-FJDCFHW5.js → chunk-C6JHBLMW.js} +3 -81
  9. package/dist/{chunk-EXHBXA4U.js → chunk-DBCLBYHP.js} +13 -1
  10. package/dist/chunk-EPNDZ5DH.js +1010 -0
  11. package/dist/{chunk-P4RUVDU7.js → chunk-H2MEFVH6.js} +12 -141
  12. package/dist/chunk-KXZUCCEY.js +142 -0
  13. package/dist/{chunk-6JZIX5WW.js → chunk-LLXV32HA.js} +82 -5
  14. package/dist/{chunk-LWUR4CGG.js → chunk-MLRCSJYX.js} +11 -3
  15. package/dist/{chunk-2ABMGUDS.js → chunk-NFRM5QYP.js} +83 -10
  16. package/dist/chunk-RW5LP57K.js +44 -0
  17. package/dist/chunk-YDIYXGYL.js +384 -0
  18. package/dist/{chunk-JGGI65I3.js → chunk-Z4BNLUZF.js} +1 -150
  19. package/dist/{credit-ledger-ED6JXKVD.js → credit-ledger-2DFQHNLB.js} +2 -2
  20. package/dist/{credit-menu-BM4qCD5U.d.ts → credit-menu-C7zAJElJ.d.ts} +1632 -1946
  21. package/dist/{fx-C-liI3oY.d.ts → fx-BF_SG2i0.d.ts} +1 -1
  22. package/dist/index.d.ts +7 -6
  23. package/dist/index.js +8 -4
  24. package/dist/internal/caller.d.ts +10429 -0
  25. package/dist/internal/caller.js +9957 -0
  26. package/dist/internal/index.d.ts +2 -10816
  27. package/dist/internal/index.js +2 -10130
  28. package/dist/internal/server.d.ts +404 -0
  29. package/dist/internal/server.js +202 -0
  30. package/dist/job-store-Bn23V3QU.d.ts +576 -0
  31. package/dist/lightning-backend-C04nH94l.d.ts +367 -0
  32. package/dist/{memory-credit-ledger-XJ5VQEVP.js → memory-credit-ledger-OP24Z2KO.js} +3 -3
  33. package/dist/{postgres-job-store-J5F4GUWU.js → postgres-job-store-TAONYLIF.js} +1 -1
  34. package/dist/{revenue-reporter-JIKUPXOK.js → revenue-reporter-XXSU5KVB.js} +1 -1
  35. package/dist/server/index.d.ts +27 -11
  36. package/dist/server/index.js +93 -69
  37. package/dist/{job-store-C53VQ5uu.d.ts → step-cache-3cT4Shk0.d.ts} +31 -585
  38. package/dist/{tempo-session-store-DALMRIWN.js → tempo-session-store-2JNOKJGX.js} +2 -2
  39. package/dist/testing/index.d.ts +16 -4
  40. package/dist/testing/index.js +7 -3
  41. package/dist/{usd-gLcJB1ps.d.ts → usd-BnuXoFl5.d.ts} +1 -1
  42. package/dist/wallet-CJC8lwxx.d.ts +29 -0
  43. package/dist/{x402-FTG2GRAQ.js → x402-T2C5MX3T.js} +6 -3
  44. package/package.json +6 -2
  45. package/dist/{chunk-RU7SXHLO.js → chunk-UP2F5RRT.js} +3 -3
@@ -1,35 +1,15 @@
1
1
  import { Hono, Context } from 'hono';
2
2
  import { Pool } from 'pg';
3
- import { X as JsonValue, bR as CreditLedgerLike, b_ as CreditInvoiceRecord, d3 as CreditDepositEnqueue, b$ as InvoiceSettlement, Z as ZodLike, a as DVMDescriptor, K as KVStore, o as JobStore, ah as CashuMode, at as MppxServer, aa as X402Config, p as PaymentMethod, cu as ClientCompatibilityGate, a2 as Message, ap as FundingMethod, Y as FundingReceipt, bT as CreditSnapshot, d4 as TopUpCapUnenforcedReason, _ as JobReceipt, cn as AppendOutgoingOptions, S as SDKJobContext, aQ as StepCache, v as ResponseContent, P as PaymentContent, J as JobRecord, a3 as MessageType, ac as X402Receipt, ab as X402ExactVersionSupport, d5 as X402SettlementIntent, aE as PaymentRequirementsV2, c6 as CreditLedgerQuerier, d6 as X402SettlementCursor, bV as X402SettlementStatus, d7 as X402SettlementWriteOff, bS as X402RefundSettlementGate, d8 as X402FacilitatorAuth, d9 as X402BatchSettlementConfig, cK as PostgresX402ChannelStorage, da as X402PayoutObserver, db as X402SettlementReconciliationReason, aC as PaymentRequirements, a1 as MppxCredential, aj as CreditDepositPayload, bU as DrawResult, cA as CreditLedgerError, a7 as ReceiptCredit, al as DrainReceiptEvent, ak as DrainReceipt, z as SignedRequestAudience, dc as CreditDrawReleaseEnqueue, cM as RevenueSkippedNoRailPayload, cs as ClientCompatibility, aF as PayoutReporter, R as ResolvedCreditConfig, g as CreditView } from './job-store-C53VQ5uu.js';
4
- import { F as FxFetcher, b as FxRateSnapshot } from './fx-C-liI3oY.js';
3
+ import { a0 as Message, ag as FundingMethod, W as FundingReceipt, bN as CreditSnapshot, X as JobReceipt, S as SDKJobContext, av as StepCache, u as ResponseContent, P as PaymentContent, a1 as MessageType, bi as X402Receipt, aD as X402Config, bh as X402ExactVersionSupport, cQ as X402SettlementIntent, ap as PaymentRequirementsV2, c0 as CreditLedgerQuerier, cR as X402SettlementCursor, bP as X402SettlementStatus, cS as X402SettlementWriteOff, bM as X402RefundSettlementGate, cT as X402FacilitatorAuth, cU as X402BatchSettlementConfig, cw as PostgresX402ChannelStorage, cV as X402PayoutObserver, bq as MppxServer, aa as CashuMode, bL as CreditLedgerLike, cW as CreditDepositEnqueue, cX as X402SettlementReconciliationReason, an as PaymentRequirements, $ as MppxCredential, o as PaymentMethod, bl as CreditDepositPayload, bO as DrawResult, cm as CreditLedgerError, a5 as ReceiptCredit, ad as DrainReceiptEvent, ac as DrainReceipt, K as KVStore, y as SignedRequestAudience, cY as CreditDrawReleaseEnqueue, cy as RevenueSkippedNoRailPayload, Z as ZodLike, a as DVMDescriptor, bU as CreditInvoiceRecord, bV as InvoiceSettlement, cg as ClientCompatibilityGate, ce as ClientCompatibility, bt as PayoutReporter, R as ResolvedCreditConfig, g as CreditView } from './step-cache-3cT4Shk0.js';
4
+ import { b as FxRateSnapshot, F as FxFetcher } from './fx-BF_SG2i0.js';
5
+ import { g as LockPubkey, f as CheckMintHealthOptions, b as LightningBackend, A as AttestationPayload } from './lightning-backend-C04nH94l.js';
6
+ import { T as TopUpCapUnenforcedReason, A as AppendOutgoingOptions, J as JobRecord, b as JobStore } from './job-store-Bn23V3QU.js';
5
7
  import { ProofLike, SerializedDLEQ } from '@cashu/cashu-ts';
6
8
  import { Challenge } from 'mppx';
7
9
  import { SettleResponse, SupportedResponse } from '@x402/core/types';
8
10
  import { Channel, AutoSettlementConfig } from '@x402/evm/batch-settlement/server';
9
11
  import { FacilitatorClient } from '@x402/core/server';
10
12
 
11
- declare const lockPubkeyBrand: unique symbol;
12
- /**
13
- * NUT-11 P2PK lock pubkey, lowercase-by-construction. Hex-encoded compressed
14
- * secp256k1 pubkey (66 chars starting with `02` / `03`), normalised via
15
- * `toLockPubkey`. The brand exists so any in-memory site that compares
16
- * lock-pubkeys against each other — receive matching, monitor orphan/retired
17
- * lookups, melt-pending keypair maps — gets a compile-time guarantee that
18
- * both sides have been normalised. Raw strings off the wire or out of the DB
19
- * must pass through `toLockPubkey` before they're treated as one.
20
- */
21
- type LockPubkey = string & {
22
- readonly [lockPubkeyBrand]: never;
23
- };
24
- /**
25
- * Brand a raw lock-pubkey string. Trims surrounding whitespace and lowercases
26
- * the hex so equality is structural. Idempotent on values that are already
27
- * lowercase. Does not validate the curve point — assertions about hex shape
28
- * live at the wire-validation layer (e.g. the `/admin/cashu/rotate-lock-pubkey`
29
- * regex in `admin-cashu.ts`).
30
- */
31
- declare function toLockPubkey(raw: string): LockPubkey;
32
-
33
13
  /** A row in the per-DVM `wallet_accumulator` table (internal-review). */
34
14
  interface WalletAccumulatorRow {
35
15
  id: string;
@@ -50,11 +30,10 @@ interface WalletAccumulatorRow {
50
30
  meltedAt: number | null;
51
31
  meltStatus: "melted" | "failed" | "drained" | null;
52
32
  /**
53
- * Mint-issued melt-quote id captured at claim time so a crashed-mid-melt
54
- * row can be auto-resolved on next scheduler boot via the mint's
55
- * `checkMeltQuoteBolt11` endpoint (internal-review). Null on rows from before the
56
- * column existed, or rows that crashed mid-melt before the quote id was
57
- * persisted — those fall through to the manual-`UPDATE` hint path.
33
+ * Mint-issued melt-quote id recorded by the admin `mark-melted` endpoint
34
+ * alongside the payment preimage after an external payout completes. Null
35
+ * until payout audit data is recorded, including on pending rows and legacy
36
+ * rows created before the column existed.
58
37
  */
59
38
  meltQuoteId: string | null;
60
39
  /**
@@ -66,8 +45,8 @@ interface WalletAccumulatorRow {
66
45
  }
67
46
  /**
68
47
  * Subset of `pg.Pool` the store uses for queries. `Pool` and `PoolClient`
69
- * both satisfy this — important so the scheduler can run reads on a single
70
- * client that holds the advisory lock.
48
+ * both satisfy this — important so the accumulator monitor can run reads on a
49
+ * single client that holds the advisory lock.
71
50
  */
72
51
  interface AccumulatorQuerier {
73
52
  query: Pool["query"];
@@ -131,348 +110,6 @@ declare function clearAccumulatorForDvm(db: AccumulatorQuerier, dvmId: string):
131
110
  */
132
111
  declare function hashLockKey(input: string): bigint;
133
112
 
134
- /**
135
- * Canonical deploy-time attestation payload (internal-review). One signature serves
136
- * both auth-on-deploy and the `/v1/info#builder` attestation — the bytes the
137
- * builder signs are the canonical JSON of this object. Making a field required
138
- * also requires the coordinated fleet-before-caller release note in
139
- * `public compatibility guide`.
140
- */
141
- interface AttestationPayload {
142
- /** Immutable platform or self-hosted DVM identifier this attestation applies to. */
143
- dvm_id: string;
144
- /** DVM slug the attestation applies to (must match the deploy request's slug). */
145
- slug: string;
146
- /** sha256(`canonicaliseForSigning(capabilities)`) hex. Binds the attestation
147
- * to the capability shape declared at deploy time. */
148
- capabilities_hash: string;
149
- /** Builder identity x-only secp256k1 pubkey, hex (32 bytes / 64 chars). */
150
- builder_pubkey: string;
151
- /**
152
- * Per-DVM receipt-signing x-only pubkey, hex (internal-review). Binds the key the
153
- * deployed DVM signs job receipts with to the cold builder identity: the
154
- * builder signs this payload, so a receipt verifying under
155
- * `receipt_pubkey` inherits the builder's authority.
156
- *
157
- * Optional: attestations signed before receipts shipped omit it and still
158
- * verify byte-for-byte (canonical JSON drops absent keys), and deploy paths
159
- * that can't provision the matching secret leave it unset rather than
160
- * attesting a key the running process doesn't hold.
161
- */
162
- receipt_pubkey?: string;
163
- /** Unix seconds at which the attestation was signed. */
164
- deployed_at: number;
165
- }
166
- /** Result of {@link generateIdentity}: hex pubkey + hex secret. */
167
- interface BuilderIdentityKeypair {
168
- /** secp256k1 BIP-340 x-only pubkey, 32 bytes hex (64 chars). */
169
- pubkey: string;
170
- /** secp256k1 secret, 32 bytes hex (64 chars). */
171
- secret: string;
172
- }
173
- /** Generate a fresh BIP-340 Schnorr keypair for builder identity. */
174
- declare function generateIdentity(): BuilderIdentityKeypair;
175
- /**
176
- * Read the builder identity secret from `~/.dvmkit/builder.key`. Returns
177
- * `null` when absent so callers can surface an `identity_required` error.
178
- *
179
- * `writeIdentity` persists the key `0o600`, but a destination mode survives
180
- * `cp`/`scp`/git-checkout intact only by luck — those can land it `0o644`,
181
- * leaving the signing key world-readable on a multi-user box. On POSIX we
182
- * detect loose group/other bits, warn, and best-effort re-tighten to `0o600`
183
- * (same self-heal posture as `writeIdentity`) rather than throw — a hard
184
- * failure would brick a CLI whose key got widened in transit.
185
- */
186
- declare function loadIdentitySecret(): string | null;
187
- /**
188
- * Persist a builder identity keypair: pubkey into `~/.dvmkit/builder.json`
189
- * (top-level), secret into `~/.dvmkit/builder.key` (mode 0600 on POSIX).
190
- * Atomic tmp+rename on the secret file so a crash mid-write never leaves a
191
- * truncated key on disk — a torn write here is unrecoverable; the operator
192
- * loses the ability to re-sign attestations for every DVM under this key.
193
- *
194
- * Refuses to overwrite when an identity already exists unless `force` is
195
- * passed; the force path warns that any DVMs deployed under the old key
196
- * need to be re-deployed to refresh their attestations.
197
- */
198
- declare function writeIdentity(keypair: BuilderIdentityKeypair, opts?: {
199
- force?: boolean;
200
- }): void;
201
- /**
202
- * Compute the canonical `capabilities_hash` (sha256-hex of canonical JSON)
203
- * for the supplied capabilities block. Shared between the CLI signer and any
204
- * future verifier so both compute identical bytes.
205
- */
206
- declare function hashCapabilities(capabilities: JsonValue): string;
207
- /**
208
- * Assemble the deploy-time attestation payload (internal-review). `deployedAt`
209
- * defaults to the current Unix second; tests can pin it for reproducibility.
210
- */
211
- declare function buildAttestation(args: {
212
- dvmId: string;
213
- slug: string;
214
- capabilities: JsonValue;
215
- builderPubkey: string;
216
- /** Receipt-signing pubkey to attest (internal-review). Omitted when the deploy path
217
- * can't provision the matching `DVMKIT_RECEIPT_KEY`. */
218
- receiptPubkey?: string;
219
- deployedAt?: number;
220
- }): AttestationPayload;
221
- /**
222
- * Sign an attestation payload with the builder identity secret. Returns the
223
- * 64-byte Schnorr signature as hex. The canonical bytes are produced via the
224
- * same `canonicaliseForSigning` the verifier uses — both sides hash identical
225
- * input regardless of object key order.
226
- */
227
- declare function signAttestation(payload: AttestationPayload, secretHex: string): string;
228
- /**
229
- * Verify a deploy-time attestation. Returns `true` iff the signature is a
230
- * valid BIP-340 Schnorr signature of `canonicaliseForSigning(payload)` under
231
- * `pubkeyHex`. Independent of the platform binding — callers (the deploy
232
- * route, `/v1/info` clients) cross-check the pubkey against their trust root.
233
- */
234
- declare function verifyAttestation(payload: AttestationPayload, signatureHex: string, pubkeyHex: string): boolean;
235
-
236
- /**
237
- * Per-method amount limits a mint advertises on its NUT-4 / NUT-5 bolt11/sat
238
- * entry (internal-review). Both values are in the method's own unit — `sat` here,
239
- * never msats.
240
- *
241
- * `null` means the mint advertised nothing in that direction, which reads as
242
- * unbounded. A mint saying nothing is not a mint saying zero, so silence never
243
- * narrows what we'll attempt — same defensive posture as the absent-`disabled`
244
- * rule in {@link canMintBolt11Sat}.
245
- */
246
- interface MintAmountBounds {
247
- /** Smallest amount the mint will quote, or `null` when unadvertised. */
248
- minSats: number | null;
249
- /** Largest amount the mint will quote, or `null` when unadvertised. */
250
- maxSats: number | null;
251
- }
252
- /** Error categories returned when a mint health probe fails. */
253
- type HealthErrorCode = "timeout" | "http_error" | "network" | "parse_error" | "missing_fields";
254
- /** Outcome of a single health probe against one mint. */
255
- interface MintHealthCheckResult {
256
- /** Mint URL exactly as passed in (not normalised). */
257
- mintUrl: string;
258
- /** True iff `/v1/info` returned 200, parsed as JSON, and had `pubkey` + `version`. */
259
- ok: boolean;
260
- /** Wall-clock duration of the successful (or final failed) attempt in ms. */
261
- latencyMs: number;
262
- /** NUT-06 mint pubkey when present. */
263
- pubkey?: string;
264
- /** NUT-06 mint version string (e.g. "Nutshell/0.20.0") when present. */
265
- version?: string;
266
- /** Sorted list of NUT keys the mint declares support for. */
267
- supportedNuts?: string[];
268
- /** Subset of `DEFAULT_REQUIRED_NUTS` the mint does not declare. Empty array when fully supported. */
269
- missingRequiredNuts?: string[];
270
- /** True when the mint advertises NUT-04 with method `bolt11` and unit `sat`. */
271
- bolt11SatMintSupported?: boolean;
272
- /** True when the mint advertises NUT-05 with method `bolt11` and unit `sat`. */
273
- bolt11SatMeltSupported?: boolean;
274
- /**
275
- * True when the mint can currently *issue* — NUT-04 advertises bolt11/sat and
276
- * is not switched off (`disabled: true`). Distinct from
277
- * {@link bolt11SatMintSupported}, which only asks whether the method exists:
278
- * a mint in melt-only recovery keeps advertising the method and turns the
279
- * flag on. Consulted when picking a funding default (internal-review).
280
- */
281
- mintingEnabled?: boolean;
282
- /** Same question for NUT-05 (melt) — whether the mint can currently pay out. */
283
- meltingEnabled?: boolean;
284
- /**
285
- * True when the mint can currently serve `/v1/swap` — see {@link canSwap} for
286
- * why an unadvertised NUT-3 reads as `true`. This is what a per-call spend
287
- * needs, so a `false` here is the reason a spend routes around this mint
288
- * (internal-review).
289
- */
290
- swapEnabled?: boolean;
291
- /**
292
- * Amount limits the mint advertises for issuing (NUT-04 bolt11/sat), in sats
293
- * (internal-review). Consulted when picking a funding default and when refusing an
294
- * amount locally, so a fund outside the range never opens a Lightning quote.
295
- */
296
- mintAmountBounds?: MintAmountBounds;
297
- /** The NUT-05 twin — limits on a single payout, which `dvm wallet cash-out` sizes against. */
298
- meltAmountBounds?: MintAmountBounds;
299
- /**
300
- * True when the mint passes the full {@link assertNutSupport} gate — every
301
- * required NUT present (not `supported: false`) and bolt11/sat on NUT-4 +
302
- * NUT-5. The runtime `MintHealthTracker` treats a mint as healthy only when
303
- * this is true, reproducing the boot-time NUT gate post-`listen` (internal-review).
304
- */
305
- nutCompliant?: boolean;
306
- /** Raw `nuts` object from the `/v1/info` response. Used by `assertNutSupport`. */
307
- nutsRaw?: Record<string, unknown>;
308
- /** Populated when `ok` is false. */
309
- error?: {
310
- code: HealthErrorCode;
311
- message: string;
312
- };
313
- }
314
- /** Tunables for `checkMintHealth`. */
315
- interface CheckMintHealthOptions {
316
- /** Per-attempt timeout in ms. Default: 5000. */
317
- timeoutMs?: number;
318
- /** Total attempts including the first. Default: 3. */
319
- retries?: number;
320
- /** Inter-attempt backoff schedule in ms. Default: [200, 500, 1000]. */
321
- backoffMs?: number[];
322
- /** Injected `fetch` for tests. Default: global fetch. */
323
- fetchImpl?: typeof fetch;
324
- }
325
- /**
326
- * Probe a Cashu mint's `/v1/info` endpoint with timeout + retries, and
327
- * report whether it serves the NUT-04 + NUT-05 capabilities first-party DVMs
328
- * depend on. Never throws — failures are returned as `{ ok: false, error }`.
329
- */
330
- declare function checkMintHealth(mintUrl: string, opts?: CheckMintHealthOptions): Promise<MintHealthCheckResult>;
331
- /**
332
- * Whether the mint can currently serve `/v1/swap` — the operation every
333
- * per-call Cashu payment depends on (internal-review).
334
- *
335
- * **An absent NUT-3 means yes.** Swap is a baseline mint operation and healthy
336
- * mints do not enumerate it — neither coinos nor lnvoltz advertises a `"3"` key
337
- * (see `public design rationale`).
338
- * Reading silence as "incapable" would rule out every working mint.
339
- *
340
- * A mint that has *switched swaps off* does say so: a mint in melt-only
341
- * recovery publishes a `"3"` entry flagged `disabled` / `supported: false`
342
- * alongside its live NUT-5. Both spellings are honoured because this is the
343
- * only signal distinguishing "can't swap" from "swap failed for some other
344
- * reason", and it costs nothing to accept either.
345
- *
346
- * Never a boot-time gate — like {@link canMintBolt11Sat}, this informs mint
347
- * *selection* only, so a mint that can still melt stays loadable for cash-out.
348
- */
349
- declare function canSwap(nuts: Record<string, unknown>): boolean;
350
- /** Whether `sats` falls inside a mint's advertised limits, and which end it broke. */
351
- type AmountBoundsVerdict = "ok" | "below_min" | "above_max";
352
- /**
353
- * Judge one amount against one direction's limits. The single predicate both
354
- * `dvm wallet fund` (mint selection, pre-quote refusal) and
355
- * `dvm wallet cash-out` (per-leg sizing) ask, so the two commands can't drift
356
- * on what "out of range" means.
357
- */
358
- declare function amountBoundsVerdict(sats: number, bounds?: MintAmountBounds): AmountBoundsVerdict;
359
- /**
360
- * Assert that a mint's NUT-06 `nuts` object advertises all required capabilities.
361
- * Two-phase check:
362
- * 1. Dict-level — each NUT in `requiredNuts` must be present and not have
363
- * `supported: false`. Throws naming the missing NUT numbers and mint URL.
364
- * 2. Method-level — NUT-4 and NUT-5 must each advertise a `bolt11`/`sat`
365
- * entry in their `methods` array (covers NUT-23). Throws naming the
366
- * missing method.
367
- */
368
- declare function assertNutSupport(mintUrl: string, nuts: Record<string, unknown>, requiredNuts?: readonly number[]): void;
369
-
370
- /** A Lightning invoice created by the backend. */
371
- interface CreatedInvoice {
372
- /** BOLT11 payment request string. */
373
- bolt11: string;
374
- /** Payment hash (hex) — primary key for looking up settlement. */
375
- paymentHash: string;
376
- /** Amount in millisatoshis. */
377
- amountMsats: number;
378
- /** Unix milliseconds when the invoice expires. */
379
- expiresAt: number;
380
- }
381
- /** Result of looking up an invoice's settlement status. */
382
- interface InvoiceStatus {
383
- /** True once the invoice has been paid and preimage is known. */
384
- settled: boolean;
385
- /** Preimage (hex) — present only after settlement. */
386
- preimage?: string;
387
- /** Unix milliseconds when the invoice settled (if settled). */
388
- settledAt?: number;
389
- /** Unix milliseconds when the invoice expires. */
390
- expiresAt?: number;
391
- }
392
- /** How a payment reached a settled state — see `payInvoiceReconciling`. */
393
- type LightningPayOutcome = "paid" | "reconciled" | "republished";
394
- /** Settled payment plus the audit trail of how it got there. */
395
- interface LightningPayment {
396
- /** Payment preimage (hex) — proof the invoice was paid. */
397
- preimage: string;
398
- /** Routing fee reported by the wallet, in millisatoshis. Absent means unknown. */
399
- feesPaidMsats?: number;
400
- /** Whether the wallet answered directly, or the outcome had to be reconciled. */
401
- outcome: LightningPayOutcome;
402
- }
403
- /** Bounds for a read-only wallet transaction snapshot. */
404
- interface LightningTransactionListOptions {
405
- /** Include transactions created at or after this Unix timestamp (seconds). */
406
- from?: number;
407
- /** Maximum number of transactions the wallet may return. */
408
- limit?: number;
409
- }
410
- /**
411
- * Credential-safe transaction evidence used to account for a wallet debit.
412
- *
413
- * Invoice, payment hash and preimage are deliberately replaced by digests or
414
- * omitted. `fingerprint` identifies the complete reduced row; `invoiceHash`
415
- * lets a caller bind an outgoing row to the invoice it just paid.
416
- */
417
- interface LightningTransactionSnapshot {
418
- fingerprint: string;
419
- invoiceHash?: string;
420
- type: "incoming" | "outgoing";
421
- amountMsats: number;
422
- /** Wallet-reported routing fee. Absent when the wallet omitted it. */
423
- feesPaidMsats?: number;
424
- createdAt: number;
425
- settledAt?: number;
426
- }
427
- /** What a backend can tell us about the wallet behind it. */
428
- interface LightningWalletInfo {
429
- /** Human-readable wallet name (e.g. "Alby Hub"). */
430
- alias?: string;
431
- /** Operations the wallet supports (e.g. `pay_invoice`, `lookup_invoice`). */
432
- methods: string[];
433
- /**
434
- * Budget fields the wallet volunteered, if any.
435
- *
436
- * No rail behind this interface is required to expose a connection's
437
- * spending cap, and NWC — today's only implementation — has no method that
438
- * returns one. Present it when a wallet offers it, never infer it.
439
- */
440
- walletReportedBudget?: Record<string, unknown>;
441
- }
442
- /** Abstraction over Lightning wallet backends (NWC, future). */
443
- interface LightningBackend {
444
- /**
445
- * Pay a bolt11 invoice exactly once. Returns the preimage on success.
446
- *
447
- * Implementations must never re-send a payment on an unknown outcome without
448
- * first reconciling against the wallet — the rails behind this interface have
449
- * no idempotency key, so a blind retry is a second payment.
450
- *
451
- * `amountMsats` is passed when known (callers always have it from the
452
- * provider's payment-request). Backends that don't need it may ignore the
453
- * param.
454
- */
455
- payInvoice(bolt11: string, amountMsats?: number): Promise<LightningPayment>;
456
- /** Get wallet balance in millisatoshis. */
457
- getBalance(): Promise<number>;
458
- /** Get wallet info (name, supported methods, any volunteered budget fields). */
459
- getInfo(): Promise<LightningWalletInfo>;
460
- /** Create a new incoming invoice for the given amount and description. */
461
- createInvoice(params: {
462
- amountMsats: number;
463
- description?: string;
464
- expirySeconds?: number;
465
- }): Promise<CreatedInvoice>;
466
- /** Look up an invoice's settlement status by payment hash. */
467
- lookupInvoice(paymentHash: string): Promise<InvoiceStatus>;
468
- /**
469
- * Return a bounded, credential-safe transaction snapshot when supported.
470
- * Optional so existing backends remain source-compatible; without it an
471
- * exact total wallet debit cannot be attributed.
472
- */
473
- listTransactions?(options: LightningTransactionListOptions): Promise<LightningTransactionSnapshot[]>;
474
- }
475
-
476
113
  /**
477
114
  * Tracks MPP challenge ids that have already been consumed by an upfront-flow
478
115
  * request, scoped per-DVM realm. Entries auto-expire after a TTL and the store
@@ -525,230 +162,229 @@ interface CreditExpirySweepOpts {
525
162
  limit?: number;
526
163
  }
527
164
 
528
- /** Cached liveness signal for a platform-hosted DVM's Lightning receive rail. */
529
- interface LightningRailHealth {
530
- /** Whether the receive rail may be advertised at this instant. */
531
- available(): boolean;
532
- /** Prime the cached signal at boot when the source supports it. */
533
- refresh?(): Promise<void>;
165
+ /** Promise resolver for a pending client prompt response. */
166
+ interface PromptResolver {
167
+ resolve: (value: ResponseContent) => void;
168
+ reject: (reason: Error) => void;
534
169
  }
535
-
536
- /** Builder-facing wiring for the Lightning receive leg (internal-review). */
537
- interface LightningReceiveConfig {
170
+ /** Promise resolver for a pending client payment. */
171
+ interface PaymentResolver {
172
+ resolve: (value: PaymentContent) => void;
173
+ reject: (reason: Error) => void;
174
+ }
175
+ /** In-memory representation of a running job. */
176
+ interface ServerJob {
177
+ id: string;
178
+ tags: string[];
538
179
  /**
539
- * Receive-only NWC connection URI (`DVMKIT_NWC_RECEIVE_URI`). Validated at
540
- * boot: a connection that can *spend* is refused outright.
180
+ * Capability name this job belongs to (internal-review). Set at submission from
181
+ * `POST /v1/job` body's `capability` field; routes the SDK runtime to the
182
+ * right `onJob` / `onResponse` / `onPayment` handler and the right input
183
+ * schema. Required — the wire layer rejects requests that omit it.
541
184
  */
542
- uri?: string;
185
+ capability: string;
186
+ input: string;
187
+ params: Record<string, string>;
188
+ requesterId: string;
543
189
  /**
544
- * Pre-built backend, bypassing URI parsing and the connect-time probe. Test
545
- * seam only — production always goes through `uri` so the receive-only
546
- * property is actually checked.
190
+ * SHA-256 hex of the per-job opaque `job_token` minted at creation for
191
+ * anonymous callers (internal-review). Mirror of `JobRecord.requesterTokenHash`;
192
+ * see that field for the wire-level gating semantics.
547
193
  */
548
- backend?: LightningBackend;
549
- /** Invoice lifetime in seconds. Defaults to {@link DEFAULT_INVOICE_TTL_SECONDS}. */
550
- invoiceTtlSeconds?: number;
194
+ requesterTokenHash?: string;
551
195
  /**
552
- * Smallest funding this deployment can receive over Lightning, in sats
553
- * (`DVMKIT_LIGHTNING_FUNDING_MIN_SATS`). The floor is a property of the
554
- * receive wallet's channel policy (`htlc_minimum_msat` upstream), so it is
555
- * per-deployment and sats-physical — the menu renders it into fiat with a
556
- * margin, but everything that *enforces* it compares raw sats. Defaults to
557
- * {@link DEFAULT_LIGHTNING_FUNDING_MIN_SATS}.
196
+ * Submission provenance (internal-review) — mirrors `JobRecord.requesterToken` /
197
+ * `requestFingerprint` / `requesterPubkey`. Carried on the job purely so
198
+ * `toJobRecord` persists it; nothing in the runtime reads it. It's the
199
+ * replay gate: a retried paid submit that reproduces the fingerprint (and,
200
+ * on a signed DVM, the pubkey) is handed this token back.
558
201
  */
559
- fundingMinSats?: number;
202
+ requesterToken?: string;
203
+ requestFingerprint?: string;
204
+ requesterPubkey?: string;
205
+ requestId?: string;
206
+ /** HTTP path independently recorded when the caller proof was accepted. */
207
+ authRequestPath?: string;
208
+ status: "processing" | "completed" | "failed" | "awaiting-input" | "cancelled" | "working";
209
+ summary?: string;
210
+ messages: Message[];
211
+ seq: number;
212
+ paidMsats: number;
213
+ paymentMint?: string;
214
+ /** Rail of the most recent successful credit. Set on each credit branch in payment processing. */
215
+ paymentRail?: FundingMethod;
560
216
  /**
561
- * Optional platform-derived channel-liveness gate. Platform-hosted DVMs use
562
- * it alongside their NWC reachability probe; self-hosted DVMs omit it.
217
+ * Settlement reference for the most-recent successful credit. Threaded
218
+ * into `onJobCompleted` so the platform revenue ledger can populate
219
+ * `revenue_events.tx_hash`. mpp/x402 carry the credential's `challenge.id`
220
+ * / EVM tx hash from the facilitator (internal-review); cashu accumulator carries
221
+ * the `X-Cashu-Request-Id` UUID (internal-review). Updated on every credit
222
+ * (upfront or mid-job via `processIncomingPayment`) — internal-review.
223
+ * Single-row-per-job accounting keeps the most recent credit's hash,
224
+ * mirroring `paymentRail`.
563
225
  */
564
- railHealth?: LightningRailHealth;
565
- }
566
- /** Default bolt11 lifetime — long enough to pay by hand, short enough to retire. */
567
- declare const DEFAULT_INVOICE_TTL_SECONDS = 900;
568
- /**
569
- * Floor on the invoice lifetime: twice the signed-request drift window, so the
570
- * bolt11 always outlives the funding negotiation that produced it (the issue's
571
- * "invoice expiry ≥ the funding-negotiation window"). A shorter one would
572
- * expire inside the caller's own retry budget and strand them mid-top-up.
573
- */
574
- declare const MIN_INVOICE_TTL_SECONDS = 600;
575
- /**
576
- * The builder-side Lightning receive leg (internal-review, credits spec §4).
577
- *
578
- * Issues a bolt11 over a **receive-only** NWC connection while the DVM is
579
- * awake serving the 402, and credits the ledger when a later request observes
580
- * settlement. Two properties are load-bearing:
581
- *
582
- * - **It never holds a send credential.** `pay_invoice` on this connection is
583
- * refused at construction, so a compromised DVM can mint invoices and
584
- * nothing else. The drain/refund sender (internal-review) is a separate, budgeted
585
- * connection by rule.
586
- * - **Crediting is pull-based — there is no settlement watcher.** The caller's
587
- * next request drives `lookup_invoice`, which is what makes this work on a
588
- * suspend-to-zero fleet: a machine that is asleep has nothing to miss.
589
- * Wallet downtime at that moment delays crediting; it never loses money,
590
- * because the invoice→credit binding is a durable row.
591
- *
592
- * Reachability is **cached**, never probed per request: the funding menu is
593
- * assembled on every quote and every 402, so a live NIP-47 round trip there
594
- * would put a nostr relay in the latency path of every priced call.
595
- */
596
- declare class LightningReceive {
597
- private readonly source;
598
- readonly invoiceTtlSeconds: number;
599
- readonly fundingMinSats: number;
600
- private health;
601
- private checkedAtMs;
602
- private refreshing;
603
- private permissionRefusal;
604
- private constructor();
605
- private readonly railHealth;
226
+ paymentTxHash?: string;
227
+ /** Chain transaction returned to callers recovering x402 or Tempo acceptance. */
228
+ paymentTransactionHash?: string;
606
229
  /**
607
- * Validate the configured connection and build the receive leg, or return
608
- * `undefined` when this DVM didn't configure one.
609
- *
610
- * **Throws on a credential that is wrong, degrades on one that is merely
611
- * unreachable.** A wallet outage at boot must not stop a DVM whose other
612
- * rails are fine — the menu simply omits `lightning` until a later probe
613
- * succeeds. A connection that can spend, or one that can't state what it can
614
- * do, is a different thing entirely: it is a standing money risk that no
615
- * amount of retrying fixes, so it fails the boot loudly with the fix in the
616
- * message.
230
+ * Rail-native amount accumulated across all successful credits on this
231
+ * job (sats for mpp, USDC microunits for x402). The single
232
+ * `revenue_events` row written at completion uses this as `gross_native`,
233
+ * so multi-credit jobs sum the per-credit native amounts (internal-review).
234
+ * internal-review wired the upfront credit; mid-job credits accumulate same-rail
235
+ * and reset on a cross-rail switch (see `processIncomingPayment`).
617
236
  */
618
- static create(config: LightningReceiveConfig): Promise<LightningReceive | undefined>;
237
+ nativeAmount?: number;
238
+ /** Native asset tag — paired with `nativeAmount`. internal-review. */
239
+ nativeAsset?: "sats" | "usdc" | "usdc.e" | "usd-cents";
240
+ /** Cashu flow discriminator written into `revenue_events.metadata.cashu_flow` (internal-review). */
241
+ cashuFlow?: "p2pk_accumulator";
242
+ /** Credit the upfront payment funded/drew (internal-review). See `JobRecord.creditId`. */
243
+ creditId?: string;
244
+ /** The draw placed for this job on `creditId` (internal-review). */
245
+ drawId?: string;
246
+ /** Original funding artifact returned by an explicit fund-and-draw. */
247
+ fundingReceipt?: FundingReceipt;
248
+ fundingCredit?: CreditSnapshot;
249
+ receivedProofs: ProofLike[];
250
+ pendingPaymentMsats?: number;
619
251
  /**
620
- * Whether `lightning` belongs on the funding menu right now.
621
- *
622
- * Reads the cached observation and never blocks; a stale one kicks off a
623
- * background refresh and answers with what we last knew. Advertising a rail
624
- * whose wallet is down would hand the caller an option that 402s on use —
625
- * the §4 posture is to drop it and let the sale survive on the others.
252
+ * Per-job binding for MPP credentials. Contains the `challenge.id` of every
253
+ * mppx challenge issued in the most-recent `requestPayment` yield. mppx HMAC
254
+ * binds challenges to realm/method/amount/expiry, not to the dvmkit job-id;
255
+ * verifying the incoming credential's `challenge.id` is in this set blocks
256
+ * a credential lifted from a sibling job that requested the same fields.
257
+ * Cleared on successful credit (internal-review).
626
258
  */
627
- available(): boolean;
259
+ pendingMppChallengeIds?: string[];
628
260
  /**
629
- * Issue (or re-issue) the bolt11 funding `(creditId, fundId)`.
630
- *
631
- * The invoice is minted **for** the credit and amount named in the caller's
632
- * signed body — that binding is the internal-review condition-3 commitment on this
633
- * rail, and it is why no artifact hash rides the request: there is no
634
- * caller-supplied artifact to hash. A re-poll returns the stored row rather
635
- * than minting again; a second bolt11 for one `fund_id` would leave two
636
- * payable invoices against a funding that can only be credited once.
637
- */
638
- issue(ledger: CreditLedgerLike, args: {
639
- creditId: string;
640
- fundId: string;
641
- callerPubkey: string;
642
- currency: string;
643
- amountMicro: number;
644
- amountMsats: number;
645
- description?: string;
646
- }): Promise<CreditInvoiceRecord>;
261
+ * Per-job binding for x402 mid-job credentials (internal-review). 32-byte 0x-hex
262
+ * nonce issued by `requestPayment` and stamped onto the outbound x402
263
+ * envelope; the caller signs `TransferWithAuthorization` against this exact
264
+ * `bytes32`. Verification rejects an incoming `x402_payment` whose decoded
265
+ * authorization nonce differs, blocking a cross-job replay before the
266
+ * facilitator settles. Cleared on successful credit.
267
+ */
268
+ pendingX402Nonce?: string;
647
269
  /**
648
- * Ask the wallet whether one payment hash is paid, and when (internal-review).
649
- *
650
- * The operator's reconcile verb runs this before it credits anything. A
651
- * `blocked` row implies payment — {@link applyOne} returns above the block
652
- * classification when the lookup says unpaid — but that is *this fleet's
653
- * belief, recorded possibly weeks ago*, and the verb it gates mints balance
654
- * against it. One round trip turns the belief into a fact and recovers the
655
- * true settlement instant for `settled_at`, which a blocked row never got to
656
- * write.
657
- *
658
- * Unlike {@link settlePending} this **throws**: there is no request whose
659
- * latency it protects, and crediting on an unverifiable wallet is exactly
660
- * what it exists to prevent. `NOT_FOUND` is the one exception, and it is not
661
- * an outage — it is the wallet saying it has never seen this hash, the same
662
- * reading {@link applyOne} and the caller-side reconcile in `src/lib/nwc.ts`
663
- * take. It comes back as `known: false` because the operator's repair for it
664
- * ("you are pointed at a different wallet than the one that minted this")
665
- * is not the repair for an unpaid invoice, and certainly not for a retry.
270
+ * Exact USDC microunit amount paired with `pendingX402Nonce`. Kept as a
271
+ * decimal string so the EIP-3009 uint256 value remains lossless.
666
272
  */
667
- lookupSettlement(paymentHash: string): Promise<{
668
- settled: boolean;
669
- settledAt?: number;
670
- known: boolean;
671
- }>;
273
+ pendingX402AmountUsdcMicro?: string;
672
274
  /**
673
- * Consult the wallet about this caller's outstanding invoices and credit the
674
- * ones that settled — the pull half of pull-based crediting.
675
- *
676
- * The pending rows are read locally first, so the common case (nothing
677
- * outstanding) costs one indexed SELECT and no network at all. Bounded to
678
- * {@link MAX_SETTLE_CHECKS} invoices on a short deadline, and **never throws
679
- * into the request**: a wallet failure here means the caller's balance is
680
- * merely not updated yet, which the ordinary `insufficient_credit` path
681
- * already states honestly.
682
- *
683
- * **Each invoice gets its own failure boundary.** The sweep window is a few
684
- * rows wide, so a row that fails on its own terms — a `fund_id` the caller
685
- * reused on another rail, a credit someone else opened first — must not take
686
- * the rest of the pass down with it: the next invoice in the window may be
687
- * the paid one, and it would never be looked up. Only a transport failure
688
- * ends the pass early, because there the wallet itself is gone and the
689
- * remaining lookups would just spend the caller's latency confirming it.
275
+ * Fiat micro-units the outstanding `requestPayment` ask is worth, pinned when
276
+ * the payment-request was emitted (internal-review). See `JobRecord`.
690
277
  */
691
- settlePending(ledger: CreditLedgerLike, args: {
692
- callerPubkey: string;
693
- creditId?: string;
694
- creditTtlMs: number;
695
- dvmId?: string;
696
- enqueueCreditDeposit?: CreditDepositEnqueue;
697
- nowMs?: number;
698
- }): Promise<InvoiceSettlement[]>;
278
+ pendingPaymentFiatMicro?: number;
279
+ /** Currency of {@link pendingPaymentFiatMicro} (lowercase ISO-4217). */
280
+ pendingPaymentFiatCurrency?: string;
699
281
  /**
700
- * One invoice's settlement check. Returns the applied settlement, or
701
- * `undefined` when nothing changed (still unpaid, or retired unpaid).
702
- *
703
- * Throws whatever the wallet or the ledger threw — classifying that is
704
- * {@link retireOrLog}'s job, so this stays one invoice's happy path.
282
+ * Cumulative fiat micro this job has asked for mid-job, the ceiling on its
283
+ * draw growth (internal-review). See `JobRecord.askedTopUpMicro` for the sentinel.
705
284
  */
706
- private applyOne;
285
+ askedTopUpMicro?: number;
707
286
  /**
708
- * Decide what one invoice's failure means, and retire the invoice when the
709
- * answer is "this can never succeed".
710
- *
711
- * The split that matters is permanent-versus-transient, because it decides
712
- * whether the row keeps a slot in the sweep window. A transient failure — the
713
- * database blinked, the wallet answered oddly — leaves it `pending` and the
714
- * caller's next request retries it. A **permanent** one means the ledger will
715
- * refuse this invoice identically forever, so leaving it pending costs a
716
- * `lookup_invoice` on every subsequent request and, at
717
- * {@link MAX_SETTLE_CHECKS} of them, fills the window so a genuinely payable
718
- * invoice behind them is never even looked up. Those get `blocked`, which
719
- * drops them out of the sweep and hands the operator the payment hash.
287
+ * Currency of {@link askedTopUpMicro} — and this job's sticky ask
288
+ * denomination (internal-review). See `JobRecord.askedTopUpCurrency`.
720
289
  */
721
- private retireOrLog;
290
+ askedTopUpCurrency?: string;
722
291
  /**
723
- * The reason this invoice can never be credited, or `undefined` if the
724
- * failure was transient.
725
- *
726
- * Every code here is decided by state a retry cannot move — a row the ledger
727
- * has already committed (a `credit_fundings` entry at this
728
- * `(credit_id, fund_id)`, or a `credits` row whose owner, denomination or
729
- * rail disagrees with the invoice), or the basis this module rebuilds
730
- * identically from the invoice row on every pass. None of them is a race.
292
+ * Why this job's draw growth is not capped, when it isn't (internal-review). Mirror
293
+ * of `JobRecord.topUpCapUnenforcedReason`.
294
+ */
295
+ topUpCapUnenforcedReason?: TopUpCapUnenforcedReason;
296
+ /** Price the job was charged at (msats). Used to detect overpayment for change/melt gating. */
297
+ requiredMsats?: number;
298
+ /**
299
+ * The signed receipt for this job's terminal outcome (internal-review). Mirror of
300
+ * `JobRecord.receipt`, set once the terminal funnel has signed and persisted
301
+ * it so the local read paths (which serve from `activeJobs` during the
302
+ * cleanup window) hand back the same bytes as a cross-machine re-read.
303
+ */
304
+ receipt?: JobReceipt;
305
+ /**
306
+ * The in-flight terminal chain — persist, then sign + store the receipt
307
+ * (internal-review). Assigned synchronously by `ctx.complete()` / `ctx.fail()` at
308
+ * the instant the job goes terminal, so the `POST /v1/job` fast path can
309
+ * await it (bounded) and put the receipt on the synchronous 200 rather than
310
+ * making the caller poll for it. Never rejects — the chain swallows its own
311
+ * failures.
312
+ */
313
+ terminalWork?: Promise<void>;
314
+ listeners: Set<(msg: Message) => void>;
315
+ /**
316
+ * Cancellation signal for the running handler (internal-review). Aborted whenever
317
+ * the job is cancelled on this process — caller cancel, idle timeout,
318
+ * stale-sweep teardown, supersession. Surfaced to builders as `ctx.signal`
319
+ * and auto-threaded into `ctx.fetch`, so an in-flight provider call
320
+ * (ElevenLabs, Scrapfly, …) is torn down instead of billing the caller for
321
+ * work they just cancelled. Per-process runtime state like `listeners` —
322
+ * never persisted, and re-created fresh on reactivation.
323
+ */
324
+ abort: AbortController;
325
+ /**
326
+ * Hook for cross-machine message persistence (Tx A, internal-review). Set by
327
+ * `JobManager` when the store is streamable. The appender runs the
328
+ * outgoing-message tx atomically with any payment-request counter bump so
329
+ * `(message, pending_payment_msats)` land together — never separately.
731
330
  *
732
- * `funding_replayed` is the one that needs a second read to classify.
733
- * Usually it *is* benign — a concurrent check won the race and the money is
734
- * credited either way — but the key is `(credit_id, fund_id)` and `fund_id`
735
- * is **caller-chosen**: reuse it on cashu or x402 after this bolt11 was
736
- * minted and the row that collided is a different payment entirely, so the
737
- * sats this invoice received have nowhere to land.
331
+ * Per-job serialised (internal-review): each call chains onto `messageAppenderTail`
332
+ * so two synchronous `providerMessage` calls (e.g. `artifact` followed by
333
+ * `complete`) can't race for the DB-side seq allocation.
738
334
  */
739
- private blockingReason;
740
- private withBackend;
741
- /** Record activity without letting it promote an unvalidated connection. */
742
- private noteOperationOk;
743
- private notePermissionsOk;
744
- private notePermissionRefused;
335
+ messageAppender?: (msg: Message, opts?: AppendOutgoingOptions) => void;
745
336
  /**
746
- * Only a *transport* failure from an ordinary invoice operation is a health
747
- * signal. Permission-probe refusals are classified by {@link refresh}.
337
+ * Tail of the per-job appender chain (internal-review). `wireMessageAppender` updates
338
+ * this on every call so `persistJob` can await it before the terminal snapshot
339
+ * save runs `DELETE FROM job_messages`.
748
340
  */
749
- private noteFailure;
750
- private refresh;
341
+ messageAppenderTail?: Promise<unknown>;
342
+ sdkCtx?: SDKJobContext<unknown, unknown>;
343
+ stepCache: StepCache;
344
+ state: unknown;
345
+ pendingPrompts: Map<string, PromptResolver>;
346
+ pendingPayment: PaymentResolver | null;
347
+ /** When not null, indicates replay mode — prompt/payment scan message history for cached responses. */
348
+ replayHighSeq: number | null;
349
+ /** Number of provider messages to suppress during replay. Cleared on handler error. */
350
+ replayProviderSkip: number;
351
+ /** Index of the next payment to match during replay (for multi-payment handlers). */
352
+ replayPaymentIndex: number;
353
+ /**
354
+ * Set once Tx C applies the current payment ask during this reactivation.
355
+ * Runtime-only: replayed prompts may mutate `status`, but never clear this
356
+ * marker before the matching payment is consumed.
357
+ */
358
+ reactivationPaymentApplied?: true;
359
+ /** Unix ms timestamp of job creation. */
360
+ createdAt: number;
361
+ /** Unix ms timestamp of last activity (message, yield, terminal). */
362
+ lastActivityAt: number;
363
+ }
364
+ /** Extract the persistent fields from a ServerJob into a JobRecord. */
365
+ declare function toJobRecord(job: ServerJob): JobRecord;
366
+ /** Reconstitute a ServerJob from a persisted JobRecord with fresh runtime fields. */
367
+ declare function fromJobRecord(record: JobRecord): ServerJob;
368
+ /**
369
+ * Signal cancellation to a running handler (internal-review). Idempotent — aborting an
370
+ * already-aborted controller is a no-op, so the overlapping teardown paths
371
+ * (caller cancel then stale sweep, say) can each call it unconditionally.
372
+ */
373
+ declare function abortJob(job: ServerJob, reason: string): void;
374
+ /**
375
+ * Reason carried on `ctx.signal.reason` and thrown by post-terminal `ctx.prompt`
376
+ * / `ctx.requestPayment` calls. Builders can `instanceof`-check it to tell a
377
+ * cancellation apart from a provider-side abort.
378
+ */
379
+ declare class JobCancelledError extends Error {
380
+ constructor(message?: string);
751
381
  }
382
+ /** Append a provider-sent message to the job and notify SSE listeners. */
383
+ declare function providerMessage(job: ServerJob, type: MessageType, content: Record<string, unknown> | object): void;
384
+ /** Check if a job status is terminal (completed, failed, cancelled). */
385
+ declare function isTerminal(status: ServerJob["status"]): boolean;
386
+ /** Check if a message is a yield point (ends the provider's turn). */
387
+ declare function isYieldMessage(msg: Message): boolean;
752
388
 
753
389
  /**
754
390
  * Per-DVM runtime mint-health tracker (internal-review). Alongside the platform's
@@ -894,419 +530,38 @@ declare class MintHealthTracker {
894
530
  }
895
531
 
896
532
  /**
897
- * Owner display identity surfaced on `/v1/info#owner` (internal-review). Personal orgs
898
- * resolve to the owner builder's profile; shared orgs resolve to the org's own
899
- * profile. Container DVMs read from env vars; isolate DVMs resolve from Postgres.
533
+ * Credit fields riding the signed request body (internal-review, spec §2).
534
+ *
535
+ * An explicit draw references the credit INSIDE the secp256k1/Schnorr-signed
536
+ * `body.data` — no new caller crypto. Wire field names (snake_case, matching
537
+ * the rest of the wire surface):
538
+ *
539
+ * - `credit_id` — the credit to draw against.
540
+ * - `draw_id` — client-generated draw id; the ledger's idempotency key.
541
+ * - `fund` — optional `{ amount_micro, commitment }` funding commitment:
542
+ * present iff a funding artifact (X-Cashu / X-PAYMENT / mpp credential)
543
+ * rides the same request. `commitment` is the SHA-256 hex over the raw
544
+ * artifact bytes, binding the header-borne proof — which sits OUTSIDE the
545
+ * signed body — to the credit its sender intended (spec §2 condition 3).
546
+ *
547
+ * These are envelope-level fields, not capability input: **the SDK** removes
548
+ * them before any builder-owned schema parses ({@link stripCreditEnvelope} in
549
+ * `JobManager.parseInput` and on the quote path; {@link
550
+ * creditEnvelopeIgnoreFields} feeding the auth verifier's own schema check), so
551
+ * handlers never see them and a top-level `.strict()` capability schema is
552
+ * fine (internal-review). Relying on Zod's default strip mode instead was the trap:
553
+ * `.strict()` rejects unknown keys, so an explicit draw 400'd at parse before
554
+ * the envelope was ever extracted.
555
+ *
556
+ * The strip never touches the canonical signing bytes. Every auth call site
557
+ * verifies the raw wire body (`signedRequestInput`, internal-review), so the credit
558
+ * fields are in the verified bytes because they were never taken out of them —
559
+ * the strip is only ever applied to what a schema is about to parse.
900
560
  */
901
- interface OwnerDisplay {
902
- handle: string;
903
- displayName?: string | null;
904
- avatarUrl?: string | null;
905
- type: "builder" | "org";
906
- }
907
- /** Optional builder identity surfaced on `/v1/info` (forward-compatible stub). */
908
- interface BuilderIdentity {
909
- id?: string;
910
- name?: string;
911
- url?: string;
912
- /**
913
- * Builder identity x-only secp256k1 pubkey (internal-review). When populated, the
914
- * `/v1/info#builder` block also carries `attestation` + `signature` so
915
- * consumers can verify the deploy was signed by the holder of this key.
916
- * Populated at deploy time from `DVMKIT_BUILDER_PUBKEY` (Fly secret); the
917
- * SDK never re-signs at runtime.
918
- */
919
- pubkey?: string;
920
- /** Canonical deploy-time attestation payload (internal-review). Served verbatim. */
921
- attestation?: AttestationPayload;
922
- /** Schnorr signature over `canonicaliseForSigning(attestation)` (internal-review). */
923
- signature?: string;
924
- }
925
- /** Platform reporter overrides — the host falls back to env when omitted. */
926
- interface PlatformReporterOpts {
927
- /** Bearer token. Defaults to `DVMKIT_PLATFORM_TOKEN` env. */
928
- token?: string;
929
- /** Platform internal URL. Defaults to `DVMKIT_PLATFORM_URL` env. */
930
- url?: string;
931
- }
932
- /** Options for {@link createDVMHost}. */
933
- interface DVMHostOpts {
934
- /**
935
- * Postgres connection string. Defaults to `DATABASE_URL` env.
936
- * Required at runtime (internal-review). Boot fails when missing unless `jobStore`
937
- * is explicitly supplied or `devMode` is true (test/dev escape hatches).
938
- */
939
- database?: string;
940
- /** Listen port. Defaults to `PORT` env or 8080. */
941
- port?: number;
942
- /** Environment variables. Defaults to `process.env`. */
943
- env?: Record<string, string>;
944
- /** Builder identity for `/v1/info` (forward-compatible). */
945
- builder?: BuilderIdentity;
946
- /** Owner display identity for `/v1/info#owner` (internal-review). Falls back to env vars. */
947
- owner?: OwnerDisplay;
948
- /** Platform reporter overrides. Defaults to env-derived values. */
949
- platformReporter?: PlatformReporterOpts;
950
- /** Override the SDK's fx fetcher. Defaults to env-configured CoinGecko. */
951
- fx?: FxFetcher;
952
- /**
953
- * Custom `/health` handler. When set, the SDK installs this instead of the
954
- * default `{ status: "ok" }` responder — useful for runtime-specific health
955
- * (pool depth, draining state, 503 while draining).
956
- */
957
- healthHandler?: (c: Context) => Response | Promise<Response>;
958
- /** KV store override. Defaults to PostgresKVStore (or MemoryKVStore in dev). */
959
- store?: KVStore;
960
- /** JobStore override. Takes precedence over `database`. */
961
- jobStore?: JobStore;
962
- /**
963
- * Existing Postgres pool to reuse instead of opening one from `database`
964
- * (internal-review test seam). When set, the host builds its JobStore / KVStore /
965
- * cashu accumulator on this pool and leaves it open at shutdown — the caller
966
- * owns it. Lets the e2e harness run DVMs in `p2pk-accumulator` mode against
967
- * its single shared pool rather than spawning a pool per DVM.
968
- */
969
- pool?: Pool;
970
- /** Cashu mints accepted. Defaults to env `DVMKIT_CASHU_MINTS`. */
971
- mints?: string[];
972
- /** Cashu receive mode override. */
973
- cashuMode?: CashuMode;
974
- /** MPP handle override. Defaults to env-resolved via `createMppFromOpts`. */
975
- mpp?: MppxServer;
976
- /** x402 stablecoin payment configuration. Defaults to env-resolved. */
977
- x402?: X402Config;
978
- /** Payment methods override. Defaults to derived from configured rails. */
979
- paymentMethods?: PaymentMethod[];
980
- /**
981
- * Lightning receive leg for credit funding (internal-review). Defaults to the
982
- * env-resolved `DVMKIT_NWC_RECEIVE_URI` /
983
- * `DVMKIT_NWC_RECEIVE_INVOICE_TTL_SECONDS`. Pass `backend` to inject a
984
- * wallet directly — a test seam that skips the connect-time probe, so
985
- * production must always come through the URI.
986
- */
987
- lightningReceive?: LightningReceiveConfig;
988
- /** When true, payment is skipped if no mints are configured (dev/test). */
989
- devMode?: boolean;
990
- /** Consumed-credential store override (test seam). */
991
- consumedCredentialStore?: ConsumedCredentialStore;
992
- /** Mint-health tracker override (test seam). */
993
- mintHealthTracker?: MintHealthTracker;
994
- /**
995
- * Wrap the SDK compatibility gate before it is exposed to descriptor custom
996
- * routes. Platform hosts use this to enforce their independently released
997
- * caller rollout policy; self-hosted SDK consumers receive the default gate.
998
- */
999
- wrapRouteClientCompatibility?: (sdkGate: ClientCompatibilityGate) => ClientCompatibilityGate;
1000
- }
1001
- /** Per-mount options. Currently only `prefix` is supported. */
1002
- interface MountOpts {
1003
- /** Path prefix for this DVM's protocol routes (e.g. `/delete-feed`). */
1004
- prefix?: string;
1005
- }
1006
- /** Live DVM host. Single wiring locus for the SDK (internal-review). */
1007
- interface DVMHost {
1008
- /**
1009
- * Underlying Hono app for custom non-protocol routes that belong to the
1010
- * host (cross-DVM `/admin`, cron callbacks, etc.). For DVM-scoped routes,
1011
- * declare `routes(app)` on the descriptor.
1012
- */
1013
- readonly app: Hono;
1014
- /**
1015
- * Resolved Postgres pool — undefined before `host.serve()` completes
1016
- * database resolution, set thereafter. Exposed so DVM-local Postgres
1017
- * consumers (e.g. scrape's `ScrapeDb` per-fetch event store, internal-review)
1018
- * can reuse the host's pool rather than constructing their own. Wire
1019
- * inside descriptor `onBoot` (fires after pool init, before listener opens).
1020
- */
1021
- readonly pool: Pool | undefined;
1022
- /**
1023
- * Money-safe credit ledger (internal-review) — undefined before `host.serve()`
1024
- * resolves stores, set thereafter. Postgres-backed when a pool exists;
1025
- * pool-less hosts get a shared in-memory ledger so the internal-review fund+draw
1026
- * semantics hold everywhere. Same wiring window as `pool` (descriptor
1027
- * `onBoot` fires after init, before the listener opens).
1028
- */
1029
- readonly creditLedger: CreditLedgerLike | undefined;
1030
- /**
1031
- * Add a DVM to this host. Multiple mounts compose at distinct prefixes;
1032
- * a single mount with no prefix attaches at root.
1033
- */
1034
- mount<S, I extends ZodLike | undefined>(descriptor: DVMDescriptor<S, I>, opts?: MountOpts): void;
1035
- /** Start listening. Returns the live server handle. */
1036
- serve(opts?: {
1037
- port?: number;
1038
- }): Promise<{
1039
- url: string;
1040
- close: () => Promise<void>;
1041
- }>;
1042
- /** Graceful shutdown. Runs descriptor `onShutdown` hooks in reverse mount order. */
1043
- shutdown(): Promise<void>;
1044
- }
1045
- /**
1046
- * Single wiring locus for SDK-side construction (internal-review). Replaces both the
1047
- * `serve()` wrapper and the `createDVMServer` direct path. Reads env once,
1048
- * resolves payment rails, owns the Postgres pool, and mounts each DVM
1049
- * descriptor as a Hono sub-app on `host.app`.
1050
- */
1051
- declare function createDVMHost(opts?: DVMHostOpts): DVMHost;
1052
-
1053
- /** Promise resolver for a pending client prompt response. */
1054
- interface PromptResolver {
1055
- resolve: (value: ResponseContent) => void;
1056
- reject: (reason: Error) => void;
1057
- }
1058
- /** Promise resolver for a pending client payment. */
1059
- interface PaymentResolver {
1060
- resolve: (value: PaymentContent) => void;
1061
- reject: (reason: Error) => void;
1062
- }
1063
- /** In-memory representation of a running job. */
1064
- interface ServerJob {
1065
- id: string;
1066
- tags: string[];
1067
- /**
1068
- * Capability name this job belongs to (internal-review). Set at submission from
1069
- * `POST /v1/job` body's `capability` field; routes the SDK runtime to the
1070
- * right `onJob` / `onResponse` / `onPayment` handler and the right input
1071
- * schema. Required — the wire layer rejects requests that omit it.
1072
- */
1073
- capability: string;
1074
- input: string;
1075
- params: Record<string, string>;
1076
- requesterId: string;
1077
- /**
1078
- * SHA-256 hex of the per-job opaque `job_token` minted at creation for
1079
- * anonymous callers (internal-review). Mirror of `JobRecord.requesterTokenHash`;
1080
- * see that field for the wire-level gating semantics.
1081
- */
1082
- requesterTokenHash?: string;
1083
- /**
1084
- * Submission provenance (internal-review) — mirrors `JobRecord.requesterToken` /
1085
- * `requestFingerprint` / `requesterPubkey`. Carried on the job purely so
1086
- * `toJobRecord` persists it; nothing in the runtime reads it. It's the
1087
- * replay gate: a retried paid submit that reproduces the fingerprint (and,
1088
- * on a signed DVM, the pubkey) is handed this token back.
1089
- */
1090
- requesterToken?: string;
1091
- requestFingerprint?: string;
1092
- requesterPubkey?: string;
1093
- requestId?: string;
1094
- /** HTTP path independently recorded when the caller proof was accepted. */
1095
- authRequestPath?: string;
1096
- status: "processing" | "completed" | "failed" | "awaiting-input" | "cancelled" | "working";
1097
- summary?: string;
1098
- messages: Message[];
1099
- seq: number;
1100
- paidMsats: number;
1101
- paymentMint?: string;
1102
- /** Rail of the most recent successful credit. Set on each credit branch in payment processing. */
1103
- paymentRail?: FundingMethod;
1104
- /**
1105
- * Settlement reference for the most-recent successful credit. Threaded
1106
- * into `onJobCompleted` so the platform revenue ledger can populate
1107
- * `revenue_events.tx_hash`. mpp/x402 carry the credential's `challenge.id`
1108
- * / EVM tx hash from the facilitator (internal-review); cashu accumulator carries
1109
- * the `X-Cashu-Request-Id` UUID (internal-review). Updated on every credit
1110
- * (upfront or mid-job via `processIncomingPayment`) — internal-review.
1111
- * Single-row-per-job accounting keeps the most recent credit's hash,
1112
- * mirroring `paymentRail`.
1113
- */
1114
- paymentTxHash?: string;
1115
- /** Chain transaction returned to callers recovering x402 or Tempo acceptance. */
1116
- paymentTransactionHash?: string;
1117
- /**
1118
- * Rail-native amount accumulated across all successful credits on this
1119
- * job (sats for mpp, USDC microunits for x402). The single
1120
- * `revenue_events` row written at completion uses this as `gross_native`,
1121
- * so multi-credit jobs sum the per-credit native amounts (internal-review).
1122
- * internal-review wired the upfront credit; mid-job credits accumulate same-rail
1123
- * and reset on a cross-rail switch (see `processIncomingPayment`).
1124
- */
1125
- nativeAmount?: number;
1126
- /** Native asset tag — paired with `nativeAmount`. internal-review. */
1127
- nativeAsset?: "sats" | "usdc" | "usdc.e" | "usd-cents";
1128
- /** Cashu flow discriminator written into `revenue_events.metadata.cashu_flow` (internal-review). */
1129
- cashuFlow?: "p2pk_accumulator";
1130
- /** Credit the upfront payment funded/drew (internal-review). See `JobRecord.creditId`. */
1131
- creditId?: string;
1132
- /** The draw placed for this job on `creditId` (internal-review). */
1133
- drawId?: string;
1134
- /** Original funding artifact returned by an explicit fund-and-draw. */
1135
- fundingReceipt?: FundingReceipt;
1136
- fundingCredit?: CreditSnapshot;
1137
- receivedProofs: ProofLike[];
1138
- pendingPaymentMsats?: number;
1139
- /**
1140
- * Per-job binding for MPP credentials. Contains the `challenge.id` of every
1141
- * mppx challenge issued in the most-recent `requestPayment` yield. mppx HMAC
1142
- * binds challenges to realm/method/amount/expiry, not to the dvmkit job-id;
1143
- * verifying the incoming credential's `challenge.id` is in this set blocks
1144
- * a credential lifted from a sibling job that requested the same fields.
1145
- * Cleared on successful credit (internal-review).
1146
- */
1147
- pendingMppChallengeIds?: string[];
1148
- /**
1149
- * Per-job binding for x402 mid-job credentials (internal-review). 32-byte 0x-hex
1150
- * nonce issued by `requestPayment` and stamped onto the outbound x402
1151
- * envelope; the caller signs `TransferWithAuthorization` against this exact
1152
- * `bytes32`. Verification rejects an incoming `x402_payment` whose decoded
1153
- * authorization nonce differs, blocking a cross-job replay before the
1154
- * facilitator settles. Cleared on successful credit.
1155
- */
1156
- pendingX402Nonce?: string;
1157
- /**
1158
- * Exact USDC microunit amount paired with `pendingX402Nonce`. Kept as a
1159
- * decimal string so the EIP-3009 uint256 value remains lossless.
1160
- */
1161
- pendingX402AmountUsdcMicro?: string;
1162
- /**
1163
- * Fiat micro-units the outstanding `requestPayment` ask is worth, pinned when
1164
- * the payment-request was emitted (internal-review). See `JobRecord`.
1165
- */
1166
- pendingPaymentFiatMicro?: number;
1167
- /** Currency of {@link pendingPaymentFiatMicro} (lowercase ISO-4217). */
1168
- pendingPaymentFiatCurrency?: string;
1169
- /**
1170
- * Cumulative fiat micro this job has asked for mid-job, the ceiling on its
1171
- * draw growth (internal-review). See `JobRecord.askedTopUpMicro` for the sentinel.
1172
- */
1173
- askedTopUpMicro?: number;
1174
- /**
1175
- * Currency of {@link askedTopUpMicro} — and this job's sticky ask
1176
- * denomination (internal-review). See `JobRecord.askedTopUpCurrency`.
1177
- */
1178
- askedTopUpCurrency?: string;
1179
- /**
1180
- * Why this job's draw growth is not capped, when it isn't (internal-review). Mirror
1181
- * of `JobRecord.topUpCapUnenforcedReason`.
1182
- */
1183
- topUpCapUnenforcedReason?: TopUpCapUnenforcedReason;
1184
- /** Price the job was charged at (msats). Used to detect overpayment for change/melt gating. */
1185
- requiredMsats?: number;
1186
- /**
1187
- * The signed receipt for this job's terminal outcome (internal-review). Mirror of
1188
- * `JobRecord.receipt`, set once the terminal funnel has signed and persisted
1189
- * it so the local read paths (which serve from `activeJobs` during the
1190
- * cleanup window) hand back the same bytes as a cross-machine re-read.
1191
- */
1192
- receipt?: JobReceipt;
1193
- /**
1194
- * The in-flight terminal chain — persist, then sign + store the receipt
1195
- * (internal-review). Assigned synchronously by `ctx.complete()` / `ctx.fail()` at
1196
- * the instant the job goes terminal, so the `POST /v1/job` fast path can
1197
- * await it (bounded) and put the receipt on the synchronous 200 rather than
1198
- * making the caller poll for it. Never rejects — the chain swallows its own
1199
- * failures.
1200
- */
1201
- terminalWork?: Promise<void>;
1202
- listeners: Set<(msg: Message) => void>;
1203
- /**
1204
- * Cancellation signal for the running handler (internal-review). Aborted whenever
1205
- * the job is cancelled on this process — caller cancel, idle timeout,
1206
- * stale-sweep teardown, supersession. Surfaced to builders as `ctx.signal`
1207
- * and auto-threaded into `ctx.fetch`, so an in-flight provider call
1208
- * (ElevenLabs, Scrapfly, …) is torn down instead of billing the caller for
1209
- * work they just cancelled. Per-process runtime state like `listeners` —
1210
- * never persisted, and re-created fresh on reactivation.
1211
- */
1212
- abort: AbortController;
1213
- /**
1214
- * Hook for cross-machine message persistence (Tx A, internal-review). Set by
1215
- * `JobManager` when the store is streamable. The appender runs the
1216
- * outgoing-message tx atomically with any payment-request counter bump so
1217
- * `(message, pending_payment_msats)` land together — never separately.
1218
- *
1219
- * Per-job serialised (internal-review): each call chains onto `messageAppenderTail`
1220
- * so two synchronous `providerMessage` calls (e.g. `artifact` followed by
1221
- * `complete`) can't race for the DB-side seq allocation.
1222
- */
1223
- messageAppender?: (msg: Message, opts?: AppendOutgoingOptions) => void;
1224
- /**
1225
- * Tail of the per-job appender chain (internal-review). `wireMessageAppender` updates
1226
- * this on every call so `persistJob` can await it before the terminal snapshot
1227
- * save runs `DELETE FROM job_messages`.
1228
- */
1229
- messageAppenderTail?: Promise<unknown>;
1230
- sdkCtx?: SDKJobContext<unknown, unknown>;
1231
- stepCache: StepCache;
1232
- state: unknown;
1233
- pendingPrompts: Map<string, PromptResolver>;
1234
- pendingPayment: PaymentResolver | null;
1235
- /** When not null, indicates replay mode — prompt/payment scan message history for cached responses. */
1236
- replayHighSeq: number | null;
1237
- /** Number of provider messages to suppress during replay. Cleared on handler error. */
1238
- replayProviderSkip: number;
1239
- /** Index of the next payment to match during replay (for multi-payment handlers). */
1240
- replayPaymentIndex: number;
1241
- /**
1242
- * Set once Tx C applies the current payment ask during this reactivation.
1243
- * Runtime-only: replayed prompts may mutate `status`, but never clear this
1244
- * marker before the matching payment is consumed.
1245
- */
1246
- reactivationPaymentApplied?: true;
1247
- /** Unix ms timestamp of job creation. */
1248
- createdAt: number;
1249
- /** Unix ms timestamp of last activity (message, yield, terminal). */
1250
- lastActivityAt: number;
1251
- }
1252
- /** Extract the persistent fields from a ServerJob into a JobRecord. */
1253
- declare function toJobRecord(job: ServerJob): JobRecord;
1254
- /** Reconstitute a ServerJob from a persisted JobRecord with fresh runtime fields. */
1255
- declare function fromJobRecord(record: JobRecord): ServerJob;
1256
- /**
1257
- * Signal cancellation to a running handler (internal-review). Idempotent — aborting an
1258
- * already-aborted controller is a no-op, so the overlapping teardown paths
1259
- * (caller cancel then stale sweep, say) can each call it unconditionally.
1260
- */
1261
- declare function abortJob(job: ServerJob, reason: string): void;
1262
- /**
1263
- * Reason carried on `ctx.signal.reason` and thrown by post-terminal `ctx.prompt`
1264
- * / `ctx.requestPayment` calls. Builders can `instanceof`-check it to tell a
1265
- * cancellation apart from a provider-side abort.
1266
- */
1267
- declare class JobCancelledError extends Error {
1268
- constructor(message?: string);
1269
- }
1270
- /** Append a provider-sent message to the job and notify SSE listeners. */
1271
- declare function providerMessage(job: ServerJob, type: MessageType, content: Record<string, unknown> | object): void;
1272
- /** Check if a job status is terminal (completed, failed, cancelled). */
1273
- declare function isTerminal(status: ServerJob["status"]): boolean;
1274
- /** Check if a message is a yield point (ends the provider's turn). */
1275
- declare function isYieldMessage(msg: Message): boolean;
1276
-
1277
- /**
1278
- * Credit fields riding the signed request body (internal-review, spec §2).
1279
- *
1280
- * An explicit draw references the credit INSIDE the secp256k1/Schnorr-signed
1281
- * `body.data` — no new caller crypto. Wire field names (snake_case, matching
1282
- * the rest of the wire surface):
1283
- *
1284
- * - `credit_id` — the credit to draw against.
1285
- * - `draw_id` — client-generated draw id; the ledger's idempotency key.
1286
- * - `fund` — optional `{ amount_micro, commitment }` funding commitment:
1287
- * present iff a funding artifact (X-Cashu / X-PAYMENT / mpp credential)
1288
- * rides the same request. `commitment` is the SHA-256 hex over the raw
1289
- * artifact bytes, binding the header-borne proof — which sits OUTSIDE the
1290
- * signed body — to the credit its sender intended (spec §2 condition 3).
1291
- *
1292
- * These are envelope-level fields, not capability input: **the SDK** removes
1293
- * them before any builder-owned schema parses ({@link stripCreditEnvelope} in
1294
- * `JobManager.parseInput` and on the quote path; {@link
1295
- * creditEnvelopeIgnoreFields} feeding the auth verifier's own schema check), so
1296
- * handlers never see them and a top-level `.strict()` capability schema is
1297
- * fine (internal-review). Relying on Zod's default strip mode instead was the trap:
1298
- * `.strict()` rejects unknown keys, so an explicit draw 400'd at parse before
1299
- * the envelope was ever extracted.
1300
- *
1301
- * The strip never touches the canonical signing bytes. Every auth call site
1302
- * verifies the raw wire body (`signedRequestInput`, internal-review), so the credit
1303
- * fields are in the verified bytes because they were never taken out of them —
1304
- * the strip is only ever applied to what a schema is about to parse.
1305
- */
1306
- interface CreditEnvelope {
1307
- creditId: string;
1308
- drawId: string;
1309
- fund?: CreditFundCommitment;
561
+ interface CreditEnvelope {
562
+ creditId: string;
563
+ drawId: string;
564
+ fund?: CreditFundCommitment;
1310
565
  }
1311
566
  /** The signed funding commitment for a fund-and-draw request. */
1312
567
  interface CreditFundCommitment {
@@ -1620,16 +875,11 @@ declare function fundedMicroFor(priceFiat: PriceFiat, paidMsats: number, require
1620
875
  * quote is 11 sats and the old advertisement demanded 10,718 µUSDC — 7% above
1621
876
  * the price the caller was quoted and the ledger recorded.
1622
877
  *
1623
- * Both call sites — `buildX402Requirements` and the upfront verify branch —
1624
- * MUST derive through here rather than recomputing. Two independent derivations
1625
- * is exactly the shape that produced this bug.
1626
- *
1627
- * The rate-derived path survives for anything this rule can't express: a
1628
- * non-USD `priceFiat` (the µUSDC figure is a real fx conversion there, and
1629
- * still drifts — internal-review), and a host that verifies without a `priceFiat` at
1630
- * all. Both keep today's behaviour rather than guessing a denomination.
878
+ * Every caller gates x402 on a USD-priced DVM before reaching this helper.
879
+ * Keeping the helper USD-only makes a future advertisement call site fail
880
+ * closed instead of quietly restoring the rate-derived non-USD path.
1631
881
  */
1632
- declare function x402RequiredUsdcMicro(priceFiat: PriceFiat | undefined, requiredMsats: number, btcUsdRate: number): number;
882
+ declare function x402RequiredUsdcMicro(priceFiat: PriceFiat): number;
1633
883
  /**
1634
884
  * A settled x402 authorization's share of a quoted figure, taken against the
1635
885
  * `maxAmountRequired` the 402 advertised (internal-review) — `quoted` unmodified when
@@ -1639,7 +889,7 @@ declare function x402RequiredUsdcMicro(priceFiat: PriceFiat | undefined, require
1639
889
  *
1640
890
  * x402 must NOT reach {@link fundedMicroFor} through `usdcToMsats`. The
1641
891
  * advertised figure is stated in µUSDC by {@link x402RequiredUsdcMicro} — the
1642
- * price verbatim for a USD job, a `Math.ceil` off the rate otherwise — and the
892
+ * price verbatim for a USD job — and the
1643
893
  * receipt converts back with `usdcToMsats`, a `Math.round`, which is the
1644
894
  * inverse of neither. The round trip lands at or above `requiredMsats`, which
1645
895
  * took the pro-rata branch and valued a payment of exactly the advertised
@@ -2524,6 +1774,8 @@ interface PaymentInfo {
2524
1774
  */
2525
1775
  interface CreditFundingReport {
2526
1776
  creditId: string;
1777
+ /** Canonical owner returned by the ledger after applying the funding. */
1778
+ callerPubkey: string;
2527
1779
  /** The rail's own settlement reference — the deposit's idempotency key. */
2528
1780
  fundingId: string;
2529
1781
  rail: FundingMethod;
@@ -2931,7 +2183,7 @@ declare function verifyTempoSessionManagementCredential(args: {
2931
2183
  * here: the former is already complete, the latter is a challenge envelope, not
2932
2184
  * an error report.
2933
2185
  */
2934
- type PaymentErrorCode = "payment_invalid" | "x402_settlement_ambiguous" | "cashu_unavailable" | "cashu_not_p2pk_locked" | "mint_health_pending" | "cashu_mint_sick" | "payment_insufficient" | "payment_unapplied" | "insufficient_credit" | "credit_expired" | "credit_unbacked" | "credit_not_found" | "tempo_channel_closing" | "settlement_pending" | "draw_conflict" | "funding_commitment_mismatch" | "credit_not_supported" | "one_shot_credit_not_supported" | "credit_below_min" | "below_rail_minimum" | "credit_over_max" | "credit_id_unavailable" | "nothing_to_drain" | "drain_below_dust" | "drain_not_found" | "drain_conflict" | "drain_method_unsupported" | "tempo_channel_exit_required" | "x402_settlement_submission_failed" | X402SettlementReconciliationReason | "lightning_settlement_pending" | "lightning_invoice_expired" | "lightning_unavailable" | AccumulatorRejectionCode;
2186
+ type PaymentErrorCode = "payment_invalid" | "x402_not_offered" | "x402_settlement_ambiguous" | "cashu_unavailable" | "cashu_not_p2pk_locked" | "mint_health_pending" | "cashu_mint_sick" | "payment_insufficient" | "payment_unapplied" | "insufficient_credit" | "credit_expired" | "credit_unbacked" | "credit_not_found" | "tempo_channel_closing" | "settlement_pending" | "draw_conflict" | "funding_commitment_mismatch" | "credit_not_supported" | "one_shot_credit_not_supported" | "credit_below_min" | "below_rail_minimum" | "credit_over_max" | "credit_id_unavailable" | "nothing_to_drain" | "drain_below_dust" | "drain_not_found" | "drain_conflict" | "drain_method_unsupported" | "tempo_channel_exit_required" | "x402_settlement_submission_failed" | X402SettlementReconciliationReason | "lightning_settlement_pending" | "lightning_invoice_expired" | "lightning_unavailable" | AccumulatorRejectionCode;
2935
2187
  /**
2936
2188
  * Build a flat payment-error body carrying the four agent-facing fields every
2937
2189
  * payment error must have (internal-review): the machine `code`, the operator-facing
@@ -2984,6 +2236,8 @@ declare function buildPaymentErrorResponse(body: Record<string, unknown>, challe
2984
2236
  interface IncomingPaymentOpts {
2985
2237
  mints?: string[];
2986
2238
  x402Config?: X402Config;
2239
+ /** Declared DVM pricing currency. x402 is accepted only when this is USD. */
2240
+ pricingCurrency?: string;
2987
2241
  /** Durable exact-settlement coordinator shared with the upfront path. */
2988
2242
  x402ExactSettlement?: X402ExactSettlementServer;
2989
2243
  /** Exact generations the facilitator currently advertises. Defaults to both. */
@@ -3312,1094 +2566,1522 @@ interface X402FacilitatorHealthOpts extends X402FacilitatorProbeOpts {
3312
2566
  refreshTtlMs?: number;
3313
2567
  maxStalenessMs?: number;
3314
2568
  }
3315
- /** Live, synchronous capability view consumed while payment responses are rendered. */
3316
- interface X402FacilitatorHealthLike {
3317
- /** Prime or refresh the capability observation. */
3318
- refresh(): Promise<void>;
3319
- /** Return current support and begin a background refresh when its TTL has elapsed. */
3320
- support(): X402FacilitatorSupport;
3321
- /** Last successful raw response, reused by batch initialization without another request. */
3322
- supportedResponse?(): SupportedResponse | undefined;
2569
+ /** Live, synchronous capability view consumed while payment responses are rendered. */
2570
+ interface X402FacilitatorHealthLike {
2571
+ /** Prime or refresh the capability observation. */
2572
+ refresh(): Promise<void>;
2573
+ /** Return current support and begin a background refresh when its TTL has elapsed. */
2574
+ support(): X402FacilitatorSupport;
2575
+ /** Last successful raw response, reused by batch initialization without another request. */
2576
+ supportedResponse?(): SupportedResponse | undefined;
2577
+ }
2578
+ /**
2579
+ * TTL-cached facilitator capability gate. Failed refreshes retain the last
2580
+ * successful observation until the same three-hour ceiling used by the
2581
+ * Lightning rail; a cold-start failure temporarily assumes exact dual-serve
2582
+ * so a facilitator blip does not suppress a healthy payment rail for a whole
2583
+ * process lifetime. Batch settlement is never assumed without an observation.
2584
+ */
2585
+ declare class X402FacilitatorHealth implements X402FacilitatorHealthLike {
2586
+ private readonly config;
2587
+ private readonly opts;
2588
+ private readonly now;
2589
+ private readonly refreshTtlMs;
2590
+ private readonly maxStalenessMs;
2591
+ private readonly startedAtMs;
2592
+ private observation;
2593
+ private lastAttemptAtMs;
2594
+ private inFlight;
2595
+ private ceilingLoggedForMs;
2596
+ private lastSupportedResponse;
2597
+ constructor(config: X402Config, opts?: X402FacilitatorHealthOpts);
2598
+ /** Refresh the observation, coalescing concurrent requests. */
2599
+ refresh(): Promise<void>;
2600
+ /** Answer synchronously from cache and start a non-blocking refresh when due. */
2601
+ support(): X402FacilitatorSupport;
2602
+ /** Return the raw response paired with the last successful observation. */
2603
+ supportedResponse(): SupportedResponse | undefined;
2604
+ private get facilitator();
2605
+ private get network();
2606
+ private refreshOnce;
2607
+ }
2608
+
2609
+ /** Options for creating a JobManager. */
2610
+ interface JobManagerOpts {
2611
+ /** Environment variables for ctx.env. */
2612
+ env: Record<string, string>;
2613
+ /** KV store for ctx.store. */
2614
+ store: KVStore;
2615
+ /** Cashu mints accepted for payment. */
2616
+ mints?: string[];
2617
+ /** Persistent backing store for job records. Default: in-memory. */
2618
+ jobStore?: JobStore;
2619
+ /** When true, payment messages auto-credit without verification. */
2620
+ devMode?: boolean;
2621
+ /** Payment methods this DVM accepts. Default: derived by the server from configured rails. */
2622
+ paymentMethods?: PaymentMethod[];
2623
+ /** x402 stablecoin payment configuration. */
2624
+ x402?: X402Config;
2625
+ /** Durable exact-settlement coordinator shared with upfront verification. */
2626
+ x402ExactSettlement?: X402ExactSettlementServer;
2627
+ /** Live facilitator capabilities for version-specific mid-job payment handling. */
2628
+ x402FacilitatorHealth?: X402FacilitatorHealthLike;
2629
+ /** MPP multi-rail payment handle (built via `createMppFromOpts`). */
2630
+ mpp?: MppxServer;
2631
+ /** Cashu receive mode (internal-review). */
2632
+ cashuMode?: CashuMode;
2633
+ /** Builder's NUT-11 P2PK lock pubkey (internal-review). Required for accumulator mode. */
2634
+ lockPubkey?: string;
2635
+ /** Postgres pool for wallet accumulator persistence. */
2636
+ db?: Pool;
2637
+ /**
2638
+ * Agent-wallet (internal-review): per-DVM identifier used for the scheduler advisory
2639
+ * lock and the `(dvm_id, request_id)` replay key. Defaults to `DVMKIT_DVM_ID`
2640
+ * env at boot in `createDVMHost()`.
2641
+ */
2642
+ dvmId?: string;
2643
+ /** Audience expected by persisted v2 caller statements. */
2644
+ authAudience?: SignedRequestAudience;
2645
+ /**
2646
+ * Shared fx fetcher used to convert USD-string descriptor prices and
2647
+ * fiat-form `requestPayment` calls into sats (internal-review). The SDK server owns
2648
+ * one per process so concurrent quotes share the 60s cache window.
2649
+ */
2650
+ fxFetcher: FxFetcher;
2651
+ /**
2652
+ * Signs a receipt at every terminal transition (internal-review). Absent when the
2653
+ * DVM has no receipt key wired — jobs then terminate exactly as before and
2654
+ * `/v1/info` doesn't advertise `receipts`.
2655
+ */
2656
+ receiptIssuer?: ReceiptIssuer;
2657
+ /**
2658
+ * Credit ledger for the terminal funnel (internal-review): a job carrying a
2659
+ * `creditId`/`drawId` settles its draw on success and releases it on
2660
+ * failure/cancel — this is how "no debit on job failure" is mechanically
2661
+ * real. The resolution runs before receipt issuance so the receipt's
2662
+ * `credit` block countersigns the post-resolution balance.
2663
+ */
2664
+ creditLedger?: CreditLedgerLike;
2665
+ /**
2666
+ * Durable processed-payment markers (internal-review), threaded through to the
2667
+ * mid-job top-up path (internal-review) so an x402/mpp payment's marker, `fund`, and
2668
+ * `growDraw` commit together.
2669
+ */
2670
+ processedPayments?: ProcessedPaymentStore;
2671
+ /**
2672
+ * Reporter-owned transactional deposit enqueue threaded into mid-job rail
2673
+ * commits (internal-review).
2674
+ */
2675
+ enqueueCreditDeposit?: CreditDepositEnqueue;
2676
+ /** Reporter outbox inserted atomically with a terminal draw release (internal-review). */
2677
+ enqueueCreditDrawRelease?: CreditDrawReleaseEnqueue;
2678
+ /** Credit lifetime a top-up mints with, from the DVM's advertised `credit.ttl`. */
2679
+ creditTtlMs?: number;
2680
+ /**
2681
+ * The DVM's declared pricing currency (internal-review), which is the currency of
2682
+ * every credit it opens. Threaded into the job context so a mid-job ask on a
2683
+ * job that holds no credit yet is still pinned in the denomination its first
2684
+ * payment will open the credit in (internal-review). `DVMServer` passes its resolved
2685
+ * `pricingCurrency`; a host building a `JobManager` directly falls back to the
2686
+ * descriptor, and then to `"usd"`.
2687
+ */
2688
+ pricingCurrency?: string;
2689
+ /**
2690
+ * Callback fired when a mid-job top-up moves money onto a credit (internal-review).
2691
+ * The platform books it as a **deposit** — a liability until drawn, never
2692
+ * summed into revenue — so a top-up that skipped this would leave the
2693
+ * outstanding-liability view short. Mirrors `DVMServer.reportCreditDeposit`,
2694
+ * which handles the upfront and `/v1/credit` legs.
2695
+ */
2696
+ onCreditFunded?: (info: CreditDepositPayload) => void | Promise<void>;
2697
+ /**
2698
+ * Callback fired when a paid job completes (internal-review). Container-runtime DVMs
2699
+ * use this to report revenue to the platform via `RevenueReporter`. Mirrors
2700
+ * the isolate path's `IsolateJobManager.opts.onJobCompleted`.
2701
+ */
2702
+ onJobCompleted?: (info: {
2703
+ dvmId: string;
2704
+ jobId: string;
2705
+ paidMsats: number;
2706
+ paymentMint?: string;
2707
+ rail: string;
2708
+ paymentTxHash?: string;
2709
+ nativeAmount?: number;
2710
+ nativeAsset?: string;
2711
+ cashuFlow?: string;
2712
+ creditId?: string;
2713
+ drawId?: string;
2714
+ drawAmountMicro?: number;
2715
+ creditCurrency?: string;
2716
+ settledAt?: number;
2717
+ fundingRef?: string;
2718
+ kind?: string;
2719
+ }) => void | Promise<void>;
2720
+ /**
2721
+ * Callback fired when the stale-job reaper force-fails a *paid* job — its
2722
+ * pending credit draw is released rather than settled, because only a
2723
+ * completed job settles a draw (internal-review). Container-runtime DVMs wire this
2724
+ * to a paid-job-death report → operator `notice` (internal-review). Never fires for
2725
+ * free jobs (`paidMsats === 0`). Fired fire-and-forget so a throw or a slow
2726
+ * report can't wedge the sweep.
2727
+ */
2728
+ onPaidJobDeath?: (info: {
2729
+ dvmId: string;
2730
+ jobId: string;
2731
+ capability: string;
2732
+ paidMsats: number;
2733
+ rail: string;
2734
+ reason: "worker_died_mid_job" | "stale_no_terminal_status";
2735
+ nativeAmount?: number;
2736
+ nativeAsset?: string;
2737
+ paymentMint?: string;
2738
+ }) => void | Promise<void>;
2739
+ /**
2740
+ * Callback fired when a settled credit draw cannot book a revenue event
2741
+ * because no usable payment rail is available (internal-review). Container-runtime
2742
+ * DVMs auto-wire this to the platform's deduped operator notice. Fired
2743
+ * fire-and-forget so alert delivery cannot block terminal handling or draw
2744
+ * reconciliation.
2745
+ */
2746
+ onRevenueSkippedNoRail?: (info: RevenueSkippedNoRailPayload) => void | Promise<void>;
2747
+ /**
2748
+ * Stale-job sweep threshold in ms (internal-review). Jobs in a non-terminal status
2749
+ * (`processing` / `working` / `awaiting-input`) whose `lastActivityAt` is
2750
+ * older than this are force-cancelled with reason `stale_no_terminal_status`,
2751
+ * so callers always reach a terminal status instead of polling forever after
2752
+ * a worker crash, OOM, or silent `messageAppender` failure. Default:
2753
+ * `2 × descriptor.idleTimeout + STALE_JOB_WATCHDOG_HEADROOM_MS`, matching
2754
+ * the internal-review acceptance criterion against scrape's `JOB_TIMEOUT_MS`.
2755
+ * Builders whose per-handler wall-clock bound exceeds 90s must override
2756
+ * this — otherwise the watchdog fires on a legitimate in-flight handler.
2757
+ * Set to `0` to disable (test seam).
2758
+ */
2759
+ staleJobTimeoutMs?: number;
2760
+ /**
2761
+ * Sweep interval for the stale-job watchdog. Default: `min(60s, max(15s,
2762
+ * tightestActiveTimeout / 4))`. Tests override to fire deterministically.
2763
+ */
2764
+ staleJobSweepIntervalMs?: number;
2765
+ /**
2766
+ * Worker-liveness watchdog in ms (internal-review). Jobs in `processing`/`working`
2767
+ * whose `lastActivityAt` is older than this are marked `failed` (dead
2768
+ * worker), separately from the longer `awaiting-input` idle timeout above.
2769
+ * Default: `descriptor.processingWatchdog * 1000` or
2770
+ * `DEFAULT_PROCESSING_WATCHDOG_MS`. Set to `0` to disable (test seam).
2771
+ */
2772
+ processingWatchdogMs?: number;
2773
+ /**
2774
+ * Interval at which locally-active `processing`/`working` jobs have their
2775
+ * `lastActivityAt` bumped so the processing watchdog only fires on a truly
2776
+ * dead worker (internal-review). Default: `DEFAULT_HEARTBEAT_INTERVAL_MS`. Set to
2777
+ * `0` to disable (test seam). Also floors how far the internal-review clock guard
2778
+ * may compensate a backwards step on the processing arm — a cadence close to
2779
+ * `processingWatchdogMs` leaves it no room and it compensates nothing.
2780
+ */
2781
+ heartbeatIntervalMs?: number;
2782
+ /**
2783
+ * Age in ms at which a `pending` credit draw whose `job_id` matches no row in
2784
+ * the job store is released as orphaned (internal-review). The draw commits before
2785
+ * the job row is persisted, so a crash or a fail-closed refusal in that
2786
+ * window strands a hold no terminal path can ever reach. Must stay
2787
+ * comfortably above the submit path's draw→persist latency. Set to `0` to
2788
+ * disable the watchdog entirely — stranded holds then stay stranded, so
2789
+ * treat it as a money knob, not a tuning one. Default:
2790
+ * {@link DEFAULT_ORPHAN_DRAW_AGE_MS}.
2791
+ */
2792
+ orphanDrawAgeMs?: number;
2793
+ /**
2794
+ * Age in ms at which a `pending` credit draw whose job row **is** terminal is
2795
+ * reconciled against that row's outcome (internal-review) — settled for a
2796
+ * `completed` job, released for a `failed`/`cancelled` one. Covers the hole
2797
+ * `orphanDrawAgeMs` cannot: `resolveCreditDraw` throwing on a ledger error
2798
+ * leaves a terminal job whose hold no later path retries.
2799
+ *
2800
+ * Rides the same scan as the orphan arm, which has two consequences for the
2801
+ * value you set here. Setting `orphanDrawAgeMs` to `0` disables this arm too.
2802
+ * And the effective gate is `max(orphanDrawAgeMs, terminalDrawReconcileAgeMs)`
2803
+ * — the scan never returns a row younger than its own cutoff, so anything
2804
+ * below `orphanDrawAgeMs` is silently clamped up to it. That floor is
2805
+ * deliberate rather than a wart: honouring a narrower value would mean
2806
+ * widening the scan, which hands the orphan arm rows younger than
2807
+ * `orphanDrawAgeMs` that it must not release. It can only ever delay a
2808
+ * settle, never advance one, which is the safe direction for a debit.
2809
+ *
2810
+ * Set to `0` to disable this arm alone — a money knob, not a tuning one.
2811
+ * Default: {@link DEFAULT_TERMINAL_DRAW_RECONCILE_AGE_MS}.
2812
+ */
2813
+ terminalDrawReconcileAgeMs?: number;
2814
+ /**
2815
+ * Sweep interval for the orphan-draw watchdog. Default:
2816
+ * {@link DEFAULT_ORPHAN_DRAW_SWEEP_INTERVAL_MS}. Set to `0` to disable — the
2817
+ * test seam, since suites drive `sweepOrphanDraws()` directly.
2818
+ */
2819
+ orphanDrawSweepIntervalMs?: number;
2820
+ /** Retention sweep cadence. Default: daily; `0` disables the interval. */
2821
+ jobRetentionSweepIntervalMs?: number;
2822
+ /** Delay before the first retention pass. Default: 30s; `0` disables it. */
2823
+ jobRetentionSweepBootDelayMs?: number;
2824
+ /** Operator-alert boundary invoked whenever a retention pass fails. */
2825
+ onJobRetentionSweepFailed?: (info: {
2826
+ dvmId: string;
2827
+ dvmName: string;
2828
+ retentionDays: number;
2829
+ error: string;
2830
+ }) => void | Promise<void>;
2831
+ /** Clear-on-recovery boundary invoked after every complete, clean pass. */
2832
+ onJobRetentionSweepRecovered?: (info: {
2833
+ dvmId: string;
2834
+ dvmName: string;
2835
+ retentionDays: number;
2836
+ examined: number;
2837
+ redacted: number;
2838
+ }) => void | Promise<void>;
2839
+ /**
2840
+ * Grace window in ms a reactivating machine waits for a live original handler
2841
+ * to win the single-execution claim before replaying an `awaiting-input` job
2842
+ * itself (internal-review). Default: `DEFAULT_REACTIVATION_CLAIM_GRACE_MS`. Widen if
2843
+ * cross-machine NOTIFY latency causes spurious double executions; lower in
2844
+ * tests for speed.
2845
+ */
2846
+ reactivationClaimGraceMs?: number;
2847
+ }
2848
+ type ResolvedInput<I extends ZodLike | undefined> = I extends ZodLike<infer O> ? O : string;
2849
+ /**
2850
+ * A deposit this payment funded that the job row did **not** take (internal-review).
2851
+ *
2852
+ * Reached when two mid-job top-ups race on a free-then-paid job: each opens its
2853
+ * own implicit credit, and `verifyAndCredit`'s set-if-null bind keeps one. The
2854
+ * loser's money is real, owned by the caller, and reclaimable — but nothing
2855
+ * named it, so the caller would have to derive `imp:<rail>:<payment_id>` to
2856
+ * reach their own balance. This rides the 201 so they don't have to.
2857
+ *
2858
+ * The response is the fast surface, not the only one: the credit is opened
2859
+ * under the caller's verified pubkey, so `POST /v1/credit` op `balance` lists
2860
+ * it and op `drain` reclaims it once the hold resolves.
2861
+ */
2862
+ interface UnappliedCredit {
2863
+ credit_id: string;
2864
+ credited_micro: number;
2865
+ credit_currency: string;
2866
+ display: string;
2867
+ hint: string;
3323
2868
  }
3324
2869
  /**
3325
- * TTL-cached facilitator capability gate. Failed refreshes retain the last
3326
- * successful observation until the same three-hour ceiling used by the
3327
- * Lightning rail; a cold-start failure temporarily assumes exact dual-serve
3328
- * so a facilitator blip does not suppress a healthy payment rail for a whole
3329
- * process lifetime. Batch settlement is never assumed without an observation.
2870
+ * What {@link JobManager.processPayment} tells the route. `error` is a refusal
2871
+ * to serialise verbatim; otherwise the payment was accepted, optionally
2872
+ * carrying an {@link UnappliedCredit} to fold into the 201.
3330
2873
  */
3331
- declare class X402FacilitatorHealth implements X402FacilitatorHealthLike {
3332
- private readonly config;
2874
+ type ProcessPaymentOutcome = {
2875
+ error: {
2876
+ body: Record<string, unknown>;
2877
+ status: number;
2878
+ };
2879
+ unappliedCredit?: undefined;
2880
+ } | {
2881
+ error?: undefined;
2882
+ unappliedCredit?: UnappliedCredit;
2883
+ };
2884
+ /**
2885
+ * Transport-agnostic job lifecycle manager.
2886
+ *
2887
+ * Handles job creation, handler invocation, message dispatch, persistence,
2888
+ * idle timeout, and durable replay. Shared by both the HTTP server and
2889
+ * relay provider.
2890
+ */
2891
+ declare class JobManager<State, InputSchema extends ZodLike | undefined> {
2892
+ private readonly descriptor;
3333
2893
  private readonly opts;
3334
- private readonly now;
3335
- private readonly refreshTtlMs;
3336
- private readonly maxStalenessMs;
3337
- private readonly startedAtMs;
3338
- private observation;
3339
- private lastAttemptAtMs;
3340
- private inFlight;
3341
- private ceilingLoggedForMs;
3342
- private lastSupportedResponse;
3343
- constructor(config: X402Config, opts?: X402FacilitatorHealthOpts);
3344
- /** Refresh the observation, coalescing concurrent requests. */
3345
- refresh(): Promise<void>;
3346
- /** Answer synchronously from cache and start a non-blocking refresh when due. */
3347
- support(): X402FacilitatorSupport;
3348
- /** Return the raw response paired with the last successful observation. */
3349
- supportedResponse(): SupportedResponse | undefined;
3350
- private get facilitator();
3351
- private get network();
3352
- private refreshOnce;
3353
- }
3354
-
3355
- /** Options for creating a JobManager. */
3356
- interface JobManagerOpts {
3357
- /** Environment variables for ctx.env. */
3358
- env: Record<string, string>;
3359
- /** KV store for ctx.store. */
3360
- store: KVStore;
3361
- /** Cashu mints accepted for payment. */
3362
- mints?: string[];
3363
- /** Persistent backing store for job records. Default: in-memory. */
3364
- jobStore?: JobStore;
3365
- /** When true, payment messages auto-credit without verification. */
3366
- devMode?: boolean;
3367
- /** Payment methods this DVM accepts. Default: derived by the server from configured rails. */
3368
- paymentMethods?: PaymentMethod[];
3369
- /** x402 stablecoin payment configuration. */
3370
- x402?: X402Config;
3371
- /** Durable exact-settlement coordinator shared with upfront verification. */
3372
- x402ExactSettlement?: X402ExactSettlementServer;
3373
- /** Live facilitator capabilities for version-specific mid-job payment handling. */
3374
- x402FacilitatorHealth?: X402FacilitatorHealthLike;
3375
- /** MPP multi-rail payment handle (built via `createMppFromOpts`). */
3376
- mpp?: MppxServer;
3377
- /** Cashu receive mode (internal-review). */
3378
- cashuMode?: CashuMode;
3379
- /** Builder's NUT-11 P2PK lock pubkey (internal-review). Required for accumulator mode. */
3380
- lockPubkey?: string;
3381
- /** Postgres pool for wallet accumulator persistence. */
3382
- db?: Pool;
2894
+ private readonly _activeJobs;
2895
+ private readonly jobStore;
2896
+ private readonly idleTimers;
2897
+ private readonly cleanupTimers;
2898
+ /** Per-job NOTIFY unsubscribe handles for the durable-status watch (internal-review). */
2899
+ private readonly statusWatchers;
2900
+ /** Job ids whose durable-status re-read is in flight — coalesces notify storms (internal-review). */
2901
+ private readonly statusChecksInFlight;
2902
+ /** Job ids that were notified mid-re-read and must be re-checked (internal-review). */
2903
+ private readonly statusChecksQueued;
2904
+ private readonly sessionId;
2905
+ private nextJobId;
2906
+ private accumulatorMonitor?;
2907
+ private readonly staleJobTimeoutMs;
2908
+ private readonly processingWatchdogMs;
2909
+ private readonly reactivationClaimGraceMs;
3383
2910
  /**
3384
- * Agent-wallet (internal-review): per-DVM identifier used for the scheduler advisory
3385
- * lock and the `(dvm_id, request_id)` replay key. Defaults to `DVMKIT_DVM_ID`
3386
- * env at boot in `createDVMHost()`.
2911
+ * Worker-heartbeat cadence (internal-review). Also floors the internal-review clock
2912
+ * guard's compensation on the processing arm — see
2913
+ * {@link staleSweepCompensationCapMs}.
3387
2914
  */
3388
- dvmId?: string;
3389
- /** Audience expected by persisted v2 caller statements. */
3390
- authAudience?: SignedRequestAudience;
2915
+ private readonly heartbeatIntervalMs;
2916
+ private readonly orphanDrawAgeMs;
2917
+ private readonly terminalDrawReconcileAgeMs;
2918
+ private staleSweepTimer?;
2919
+ private heartbeatTimer?;
2920
+ private orphanDrawSweepTimer?;
2921
+ private jobRetentionSweepTimer?;
2922
+ private jobRetentionBootTimer?;
2923
+ /** This manager holds {@link ORPHAN_SWEEP_CLAIMS} for its ledger. */
2924
+ private orphanSweepClaimed;
2925
+ /** Where a page-capped tick left off; cleared once a tick reaches the end. */
2926
+ private orphanSweepResumeFrom?;
2927
+ private jobRetentionResumeFrom?;
3391
2928
  /**
3392
- * Shared fx fetcher used to convert USD-string descriptor prices and
3393
- * fiat-form `requestPayment` calls into sats (internal-review). The SDK server owns
3394
- * one per process so concurrent quotes share the 60s cache window.
2929
+ * A `Date.now()` reading that cannot regress within this manager's lifetime
2930
+ * (internal-review). Anchored at construction, so a manager already running when
2931
+ * the clock stepped is covered. The stale-job sweep's two cutoffs, the
2932
+ * orphan-draw age gate and the reactivation claim grace all read it; see
2933
+ * {@link createMonotonicClock} for what it costs and where it is clamped.
3395
2934
  */
3396
- fxFetcher: FxFetcher;
2935
+ private readonly monotonicNowMs;
2936
+ private sweepInFlight;
2937
+ private orphanSweepInFlight;
2938
+ private jobRetentionSweepInFlight;
2939
+ private heartbeatInFlight;
3397
2940
  /**
3398
- * Signs a receipt at every terminal transition (internal-review). Absent when the
3399
- * DVM has no receipt key wired — jobs then terminate exactly as before and
3400
- * `/v1/info` doesn't advertise `receipts`.
2941
+ * The denomination of every credit this DVM opens (internal-review). Resolved once,
2942
+ * with the same boundary fallback `DVMServer` applies for direct JavaScript
2943
+ * callers that force an incomplete value past the descriptor contract.
3401
2944
  */
3402
- receiptIssuer?: ReceiptIssuer;
2945
+ private readonly pricingCurrency;
2946
+ constructor(descriptor: DVMDescriptor<State, InputSchema>, opts: JobManagerOpts);
2947
+ /** Active in-memory jobs. */
2948
+ get activeJobs(): Map<string, ServerJob>;
2949
+ /** The backing job store. */
2950
+ get store(): JobStore;
3403
2951
  /**
3404
- * Credit ledger for the terminal funnel (internal-review): a job carrying a
3405
- * `creditId`/`drawId` settles its draw on success and releases it on
3406
- * failure/cancel — this is how "no debit on job failure" is mechanically
3407
- * real. The resolution runs before receipt issuance so the receipt's
3408
- * `credit` block countersigns the post-resolution balance.
2952
+ * True when this DVM will actually issue receipts (internal-review) — a key *and*
2953
+ * a store that can allocate sequence numbers and persist bytes write-once.
2954
+ * `/v1/info#receipts` reads this rather than the key alone, so the flag can
2955
+ * never promise something `issueReceipt` silently declines to do.
3409
2956
  */
3410
- creditLedger?: CreditLedgerLike;
2957
+ get receiptsEnabled(): boolean;
2958
+ /** True when the named capability exists on this descriptor. */
2959
+ hasCapability(name: string): boolean;
2960
+ /** Names of every capability this descriptor exposes — used for error envelopes. */
2961
+ capabilityNames(): string[];
3411
2962
  /**
3412
- * Durable processed-payment markers (internal-review), threaded through to the
3413
- * mid-job top-up path (internal-review) so an x402/mpp payment's marker, `fund`, and
3414
- * `growDraw` commit together.
2963
+ * Resolve a capability's static `price` to msats (internal-review).
2964
+ *
2965
+ * `"$X.XX"` is parsed as USD and converted via the SDK's shared fx fetcher.
2966
+ * Returns `undefined` for dynamic-priced capabilities (those that declare
2967
+ * `onQuote`) — the caller falls back to `computeDynamicPrice` in that path.
3415
2968
  */
3416
- processedPayments?: ProcessedPaymentStore;
2969
+ currentPriceMsats(capability: string): Promise<number | undefined>;
2970
+ /**
2971
+ * Translate a capability's static `price` into the fiat envelope used by
2972
+ * the per-capability `pricing.max` advertised on `/v1/info` (internal-review).
2973
+ * Returns `undefined` for dynamic-priced capabilities and for unknown
2974
+ * names (the route handler validates the name before invoking this).
2975
+ */
2976
+ currentPricingMax(capability: string): {
2977
+ amount: number;
2978
+ currency: string;
2979
+ } | undefined;
2980
+ /**
2981
+ * Parse a request body through a capability's input schema (internal-review).
2982
+ *
2983
+ * Resolution order when a capability declares an `input` schema:
2984
+ * 1. `body.data` is preferred — agents pass structured fields directly
2985
+ * (CLI builds this from `--param k=v`).
2986
+ * 2. Fallback: `JSON.parse(body.input)` for clients still on the legacy
2987
+ * JSON-string contract.
2988
+ * 3. Neither usable → throw `MissingStructuredInputError` so the route can
2989
+ * return `invalid_input` with a hint pointing at `/v1/info`.
2990
+ *
2991
+ * When the capability has no `input` schema, returns `body.input` raw
2992
+ * (primitive path).
2993
+ *
2994
+ * The internal-review credit envelope is removed before the schema runs, on both
2995
+ * branches (internal-review) — it rides the signed body but is protocol-level, so a
2996
+ * top-level `.strict()` capability schema would otherwise reject every
2997
+ * explicit draw as an unknown key, before the envelope was even extracted.
2998
+ * The persisted `record.input` the internal-review reactivation path re-reads is the
2999
+ * pre-Zod wire form, so it comes through the second branch carrying them too.
3000
+ */
3001
+ parseInput(body: {
3002
+ input?: string;
3003
+ data?: unknown;
3004
+ }, capability: string): unknown;
3417
3005
  /**
3418
- * Reporter-owned transactional deposit enqueue threaded into mid-job rail
3419
- * commits (internal-review).
3006
+ * Look up the per-capability descriptor by name (internal-review). Throws when the
3007
+ * name doesn't exist — the route layer validates body.capability first, so
3008
+ * reaching this with an unknown name is a programmer error.
3420
3009
  */
3421
- enqueueCreditDeposit?: CreditDepositEnqueue;
3422
- /** Reporter outbox inserted atomically with a terminal draw release (internal-review). */
3423
- enqueueCreditDrawRelease?: CreditDrawReleaseEnqueue;
3424
- /** Credit lifetime a top-up mints with, from the DVM's advertised `credit.ttl`. */
3425
- creditTtlMs?: number;
3010
+ private requireCapability;
3426
3011
  /**
3427
- * The DVM's declared pricing currency (internal-review), which is the currency of
3428
- * every credit it opens. Threaded into the job context so a mid-job ask on a
3429
- * job that holds no credit yet is still pinned in the denomination its first
3430
- * payment will open the credit in (internal-review). `DVMServer` passes its resolved
3431
- * `pricingCurrency`; a host building a `JobManager` directly falls back to the
3432
- * descriptor, and then to `"usd"`.
3012
+ * Soft capability lookup — `undefined` when the name isn't defined. The
3013
+ * single place the `descriptor.capabilities` index cast lives; callers that
3014
+ * tolerate a missing capability (the route layer pre-validates, or the cap
3015
+ * was deleted from the descriptor after a job was persisted) route through
3016
+ * here instead of re-casting inline.
3433
3017
  */
3434
- pricingCurrency?: string;
3018
+ private getCapability;
3435
3019
  /**
3436
- * Callback fired when a mid-job top-up moves money onto a credit (internal-review).
3437
- * The platform books it as a **deposit** — a liability until drawn, never
3438
- * summed into revenue — so a top-up that skipped this would leave the
3439
- * outstanding-liability view short. Mirrors `DVMServer.reportCreditDeposit`,
3440
- * which handles the upfront and `/v1/credit` legs.
3020
+ * Allocate the id the next job will be created under (internal-review). The
3021
+ * submit path calls this BEFORE payment verification so the ledger draw
3022
+ * commits with its job linkage, then passes the id back through
3023
+ * `createJob`'s provenance. Ids allocated for requests whose payment is
3024
+ * refused are simply never used — the sequence has gaps, which nothing
3025
+ * reads meaning into.
3441
3026
  */
3442
- onCreditFunded?: (info: CreditDepositPayload) => void | Promise<void>;
3027
+ allocateJobId(): string;
3443
3028
  /**
3444
- * Callback fired when a paid job completes (internal-review). Container-runtime DVMs
3445
- * use this to report revenue to the platform via `RevenueReporter`. Mirrors
3446
- * the isolate path's `IsolateJobManager.opts.onJobCompleted`.
3029
+ * Create a new job from a request body. `provenance` (internal-review) is what the
3030
+ * idempotent-replay path on `POST /v1/job` reads back: the raw `job_token`
3031
+ * to re-issue, and the fingerprint + caller pubkey a retry must reproduce
3032
+ * to be given it.
3447
3033
  */
3448
- onJobCompleted?: (info: {
3449
- dvmId: string;
3450
- jobId: string;
3451
- paidMsats: number;
3452
- paymentMint?: string;
3453
- rail: string;
3454
- paymentTxHash?: string;
3455
- nativeAmount?: number;
3456
- nativeAsset?: string;
3457
- cashuFlow?: string;
3458
- creditId?: string;
3459
- drawId?: string;
3460
- drawAmountMicro?: number;
3461
- creditCurrency?: string;
3462
- settledAt?: number;
3463
- fundingRef?: string;
3464
- kind?: string;
3465
- }) => void | Promise<void>;
3034
+ createJob(body: {
3035
+ input: string;
3036
+ params?: Record<string, string>;
3037
+ }, requesterId: string, payment: PaymentInfo, requiredMsats: number | undefined, capability: string, requesterTokenHash?: string, provenance?: {
3038
+ requesterToken?: string;
3039
+ requestFingerprint?: string;
3040
+ requesterPubkey?: string;
3041
+ requestId?: string;
3042
+ authRequestPath?: string;
3043
+ /**
3044
+ * Pre-allocated id from {@link allocateJobId} (internal-review) — the submit
3045
+ * path allocates before payment verification so the ledger draw can
3046
+ * record the job it pays for.
3047
+ */
3048
+ jobId?: string;
3049
+ }): ServerJob;
3050
+ /** Build an SDKJobContext and attach it to the job. */
3051
+ buildAndAttachContext(job: ServerJob, parsedInput: unknown): SDKJobContext<State, ResolvedInput<InputSchema>>;
3052
+ /** Start the handler for a job. */
3053
+ runHandler(job: ServerJob, sdkCtx: SDKJobContext<unknown, unknown>): void;
3054
+ /** Dispatch a validated incoming message to the appropriate handler or pending resolver. */
3055
+ dispatchMessage(job: ServerJob, type: MessageType, content: unknown): Promise<void>;
3466
3056
  /**
3467
- * Callback fired when the stale-job reaper force-fails a *paid* job — its
3468
- * pending credit draw is released rather than settled, because only a
3469
- * completed job settles a draw (internal-review). Container-runtime DVMs wire this
3470
- * to a paid-job-death report → operator `notice` (internal-review). Never fires for
3471
- * free jobs (`paidMsats === 0`). Fired fire-and-forget so a throw or a slow
3472
- * report can't wedge the sweep.
3057
+ * Persist a job to the backing store.
3058
+ *
3059
+ * Awaits the per-job appender tail (internal-review) so that:
3060
+ * 1. The snapshot's `messages` is consistent with the `job_messages` table
3061
+ * — no row is still in flight at snapshot time.
3062
+ * 2. For terminal saves, the `DELETE FROM job_messages` inside `save()`
3063
+ * can't race a still-pending `appendOutgoing` for the final yield
3064
+ * message (which would otherwise wipe the row before an in-flight SSE
3065
+ * subscriber's NOTIFY-driven fetch can see it).
3473
3066
  */
3474
- onPaidJobDeath?: (info: {
3475
- dvmId: string;
3476
- jobId: string;
3477
- capability: string;
3478
- paidMsats: number;
3479
- rail: string;
3480
- reason: "worker_died_mid_job" | "stale_no_terminal_status";
3481
- nativeAmount?: number;
3482
- nativeAsset?: string;
3483
- paymentMint?: string;
3484
- }) => void | Promise<void>;
3067
+ persistJob(job: ServerJob): Promise<void>;
3485
3068
  /**
3486
- * Callback fired when a settled credit draw cannot book a revenue event
3487
- * because no usable payment rail is available (internal-review). Container-runtime
3488
- * DVMs auto-wire this to the platform's deduped operator notice. Fired
3489
- * fire-and-forget so alert delivery cannot block terminal handling or draw
3490
- * reconciliation.
3069
+ * Cross-machine terminal guard (internal-review). `buildContext`'s guard reads the
3070
+ * in-memory `job.status`, so on its own it only protects the machine running
3071
+ * the handler. On a multi-machine DVM a `DELETE /v1/job/:id` routinely lands
3072
+ * on a machine that isn't running the job — `app.ts` cancels it in the store
3073
+ * and this process learns of it only through the durable row.
3074
+ *
3075
+ * Adopting the durable status blocks the rest of the handler's writes via the
3076
+ * local guard, makes `handleJobTerminal` see a non-completed job so it skips
3077
+ * the revenue report, rejects a suspended handler's prompt/payment yields, and
3078
+ * fires `job.abort` so in-flight `ctx.fetch` calls tear down. Returns true when
3079
+ * the durable state won and the caller must skip its save.
3080
+ *
3081
+ * Two triggers: `watchDurableStatus`'s NOTIFY subscription (internal-review) fires
3082
+ * this within a round-trip of the remote cancel committing, which is what
3083
+ * actually stops the provider spend; `persistJob` calls it again on every save
3084
+ * as the backstop for the window where a cancel commits between a handler's
3085
+ * read and its write (the check-then-write it can still lose is covered by the
3086
+ * stores' terminal-sticky `save`, which keeps the row itself correct).
3491
3087
  */
3492
- onRevenueSkippedNoRail?: (info: RevenueSkippedNoRailPayload) => void | Promise<void>;
3088
+ private adoptDurableTerminal;
3493
3089
  /**
3494
- * Stale-job sweep threshold in ms (internal-review). Jobs in a non-terminal status
3495
- * (`processing` / `working` / `awaiting-input`) whose `lastActivityAt` is
3496
- * older than this are force-cancelled with reason `stale_no_terminal_status`,
3497
- * so callers always reach a terminal status instead of polling forever after
3498
- * a worker crash, OOM, or silent `messageAppender` failure. Default:
3499
- * `2 × descriptor.idleTimeout + STALE_JOB_WATCHDOG_HEADROOM_MS`, matching
3500
- * the internal-review acceptance criterion against scrape's `JOB_TIMEOUT_MS`.
3501
- * Builders whose per-handler wall-clock bound exceeds 90s must override
3502
- * this — otherwise the watchdog fires on a legitimate in-flight handler.
3503
- * Set to `0` to disable (test seam).
3090
+ * Watch the durable status of a locally-active job (internal-review).
3091
+ *
3092
+ * `ctx.signal` is raised by `dispatchMessage`, which only runs on the machine
3093
+ * holding the job in `activeJobs`. On a multi-machine DVM the `DELETE` usually
3094
+ * lands somewhere else, and that machine's store-only cancel (`cancelJob`)
3095
+ * commits the terminal status, appends the `cancel` message, and `pg_notify`s
3096
+ * in one transaction. This subscription is how the handler's machine hears it:
3097
+ * on each notify, re-read the durable status and adopt it if it went terminal.
3098
+ * Without it, the handler only notices at its next store write, and the
3099
+ * in-flight provider call — the spend the cancel was meant to stop — runs to
3100
+ * completion.
3101
+ *
3102
+ * Every locally-emitted message notifies this machine too, so the re-read is
3103
+ * coalesced: at most one `getCounters` in flight per job, and none once the
3104
+ * job is locally terminal.
3504
3105
  */
3505
- staleJobTimeoutMs?: number;
3106
+ private watchDurableStatus;
3506
3107
  /**
3507
- * Sweep interval for the stale-job watchdog. Default: `min(60s, max(15s,
3508
- * tightestActiveTimeout / 4))`. Tests override to fire deterministically.
3108
+ * Tear down a job's durable-status subscription (internal-review). Must be called
3109
+ * wherever a job leaves `activeJobs` — a leaked subscriber outlives the job on
3110
+ * the store's shared LISTEN connection. Idempotent.
3509
3111
  */
3510
- staleJobSweepIntervalMs?: number;
3112
+ unwatchDurableStatus(jobId: string): void;
3511
3113
  /**
3512
- * Worker-liveness watchdog in ms (internal-review). Jobs in `processing`/`working`
3513
- * whose `lastActivityAt` is older than this are marked `failed` (dead
3514
- * worker), separately from the longer `awaiting-input` idle timeout above.
3515
- * Default: `descriptor.processingWatchdog * 1000` or
3516
- * `DEFAULT_PROCESSING_WATCHDOG_MS`. Set to `0` to disable (test seam).
3114
+ * NOTIFY-driven durable-status re-read, coalesced per job (internal-review).
3115
+ *
3116
+ * A notify that arrives while a re-read is in flight is queued rather than
3117
+ * dropped: the in-flight read may have observed the row a moment *before* the
3118
+ * cancel committed, and dropping its notify would put the abort back where
3119
+ * this issue found it — waiting for the handler's next store write.
3517
3120
  */
3518
- processingWatchdogMs?: number;
3121
+ private checkDurableTerminal;
3519
3122
  /**
3520
- * Interval at which locally-active `processing`/`working` jobs have their
3521
- * `lastActivityAt` bumped so the processing watchdog only fires on a truly
3522
- * dead worker (internal-review). Default: `DEFAULT_HEARTBEAT_INTERVAL_MS`. Set to
3523
- * `0` to disable (test seam). Also floors how far the internal-review clock guard
3524
- * may compensate a backwards step on the processing arm — a cadence close to
3525
- * `processingWatchdogMs` leaves it no room and it compensates nothing.
3123
+ * Wire the Tx A appender when the store supports streaming.
3124
+ *
3125
+ * Tx A (internal-review): outgoing message + `pending_payment_msats` bump land in
3126
+ * the same transaction. The DB allocates the row's seq via
3127
+ * `UPDATE jobs SET next_seq = next_seq + 1 RETURNING` so concurrent inbound
3128
+ * traffic on different machines can never collide on the `(job_id, seq)` PK.
3129
+ * In-memory `job.seq` keeps its own counter for same-machine SSE listeners.
3130
+ *
3131
+ * Per-job serialisation (internal-review): chained through `job.messageAppenderTail`
3132
+ * so two synchronous `providerMessage` calls (e.g. `artifact` followed by
3133
+ * `complete`) commit their `appendOutgoing` transactions in call order.
3134
+ * Without this, the two transactions race for the `jobs` row lock and the
3135
+ * DB-side seq can be allocated in the opposite order — which both renumbers
3136
+ * the persisted messages and reorders the NOTIFY events feeding the
3137
+ * cross-machine SSE subscriber. The subscriber closes the stream on the
3138
+ * first yield message it sees, so a NOTIFY for `complete` arriving before
3139
+ * `artifact`'s NOTIFY silently drops the artifact.
3526
3140
  */
3527
- heartbeatIntervalMs?: number;
3141
+ private wireMessageAppender;
3142
+ /** Start (or restart) the idle timer for a job. */
3143
+ startIdleTimer(job: ServerJob): void;
3144
+ /** Clear the idle timer for a job. */
3145
+ clearIdleTimer(id: string): void;
3146
+ /** Cancel a job due to idle timeout. Exposed for the reactivation path's idle-expiry guard (internal-review). */
3147
+ cancelJobIdle(job: ServerJob): Promise<void>;
3528
3148
  /**
3529
- * Age in ms at which a `pending` credit draw whose `job_id` matches no row in
3530
- * the job store is released as orphaned (internal-review). The draw commits before
3531
- * the job row is persisted, so a crash or a fail-closed refusal in that
3532
- * window strands a hold no terminal path can ever reach. Must stay
3533
- * comfortably above the submit path's draw→persist latency. Set to `0` to
3534
- * disable the watchdog entirely — stranded holds then stay stranded, so
3535
- * treat it as a money knob, not a tuning one. Default:
3536
- * {@link DEFAULT_ORPHAN_DRAW_AGE_MS}.
3149
+ * Force-terminate stale non-terminal jobs. Two status-aware arms:
3150
+ * • `awaiting-input` past `staleJobTimeoutMs` → `cancelled`
3151
+ * (`stale_no_terminal_status`) — the caller never paid / responded.
3152
+ * • `processing`/`working` past `processingWatchdogMs` → `failed`
3153
+ * (`worker_died_mid_job`) — the worker died mid-job. The heartbeat keeps
3154
+ * live workers fresh, so a stale row here means a dead process.
3155
+ *
3156
+ * Fires on a timer (wired in the constructor) and also callable on demand
3157
+ * (tests, ops). The CAS in `cancelStaleJob` makes this safe to run
3158
+ * concurrently from multiple DVM machines. Scoped to capabilities this
3159
+ * JobManager owns so multi-mount hosts don't steal stuck rows from sibling
3160
+ * descriptors — the local activeJobs teardown only makes sense on the
3161
+ * JobManager that actually hosted the zombie handler.
3162
+ *
3163
+ * **Both cutoffs float off the wall clock (internal-review).** Each is one
3164
+ * `Date.now()` sample compared against `last_activity_at`, stamped from a
3165
+ * different sample at a different moment, so a backwards step between the
3166
+ * two makes every row look more recently active than it is: the query
3167
+ * matches nothing and both arms go silent for the length of the skew. The
3168
+ * dead worker's job keeps its `processing` status, its credit hold stays
3169
+ * `pending`, and the internal-review paid-job-death alert — wired to this reaper —
3170
+ * never fires. Same defect, same host class, as internal-review's orphan sweep.
3171
+ *
3172
+ * **The compensation is capped** — see {@link staleSweepCompensationCapMs}
3173
+ * for where each arm's cap comes from — which internal-review did not need to do. Its eagerness costs at worst an early release
3174
+ * of an unclaimed hold, refused outright for any live job row; ours
3175
+ * force-*fails* a running job. `cancelStaleJob`'s CAS does not cover that:
3176
+ * `expectedActivityBefore` is the same compensated threshold the query used,
3177
+ * and a live worker's post-step heartbeat writes the low, post-step
3178
+ * `Date.now()`, so `last_activity_at <= expectedActivityBefore` still holds.
3179
+ * The CAS guards a job checking in *between* the find and the claim with a
3180
+ * stamp above the cutoff — a race, not a clock.
3181
+ *
3182
+ * **Some blindness is inherent, and no cap choice removes it.** A backwards
3183
+ * step of `S` inverts the stamp ordering: everything stamped after it reads
3184
+ * `S` older than everything stamped before. A live worker heartbeating right
3185
+ * now stamps `wall - S`; a job genuinely idle for `age` stamps `wall - age`.
3186
+ * The stale row is the lower of the two — the one a cutoff reaches first —
3187
+ * only once `age` exceeds `S`. Below that the reaper cannot tell them apart,
3188
+ * so any cutoff that claimed the stale job would force-fail every live
3189
+ * worker on the host with it. The cap picks where on that curve to sit: on
3190
+ * the defaults a row must have been silent for `S + window / 2`, and a live
3191
+ * worker keeps five missed heartbeats of margin. The
3192
+ * alternative to a missed reap is mild and self-healing — the orphan sweep
3193
+ * backstops the hold, and the arm recovers when the wall catches up. The
3194
+ * alternative to a false reap is an irreversible force-fail of paid work.
3195
+ *
3196
+ * The stamps themselves stay wall-clock epoch-ms on purpose — they are
3197
+ * written by whichever machine touched the job and read by every other one,
3198
+ * so only the reading side can float.
3537
3199
  */
3538
- orphanDrawAgeMs?: number;
3200
+ sweepStaleJobs(): Promise<{
3201
+ swept: number;
3202
+ }>;
3539
3203
  /**
3540
- * Age in ms at which a `pending` credit draw whose job row **is** terminal is
3541
- * reconciled against that row's outcome (internal-review) — settled for a
3542
- * `completed` job, released for a `failed`/`cancelled` one. Covers the hole
3543
- * `orphanDrawAgeMs` cannot: `resolveCreditDraw` throwing on a ledger error
3544
- * leaves a terminal job whose hold no later path retries.
3204
+ * How far one watchdog arm may lean on {@link monotonicNowMs} past the wall
3205
+ * clock to cover a backwards step (internal-review). The compensation is capped,
3206
+ * not free: a claimed row must still have been silent for
3207
+ * `windowMs - cap`, and unlike the orphan sweep, claiming eagerly here
3208
+ * force-fails a job that may well be alive.
3545
3209
  *
3546
- * Rides the same scan as the orphan arm, which has two consequences for the
3547
- * value you set here. Setting `orphanDrawAgeMs` to `0` disables this arm too.
3548
- * And the effective gate is `max(orphanDrawAgeMs, terminalDrawReconcileAgeMs)`
3549
- * — the scan never returns a row younger than its own cutoff, so anything
3550
- * below `orphanDrawAgeMs` is silently clamped up to it. That floor is
3551
- * deliberate rather than a wart: honouring a narrower value would mean
3552
- * widening the scan, which hands the orphan arm rows younger than
3553
- * `orphanDrawAgeMs` that it must not release. It can only ever delay a
3554
- * settle, never advance one, which is the safe direction for a debit.
3210
+ * **Half the window** is the floor that governs a wide window — five missed
3211
+ * heartbeats on the processing defaults. On the `awaiting-input` arm it also
3212
+ * preserves a real invariant: with the derived
3213
+ * `2 x idleTimeout + headroom`, half is `idleTimeout + 45s`, so the sweeper
3214
+ * still cannot fire before the in-process idle timer would have. (A builder
3215
+ * who overrides `staleJobTimeoutMs` below `2 x idleTimeout` has already
3216
+ * opted out of that, guard or no guard.)
3555
3217
  *
3556
- * Set to `0` to disable this arm alone — a money knob, not a tuning one.
3557
- * Default: {@link DEFAULT_TERMINAL_DRAW_RECONCILE_AGE_MS}.
3218
+ * **Two heartbeat intervals** is the floor that governs a window configured
3219
+ * close to the beat cadence — `processingWatchdog: 40` against the 30s
3220
+ * default beat, where half the window is less than one beat and a worker
3221
+ * heartbeating exactly on schedule would be reaped the moment the host's
3222
+ * clock stepped. That pair is marginal already; the guard must not make it
3223
+ * deterministic. The cap collapses to zero there, which is today's
3224
+ * behaviour: late, never wrong. The `awaiting-input` arm takes no such term
3225
+ * — nothing heartbeats it by design, and its refresh is an inbound caller
3226
+ * message on no cadence at all.
3227
+ *
3228
+ * **Why a magnitude and not a predicate.** The tempting sharper rule is to
3229
+ * compensate per row, fully, for any stamp that reads ahead of our wall —
3230
+ * one our own post-step heartbeat could not have written. It is unsafe: a
3231
+ * future-stamped row is equally what a live job heartbeated by a peer whose
3232
+ * clock runs fast looks like, and from the stamp alone the two are
3233
+ * indistinguishable. The same reasoning bounds the cap from the other side —
3234
+ * while our clock is stepped, the guard force-fails the live jobs of any
3235
+ * peer slower than `windowMs - cap`, which is 2.5 minutes of tolerated
3236
+ * disagreement on the defaults, far outside anything NTP-managed hosts show.
3558
3237
  */
3559
- terminalDrawReconcileAgeMs?: number;
3238
+ private staleSweepCompensationCapMs;
3560
3239
  /**
3561
- * Sweep interval for the orphan-draw watchdog. Default:
3562
- * {@link DEFAULT_ORPHAN_DRAW_SWEEP_INTERVAL_MS}. Set to `0` to disable — the
3563
- * test seam, since suites drive `sweepOrphanDraws()` directly.
3564
- */
3565
- orphanDrawSweepIntervalMs?: number;
3240
+ * Redact terminal job content beyond this DVM's configured window.
3241
+ *
3242
+ * The scan advances over every candidate, including rows that fail draw
3243
+ * resolution, so one money-path fault cannot starve everything behind it.
3244
+ * Any failed row remains unredacted and the next tick restarts from the head;
3245
+ * a page-capped clean pass instead retains its cursor and resumes at the
3246
+ * tail. Failure and recovery callbacks are awaited but never allowed to
3247
+ * change the sweep result.
3248
+ */
3249
+ sweepJobRetention(): Promise<{
3250
+ examined: number;
3251
+ redacted: number;
3252
+ complete: boolean;
3253
+ }>;
3254
+ /** Resolve every hold and persist any configured receipt before content disappears. */
3255
+ private prepareJobForRedaction;
3256
+ private notifyJobRetentionFailed;
3257
+ private notifyJobRetentionRecovered;
3566
3258
  /**
3567
- * Grace window in ms a reactivating machine waits for a live original handler
3568
- * to win the single-execution claim before replaying an `awaiting-input` job
3569
- * itself (internal-review). Default: `DEFAULT_REACTIVATION_CLAIM_GRACE_MS`. Widen if
3570
- * cross-machine NOTIFY latency causes spurious double executions; lower in
3571
- * tests for speed.
3259
+ * Resolve `pending` credit draws the terminal funnel can no longer reach.
3260
+ * Two arms over one scan, distinguished by whether the draw's job row exists.
3261
+ *
3262
+ * **No job row (internal-review) — release.** internal-review allocates the job id and
3263
+ * commits the ledger draw, with its `job_id` linkage, before `recordReplay`
3264
+ * and `persistJob` write the job row. A crash, a `draw_conflict` 409, or a
3265
+ * fail-closed `replay_detected` 401 in that window leaves a committed hold
3266
+ * that **no** terminal path can ever reach: `issueReceipt` →
3267
+ * `resolveCreditDraw` keys off the `creditId`/`drawId` persisted on the job
3268
+ * row, so with no row there is no release path at all and the caller's
3269
+ * available balance stays reduced forever. Cosmetic for an implicit N=1
3270
+ * credit, a silent balance shrink for an explicit N>1 one.
3271
+ *
3272
+ * **Terminal job row (internal-review) — reconcile to its outcome.** The funnel
3273
+ * demonstrably fails: `resolveCreditDraw` throws on a ledger/DB error,
3274
+ * `issueReceipt` catches it, logs `credit_resolve_failed` and returns
3275
+ * nothing, and until now nothing retried. Same stranded hold, reached through
3276
+ * a different door — and for a `completed` job it strands the platform's
3277
+ * books too, since draws *are* the revenue events (internal-review): the caller is
3278
+ * charged nothing, the balance never moves, and outstanding liability
3279
+ * (`deposits − draw revenue`) overstates forever. So a late settle books, off
3280
+ * the committed draw row, through the ordinary reporter path.
3281
+ *
3282
+ * **A non-terminal job row is still never touched** — the stale-job sweep
3283
+ * above drives those to terminal first, and its `issueReceiptForStoredJob`
3284
+ * resolves the draw on the way. Those skipped holds are why the scan
3285
+ * keyset-paginates rather than re-reading one batch: they stay `pending`
3286
+ * indefinitely (an `awaiting-input` job on a 30-minute idle timeout sits well
3287
+ * past the orphan cutoff), so a fixed first page of them would starve every
3288
+ * real orphan behind it, on every tick, forever. The cursor advances over
3289
+ * every row examined; a tick that hits {@link ORPHAN_DRAW_SWEEP_MAX_PAGES}
3290
+ * logs `orphan_draw_sweep_truncated` and the next one resumes where it
3291
+ * stopped, so progress is bounded per tick but never blocked.
3292
+ *
3293
+ * Fires on a timer (wired in the constructor) and also callable on demand
3294
+ * (tests, ops). Both `settle` and `release` are idempotent under the credit
3295
+ * row's `FOR UPDATE`, so concurrent sweepers on several machines — and a
3296
+ * sweeper racing the in-line funnel — resolve the hold exactly once; the
3297
+ * losers see `replayed: true` and book nothing.
3572
3298
  */
3573
- reactivationClaimGraceMs?: number;
3574
- }
3575
- type ResolvedInput<I extends ZodLike | undefined> = I extends ZodLike<infer O> ? O : string;
3576
- /**
3577
- * A deposit this payment funded that the job row did **not** take (internal-review).
3578
- *
3579
- * Reached when two mid-job top-ups race on a free-then-paid job: each opens its
3580
- * own implicit credit, and `verifyAndCredit`'s set-if-null bind keeps one. The
3581
- * loser's money is real, owned by the caller, and reclaimable — but nothing
3582
- * named it, so the caller would have to derive `imp:<rail>:<payment_id>` to
3583
- * reach their own balance. This rides the 201 so they don't have to.
3584
- *
3585
- * The response is the fast surface, not the only one: the credit is opened
3586
- * under the caller's verified pubkey, so `POST /v1/credit` op `balance` lists
3587
- * it and op `drain` reclaims it once the hold resolves.
3588
- */
3589
- interface UnappliedCredit {
3590
- credit_id: string;
3591
- credited_micro: number;
3592
- credit_currency: string;
3593
- display: string;
3594
- hint: string;
3595
- }
3596
- /**
3597
- * What {@link JobManager.processPayment} tells the route. `error` is a refusal
3598
- * to serialise verbatim; otherwise the payment was accepted, optionally
3599
- * carrying an {@link UnappliedCredit} to fold into the 201.
3600
- */
3601
- type ProcessPaymentOutcome = {
3602
- error: {
3603
- body: Record<string, unknown>;
3604
- status: number;
3605
- };
3606
- unappliedCredit?: undefined;
3607
- } | {
3608
- error?: undefined;
3609
- unappliedCredit?: UnappliedCredit;
3610
- };
3611
- /**
3612
- * Transport-agnostic job lifecycle manager.
3613
- *
3614
- * Handles job creation, handler invocation, message dispatch, persistence,
3615
- * idle timeout, and durable replay. Shared by both the HTTP server and
3616
- * relay provider.
3617
- */
3618
- declare class JobManager<State, InputSchema extends ZodLike | undefined> {
3619
- private readonly descriptor;
3620
- private readonly opts;
3621
- private readonly _activeJobs;
3622
- private readonly jobStore;
3623
- private readonly idleTimers;
3624
- private readonly cleanupTimers;
3625
- /** Per-job NOTIFY unsubscribe handles for the durable-status watch (internal-review). */
3626
- private readonly statusWatchers;
3627
- /** Job ids whose durable-status re-read is in flight — coalesces notify storms (internal-review). */
3628
- private readonly statusChecksInFlight;
3629
- /** Job ids that were notified mid-re-read and must be re-checked (internal-review). */
3630
- private readonly statusChecksQueued;
3631
- private readonly sessionId;
3632
- private nextJobId;
3633
- private accumulatorMonitor?;
3634
- private readonly staleJobTimeoutMs;
3635
- private readonly processingWatchdogMs;
3636
- private readonly reactivationClaimGraceMs;
3299
+ sweepOrphanDraws(): Promise<{
3300
+ released: number;
3301
+ reconciled: number;
3302
+ }>;
3637
3303
  /**
3638
- * Worker-heartbeat cadence (internal-review). Also floors the internal-review clock
3639
- * guard's compensation on the processing arm — see
3640
- * {@link staleSweepCompensationCapMs}.
3304
+ * The end-of-scan line for a tick that resolved nothing (internal-review, extended
3305
+ * by internal-review). Two shapes are worth a warning, and neither is the ordinary
3306
+ * one — the timer runs every 5 minutes per ledger in production.
3307
+ *
3308
+ * `examined > 0`: aged candidates were looked at and none moved. Usually
3309
+ * legitimate (their jobs are still running, or terminal inside the reconcile
3310
+ * grace period), but it is also what a silently skipped hold looks like.
3311
+ *
3312
+ * `examined === 0`: the query returned nothing. Ordinarily that means there
3313
+ * is nothing to do — but it is equally what a blinded scan looks like, which
3314
+ * is the case the internal-review gate could not reach. Probe for the oldest
3315
+ * `pending` hold at any age and speak up when one exists the scan should
3316
+ * have seen and didn't: already past the age gate outright, stamped ahead of
3317
+ * our clock (some other process's clock is fast), or hidden while our own
3318
+ * clock is held forward over a backwards step. A ledger holding nothing but
3319
+ * young draws stays quiet.
3320
+ *
3321
+ * The probe is diagnostics, so it is wrapped: a tick that swept cleanly must
3322
+ * never report failure because the extra read fell over. A probe that throws
3323
+ * says so on the same line rather than replacing the tick's outcome.
3641
3324
  */
3642
- private readonly heartbeatIntervalMs;
3643
- private readonly orphanDrawAgeMs;
3644
- private readonly terminalDrawReconcileAgeMs;
3645
- private staleSweepTimer?;
3646
- private heartbeatTimer?;
3647
- private orphanDrawSweepTimer?;
3648
- /** This manager holds {@link ORPHAN_SWEEP_CLAIMS} for its ledger. */
3649
- private orphanSweepClaimed;
3650
- /** Where a page-capped tick left off; cleared once a tick reaches the end. */
3651
- private orphanSweepResumeFrom?;
3325
+ private reportSweepOutcome;
3652
3326
  /**
3653
- * A `Date.now()` reading that cannot regress within this manager's lifetime
3654
- * (internal-review). Anchored at construction, so a manager already running when
3655
- * the clock stepped is covered. The stale-job sweep's two cutoffs, the
3656
- * orphan-draw age gate and the reactivation claim grace all read it; see
3657
- * {@link createMonotonicClock} for what it costs and where it is clamped.
3327
+ * Resolve one aged `pending` draw whose job row reached terminal (internal-review),
3328
+ * mirroring what `resolveCreditDraw` would have done in line. Returns true
3329
+ * when this call is the one that moved the draw — the caller counts it, and
3330
+ * only it books.
3331
+ *
3332
+ * **The settle is gated on the job row naming _this_ draw.** The scan finds
3333
+ * the draw by `credit_draws.job_id`, but the job's own `credit_id`/`draw_id`
3334
+ * are written by a *later* transaction than the one that committed the draw
3335
+ * (`verifyIncomingPayment` commits fund+draw; `verifyAndCredit` binds the
3336
+ * row), and a free-then-paid job binds set-if-null. A store failure or a
3337
+ * second concurrent top-up therefore leaves a hold tagged with the job whose
3338
+ * row points somewhere else, or nowhere. Settling that would debit the caller
3339
+ * a second time for one job — and invisibly, since `revenue_events`'
3340
+ * `UNIQUE (dvm_id, job_id, rail)` would drop the booking. So an unbound hold
3341
+ * is **released**, never settled: the job's payment was the draw it is bound
3342
+ * to, and this one bought nothing.
3343
+ *
3344
+ * Concurrency needs no guard of its own. `settle`/`release` run under the
3345
+ * credit row's `FOR UPDATE`, so exactly one caller — a sibling reconciler,
3346
+ * or the in-line funnel arriving late — sees `replayed: false`, which makes
3347
+ * the booking gate a true mutex rather than a hopeful one. The one race left
3348
+ * open is another machine's `handleJobTerminal` stalled between resolving the
3349
+ * draw and booking it, which would double-report; the platform's
3350
+ * `(dvm_id, credit_id, draw_id)` and `(dvm_id, job_id, rail)` unique indexes
3351
+ * absorb that.
3352
+ *
3353
+ * `invalid_draw_state` — the funnel having already resolved the draw to the
3354
+ * other state — is the expected loss, logged and swallowed like the orphan
3355
+ * arm's, so one contended hold can't strand the rest of the batch.
3658
3356
  */
3659
- private readonly monotonicNowMs;
3660
- private sweepInFlight;
3661
- private orphanSweepInFlight;
3662
- private heartbeatInFlight;
3357
+ private reconcileTerminalDraw;
3663
3358
  /**
3664
- * The denomination of every credit this DVM opens (internal-review). Resolved once,
3665
- * with the same boundary fallback `DVMServer` applies for direct JavaScript
3666
- * callers that force an incomplete value past the descriptor contract.
3359
+ * Refresh `lastActivityAt` for jobs this process is actively running
3360
+ * (`processing`/`working`) so the processing watchdog only fires once the
3361
+ * worker is genuinely dead (internal-review). `awaiting-input` jobs are
3362
+ * deliberately excluded — their idle timeout must still elapse. Fires on the
3363
+ * heartbeat timer; also callable on demand for tests.
3667
3364
  */
3668
- private readonly pricingCurrency;
3669
- constructor(descriptor: DVMDescriptor<State, InputSchema>, opts: JobManagerOpts);
3670
- /** Active in-memory jobs. */
3671
- get activeJobs(): Map<string, ServerJob>;
3672
- /** The backing job store. */
3673
- get store(): JobStore;
3365
+ heartbeatActiveJobs(): Promise<{
3366
+ beat: number;
3367
+ }>;
3674
3368
  /**
3675
- * True when this DVM will actually issue receipts (internal-review) — a key *and*
3676
- * a store that can allocate sequence numbers and persist bytes write-once.
3677
- * `/v1/info#receipts` reads this rather than the key alone, so the flag can
3678
- * never promise something `issueReceipt` silently declines to do.
3369
+ * Single-execution claim for a reactivating machine (internal-review). Before
3370
+ * replaying an `awaiting-input` job, wait a bounded grace window for a live
3371
+ * original handler to win the `awaiting-input → processing` CAS via its
3372
+ * NOTIFY wake. Resolves:
3373
+ * • `false` — the row left `awaiting-input` during the window (the original
3374
+ * handler claimed it / drove it terminal). Stand down; do not replay.
3375
+ * • `true` — the window elapsed still `awaiting-input` (the original worker
3376
+ * is gone or has no live resolver) AND this machine won the claim CAS.
3377
+ * Replay for dead-worker recovery.
3378
+ *
3379
+ * Biasing the original handler to win kills the double execution (double
3380
+ * substrate spend, clobbered artifact) without heartbeating `awaiting-input`
3381
+ * (which would break the caller-input idle timeout). Non-streamable stores
3382
+ * have no cross-machine race, so the claim is a no-op `true`.
3679
3383
  */
3680
- get receiptsEnabled(): boolean;
3681
- /** True when the named capability exists on this descriptor. */
3682
- hasCapability(name: string): boolean;
3683
- /** Names of every capability this descriptor exposes — used for error envelopes. */
3684
- capabilityNames(): string[];
3384
+ claimAwaitingInputForReactivation(jobId: string): Promise<boolean>;
3685
3385
  /**
3686
- * Resolve a capability's static `price` to msats (internal-review).
3386
+ * Record an inbound client message durably via Tx B (internal-review) and append
3387
+ * it to the in-memory `job.messages` array. Returns the DB-allocated seq
3388
+ * (`null` when the store isn't streamable — non-Postgres test fallback).
3687
3389
  *
3688
- * `"$X.XX"` is parsed as USD and converted via the SDK's shared fx fetcher.
3689
- * Returns `undefined` for dynamic-priced capabilities (those that declare
3690
- * `onQuote`) — the caller falls back to `computeDynamicPrice` in that path.
3390
+ * Status starts as `pending-verification`; a follow-up Tx C call
3391
+ * (`processPayment` for payments, `markInboundVerified` for everything
3392
+ * else) transitions it to `verified` once the inbound is accepted.
3691
3393
  */
3692
- currentPriceMsats(capability: string): Promise<number | undefined>;
3394
+ recordInboundPending(job: ServerJob, msgType: MessageType, msgContent: Record<string, unknown>): Promise<number | null>;
3693
3395
  /**
3694
- * Translate a capability's static `price` into the fiat envelope used by
3695
- * the per-capability `pricing.max` advertised on `/v1/info` (internal-review).
3696
- * Returns `undefined` for dynamic-priced capabilities and for unknown
3697
- * names (the route handler validates the name before invoking this).
3396
+ * Tx C complement for non-payment inbound messages — flip the row to
3397
+ * `verified` so cross-machine readers see it via `subscribeMessages`.
3698
3398
  */
3699
- currentPricingMax(capability: string): {
3700
- amount: number;
3701
- currency: string;
3702
- } | undefined;
3399
+ markInboundVerified(jobId: string, inboundSeq: number | null): Promise<void>;
3703
3400
  /**
3704
- * Parse a request body through a capability's input schema (internal-review).
3705
- *
3706
- * Resolution order when a capability declares an `input` schema:
3707
- * 1. `body.data` is preferred — agents pass structured fields directly
3708
- * (CLI builds this from `--param k=v`).
3709
- * 2. Fallback: `JSON.parse(body.input)` for clients still on the legacy
3710
- * JSON-string contract.
3711
- * 3. Neither usable → throw `MissingStructuredInputError` so the route can
3712
- * return `invalid_input` with a hint pointing at `/v1/info`.
3401
+ * Reactivate a suspended/event-driven job from the store. Builds the
3402
+ * in-memory `ServerJob`, wires the appender, builds the context, starts
3403
+ * the idle timer. The caller has already recorded the inbound via Tx B
3404
+ * and run Tx C (verify + credit); reactivation is now decoupled from
3405
+ * credit (internal-review, internal-review). The caller decides whether to run the
3406
+ * handler (Pattern A: status was `awaiting-input`) or to dispatch the
3407
+ * message to a custom handler (Pattern B).
3713
3408
  *
3714
- * When the capability has no `input` schema, returns `body.input` raw
3715
- * (primitive path).
3409
+ * For DVMs that declare descriptor-level auth, the persisted
3410
+ * `record.input` carries the original signed envelope. We re-verify the
3411
+ * signature here (internal-review) so a DB-layer tamper — compromised admin, SQL
3412
+ * injection, malicious operator with DB access — can't silently feed an
3413
+ * attacker-supplied pubkey/envelope into the handler. The drift window
3414
+ * and replay store are deliberately skipped: the persisted timestamp is
3415
+ * from the original signing instant (long past the 5-min drift bound for
3416
+ * any long-running job), and the nonce was already committed at submission.
3417
+ * On failure: throws `SignedRequestError` ({@link AuthSchemaError} when
3418
+ * the capability has no input schema to verify against); the caller is
3419
+ * expected to mark the row failed and surface a 401 / 500 to the inbound
3420
+ * caller.
3716
3421
  *
3717
- * The internal-review credit envelope is removed before the schema runs, on both
3718
- * branches (internal-review) — it rides the signed body but is protocol-level, so a
3719
- * top-level `.strict()` capability schema would otherwise reject every
3720
- * explicit draw as an unknown key, before the envelope was even extracted.
3721
- * The persisted `record.input` the internal-review reactivation path re-reads is the
3722
- * pre-Zod wire form, so it comes through the second branch carrying them too.
3723
- */
3724
- parseInput(body: {
3725
- input?: string;
3726
- data?: unknown;
3727
- }, capability: string): unknown;
3728
- /**
3729
- * Look up the per-capability descriptor by name (internal-review). Throws when the
3730
- * name doesn't exist — the route layer validates body.capability first, so
3731
- * reaching this with an unknown name is a programmer error.
3422
+ * What gets re-verified is the persisted row read back through
3423
+ * {@link signedRequestInput} — `app.ts` stores the pre-Zod wire form, so
3424
+ * those are the caller's own signed bytes, the same ones `/v1/quote` and the
3425
+ * submit checked (internal-review). That makes the tamper check strictly stronger
3426
+ * than the parsed form it replaced: an injected key the capability schema
3427
+ * doesn't declare used to be stripped before the signature was checked, so
3428
+ * it re-verified clean.
3732
3429
  */
3733
- private requireCapability;
3430
+ reactivateJob(record: JobRecord): {
3431
+ job: ServerJob;
3432
+ sdkCtx: SDKJobContext<unknown, unknown>;
3433
+ };
3734
3434
  /**
3735
- * Soft capability lookup — `undefined` when the name isn't defined. The
3736
- * single place the `descriptor.capabilities` index cast lives; callers that
3737
- * tolerate a missing capability (the route layer pre-validates, or the cap
3738
- * was deleted from the descriptor after a job was persisted) route through
3739
- * here instead of re-casting inline.
3435
+ * Verify an inbound payment and apply the credit via Tx C (internal-review).
3436
+ * The inbound message must have been recorded via Tx B already
3437
+ * (`recordInboundPending` returned `inboundSeq`). For non-streamable
3438
+ * stores (`inboundSeq === null`), falls back to in-memory mutation via
3439
+ * `applyPaymentInfoToJob`.
3440
+ *
3441
+ * Dev-mode short-circuit: auto-credits the outstanding
3442
+ * `pendingPaymentMsats` without external verification. Gated on
3443
+ * `devModeSkipsPaymentVerification` — the same predicate the upfront path
3444
+ * reads, so a dev server wired to a mint verifies mid-job payments for real
3445
+ * (internal-review) — plus the explicit `dev_auto` opt-out the dev console uses,
3446
+ * which is the one caller that has no wallet to pay from. `dev_auto` loses to
3447
+ * any proof riding the same message: money on the wire always takes the rail,
3448
+ * so the flag can never leave a real token unspent against a credited job.
3449
+ *
3450
+ * Tx C is not atomic with the rail commit (internal-review). `verifyIncomingPayment`
3451
+ * commits the proofs *and* the ledger leg in one transaction and returns;
3452
+ * only then does `verifyAndCredit` write the job row, on its own connection.
3453
+ * Everything after that call therefore runs with the caller's money already
3454
+ * moved, which is why the write is retried rather than left to throw, and why
3455
+ * an exhausted retry answers with the credit it landed on instead of a bare
3456
+ * 500. See `creditInbound`.
3740
3457
  */
3741
- private getCapability;
3458
+ processPayment(job: ServerJob, body: {
3459
+ type: MessageType;
3460
+ content: Record<string, unknown>;
3461
+ }, inboundSeq: number | null, extra?: {
3462
+ lockPubkeys?: LockPubkey[];
3463
+ mintHealthTracker?: MintHealthTracker;
3464
+ }): Promise<ProcessPaymentOutcome>;
3742
3465
  /**
3743
- * Allocate the id the next job will be created under (internal-review). The
3744
- * submit path calls this BEFORE payment verification so the ledger draw
3745
- * commits with its job linkage, then passes the id back through
3746
- * `createJob`'s provenance. Ids allocated for requests whose payment is
3747
- * refused are simply never used — the sequence has gaps, which nothing
3748
- * reads meaning into.
3466
+ * Tx C with a bounded retry (internal-review).
3467
+ *
3468
+ * By the time this runs on the verified path, the rail commit and the ledger
3469
+ * leg have already committed in a transaction this call is not part of — so a
3470
+ * throw here is money moved against a job row that records none of it, and
3471
+ * letting it propagate was the bug. A transient store error is the ordinary
3472
+ * cause and a second attempt clears it.
3473
+ *
3474
+ * The retry is safe by construction, not by convention: `verifyAndCredit`
3475
+ * CASes on `job_messages.status = 'pending-verification'` inside its own
3476
+ * transaction, so a call whose COMMIT actually landed before the ack was lost
3477
+ * finds the row already `verified` and returns `alreadyVerified: true`
3478
+ * carrying the counters and binding that first attempt committed. Re-applying
3479
+ * the delta is impossible either way.
3480
+ *
3481
+ * That argument covers every attempt but the last, whose error nothing
3482
+ * re-checks — and a connection-level fault is exactly the shape that loses
3483
+ * the ack on *all* of them, idempotent replays included (the no-op path
3484
+ * COMMITs too). So the exhausted path asks the row before reporting a
3485
+ * failure: `getVerifiedInbound` reads, transaction-free, whether a Tx C for
3486
+ * this seq committed. Without it a fully credited, fully bound job answers
3487
+ * `payment_unapplied` and invites a retry, and a caller who takes it pays
3488
+ * twice for one ask — the second payment growing the now-bound draw and
3489
+ * settling as revenue.
3490
+ *
3491
+ * The read runs on the pool that just failed those COMMITs, so it is retried
3492
+ * too, and a read that fails all the way through is reported as *unknown*
3493
+ * rather than as nothing: `CreditInboundExhaustedError.readError` is what
3494
+ * separates "checked the row, nothing landed" from "couldn't check", both on
3495
+ * the operator's log line and in the copy the caller acts on.
3749
3496
  */
3750
- allocateJobId(): string;
3497
+ private creditInbound;
3751
3498
  /**
3752
- * Create a new job from a request body. `provenance` (internal-review) is what the
3753
- * idempotent-replay path on `POST /v1/job` reads back: the raw `job_token`
3754
- * to re-issue, and the fingerprint + caller pubkey a retry must reproduce
3755
- * to be given it.
3756
- */
3757
- createJob(body: {
3758
- input: string;
3759
- params?: Record<string, string>;
3760
- }, requesterId: string, payment: PaymentInfo, requiredMsats: number | undefined, capability: string, requesterTokenHash?: string, provenance?: {
3761
- requesterToken?: string;
3762
- requestFingerprint?: string;
3763
- requesterPubkey?: string;
3764
- requestId?: string;
3765
- authRequestPath?: string;
3766
- /**
3767
- * Pre-allocated id from {@link allocateJobId} (internal-review) — the submit
3768
- * path allocates before payment verification so the ledger draw can
3769
- * record the job it pays for.
3770
- */
3771
- jobId?: string;
3772
- }): ServerJob;
3773
- /** Build an SDKJobContext and attach it to the job. */
3774
- buildAndAttachContext(job: ServerJob, parsedInput: unknown): SDKJobContext<State, ResolvedInput<InputSchema>>;
3775
- /** Start the handler for a job. */
3776
- runHandler(job: ServerJob, sdkCtx: SDKJobContext<unknown, unknown>): void;
3777
- /** Dispatch a validated incoming message to the appropriate handler or pending resolver. */
3778
- dispatchMessage(job: ServerJob, type: MessageType, content: unknown): Promise<void>;
3499
+ * Answer a payment whose rail and ledger legs committed but whose job row
3500
+ * could not be written (internal-review) — every retry spent, the money real.
3501
+ *
3502
+ * The refusal names the credit the money landed on so the caller can act on
3503
+ * it directly, mirroring the short mid-job pay's 402 (`payment.ts`). It is a
3504
+ * 500 rather than a 402 on purpose: the payment was valid and was accepted,
3505
+ * so telling the caller it was insufficient would be a lie, and this is a
3506
+ * fault an operator should see in their 5xx rate. `retryable` is true because
3507
+ * the ask is genuinely still outstanding.
3508
+ *
3509
+ * Nothing is released here. The hold is left to the internal-review reconciler,
3510
+ * which makes the same decision — unbound draw ⇒ release, never settle — but
3511
+ * under the credit row's `FOR UPDATE`, after the job is terminal, where it
3512
+ * cannot race an in-flight COMMIT. Releasing from here would also be
3513
+ * irreversible: a released draw can be neither re-grown nor re-opened, so one
3514
+ * bad call would silently de-ledger every later top-up on the job.
3515
+ *
3516
+ * Whether the row was *read* is load-bearing on both surfaces. When the
3517
+ * post-exhaustion read-back failed as well, `credit_job_row_unwritten` says so
3518
+ * (`row_checked: false` plus the read's own error) instead of implying an
3519
+ * operator can trust the row is clean, and the caller gets the `"unconfirmed"`
3520
+ * copy rather than a flat "pay it again" it could be charged twice for.
3521
+ *
3522
+ * That copy overrides `display` as well as `hint`, on every branch including
3523
+ * the ledger-less one. `display` is the sentence an agent relays to its human
3524
+ * (repo convention: JSON carries the words, not just the facts), so a body
3525
+ * whose `hint` says the outcome is unknown while its `display` still asserts
3526
+ * the ask is outstanding is read as "pay again" by the audience that acts on
3527
+ * it — and a payment that did land is charged on top.
3528
+ */
3529
+ private reportUnappliedPayment;
3779
3530
  /**
3780
- * Persist a job to the backing store.
3781
- *
3782
- * Awaits the per-job appender tail (internal-review) so that:
3783
- * 1. The snapshot's `messages` is consistent with the `job_messages` table
3784
- * — no row is still in flight at snapshot time.
3785
- * 2. For terminal saves, the `DELETE FROM job_messages` inside `save()`
3786
- * can't race a still-pending `appendOutgoing` for the final yield
3787
- * message (which would otherwise wipe the row before an in-flight SSE
3788
- * subscriber's NOTIFY-driven fetch can see it).
3531
+ * Describe a deposit the job row never took, in the shape both the 201 and
3532
+ * the `payment_unapplied` 500 carry (internal-review). `undefined` when the payment
3533
+ * ran ledger-less (no credit ledger wired, or the top-up preflight skipped) —
3534
+ * there is no credit to name, so the 201 carries no `credit` block and the
3535
+ * 500 takes its words from `unappliedCopy` directly.
3789
3536
  */
3790
- persistJob(job: ServerJob): Promise<void>;
3537
+ private unappliedCredit;
3538
+ /** Book a mid-job top-up's funding as a deposit (internal-review, spec §10). */
3539
+ private reportCreditDeposit;
3791
3540
  /**
3792
- * Cross-machine terminal guard (internal-review). `buildContext`'s guard reads the
3793
- * in-memory `job.status`, so on its own it only protects the machine running
3794
- * the handler. On a multi-machine DVM a `DELETE /v1/job/:id` routinely lands
3795
- * on a machine that isn't running the job — `app.ts` cancels it in the store
3796
- * and this process learns of it only through the durable row.
3541
+ * Internal: build the BuildContextOpts shared between `createJob` and
3542
+ * `reactivateJob`. Centralised so persistence/terminal callbacks and the
3543
+ * internal-review NOTIFY-driven cross-machine wake stay in one place.
3544
+ */
3545
+ private buildContextOpts;
3546
+ /**
3547
+ * Internal: what unit `ctx.requestPayment`'s auto-credit gate may measure this
3548
+ * job's remaining pool in (internal-review).
3797
3549
  *
3798
- * Adopting the durable status blocks the rest of the handler's writes via the
3799
- * local guard, makes `handleJobTerminal` see a non-completed job so it skips
3800
- * the revenue report, rejects a suspended handler's prompt/payment yields, and
3801
- * fires `job.abort` so in-flight `ctx.fetch` calls tear down. Returns true when
3802
- * the durable state won and the caller must skip its save.
3550
+ * The regime is read off the DRAW, never off the job. Implicit N=1 is exactly
3551
+ * `credit_id === imp:<rail>:<draw_id>`, because `fundAndDraw` derives both
3552
+ * from the same rail payment id — a fact about a row that was written once
3553
+ * and never moves. The tempting alternative, comparing `job.payment_tx_hash`
3554
+ * against `drawSettlementRef` (what the mid-job preflight does to inherit
3555
+ * `drawBasis`), silently rots: Tx C overwrites that column on every mid-job
3556
+ * credit, and a payment whose ledger leg was skipped — a cross-rail top-up,
3557
+ * an fx outage, a draw already resolved — leaves the rail's own reference
3558
+ * there, so an explicit-draw job would read as implicit from then on.
3803
3559
  *
3804
- * Two triggers: `watchDurableStatus`'s NOTIFY subscription (internal-review) fires
3805
- * this within a round-trip of the remote cancel committing, which is what
3806
- * actually stops the provider spend; `persistJob` calls it again on every save
3807
- * as the backstop for the window where a cancel commits between a handler's
3808
- * read and its write (the check-then-write it can still lose is covered by the
3809
- * stores' terminal-sticky `save`, which keeps the row itself correct).
3560
+ * Never throws, and never answers `rail` on a guess. `rail` is claimed only
3561
+ * where `withDrawBasis` demonstrably left `PaymentInfo.paidMsats` alone — no
3562
+ * draw at all, or a draw carrying no rail value to overlay it with. A job that
3563
+ * drew and whose pool this can't read or denominate answers `unpriced`, which
3564
+ * the gate declines to auto-credit from: the msat figure it would otherwise
3565
+ * fall back on is a slice of the credit's rail value at a ratio pinned
3566
+ * whenever that credit was funded, which is the exact figure this issue exists
3567
+ * to stop spending against.
3810
3568
  */
3811
- private adoptDurableTerminal;
3569
+ private resolveDrawPool;
3570
+ /** Internal: structured warn for the one path that can't price a drawn job's pool. */
3571
+ private warnDrawPool;
3812
3572
  /**
3813
- * Watch the durable status of a locally-active job (internal-review).
3814
- *
3815
- * `ctx.signal` is raised by `dispatchMessage`, which only runs on the machine
3816
- * holding the job in `activeJobs`. On a multi-machine DVM the `DELETE` usually
3817
- * lands somewhere else, and that machine's store-only cancel (`cancelJob`)
3818
- * commits the terminal status, appends the `cancel` message, and `pg_notify`s
3819
- * in one transaction. This subscription is how the handler's machine hears it:
3820
- * on each notify, re-read the durable status and adopt it if it went terminal.
3821
- * Without it, the handler only notices at its next store write, and the
3822
- * in-flight provider call — the spend the cancel was meant to stop — runs to
3823
- * completion.
3573
+ * Issue the signed receipt for a job that has reached a terminal status
3574
+ * (internal-review). Idempotent and safe to call from every terminal path — the
3575
+ * store owns both the sequence allocation and the write-once persist, so
3576
+ * two machines racing on the same job converge on identical bytes and burn
3577
+ * exactly one sequence number.
3824
3578
  *
3825
- * Every locally-emitted message notifies this machine too, so the re-read is
3826
- * coalesced: at most one `getCounters` in flight per job, and none once the
3827
- * job is locally terminal.
3579
+ * Never throws. A receipt is evidence about a job, not part of delivering
3580
+ * it: if signing or persistence fails we log structured and serve the
3581
+ * response without one (same posture as the `cashu_refund_failed` branch in
3582
+ * `context.ts`). The gap is visible to callers — the `seq` series skips a
3583
+ * number — which is precisely the completeness signal receipts exist for.
3828
3584
  */
3829
- private watchDurableStatus;
3585
+ issueReceipt(record: JobRecord): Promise<JobReceipt | undefined>;
3586
+ /** Build the common settle/release args, adding the hosted release outbox when wired. */
3587
+ private drawResolutionArgs;
3830
3588
  /**
3831
- * Tear down a job's durable-status subscription (internal-review). Must be called
3832
- * wherever a job leaves `activeJobs` — a leaked subscriber outlives the job on
3833
- * the store's shared LISTEN connection. Idempotent.
3589
+ * Settle or release a terminal job's credit draw (internal-review): `completed`
3590
+ * settles (the hold becomes a real debit); `failed`/`cancelled` releases
3591
+ * (the hold evaporates — "no debit on job failure", mechanically
3592
+ * superseding the internal-review/974 no-op refund for credit-paid jobs, including
3593
+ * the `ctx.fail` path and the stale-sweeper reap). Idempotent — a replayed
3594
+ * resolution returns the recorded state. Returns the `ReceiptCredit` block
3595
+ * for the receipt: `balance_after` is the recorded draw trajectory for a
3596
+ * settled draw, and the restored available balance for a released one.
3834
3597
  */
3835
- unwatchDurableStatus(jobId: string): void;
3598
+ private resolveCreditDraw;
3836
3599
  /**
3837
- * NOTIFY-driven durable-status re-read, coalesced per job (internal-review).
3838
- *
3839
- * A notify that arrives while a re-read is in flight is queued rather than
3840
- * dropped: the in-flight read may have observed the row a moment *before* the
3841
- * cancel committed, and dropping its notify would put the abort back where
3842
- * this issue found it — waiting for the handler's next store write.
3600
+ * The one place a terminal job's status becomes a ledger verb (internal-review):
3601
+ * `completed` settles the hold into a real debit, `failed`/`cancelled`
3602
+ * release it. Read by the in-line funnel (`resolveCreditDraw`) and by the
3603
+ * late reconciler (`reconcileTerminalDraw`) — two paths that must never
3604
+ * disagree about what an outcome means for the caller's money.
3843
3605
  */
3844
- private checkDurableTerminal;
3606
+ private settlesDraw;
3845
3607
  /**
3846
- * Wire the Tx A appender when the store supports streaming.
3847
- *
3848
- * Tx A (internal-review): outgoing message + `pending_payment_msats` bump land in
3849
- * the same transaction. The DB allocates the row's seq via
3850
- * `UPDATE jobs SET next_seq = next_seq + 1 RETURNING` so concurrent inbound
3851
- * traffic on different machines can never collide on the `(job_id, seq)` PK.
3852
- * In-memory `job.seq` keeps its own counter for same-machine SSE listeners.
3853
- *
3854
- * Per-job serialisation (internal-review): chained through `job.messageAppenderTail`
3855
- * so two synchronous `providerMessage` calls (e.g. `artifact` followed by
3856
- * `complete`) commit their `appendOutgoing` transactions in call order.
3857
- * Without this, the two transactions race for the `jobs` row lock and the
3858
- * DB-side seq can be allocated in the opposite order — which both renumbers
3859
- * the persisted messages and reorders the NOTIFY events feeding the
3860
- * cross-machine SSE subscriber. The subscriber closes the stream on the
3861
- * first yield message it sees, so a NOTIFY for `complete` arriving before
3862
- * `artifact`'s NOTIFY silently drops the artifact.
3608
+ * Sign a locally-held terminal job's receipt and mirror it onto the
3609
+ * in-memory job, so the read paths served out of `activeJobs` during the
3610
+ * cleanup window hand back the same bytes as a cross-machine re-read.
3863
3611
  */
3864
- private wireMessageAppender;
3865
- /** Start (or restart) the idle timer for a job. */
3866
- startIdleTimer(job: ServerJob): void;
3867
- /** Clear the idle timer for a job. */
3868
- clearIdleTimer(id: string): void;
3869
- /** Cancel a job due to idle timeout. Exposed for the reactivation path's idle-expiry guard (internal-review). */
3870
- cancelJobIdle(job: ServerJob): Promise<void>;
3612
+ private attachReceipt;
3871
3613
  /**
3872
- * Force-terminate stale non-terminal jobs. Two status-aware arms:
3873
- * • `awaiting-input` past `staleJobTimeoutMs` → `cancelled`
3874
- * (`stale_no_terminal_status`) — the caller never paid / responded.
3875
- * • `processing`/`working` past `processingWatchdogMs` → `failed`
3876
- * (`worker_died_mid_job`) — the worker died mid-job. The heartbeat keeps
3877
- * live workers fresh, so a stale row here means a dead process.
3878
- *
3879
- * Fires on a timer (wired in the constructor) and also callable on demand
3880
- * (tests, ops). The CAS in `cancelStaleJob` makes this safe to run
3881
- * concurrently from multiple DVM machines. Scoped to capabilities this
3882
- * JobManager owns so multi-mount hosts don't steal stuck rows from sibling
3883
- * descriptors — the local activeJobs teardown only makes sense on the
3884
- * JobManager that actually hosted the zombie handler.
3885
- *
3886
- * **Both cutoffs float off the wall clock (internal-review).** Each is one
3887
- * `Date.now()` sample compared against `last_activity_at`, stamped from a
3888
- * different sample at a different moment, so a backwards step between the
3889
- * two makes every row look more recently active than it is: the query
3890
- * matches nothing and both arms go silent for the length of the skew. The
3891
- * dead worker's job keeps its `processing` status, its credit hold stays
3892
- * `pending`, and the internal-review paid-job-death alert — wired to this reaper —
3893
- * never fires. Same defect, same host class, as internal-review's orphan sweep.
3894
- *
3895
- * **The compensation is capped** — see {@link staleSweepCompensationCapMs}
3896
- * for where each arm's cap comes from — which internal-review did not need to do. Its eagerness costs at worst an early release
3897
- * of an unclaimed hold, refused outright for any live job row; ours
3898
- * force-*fails* a running job. `cancelStaleJob`'s CAS does not cover that:
3899
- * `expectedActivityBefore` is the same compensated threshold the query used,
3900
- * and a live worker's post-step heartbeat writes the low, post-step
3901
- * `Date.now()`, so `last_activity_at <= expectedActivityBefore` still holds.
3902
- * The CAS guards a job checking in *between* the find and the claim with a
3903
- * stamp above the cutoff — a race, not a clock.
3904
- *
3905
- * **Some blindness is inherent, and no cap choice removes it.** A backwards
3906
- * step of `S` inverts the stamp ordering: everything stamped after it reads
3907
- * `S` older than everything stamped before. A live worker heartbeating right
3908
- * now stamps `wall - S`; a job genuinely idle for `age` stamps `wall - age`.
3909
- * The stale row is the lower of the two — the one a cutoff reaches first —
3910
- * only once `age` exceeds `S`. Below that the reaper cannot tell them apart,
3911
- * so any cutoff that claimed the stale job would force-fail every live
3912
- * worker on the host with it. The cap picks where on that curve to sit: on
3913
- * the defaults a row must have been silent for `S + window / 2`, and a live
3914
- * worker keeps five missed heartbeats of margin. The
3915
- * alternative to a missed reap is mild and self-healing — the orphan sweep
3916
- * backstops the hold, and the arm recovers when the wall catches up. The
3917
- * alternative to a false reap is an irreversible force-fail of paid work.
3614
+ * Read-path repair for a terminal job carrying no receipt (internal-review), for
3615
+ * either an in-memory job or a store record. Free in the steady state — it
3616
+ * returns on the `receipt` check without touching the store — so it costs
3617
+ * only on the cases it exists for:
3918
3618
  *
3919
- * The stamps themselves stay wall-clock epoch-ms on purpose — they are
3920
- * written by whichever machine touched the job and read by every other one,
3921
- * so only the reading side can float.
3619
+ * - **A crash between the two store calls.** `claimReceiptSeq` commits the
3620
+ * sequence number to the job row before `saveReceipt` writes the bytes; a
3621
+ * process death in that window would otherwise strand that number
3622
+ * forever, and a permanent gap is indistinguishable from the deliberate
3623
+ * suppression `seq` exists to expose. Re-issuing here reuses the already
3624
+ * committed number (the claim is idempotent per job) rather than
3625
+ * allocating a second one.
3626
+ * - **A transient store failure** at the terminal: `issueReceipt` logs
3627
+ * `receipt_issue_failed` and returns nothing rather than failing the job,
3628
+ * so the next read retries.
3629
+ * - **Jobs that terminated before the DVM had a receipt key.** They pick one
3630
+ * up on first read, with `issued_at` reflecting when it was signed.
3922
3631
  */
3923
- sweepStaleJobs(): Promise<{
3924
- swept: number;
3925
- }>;
3632
+ ensureReceipt(target: ServerJob | JobRecord): Promise<void>;
3926
3633
  /**
3927
- * How far one watchdog arm may lean on {@link monotonicNowMs} past the wall
3928
- * clock to cover a backwards step (internal-review). The compensation is capped,
3929
- * not free: a claimed row must still have been silent for
3930
- * `windowMs - cap`, and unlike the orphan sweep, claiming eagerly here
3931
- * force-fails a job that may well be alive.
3932
- *
3933
- * **Half the window** is the floor that governs a wide window — five missed
3934
- * heartbeats on the processing defaults. On the `awaiting-input` arm it also
3935
- * preserves a real invariant: with the derived
3936
- * `2 x idleTimeout + headroom`, half is `idleTimeout + 45s`, so the sweeper
3937
- * still cannot fire before the in-process idle timer would have. (A builder
3938
- * who overrides `staleJobTimeoutMs` below `2 x idleTimeout` has already
3939
- * opted out of that, guard or no guard.)
3940
- *
3941
- * **Two heartbeat intervals** is the floor that governs a window configured
3942
- * close to the beat cadence — `processingWatchdog: 40` against the 30s
3943
- * default beat, where half the window is less than one beat and a worker
3944
- * heartbeating exactly on schedule would be reaped the moment the host's
3945
- * clock stepped. That pair is marginal already; the guard must not make it
3946
- * deterministic. The cap collapses to zero there, which is today's
3947
- * behaviour: late, never wrong. The `awaiting-input` arm takes no such term
3948
- * — nothing heartbeats it by design, and its refresh is an inbound caller
3949
- * message on no cadence at all.
3950
- *
3951
- * **Why a magnitude and not a predicate.** The tempting sharper rule is to
3952
- * compensate per row, fully, for any stamp that reads ahead of our wall —
3953
- * one our own post-step heartbeat could not have written. It is unsafe: a
3954
- * future-stamped row is equally what a live job heartbeated by a peer whose
3955
- * clock runs fast looks like, and from the stamp alone the two are
3956
- * indistinguishable. The same reasoning bounds the cap from the other side —
3957
- * while our clock is stepped, the guard force-fails the live jobs of any
3958
- * peer slower than `windowMs - cap`, which is 2.5 minutes of tolerated
3959
- * disagreement on the defaults, far outside anything NTP-managed hosts show.
3634
+ * Close out a terminal job whose caller hasn't already persisted it: sign
3635
+ * the receipt, then save the snapshot. The snapshot save never writes the
3636
+ * receipt column, so ordering only affects how soon a reader sees the
3637
+ * receipt — this way a caller polling immediately after the terminal
3638
+ * already finds it.
3639
+ */
3640
+ private finalizeTerminal;
3641
+ /**
3642
+ * Issue a receipt for a job this process doesn't hold in `activeJobs` — a
3643
+ * cross-machine cancel, or a row the stale sweeper just reaped. Re-reads the
3644
+ * record so the receipt is built from the committed terminal row rather than
3645
+ * from whatever the caller happened to have in hand.
3960
3646
  */
3961
- private staleSweepCompensationCapMs;
3647
+ issueReceiptForStoredJob(jobId: string): Promise<JobReceipt | undefined>;
3648
+ /** Handle job reaching terminal state (completed, failed, cancelled). */
3649
+ private handleJobTerminal;
3962
3650
  /**
3963
- * Resolve `pending` credit draws the terminal funnel can no longer reach.
3964
- * Two arms over one scan, distinguished by whether the draw's job row exists.
3965
- *
3966
- * **No job row (internal-review) — release.** internal-review allocates the job id and
3967
- * commits the ledger draw, with its `job_id` linkage, before `recordReplay`
3968
- * and `persistJob` write the job row. A crash, a `draw_conflict` 409, or a
3969
- * fail-closed `replay_detected` 401 in that window leaves a committed hold
3970
- * that **no** terminal path can ever reach: `issueReceipt` →
3971
- * `resolveCreditDraw` keys off the `creditId`/`drawId` persisted on the job
3972
- * row, so with no row there is no release path at all and the caller's
3973
- * available balance stays reduced forever. Cosmetic for an implicit N=1
3974
- * credit, a silent balance shrink for an explicit N>1 one.
3975
- *
3976
- * **Terminal job row (internal-review) — reconcile to its outcome.** The funnel
3977
- * demonstrably fails: `resolveCreditDraw` throws on a ledger/DB error,
3978
- * `issueReceipt` catches it, logs `credit_resolve_failed` and returns
3979
- * nothing, and until now nothing retried. Same stranded hold, reached through
3980
- * a different door — and for a `completed` job it strands the platform's
3981
- * books too, since draws *are* the revenue events (internal-review): the caller is
3982
- * charged nothing, the balance never moves, and outstanding liability
3983
- * (`deposits − draw revenue`) overstates forever. So a late settle books, off
3984
- * the committed draw row, through the ordinary reporter path.
3651
+ * Report a completed paid job's revenue (internal-review, re-keyed by internal-review).
3652
+ * Fire-and-forget — the `RevenueReporter` owns persistence and retry.
3985
3653
  *
3986
- * **A non-terminal job row is still never touched** — the stale-job sweep
3987
- * above drives those to terminal first, and its `issueReceiptForStoredJob`
3988
- * resolves the draw on the way. Those skipped holds are why the scan
3989
- * keyset-paginates rather than re-reading one batch: they stay `pending`
3990
- * indefinitely (an `awaiting-input` job on a 30-minute idle timeout sits well
3991
- * past the orphan cutoff), so a fixed first page of them would starve every
3992
- * real orphan behind it, on every tick, forever. The cursor advances over
3993
- * every row examined; a tick that hits {@link ORPHAN_DRAW_SWEEP_MAX_PAGES}
3994
- * logs `orphan_draw_sweep_truncated` and the next one resumes where it
3995
- * stopped, so progress is bounded per tick but never blocked.
3654
+ * For a **credit-backed** job the revenue event is the *settled draw* (spec
3655
+ * §10), and since internal-review the sats figure corrects the job's mirror of that
3656
+ * draw against the draw itself: `withDrawBasis` stamps the job when the
3657
+ * payment lands, but a Bitcoin credit's settle re-prices the draw against
3658
+ * the funding lots it consumed. The correction is a delta, because a job's
3659
+ * counter can carry legs no draw ever absorbed — except where the draw's
3660
+ * forecast was zero and the counter therefore never held a component to
3661
+ * correct. The event is dated at settlement rather than at report time. A
3662
+ * released draw — the failed/cancelled path — is deliberately unreachable
3663
+ * here, which is what makes "no debit on job failure" true in the books as
3664
+ * well as the ledger.
3996
3665
  *
3997
- * Fires on a timer (wired in the constructor) and also callable on demand
3998
- * (tests, ops). Both `settle` and `release` are idempotent under the credit
3999
- * row's `FOR UPDATE`, so concurrent sweepers on several machines — and a
4000
- * sweeper racing the in-line funnel — resolve the hold exactly once; the
4001
- * losers see `replayed: true` and book nothing.
4002
- */
4003
- sweepOrphanDraws(): Promise<{
4004
- released: number;
4005
- reconciled: number;
4006
- }>;
4007
- /**
4008
- * The end-of-scan line for a tick that resolved nothing (internal-review, extended
4009
- * by internal-review). Two shapes are worth a warning, and neither is the ordinary
4010
- * one — the timer runs every 5 minutes per ledger in production.
3666
+ * For everything else (free-then-paid jobs, isolate-shaped flows, a DVM
3667
+ * whose price couldn't be fiat-denominated) nothing changes: the pre-credits
3668
+ * payload is reported verbatim.
4011
3669
  *
4012
- * `examined > 0`: aged candidates were looked at and none moved. Usually
4013
- * legitimate (their jobs are still running, or terminal inside the reconcile
4014
- * grace period), but it is also what a silently skipped hold looks like.
3670
+ * Takes the fields rather than a `ServerJob` so the internal-review reconciler — a
3671
+ * background sweep that holds no in-process job — books through this exact
3672
+ * path off the row it read. `paymentTxHash` is passed **verbatim**, never
3673
+ * recomputed as a `drawSettlementRef`: it is the same column the in-line
3674
+ * funnel reads, so the two bookings are byte-identical by construction and
3675
+ * the platform's `UNIQUE (rail, tx_hash)` dedupes them. Deriving it instead
3676
+ * would diverge on an implicit N=1 job (whose row carries the rail's own
3677
+ * reference) and on any job whose column a mid-job top-up overwrote — the
3678
+ * same rot `resolveDrawPool` documents as the reason not to read regime off
3679
+ * this column.
4015
3680
  *
4016
- * `examined === 0`: the query returned nothing. Ordinarily that means there
4017
- * is nothing to do — but it is equally what a blinded scan looks like, which
4018
- * is the case the internal-review gate could not reach. Probe for the oldest
4019
- * `pending` hold at any age and speak up when one exists the scan should
4020
- * have seen and didn't: already past the age gate outright, stamped ahead of
4021
- * our clock (some other process's clock is fast), or hidden while our own
4022
- * clock is held forward over a backwards step. A ledger holding nothing but
4023
- * young draws stays quiet.
3681
+ * Returns whether the report was handed to `onJobCompleted`, so a caller
3682
+ * that logs the booking says what actually happened rather than what it
3683
+ * assumed — the guards below still drop legitimate shapes (a free job, a
3684
+ * DVM with no reporter). Every drop that costs a booking logs first.
4024
3685
  *
4025
- * The probe is diagnostics, so it is wrapped: a tick that swept cleanly must
4026
- * never report failure because the extra read fell over. A probe that throws
4027
- * says so on the same line rather than replacing the tick's outcome.
3686
+ * The rail check sits **below** the draw re-read, not in the entry guard
3687
+ * (internal-review): a settled draw the terminal can't report under is a real debit
3688
+ * with no revenue event, permanently overstating outstanding liability
3689
+ * (`deposits − draw revenue`), and it used to return here in silence. Placed
3690
+ * after the re-read, `revenue_skipped_no_rail` can name the draw and its
3691
+ * amount, and can distinguish the four causes — see its `reason` below.
4028
3692
  */
4029
- private reportSweepOutcome;
3693
+ private bookRevenue;
3694
+ /** Schedule job removal from activeJobs after a delay so clients can still poll final status. */
3695
+ scheduleCleanup(job: ServerJob): void;
3696
+ /** Clear all timers. Called on server shutdown to allow clean exit. */
3697
+ shutdown(): void;
3698
+ }
3699
+
3700
+ /** Cached liveness signal for a platform-hosted DVM's Lightning receive rail. */
3701
+ interface LightningRailHealth {
3702
+ /** Whether the receive rail may be advertised at this instant. */
3703
+ available(): boolean;
3704
+ /** Prime the cached signal at boot when the source supports it. */
3705
+ refresh?(): Promise<void>;
3706
+ }
3707
+
3708
+ /** Builder-facing wiring for the Lightning receive leg (internal-review). */
3709
+ interface LightningReceiveConfig {
4030
3710
  /**
4031
- * Resolve one aged `pending` draw whose job row reached terminal (internal-review),
4032
- * mirroring what `resolveCreditDraw` would have done in line. Returns true
4033
- * when this call is the one that moved the draw — the caller counts it, and
4034
- * only it books.
4035
- *
4036
- * **The settle is gated on the job row naming _this_ draw.** The scan finds
4037
- * the draw by `credit_draws.job_id`, but the job's own `credit_id`/`draw_id`
4038
- * are written by a *later* transaction than the one that committed the draw
4039
- * (`verifyIncomingPayment` commits fund+draw; `verifyAndCredit` binds the
4040
- * row), and a free-then-paid job binds set-if-null. A store failure or a
4041
- * second concurrent top-up therefore leaves a hold tagged with the job whose
4042
- * row points somewhere else, or nowhere. Settling that would debit the caller
4043
- * a second time for one job — and invisibly, since `revenue_events`'
4044
- * `UNIQUE (dvm_id, job_id, rail)` would drop the booking. So an unbound hold
4045
- * is **released**, never settled: the job's payment was the draw it is bound
4046
- * to, and this one bought nothing.
4047
- *
4048
- * Concurrency needs no guard of its own. `settle`/`release` run under the
4049
- * credit row's `FOR UPDATE`, so exactly one caller — a sibling reconciler,
4050
- * or the in-line funnel arriving late — sees `replayed: false`, which makes
4051
- * the booking gate a true mutex rather than a hopeful one. The one race left
4052
- * open is another machine's `handleJobTerminal` stalled between resolving the
4053
- * draw and booking it, which would double-report; the platform's
4054
- * `(dvm_id, credit_id, draw_id)` and `(dvm_id, job_id, rail)` unique indexes
4055
- * absorb that.
4056
- *
4057
- * `invalid_draw_state` — the funnel having already resolved the draw to the
4058
- * other state — is the expected loss, logged and swallowed like the orphan
4059
- * arm's, so one contended hold can't strand the rest of the batch.
3711
+ * Receive-only NWC connection URI (`DVMKIT_NWC_RECEIVE_URI`). Validated at
3712
+ * boot: a connection that can *spend* is refused outright.
4060
3713
  */
4061
- private reconcileTerminalDraw;
3714
+ uri?: string;
4062
3715
  /**
4063
- * Refresh `lastActivityAt` for jobs this process is actively running
4064
- * (`processing`/`working`) so the processing watchdog only fires once the
4065
- * worker is genuinely dead (internal-review). `awaiting-input` jobs are
4066
- * deliberately excluded — their idle timeout must still elapse. Fires on the
4067
- * heartbeat timer; also callable on demand for tests.
3716
+ * Pre-built backend, bypassing URI parsing and the connect-time probe. Test
3717
+ * seam only — production always goes through `uri` so the receive-only
3718
+ * property is actually checked.
4068
3719
  */
4069
- heartbeatActiveJobs(): Promise<{
4070
- beat: number;
4071
- }>;
3720
+ backend?: LightningBackend;
3721
+ /** Invoice lifetime in seconds. Defaults to {@link DEFAULT_INVOICE_TTL_SECONDS}. */
3722
+ invoiceTtlSeconds?: number;
4072
3723
  /**
4073
- * Single-execution claim for a reactivating machine (internal-review). Before
4074
- * replaying an `awaiting-input` job, wait a bounded grace window for a live
4075
- * original handler to win the `awaiting-input → processing` CAS via its
4076
- * NOTIFY wake. Resolves:
4077
- * • `false` — the row left `awaiting-input` during the window (the original
4078
- * handler claimed it / drove it terminal). Stand down; do not replay.
4079
- * • `true` — the window elapsed still `awaiting-input` (the original worker
4080
- * is gone or has no live resolver) AND this machine won the claim CAS.
4081
- * Replay for dead-worker recovery.
4082
- *
4083
- * Biasing the original handler to win kills the double execution (double
4084
- * substrate spend, clobbered artifact) without heartbeating `awaiting-input`
4085
- * (which would break the caller-input idle timeout). Non-streamable stores
4086
- * have no cross-machine race, so the claim is a no-op `true`.
3724
+ * Smallest funding this deployment can receive over Lightning, in sats
3725
+ * (`DVMKIT_LIGHTNING_FUNDING_MIN_SATS`). The floor is a property of the
3726
+ * receive wallet's channel policy (`htlc_minimum_msat` upstream), so it is
3727
+ * per-deployment and sats-physical — the menu renders it into fiat with a
3728
+ * margin, but everything that *enforces* it compares raw sats. Defaults to
3729
+ * {@link DEFAULT_LIGHTNING_FUNDING_MIN_SATS}.
4087
3730
  */
4088
- claimAwaitingInputForReactivation(jobId: string): Promise<boolean>;
3731
+ fundingMinSats?: number;
4089
3732
  /**
4090
- * Record an inbound client message durably via Tx B (internal-review) and append
4091
- * it to the in-memory `job.messages` array. Returns the DB-allocated seq
4092
- * (`null` when the store isn't streamable — non-Postgres test fallback).
4093
- *
4094
- * Status starts as `pending-verification`; a follow-up Tx C call
4095
- * (`processPayment` for payments, `markInboundVerified` for everything
4096
- * else) transitions it to `verified` once the inbound is accepted.
3733
+ * Optional platform-derived channel-liveness gate. Platform-hosted DVMs use
3734
+ * it alongside their NWC reachability probe; self-hosted DVMs omit it.
4097
3735
  */
4098
- recordInboundPending(job: ServerJob, msgType: MessageType, msgContent: Record<string, unknown>): Promise<number | null>;
3736
+ railHealth?: LightningRailHealth;
3737
+ }
3738
+ /** Default bolt11 lifetime — long enough to pay by hand, short enough to retire. */
3739
+ declare const DEFAULT_INVOICE_TTL_SECONDS = 900;
3740
+ /**
3741
+ * Floor on the invoice lifetime: twice the signed-request drift window, so the
3742
+ * bolt11 always outlives the funding negotiation that produced it (the issue's
3743
+ * "invoice expiry ≥ the funding-negotiation window"). A shorter one would
3744
+ * expire inside the caller's own retry budget and strand them mid-top-up.
3745
+ */
3746
+ declare const MIN_INVOICE_TTL_SECONDS = 600;
3747
+ /**
3748
+ * The builder-side Lightning receive leg (internal-review, credits spec §4).
3749
+ *
3750
+ * Issues a bolt11 over a **receive-only** NWC connection while the DVM is
3751
+ * awake serving the 402, and credits the ledger when a later request observes
3752
+ * settlement. Two properties are load-bearing:
3753
+ *
3754
+ * - **It never holds a send credential.** `pay_invoice` on this connection is
3755
+ * refused at construction, so a compromised DVM can mint invoices and
3756
+ * nothing else. The drain/refund sender (internal-review) is a separate, budgeted
3757
+ * connection by rule.
3758
+ * - **Crediting is pull-based — there is no settlement watcher.** The caller's
3759
+ * next request drives `lookup_invoice`, which is what makes this work on a
3760
+ * suspend-to-zero fleet: a machine that is asleep has nothing to miss.
3761
+ * Wallet downtime at that moment delays crediting; it never loses money,
3762
+ * because the invoice→credit binding is a durable row.
3763
+ *
3764
+ * Reachability is **cached**, never probed per request: the funding menu is
3765
+ * assembled on every quote and every 402, so a live NIP-47 round trip there
3766
+ * would put a nostr relay in the latency path of every priced call.
3767
+ */
3768
+ declare class LightningReceive {
3769
+ private readonly source;
3770
+ readonly invoiceTtlSeconds: number;
3771
+ readonly fundingMinSats: number;
3772
+ private health;
3773
+ private checkedAtMs;
3774
+ private refreshing;
3775
+ private permissionRefusal;
3776
+ private constructor();
3777
+ private readonly railHealth;
4099
3778
  /**
4100
- * Tx C complement for non-payment inbound messages — flip the row to
4101
- * `verified` so cross-machine readers see it via `subscribeMessages`.
3779
+ * Validate the configured connection and build the receive leg, or return
3780
+ * `undefined` when this DVM didn't configure one.
3781
+ *
3782
+ * **Throws on a credential that is wrong, degrades on one that is merely
3783
+ * unreachable.** A wallet outage at boot must not stop a DVM whose other
3784
+ * rails are fine — the menu simply omits `lightning` until a later probe
3785
+ * succeeds. A connection that can spend, or one that can't state what it can
3786
+ * do, is a different thing entirely: it is a standing money risk that no
3787
+ * amount of retrying fixes, so it fails the boot loudly with the fix in the
3788
+ * message.
4102
3789
  */
4103
- markInboundVerified(jobId: string, inboundSeq: number | null): Promise<void>;
3790
+ static create(config: LightningReceiveConfig): Promise<LightningReceive | undefined>;
4104
3791
  /**
4105
- * Reactivate a suspended/event-driven job from the store. Builds the
4106
- * in-memory `ServerJob`, wires the appender, builds the context, starts
4107
- * the idle timer. The caller has already recorded the inbound via Tx B
4108
- * and run Tx C (verify + credit); reactivation is now decoupled from
4109
- * credit (internal-review, internal-review). The caller decides whether to run the
4110
- * handler (Pattern A: status was `awaiting-input`) or to dispatch the
4111
- * message to a custom handler (Pattern B).
4112
- *
4113
- * For DVMs that declare descriptor-level auth, the persisted
4114
- * `record.input` carries the original signed envelope. We re-verify the
4115
- * signature here (internal-review) so a DB-layer tamper — compromised admin, SQL
4116
- * injection, malicious operator with DB access — can't silently feed an
4117
- * attacker-supplied pubkey/envelope into the handler. The drift window
4118
- * and replay store are deliberately skipped: the persisted timestamp is
4119
- * from the original signing instant (long past the 5-min drift bound for
4120
- * any long-running job), and the nonce was already committed at submission.
4121
- * On failure: throws `SignedRequestError` ({@link AuthSchemaError} when
4122
- * the capability has no input schema to verify against); the caller is
4123
- * expected to mark the row failed and surface a 401 / 500 to the inbound
4124
- * caller.
3792
+ * Whether `lightning` belongs on the funding menu right now.
4125
3793
  *
4126
- * What gets re-verified is the persisted row read back through
4127
- * {@link signedRequestInput} — `app.ts` stores the pre-Zod wire form, so
4128
- * those are the caller's own signed bytes, the same ones `/v1/quote` and the
4129
- * submit checked (internal-review). That makes the tamper check strictly stronger
4130
- * than the parsed form it replaced: an injected key the capability schema
4131
- * doesn't declare used to be stripped before the signature was checked, so
4132
- * it re-verified clean.
3794
+ * Reads the cached observation and never blocks; a stale one kicks off a
3795
+ * background refresh and answers with what we last knew. Advertising a rail
3796
+ * whose wallet is down would hand the caller an option that 402s on use —
3797
+ * the §4 posture is to drop it and let the sale survive on the others.
4133
3798
  */
4134
- reactivateJob(record: JobRecord): {
4135
- job: ServerJob;
4136
- sdkCtx: SDKJobContext<unknown, unknown>;
4137
- };
3799
+ available(): boolean;
4138
3800
  /**
4139
- * Verify an inbound payment and apply the credit via Tx C (internal-review).
4140
- * The inbound message must have been recorded via Tx B already
4141
- * (`recordInboundPending` returned `inboundSeq`). For non-streamable
4142
- * stores (`inboundSeq === null`), falls back to in-memory mutation via
4143
- * `applyPaymentInfoToJob`.
4144
- *
4145
- * Dev-mode short-circuit: auto-credits the outstanding
4146
- * `pendingPaymentMsats` without external verification. Gated on
4147
- * `devModeSkipsPaymentVerification` — the same predicate the upfront path
4148
- * reads, so a dev server wired to a mint verifies mid-job payments for real
4149
- * (internal-review) — plus the explicit `dev_auto` opt-out the dev console uses,
4150
- * which is the one caller that has no wallet to pay from. `dev_auto` loses to
4151
- * any proof riding the same message: money on the wire always takes the rail,
4152
- * so the flag can never leave a real token unspent against a credited job.
3801
+ * Issue (or re-issue) the bolt11 funding `(creditId, fundId)`.
4153
3802
  *
4154
- * Tx C is not atomic with the rail commit (internal-review). `verifyIncomingPayment`
4155
- * commits the proofs *and* the ledger leg in one transaction and returns;
4156
- * only then does `verifyAndCredit` write the job row, on its own connection.
4157
- * Everything after that call therefore runs with the caller's money already
4158
- * moved, which is why the write is retried rather than left to throw, and why
4159
- * an exhausted retry answers with the credit it landed on instead of a bare
4160
- * 500. See `creditInbound`.
3803
+ * The invoice is minted **for** the credit and amount named in the caller's
3804
+ * signed body — that binding is the internal-review condition-3 commitment on this
3805
+ * rail, and it is why no artifact hash rides the request: there is no
3806
+ * caller-supplied artifact to hash. A re-poll returns the stored row rather
3807
+ * than minting again; a second bolt11 for one `fund_id` would leave two
3808
+ * payable invoices against a funding that can only be credited once.
4161
3809
  */
4162
- processPayment(job: ServerJob, body: {
4163
- type: MessageType;
4164
- content: Record<string, unknown>;
4165
- }, inboundSeq: number | null, extra?: {
4166
- lockPubkeys?: LockPubkey[];
4167
- mintHealthTracker?: MintHealthTracker;
4168
- }): Promise<ProcessPaymentOutcome>;
3810
+ issue(ledger: CreditLedgerLike, args: {
3811
+ creditId: string;
3812
+ fundId: string;
3813
+ callerPubkey: string;
3814
+ currency: string;
3815
+ amountMicro: number;
3816
+ amountMsats: number;
3817
+ description?: string;
3818
+ }): Promise<CreditInvoiceRecord>;
4169
3819
  /**
4170
- * Tx C with a bounded retry (internal-review).
3820
+ * Ask the wallet whether one payment hash is paid, and when (internal-review).
4171
3821
  *
4172
- * By the time this runs on the verified path, the rail commit and the ledger
4173
- * leg have already committed in a transaction this call is not part of — so a
4174
- * throw here is money moved against a job row that records none of it, and
4175
- * letting it propagate was the bug. A transient store error is the ordinary
4176
- * cause and a second attempt clears it.
3822
+ * The operator's reconcile verb runs this before it credits anything. A
3823
+ * `blocked` row implies payment — {@link applyOne} returns above the block
3824
+ * classification when the lookup says unpaid — but that is *this fleet's
3825
+ * belief, recorded possibly weeks ago*, and the verb it gates mints balance
3826
+ * against it. One round trip turns the belief into a fact and recovers the
3827
+ * true settlement instant for `settled_at`, which a blocked row never got to
3828
+ * write.
4177
3829
  *
4178
- * The retry is safe by construction, not by convention: `verifyAndCredit`
4179
- * CASes on `job_messages.status = 'pending-verification'` inside its own
4180
- * transaction, so a call whose COMMIT actually landed before the ack was lost
4181
- * finds the row already `verified` and returns `alreadyVerified: true`
4182
- * carrying the counters and binding that first attempt committed. Re-applying
4183
- * the delta is impossible either way.
3830
+ * Unlike {@link settlePending} this **throws**: there is no request whose
3831
+ * latency it protects, and crediting on an unverifiable wallet is exactly
3832
+ * what it exists to prevent. `NOT_FOUND` is the one exception, and it is not
3833
+ * an outage — it is the wallet saying it has never seen this hash, the same
3834
+ * reading {@link applyOne} and the caller-side reconcile in `src/lib/nwc.ts`
3835
+ * take. It comes back as `known: false` because the operator's repair for it
3836
+ * ("you are pointed at a different wallet than the one that minted this")
3837
+ * is not the repair for an unpaid invoice, and certainly not for a retry.
3838
+ */
3839
+ lookupSettlement(paymentHash: string): Promise<{
3840
+ settled: boolean;
3841
+ settledAt?: number;
3842
+ known: boolean;
3843
+ }>;
3844
+ /**
3845
+ * Consult the wallet about this caller's outstanding invoices and credit the
3846
+ * ones that settled — the pull half of pull-based crediting.
4184
3847
  *
4185
- * That argument covers every attempt but the last, whose error nothing
4186
- * re-checks — and a connection-level fault is exactly the shape that loses
4187
- * the ack on *all* of them, idempotent replays included (the no-op path
4188
- * COMMITs too). So the exhausted path asks the row before reporting a
4189
- * failure: `getVerifiedInbound` reads, transaction-free, whether a Tx C for
4190
- * this seq committed. Without it a fully credited, fully bound job answers
4191
- * `payment_unapplied` and invites a retry, and a caller who takes it pays
4192
- * twice for one ask — the second payment growing the now-bound draw and
4193
- * settling as revenue.
3848
+ * The pending rows are read locally first, so the common case (nothing
3849
+ * outstanding) costs one indexed SELECT and no network at all. Bounded to
3850
+ * {@link MAX_SETTLE_CHECKS} invoices on a short deadline, and **never throws
3851
+ * into the request**: a wallet failure here means the caller's balance is
3852
+ * merely not updated yet, which the ordinary `insufficient_credit` path
3853
+ * already states honestly.
4194
3854
  *
4195
- * The read runs on the pool that just failed those COMMITs, so it is retried
4196
- * too, and a read that fails all the way through is reported as *unknown*
4197
- * rather than as nothing: `CreditInboundExhaustedError.readError` is what
4198
- * separates "checked the row, nothing landed" from "couldn't check", both on
4199
- * the operator's log line and in the copy the caller acts on.
3855
+ * **Each invoice gets its own failure boundary.** The sweep window is a few
3856
+ * rows wide, so a row that fails on its own terms — a `fund_id` the caller
3857
+ * reused on another rail, a credit someone else opened first — must not take
3858
+ * the rest of the pass down with it: the next invoice in the window may be
3859
+ * the paid one, and it would never be looked up. Only a transport failure
3860
+ * ends the pass early, because there the wallet itself is gone and the
3861
+ * remaining lookups would just spend the caller's latency confirming it.
4200
3862
  */
4201
- private creditInbound;
3863
+ settlePending(ledger: CreditLedgerLike, args: {
3864
+ callerPubkey: string;
3865
+ creditId?: string;
3866
+ creditTtlMs: number;
3867
+ dvmId?: string;
3868
+ enqueueCreditDeposit?: CreditDepositEnqueue;
3869
+ nowMs?: number;
3870
+ }): Promise<InvoiceSettlement[]>;
4202
3871
  /**
4203
- * Answer a payment whose rail and ledger legs committed but whose job row
4204
- * could not be written (internal-review) — every retry spent, the money real.
3872
+ * One invoice's settlement check. Returns the applied settlement, or
3873
+ * `undefined` when nothing changed (still unpaid, or retired unpaid).
4205
3874
  *
4206
- * The refusal names the credit the money landed on so the caller can act on
4207
- * it directly, mirroring the short mid-job pay's 402 (`payment.ts`). It is a
4208
- * 500 rather than a 402 on purpose: the payment was valid and was accepted,
4209
- * so telling the caller it was insufficient would be a lie, and this is a
4210
- * fault an operator should see in their 5xx rate. `retryable` is true because
4211
- * the ask is genuinely still outstanding.
3875
+ * Throws whatever the wallet or the ledger threw — classifying that is
3876
+ * {@link retireOrLog}'s job, so this stays one invoice's happy path.
3877
+ */
3878
+ private applyOne;
3879
+ /**
3880
+ * Decide what one invoice's failure means, and retire the invoice when the
3881
+ * answer is "this can never succeed".
4212
3882
  *
4213
- * Nothing is released here. The hold is left to the internal-review reconciler,
4214
- * which makes the same decision — unbound draw ⇒ release, never settle — but
4215
- * under the credit row's `FOR UPDATE`, after the job is terminal, where it
4216
- * cannot race an in-flight COMMIT. Releasing from here would also be
4217
- * irreversible: a released draw can be neither re-grown nor re-opened, so one
4218
- * bad call would silently de-ledger every later top-up on the job.
3883
+ * The split that matters is permanent-versus-transient, because it decides
3884
+ * whether the row keeps a slot in the sweep window. A transient failure — the
3885
+ * database blinked, the wallet answered oddly — leaves it `pending` and the
3886
+ * caller's next request retries it. A **permanent** one means the ledger will
3887
+ * refuse this invoice identically forever, so leaving it pending costs a
3888
+ * `lookup_invoice` on every subsequent request and, at
3889
+ * {@link MAX_SETTLE_CHECKS} of them, fills the window so a genuinely payable
3890
+ * invoice behind them is never even looked up. Those get `blocked`, which
3891
+ * drops them out of the sweep and hands the operator the payment hash.
3892
+ */
3893
+ private retireOrLog;
3894
+ /**
3895
+ * The reason this invoice can never be credited, or `undefined` if the
3896
+ * failure was transient.
4219
3897
  *
4220
- * Whether the row was *read* is load-bearing on both surfaces. When the
4221
- * post-exhaustion read-back failed as well, `credit_job_row_unwritten` says so
4222
- * (`row_checked: false` plus the read's own error) instead of implying an
4223
- * operator can trust the row is clean, and the caller gets the `"unconfirmed"`
4224
- * copy rather than a flat "pay it again" it could be charged twice for.
3898
+ * Every code here is decided by state a retry cannot move — a row the ledger
3899
+ * has already committed (a `credit_fundings` entry at this
3900
+ * `(credit_id, fund_id)`, or a `credits` row whose owner, denomination or
3901
+ * rail disagrees with the invoice), or the basis this module rebuilds
3902
+ * identically from the invoice row on every pass. None of them is a race.
4225
3903
  *
4226
- * That copy overrides `display` as well as `hint`, on every branch including
4227
- * the ledger-less one. `display` is the sentence an agent relays to its human
4228
- * (repo convention: JSON carries the words, not just the facts), so a body
4229
- * whose `hint` says the outcome is unknown while its `display` still asserts
4230
- * the ask is outstanding is read as "pay again" by the audience that acts on
4231
- * it — and a payment that did land is charged on top.
3904
+ * `funding_replayed` is the one that needs a second read to classify.
3905
+ * Usually it *is* benign — a concurrent check won the race and the money is
3906
+ * credited either way — but the key is `(credit_id, fund_id)` and `fund_id`
3907
+ * is **caller-chosen**: reuse it on cashu or x402 after this bolt11 was
3908
+ * minted and the row that collided is a different payment entirely, so the
3909
+ * sats this invoice received have nowhere to land.
4232
3910
  */
4233
- private reportUnappliedPayment;
3911
+ private blockingReason;
3912
+ private withBackend;
3913
+ /** Record activity without letting it promote an unvalidated connection. */
3914
+ private noteOperationOk;
3915
+ private notePermissionsOk;
3916
+ private notePermissionRefused;
4234
3917
  /**
4235
- * Describe a deposit the job row never took, in the shape both the 201 and
4236
- * the `payment_unapplied` 500 carry (internal-review). `undefined` when the payment
4237
- * ran ledger-less (no credit ledger wired, or the top-up preflight skipped) —
4238
- * there is no credit to name, so the 201 carries no `credit` block and the
4239
- * 500 takes its words from `unappliedCopy` directly.
3918
+ * Only a *transport* failure from an ordinary invoice operation is a health
3919
+ * signal. Permission-probe refusals are classified by {@link refresh}.
4240
3920
  */
4241
- private unappliedCredit;
4242
- /** Book a mid-job top-up's funding as a deposit (internal-review, spec §10). */
4243
- private reportCreditDeposit;
4244
- /**
4245
- * Internal: build the BuildContextOpts shared between `createJob` and
4246
- * `reactivateJob`. Centralised so persistence/terminal callbacks and the
4247
- * internal-review NOTIFY-driven cross-machine wake stay in one place.
3921
+ private noteFailure;
3922
+ private refresh;
3923
+ }
3924
+
3925
+ /**
3926
+ * Owner display identity surfaced on `/v1/info#owner` (internal-review). Personal orgs
3927
+ * resolve to the owner builder's profile; shared orgs resolve to the org's own
3928
+ * profile. Container DVMs read from env vars; isolate DVMs resolve from Postgres.
3929
+ */
3930
+ interface OwnerDisplay {
3931
+ handle: string;
3932
+ displayName?: string | null;
3933
+ avatarUrl?: string | null;
3934
+ type: "builder" | "org";
3935
+ }
3936
+ /** Optional builder identity surfaced on `/v1/info` (forward-compatible stub). */
3937
+ interface BuilderIdentity {
3938
+ id?: string;
3939
+ name?: string;
3940
+ url?: string;
3941
+ /**
3942
+ * Builder identity x-only secp256k1 pubkey (internal-review). When populated, the
3943
+ * `/v1/info#builder` block also carries `attestation` + `signature` so
3944
+ * consumers can verify the deploy was signed by the holder of this key.
3945
+ * Populated at deploy time from `DVMKIT_BUILDER_PUBKEY` (Fly secret); the
3946
+ * SDK never re-signs at runtime.
4248
3947
  */
4249
- private buildContextOpts;
3948
+ pubkey?: string;
3949
+ /** Canonical deploy-time attestation payload (internal-review). Served verbatim. */
3950
+ attestation?: AttestationPayload;
3951
+ /** Schnorr signature over `canonicaliseForSigning(attestation)` (internal-review). */
3952
+ signature?: string;
3953
+ }
3954
+ /** Platform reporter overrides — the host falls back to env when omitted. */
3955
+ interface PlatformReporterOpts {
3956
+ /** Bearer token. Defaults to `DVMKIT_PLATFORM_TOKEN` env. */
3957
+ token?: string;
3958
+ /** Platform internal URL. Defaults to `DVMKIT_PLATFORM_URL` env. */
3959
+ url?: string;
3960
+ }
3961
+ /** Options for {@link createDVMHost}. */
3962
+ interface DVMHostOpts {
4250
3963
  /**
4251
- * Internal: what unit `ctx.requestPayment`'s auto-credit gate may measure this
4252
- * job's remaining pool in (internal-review).
4253
- *
4254
- * The regime is read off the DRAW, never off the job. Implicit N=1 is exactly
4255
- * `credit_id === imp:<rail>:<draw_id>`, because `fundAndDraw` derives both
4256
- * from the same rail payment id — a fact about a row that was written once
4257
- * and never moves. The tempting alternative, comparing `job.payment_tx_hash`
4258
- * against `drawSettlementRef` (what the mid-job preflight does to inherit
4259
- * `drawBasis`), silently rots: Tx C overwrites that column on every mid-job
4260
- * credit, and a payment whose ledger leg was skipped — a cross-rail top-up,
4261
- * an fx outage, a draw already resolved — leaves the rail's own reference
4262
- * there, so an explicit-draw job would read as implicit from then on.
4263
- *
4264
- * Never throws, and never answers `rail` on a guess. `rail` is claimed only
4265
- * where `withDrawBasis` demonstrably left `PaymentInfo.paidMsats` alone — no
4266
- * draw at all, or a draw carrying no rail value to overlay it with. A job that
4267
- * drew and whose pool this can't read or denominate answers `unpriced`, which
4268
- * the gate declines to auto-credit from: the msat figure it would otherwise
4269
- * fall back on is a slice of the credit's rail value at a ratio pinned
4270
- * whenever that credit was funded, which is the exact figure this issue exists
4271
- * to stop spending against.
3964
+ * Postgres connection string. Defaults to `DATABASE_URL` env.
3965
+ * Required at runtime (internal-review). Boot fails when missing unless `jobStore`
3966
+ * is explicitly supplied or `devMode` is true (test/dev escape hatches).
4272
3967
  */
4273
- private resolveDrawPool;
4274
- /** Internal: structured warn for the one path that can't price a drawn job's pool. */
4275
- private warnDrawPool;
3968
+ database?: string;
3969
+ /** Listen port. Defaults to `PORT` env or 8080. */
3970
+ port?: number;
3971
+ /** Environment variables. Defaults to `process.env`. */
3972
+ env?: Record<string, string>;
3973
+ /** Builder identity for `/v1/info` (forward-compatible). */
3974
+ builder?: BuilderIdentity;
3975
+ /** Owner display identity for `/v1/info#owner` (internal-review). Falls back to env vars. */
3976
+ owner?: OwnerDisplay;
3977
+ /** Platform reporter overrides. Defaults to env-derived values. */
3978
+ platformReporter?: PlatformReporterOpts;
3979
+ /** Override the SDK's fx fetcher. Defaults to env-configured CoinGecko. */
3980
+ fx?: FxFetcher;
4276
3981
  /**
4277
- * Issue the signed receipt for a job that has reached a terminal status
4278
- * (internal-review). Idempotent and safe to call from every terminal path — the
4279
- * store owns both the sequence allocation and the write-once persist, so
4280
- * two machines racing on the same job converge on identical bytes and burn
4281
- * exactly one sequence number.
4282
- *
4283
- * Never throws. A receipt is evidence about a job, not part of delivering
4284
- * it: if signing or persistence fails we log structured and serve the
4285
- * response without one (same posture as the `cashu_refund_failed` branch in
4286
- * `context.ts`). The gap is visible to callers — the `seq` series skips a
4287
- * number — which is precisely the completeness signal receipts exist for.
3982
+ * Custom `/health` handler. When set, the SDK installs this instead of the
3983
+ * default `{ status: "ok" }` responder — useful for runtime-specific health
3984
+ * (pool depth, draining state, 503 while draining).
4288
3985
  */
4289
- issueReceipt(record: JobRecord): Promise<JobReceipt | undefined>;
4290
- /** Build the common settle/release args, adding the hosted release outbox when wired. */
4291
- private drawResolutionArgs;
3986
+ healthHandler?: (c: Context) => Response | Promise<Response>;
3987
+ /** KV store override. Defaults to PostgresKVStore (or MemoryKVStore in dev). */
3988
+ store?: KVStore;
3989
+ /** JobStore override. Takes precedence over `database`. */
3990
+ jobStore?: JobStore;
4292
3991
  /**
4293
- * Settle or release a terminal job's credit draw (internal-review): `completed`
4294
- * settles (the hold becomes a real debit); `failed`/`cancelled` releases
4295
- * (the hold evaporates — "no debit on job failure", mechanically
4296
- * superseding the internal-review/974 no-op refund for credit-paid jobs, including
4297
- * the `ctx.fail` path and the stale-sweeper reap). Idempotent — a replayed
4298
- * resolution returns the recorded state. Returns the `ReceiptCredit` block
4299
- * for the receipt: `balance_after` is the recorded draw trajectory for a
4300
- * settled draw, and the restored available balance for a released one.
3992
+ * Existing Postgres pool to reuse instead of opening one from `database`
3993
+ * (internal-review test seam). When set, the host builds its JobStore / KVStore /
3994
+ * cashu accumulator on this pool and leaves it open at shutdown — the caller
3995
+ * owns it. Lets the e2e harness run DVMs in `p2pk-accumulator` mode against
3996
+ * its single shared pool rather than spawning a pool per DVM.
4301
3997
  */
4302
- private resolveCreditDraw;
3998
+ pool?: Pool;
3999
+ /** Cashu mints accepted. Defaults to env `DVMKIT_CASHU_MINTS`. */
4000
+ mints?: string[];
4001
+ /** Cashu receive mode override. */
4002
+ cashuMode?: CashuMode;
4003
+ /** MPP handle override. Defaults to env-resolved via `createMppFromOpts`. */
4004
+ mpp?: MppxServer;
4005
+ /** x402 stablecoin payment configuration. Defaults to env-resolved. */
4006
+ x402?: X402Config;
4007
+ /** Payment methods override. Defaults to derived from configured rails. */
4008
+ paymentMethods?: PaymentMethod[];
4303
4009
  /**
4304
- * The one place a terminal job's status becomes a ledger verb (internal-review):
4305
- * `completed` settles the hold into a real debit, `failed`/`cancelled`
4306
- * release it. Read by the in-line funnel (`resolveCreditDraw`) and by the
4307
- * late reconciler (`reconcileTerminalDraw`) — two paths that must never
4308
- * disagree about what an outcome means for the caller's money.
4010
+ * Lightning receive leg for credit funding (internal-review). Defaults to the
4011
+ * env-resolved `DVMKIT_NWC_RECEIVE_URI` /
4012
+ * `DVMKIT_NWC_RECEIVE_INVOICE_TTL_SECONDS`. Pass `backend` to inject a
4013
+ * wallet directly — a test seam that skips the connect-time probe, so
4014
+ * production must always come through the URI.
4309
4015
  */
4310
- private settlesDraw;
4016
+ lightningReceive?: LightningReceiveConfig;
4017
+ /** When true, payment is skipped if no mints are configured (dev/test). */
4018
+ devMode?: boolean;
4019
+ /** Consumed-credential store override (test seam). */
4020
+ consumedCredentialStore?: ConsumedCredentialStore;
4021
+ /** Mint-health tracker override (test seam). */
4022
+ mintHealthTracker?: MintHealthTracker;
4023
+ /** Operator-alert boundary for failed terminal-job retention passes. */
4024
+ onJobRetentionSweepFailed?: JobManagerOpts["onJobRetentionSweepFailed"];
4025
+ /** Clear-on-recovery boundary invoked after a complete clean retention pass. */
4026
+ onJobRetentionSweepRecovered?: JobManagerOpts["onJobRetentionSweepRecovered"];
4311
4027
  /**
4312
- * Sign a locally-held terminal job's receipt and mirror it onto the
4313
- * in-memory job, so the read paths served out of `activeJobs` during the
4314
- * cleanup window hand back the same bytes as a cross-machine re-read.
4028
+ * Wrap the SDK compatibility gate before it is exposed to descriptor custom
4029
+ * routes. Platform hosts use this to enforce their independently released
4030
+ * caller rollout policy; self-hosted SDK consumers receive the default gate.
4315
4031
  */
4316
- private attachReceipt;
4032
+ wrapRouteClientCompatibility?: (sdkGate: ClientCompatibilityGate) => ClientCompatibilityGate;
4033
+ }
4034
+ /** Per-mount options. Currently only `prefix` is supported. */
4035
+ interface MountOpts {
4036
+ /** Path prefix for this DVM's protocol routes (e.g. `/delete-feed`). */
4037
+ prefix?: string;
4038
+ }
4039
+ /** Live DVM host. Single wiring locus for the SDK (internal-review). */
4040
+ interface DVMHost {
4317
4041
  /**
4318
- * Read-path repair for a terminal job carrying no receipt (internal-review), for
4319
- * either an in-memory job or a store record. Free in the steady state — it
4320
- * returns on the `receipt` check without touching the store — so it costs
4321
- * only on the cases it exists for:
4322
- *
4323
- * - **A crash between the two store calls.** `claimReceiptSeq` commits the
4324
- * sequence number to the job row before `saveReceipt` writes the bytes; a
4325
- * process death in that window would otherwise strand that number
4326
- * forever, and a permanent gap is indistinguishable from the deliberate
4327
- * suppression `seq` exists to expose. Re-issuing here reuses the already
4328
- * committed number (the claim is idempotent per job) rather than
4329
- * allocating a second one.
4330
- * - **A transient store failure** at the terminal: `issueReceipt` logs
4331
- * `receipt_issue_failed` and returns nothing rather than failing the job,
4332
- * so the next read retries.
4333
- * - **Jobs that terminated before the DVM had a receipt key.** They pick one
4334
- * up on first read, with `issued_at` reflecting when it was signed.
4042
+ * Underlying Hono app for custom non-protocol routes that belong to the
4043
+ * host (cross-DVM `/admin`, cron callbacks, etc.). For DVM-scoped routes,
4044
+ * declare `routes(app)` on the descriptor.
4335
4045
  */
4336
- ensureReceipt(target: ServerJob | JobRecord): Promise<void>;
4046
+ readonly app: Hono;
4337
4047
  /**
4338
- * Close out a terminal job whose caller hasn't already persisted it: sign
4339
- * the receipt, then save the snapshot. The snapshot save never writes the
4340
- * receipt column, so ordering only affects how soon a reader sees the
4341
- * receipt — this way a caller polling immediately after the terminal
4342
- * already finds it.
4048
+ * Resolved Postgres pool — undefined before `host.serve()` completes
4049
+ * database resolution, set thereafter. Exposed so DVM-local Postgres
4050
+ * consumers (e.g. scrape's `ScrapeDb` per-fetch event store, internal-review)
4051
+ * can reuse the host's pool rather than constructing their own. Wire
4052
+ * inside descriptor `onBoot` (fires after pool init, before listener opens).
4343
4053
  */
4344
- private finalizeTerminal;
4054
+ readonly pool: Pool | undefined;
4345
4055
  /**
4346
- * Issue a receipt for a job this process doesn't hold in `activeJobs` — a
4347
- * cross-machine cancel, or a row the stale sweeper just reaped. Re-reads the
4348
- * record so the receipt is built from the committed terminal row rather than
4349
- * from whatever the caller happened to have in hand.
4056
+ * Money-safe credit ledger (internal-review) — undefined before `host.serve()`
4057
+ * resolves stores, set thereafter. Postgres-backed when a pool exists;
4058
+ * pool-less hosts get a shared in-memory ledger so the internal-review fund+draw
4059
+ * semantics hold everywhere. Same wiring window as `pool` (descriptor
4060
+ * `onBoot` fires after init, before the listener opens).
4350
4061
  */
4351
- issueReceiptForStoredJob(jobId: string): Promise<JobReceipt | undefined>;
4352
- /** Handle job reaching terminal state (completed, failed, cancelled). */
4353
- private handleJobTerminal;
4062
+ readonly creditLedger: CreditLedgerLike | undefined;
4354
4063
  /**
4355
- * Report a completed paid job's revenue (internal-review, re-keyed by internal-review).
4356
- * Fire-and-forget — the `RevenueReporter` owns persistence and retry.
4357
- *
4358
- * For a **credit-backed** job the revenue event is the *settled draw* (spec
4359
- * §10), and since internal-review the sats figure corrects the job's mirror of that
4360
- * draw against the draw itself: `withDrawBasis` stamps the job when the
4361
- * payment lands, but a Bitcoin credit's settle re-prices the draw against
4362
- * the funding lots it consumed. The correction is a delta, because a job's
4363
- * counter can carry legs no draw ever absorbed — except where the draw's
4364
- * forecast was zero and the counter therefore never held a component to
4365
- * correct. The event is dated at settlement rather than at report time. A
4366
- * released draw — the failed/cancelled path — is deliberately unreachable
4367
- * here, which is what makes "no debit on job failure" true in the books as
4368
- * well as the ledger.
4369
- *
4370
- * For everything else (free-then-paid jobs, isolate-shaped flows, a DVM
4371
- * whose price couldn't be fiat-denominated) nothing changes: the pre-credits
4372
- * payload is reported verbatim.
4373
- *
4374
- * Takes the fields rather than a `ServerJob` so the internal-review reconciler — a
4375
- * background sweep that holds no in-process job — books through this exact
4376
- * path off the row it read. `paymentTxHash` is passed **verbatim**, never
4377
- * recomputed as a `drawSettlementRef`: it is the same column the in-line
4378
- * funnel reads, so the two bookings are byte-identical by construction and
4379
- * the platform's `UNIQUE (rail, tx_hash)` dedupes them. Deriving it instead
4380
- * would diverge on an implicit N=1 job (whose row carries the rail's own
4381
- * reference) and on any job whose column a mid-job top-up overwrote — the
4382
- * same rot `resolveDrawPool` documents as the reason not to read regime off
4383
- * this column.
4384
- *
4385
- * Returns whether the report was handed to `onJobCompleted`, so a caller
4386
- * that logs the booking says what actually happened rather than what it
4387
- * assumed — the guards below still drop legitimate shapes (a free job, a
4388
- * DVM with no reporter). Every drop that costs a booking logs first.
4389
- *
4390
- * The rail check sits **below** the draw re-read, not in the entry guard
4391
- * (internal-review): a settled draw the terminal can't report under is a real debit
4392
- * with no revenue event, permanently overstating outstanding liability
4393
- * (`deposits − draw revenue`), and it used to return here in silence. Placed
4394
- * after the re-read, `revenue_skipped_no_rail` can name the draw and its
4395
- * amount, and can distinguish the four causes — see its `reason` below.
4064
+ * Add a DVM to this host. Multiple mounts compose at distinct prefixes;
4065
+ * a single mount with no prefix attaches at root.
4396
4066
  */
4397
- private bookRevenue;
4398
- /** Schedule job removal from activeJobs after a delay so clients can still poll final status. */
4399
- scheduleCleanup(job: ServerJob): void;
4400
- /** Clear all timers. Called on server shutdown to allow clean exit. */
4401
- shutdown(): void;
4067
+ mount<S, I extends ZodLike | undefined>(descriptor: DVMDescriptor<S, I>, opts?: MountOpts): void;
4068
+ /** Start listening. Returns the live server handle. */
4069
+ serve(opts?: {
4070
+ port?: number;
4071
+ }): Promise<{
4072
+ url: string;
4073
+ close: () => Promise<void>;
4074
+ }>;
4075
+ /** Graceful shutdown. Runs descriptor `onShutdown` hooks in reverse mount order. */
4076
+ shutdown(): Promise<void>;
4402
4077
  }
4078
+ /**
4079
+ * Single wiring locus for SDK-side construction (internal-review). Replaces both the
4080
+ * `serve()` wrapper and the `createDVMServer` direct path. Reads env once,
4081
+ * resolves payment rails, owns the Postgres pool, and mounts each DVM
4082
+ * descriptor as a Hono sub-app on `host.app`.
4083
+ */
4084
+ declare function createDVMHost(opts?: DVMHostOpts): DVMHost;
4403
4085
 
4404
4086
  /**
4405
4087
  * Cached liveness signal for the platform's central Tempo close observer
@@ -4685,12 +4367,12 @@ interface SDKServerOpts {
4685
4367
  /** Cashu receive mode (internal-review). */
4686
4368
  cashuMode?: CashuMode;
4687
4369
  /**
4688
- * Builder's NUT-11 P2PK lock pubkey (internal-review). Required to enable accumulator
4689
- * mode — used as the seed pubkey for `seedLockPubkeyState` on first boot, and
4690
- * as the gate for whether `/v1/info` advertises `cashu` and the admin routes
4691
- * mount. After first boot, the authoritative `[current, ...retired]` state
4692
- * lives in the DVM's Postgres via `dvm_lock_pubkeys`; rotations (internal-review)
4693
- * mutate that store directly.
4370
+ * Builder's NUT-11 P2PK lock pubkey (internal-review). Used as the seed pubkey for
4371
+ * `seedLockPubkeyState` on first boot, as the admin-auth identity, and as
4372
+ * part of the gate for whether `/v1/info` advertises `cashu`. After first
4373
+ * boot, the authoritative `[current, ...retired]` state lives in the DVM's
4374
+ * Postgres via `dvm_lock_pubkeys`; rotations (internal-review) mutate that store
4375
+ * directly.
4694
4376
  */
4695
4377
  lockPubkey?: string;
4696
4378
  /**
@@ -4708,7 +4390,7 @@ interface SDKServerOpts {
4708
4390
  * without a separate env var.
4709
4391
  */
4710
4392
  canonicalDvmId?: string;
4711
- /** Postgres pool for wallet accumulator persistence. */
4393
+ /** Postgres pool for builder admin state and wallet accumulator persistence. */
4712
4394
  db?: Pool;
4713
4395
  /**
4714
4396
  * General SDK strict-mode flag (internal-review), sourced from `DVMKIT_FAIL_FAST` at
@@ -4766,6 +4448,10 @@ interface SDKServerOpts {
4766
4448
  * the platform reporter alongside the completion and paid-job-death paths.
4767
4449
  */
4768
4450
  onRevenueSkippedNoRail?: (info: RevenueSkippedNoRailPayload) => void | Promise<void>;
4451
+ /** Advanced operator-alert callback for failed job-retention passes. */
4452
+ onJobRetentionSweepFailed?: JobManagerOpts["onJobRetentionSweepFailed"];
4453
+ /** Advanced clear-on-recovery callback for clean job-retention passes. */
4454
+ onJobRetentionSweepRecovered?: JobManagerOpts["onJobRetentionSweepRecovered"];
4769
4455
  /**
4770
4456
  * Advanced override: callback fired when a payment funds a credit (internal-review).
4771
4457
  * Auto-wired to the `RevenueReporter`'s deposit report alongside
@@ -5122,4 +4808,4 @@ declare function attachCreditMenu(body: Record<string, unknown>, menu: CreditMen
5122
4808
  */
5123
4809
  declare function toCreditTerms(menu: CreditMenu): CreditTerms;
5124
4810
 
5125
- export { assertRevenueReporterReady as $, type AccumulatorPool as A, type BuilderIdentityKeypair as B, type CreatedInvoice as C, X402FacilitatorHealth as D, type X402SettlementEvidenceOutcome as E, type FiatDenomination as F, type X402SettlementRepair as G, type X402SettlementRepairRefusal as H, type InvoiceStatus as I, JobCancelledError as J, type X402SettlementRepaired as K, type LightningTransactionListOptions as L, type MintAmountBounds as M, type X402WedgedSettlement as N, type X402WedgedSettlementPage as O, PAYMENT_PROOF_KEYS as P, X402_BATCH_SETTLEMENT_MAINNET_NETWORK as Q, type ReporterBannerOpts as R, type SDKServerOpts as S, type TempoChannelReport as T, type UpfrontPaymentOpts as U, type VerifiedIncomingPayment as V, abortJob as W, type X402BatchChannelObservation as X, amountBoundsVerdict as Y, applyPaymentInfoToJob as Z, assertNutSupport as _, type LightningTransactionSnapshot as a, MIN_INVOICE_TTL_SECONDS as a$, buildAttestation as a0, buildPaymentErrorResponse as a1, canSwap as a2, checkMintHealth as a3, clearAccumulatorForDvm as a4, createDVMServer as a5, creditDepositPayload as a6, derivedFundCreditId as a7, devModeSkipsPaymentVerification as a8, fromJobRecord as a9, toJobRecord as aA, unknownRouteNotFound as aB, verifyAttestation as aC, verifyIncomingPayment as aD, verifyTempoSessionManagementCredential as aE, verifyUpfrontPayment as aF, writeIdentity as aG, x402RequiredUsdcMicro as aH, x402SettledShare as aI, type CreditMenu as aJ, ReceiptIssuer as aK, LightningReceive as aL, TempoSettlementReadiness as aM, type DVMHostOpts as aN, type BuildCreditMenuArgs as aO, type BuilderIdentity as aP, CREDIT_ENVELOPE_KEYS as aQ, type CreateX402BatchSettlementServerOpts as aR, type CreditEnvelope as aS, CreditEnvelopeError as aT, type CreditFundCommitment as aU, type CreditTerms as aV, DEFAULT_INVOICE_TTL_SECONDS as aW, type DVMHost as aX, JobManager as aY, type JobManagerOpts as aZ, type LightningReceiveConfig as a_, fundedMicroFor as aa, generateIdentity as ab, hasPaymentProof as ac, hashCapabilities as ad, hashLockKey as ae, implicitCreditId as af, initWalletAccumulatorTable as ag, insertAccumulatorRows as ah, isDerivedCreditId as ai, isTerminal as aj, isYieldMessage as ak, issueUpfrontChallenges as al, loadIdentitySecret as am, toLockPubkey as an, msatsToFiatMicro as ao, paymentErrorBody as ap, pinAskFiat as aq, priceFiatMicro as ar, processIncomingPayment as as, providerMessage as at, repairX402ExactSettlementEffect as au, resolveFxSnapshot as av, resolvePriceFiat as aw, revenueReporterBannerState as ax, signAttestation as ay, toCreditTerms as az, type LightningBackend as b, MemoryConsumedCredentialStore as b0, type MemoryConsumedCredentialStoreOpts as b1, MemoryProcessedPaymentStore as b2, MemoryX402ExactSettlementStore as b3, type MountOpts as b4, type OwnerDisplay as b5, type PlatformReporterOpts as b6, PostgresProcessedPaymentStore as b7, PostgresX402ExactSettlementStore as b8, type PriceFiat as b9, X402_BATCH_MIN_WITHDRAW_DELAY_SECONDS as bA, X402_BATCH_SETTLEMENT_NETWORK as bB, attachCreditMenu as bC, buildCreditMenu as bD, createDVMHost as bE, createX402BatchSettlementServer as bF, creditEnvelopeIgnoreFields as bG, drawSettlementRef as bH, extractCreditEnvelope as bI, fundingCommitment as bJ, selectPrimaryCredit as bK, stripCreditEnvelope as bL, toCreditView as bM, type ProcessedPaymentQuerier as ba, type ProcessedPaymentRail as bb, type ProcessedPaymentRecord as bc, ProcessedPaymentReplayError as bd, type ProcessedPaymentStore as be, type X402BatchAcceptance as bf, type X402BatchFunding as bg, type X402BatchRefusal as bh, type X402BatchSettlementServer as bi, type X402ExactAcceptance as bj, X402ExactIntentConflictError as bk, type X402ExactSettlementAttempt as bl, type X402ExactSettlementChainEvidence as bm, type X402ExactSettlementEffect as bn, X402ExactSettlementEvidenceMissingError as bo, type X402ExactSettlementEvidenceReader as bp, type X402ExactSettlementIntent as bq, X402ExactSettlementNotReadyError as br, X402ExactSettlementServer as bs, type X402ExactSettlementServerOpts as bt, type X402ExactSettlementStatus as bu, type X402ExactSettlementStore as bv, type X402SettlementChainEvidence as bw, type X402SettlementEvidenceReader as bx, X402SettlementSubmissionError as by, X402_BATCH_AUTO_SETTLEMENT as bz, type LightningPayment as c, type LightningWalletInfo as d, type LockPubkey as e, type AccumulatorQuerier as f, type ConsumedCredentialStore as g, type MintHealthCheckResult as h, type AppEnv as i, type AttestationPayload as j, type CheckMintHealthOptions as k, type CreditFundingReport as l, type FiatDenominationFailure as m, type FundOnlyRequest as n, type IncomingPaymentOpts as o, MintHealthTracker as p, type PaymentErrorCode as q, type PaymentErrorDetail as r, type PaymentInfo as s, type ResolvedFx as t, type RevenueBootCheckOpts as u, type ServerJob as v, type ShortPayForfeit as w, type TempoObserverHealth as x, TempoSessionChannelMismatchError as y, type VerifyIncomingSnapshot as z };
4811
+ export { isTerminal as $, type AccumulatorPool as A, buildPaymentErrorResponse as B, type ConsumedCredentialStore as C, clearAccumulatorForDvm as D, createDVMServer as E, type FiatDenomination as F, creditDepositPayload as G, derivedFundCreditId as H, type IncomingPaymentOpts as I, JobCancelledError as J, devModeSkipsPaymentVerification as K, fromJobRecord as L, MintHealthTracker as M, fundedMicroFor as N, hasPaymentProof as O, PAYMENT_PROOF_KEYS as P, hashLockKey as Q, type ReporterBannerOpts as R, type SDKServerOpts as S, type TempoChannelReport as T, type UpfrontPaymentOpts as U, type VerifiedIncomingPayment as V, implicitCreditId as W, type X402BatchChannelObservation as X, initWalletAccumulatorTable as Y, insertAccumulatorRows as Z, isDerivedCreditId as _, type AccumulatorQuerier as a, X402ExactSettlementEvidenceMissingError as a$, isYieldMessage as a0, issueUpfrontChallenges as a1, msatsToFiatMicro as a2, paymentErrorBody as a3, pinAskFiat as a4, priceFiatMicro as a5, processIncomingPayment as a6, providerMessage as a7, repairX402ExactSettlementEffect as a8, resolveFxSnapshot as a9, type JobManagerOpts as aA, type LightningReceiveConfig as aB, MIN_INVOICE_TTL_SECONDS as aC, MemoryConsumedCredentialStore as aD, type MemoryConsumedCredentialStoreOpts as aE, MemoryProcessedPaymentStore as aF, MemoryX402ExactSettlementStore as aG, type MountOpts as aH, type OwnerDisplay as aI, type PlatformReporterOpts as aJ, PostgresProcessedPaymentStore as aK, PostgresX402ExactSettlementStore as aL, type PriceFiat as aM, type ProcessedPaymentQuerier as aN, type ProcessedPaymentRail as aO, type ProcessedPaymentRecord as aP, ProcessedPaymentReplayError as aQ, type ProcessedPaymentStore as aR, type X402BatchAcceptance as aS, type X402BatchFunding as aT, type X402BatchRefusal as aU, type X402BatchSettlementServer as aV, type X402ExactAcceptance as aW, X402ExactIntentConflictError as aX, type X402ExactSettlementAttempt as aY, type X402ExactSettlementChainEvidence as aZ, type X402ExactSettlementEffect as a_, resolvePriceFiat as aa, revenueReporterBannerState as ab, toCreditTerms as ac, toJobRecord as ad, unknownRouteNotFound as ae, verifyIncomingPayment as af, verifyTempoSessionManagementCredential as ag, verifyUpfrontPayment as ah, x402RequiredUsdcMicro as ai, x402SettledShare as aj, type CreditMenu as ak, ReceiptIssuer as al, LightningReceive as am, TempoSettlementReadiness as an, type DVMHostOpts as ao, type BuildCreditMenuArgs as ap, type BuilderIdentity as aq, CREDIT_ENVELOPE_KEYS as ar, type CreateX402BatchSettlementServerOpts as as, type CreditEnvelope as at, CreditEnvelopeError as au, type CreditFundCommitment as av, type CreditTerms as aw, DEFAULT_INVOICE_TTL_SECONDS as ax, type DVMHost as ay, JobManager as az, type AppEnv as b, type X402ExactSettlementEvidenceReader as b0, type X402ExactSettlementIntent as b1, X402ExactSettlementNotReadyError as b2, X402ExactSettlementServer as b3, type X402ExactSettlementServerOpts as b4, type X402ExactSettlementStatus as b5, type X402ExactSettlementStore as b6, type X402SettlementChainEvidence as b7, type X402SettlementEvidenceReader as b8, X402SettlementSubmissionError as b9, X402_BATCH_AUTO_SETTLEMENT as ba, X402_BATCH_MIN_WITHDRAW_DELAY_SECONDS as bb, X402_BATCH_SETTLEMENT_NETWORK as bc, attachCreditMenu as bd, buildCreditMenu as be, createDVMHost as bf, createX402BatchSettlementServer as bg, creditEnvelopeIgnoreFields as bh, drawSettlementRef as bi, extractCreditEnvelope as bj, fundingCommitment as bk, selectPrimaryCredit as bl, stripCreditEnvelope as bm, toCreditView as bn, type CreditFundingReport as c, type FiatDenominationFailure as d, type FundOnlyRequest as e, type PaymentErrorCode as f, type PaymentErrorDetail as g, type PaymentInfo as h, type ResolvedFx as i, type RevenueBootCheckOpts as j, type ServerJob as k, type ShortPayForfeit as l, type TempoObserverHealth as m, TempoSessionChannelMismatchError as n, type VerifyIncomingSnapshot as o, X402FacilitatorHealth as p, type X402SettlementEvidenceOutcome as q, type X402SettlementRepair as r, type X402SettlementRepairRefusal as s, type X402SettlementRepaired as t, type X402WedgedSettlement as u, type X402WedgedSettlementPage as v, X402_BATCH_SETTLEMENT_MAINNET_NETWORK as w, abortJob as x, applyPaymentInfoToJob as y, assertRevenueReporterReady as z };