@dvmkit/sdk 0.0.0 → 0.1.0-rc.2

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 (42) hide show
  1. package/NOTICE +2 -0
  2. package/README.md +38 -2
  3. package/dist/chunk-27V2ILSR.js +291 -0
  4. package/dist/chunk-365P52XQ.js +4121 -0
  5. package/dist/chunk-5GFED3GJ.js +955 -0
  6. package/dist/chunk-6JZIX5WW.js +1155 -0
  7. package/dist/chunk-7IH5SG2A.js +1038 -0
  8. package/dist/chunk-AT6V3SY7.js +102 -0
  9. package/dist/chunk-DCNT4PJS.js +733 -0
  10. package/dist/chunk-DMNLFNTW.js +135 -0
  11. package/dist/chunk-FROTD5XQ.js +70 -0
  12. package/dist/chunk-H25M54MI.js +149 -0
  13. package/dist/chunk-KQAJVVZT.js +712 -0
  14. package/dist/chunk-KXWROQGK.js +74 -0
  15. package/dist/chunk-L4OYF4DQ.js +67 -0
  16. package/dist/chunk-OJ5WFIB2.js +1266 -0
  17. package/dist/chunk-S3XAHZQY.js +63 -0
  18. package/dist/chunk-YG7G4DPZ.js +25 -0
  19. package/dist/credit-ledger-RO4FGSHG.js +28 -0
  20. package/dist/index.d.ts +144 -0
  21. package/dist/index.js +303 -0
  22. package/dist/job-store-6gR4pZRP.d.ts +5350 -0
  23. package/dist/memory-credit-ledger-I2G64DDK.js +9 -0
  24. package/dist/mpp-secret-state-WNAQQ6K4.js +127 -0
  25. package/dist/mpp-setup-MOBWGTWJ.js +30 -0
  26. package/dist/payout-reporter-4TNWRS5F.js +753 -0
  27. package/dist/postgres-consumed-credential-store-VHBT4KEA.js +72 -0
  28. package/dist/postgres-job-store-J5F4GUWU.js +7 -0
  29. package/dist/postgres-kv-store-JFBDP5IP.js +7 -0
  30. package/dist/postgres-replay-store-UJXRT6VO.js +7 -0
  31. package/dist/pricing-4CEB34RM.js +48 -0
  32. package/dist/processed-payment-store-HAA4SFNK.js +11 -0
  33. package/dist/revenue-reporter-GB4WKLDC.js +510 -0
  34. package/dist/server/index.d.ts +4168 -0
  35. package/dist/server/index.js +22716 -0
  36. package/dist/ssrf-DZi-xJyn.d.ts +325 -0
  37. package/dist/tempo-charge-store-6GJEMNUU.js +130 -0
  38. package/dist/tempo-session-store-FTEEGZXA.js +467 -0
  39. package/dist/testing/index.d.ts +135 -0
  40. package/dist/testing/index.js +151 -0
  41. package/dist/x402-35VLYFKZ.js +1272 -0
  42. package/package.json +89 -6
@@ -0,0 +1,4168 @@
1
+ import { X as CashuMode, R as ResolvedCreditConfig, Y as FundingMethod, _ as CreditLedgerLike, $ as CreditSnapshot, g as CreditView, a0 as CreditInvoiceRecord, a1 as CreditDepositEnqueue, a2 as InvoiceSettlement, Z as ZodLike, a as DVMDescriptor, K as KVStore, o as JobStore, a3 as MppxServer, a4 as X402Config, p as PaymentMethod, a5 as ClientCompatibilityGate, a6 as Message, a7 as FundingReceipt, a8 as TopUpCapUnenforcedReason, a9 as JobReceipt, aa as AppendOutgoingOptions, S as SDKJobContext, ab as StepCache, v as ResponseContent, P as PaymentContent, ac as X402Receipt, ad as X402ExactVersionSupport, ae as X402SettlementIntent, af as X402FacilitatorAuth, ag as X402BatchSettlementConfig, ah as PostgresX402ChannelStorage, ai as PaymentRequirementsV2, aj as CreditLedgerQuerier, ak as X402SettlementCursor, al as X402SettlementStatus, am as X402SettlementWriteOff, an as X402RefundSettlementGate, ao as MppxCredential, J as JobRecord, ap as ReceiptCredit, aq as DrainReceiptEvent, ar as DrainReceipt, z as SignedRequestAudience, as as CreditDrawReleaseEnqueue, at as CreditDepositPayload, au as RevenueSkippedNoRailPayload, av as MessageType, aw as ClientCompatibility, ax as CreditDrainEnqueue, ay as DVMAuthScheme, B as SignedRequestDomain, az as DrawResult, aA as X402UnresolvedRefund, aB as GrownDrawResult, aC as DrawResolution, aD as FundingRecord, aE as BlockedInvoiceCursor, aF as InvoiceReconciliation, aG as InvoiceWriteOff, aH as DrawRecord, aI as StalePendingDrawCursor, aJ as TempoCreditLossEvidence, aK as TempoCreditLoss, aL as X402CreditLossEvidence, aM as X402CreditLoss, aN as DrainMethod, aO as DrainRequestResult, aP as BitcoinDepositLiability, aQ as FundingLot, aR as CreditDrainRecord, aS as ChannelDrainCursor, aT as DrainWriteOff, aU as DrainReleaseResult, aV as DrainFulfilment, aW as DrainTransitionResult, aX as StreamableJobStore, aY as ReceiptIssuingStore, aZ as RequestIdClaim, a_ as RequestIdClaimResult, a$ as OutgoingMessage, b0 as PaymentCreditDelta, b1 as VerifyAndCreditResult, b2 as JobCounters, G as SignedRequestReplayStore } from '../job-store-6gR4pZRP.js';
2
+ export { b3 as CLIENT_COMPATIBILITY_HEADERS, d as CanonicalEnvelope, b4 as ClientCompatibilityEnv, b5 as ClientCompatibilityRequirement, b6 as ClientSemVer, e as CreateSignedRequestVerifierOpts, b7 as CreditFundingBasis, b8 as CreditInvoiceStatus, b9 as CreditLedger, ba as CreditLedgerError, bb as CreditLedgerErrorCode, bc as CreditLedgerErrorDetails, bd as CreditLedgerPool, be as CreditStatus, bf as DRAIN_DELIVERY_RESERVE_SATS, bg as DVM_PROTOCOL_VERSION, bh as DrainConflictReason, bi as DrawRailValue, bj as DrawStatus, n as JobStatus, bk as ReplayStoreBackend, bl as RevenueSkippedNoRailReason, x as SIGNED_REQUEST_AUTH_ID, y as SIGNED_REQUEST_STATEMENT_VERSION, bm as Secp256k1AuthOpts, E as SignedRequestError, F as SignedRequestFailure, H as SignedRequestSignOpts, M as SignedRequestStatementHeader, N as SignedRequestVerifier, bn as X402ChannelStorageOpts, bo as X402RelayLockHolder, bp as X402RelaySubmissionLock, bq as X402RelaySubmissionLockError, br as allocateDrawValue, bs as clientCompatibilityAttributes, bt as clientCompatibilityMiddleware, bu as clientUpgradeRequired, O as createSignedRequestVerifier, bv as isStreamableJobStore, bw as parseClientCapabilities, bx as parseClientCompatibility, by as parseDvmClient, bz as parseProtocolVersion, bA as requireClientCompatibility, bB as secp256k1Auth, bC as signedRequestInput, V as signedRequestStatementHeader } from '../job-store-6gR4pZRP.js';
3
+ import { Pool } from 'pg';
4
+ import { F as FxFetcher } from '../ssrf-DZi-xJyn.js';
5
+ export { P as PinnedFetch, S as SSRFError, e as SSRFGuardOpts, f as SSRFReason, g as SSRFResolver, h as assertSafeUrl, j as createPinnedFetch } from '../ssrf-DZi-xJyn.js';
6
+ import { Hono, Context } from 'hono';
7
+ import { z } from 'zod';
8
+ import { ProofLike } from '@cashu/cashu-ts';
9
+ import { SupportedResponse, SettleResponse } from '@x402/core/types';
10
+ import { Channel, AutoSettlementConfig } from '@x402/evm/batch-settlement/server';
11
+ import 'mppx';
12
+ import '@x402/core/server';
13
+
14
+ /** The channel view the tracker sums claims over. */
15
+ interface X402TrackedChannel {
16
+ channelId: string;
17
+ chargedCumulativeAmount: string;
18
+ totalClaimed: string;
19
+ }
20
+ /** A refund settlement wedged between chain and ledger (internal-review), for the wedge list. */
21
+ interface X402WedgedRefund {
22
+ settlementId: string;
23
+ /** Epoch ms the settlement was first prepared. */
24
+ createdAt: number;
25
+ native?: number;
26
+ }
27
+ /** What the batch-settlement server hands the tracker once it knows its scope. */
28
+ interface X402PayoutContext {
29
+ /** `${network}|${payTo}|${token}` — the scope the settle-pending marker is keyed on. */
30
+ scope: string;
31
+ payTo: string;
32
+ network: string;
33
+ storage: {
34
+ list(): Promise<X402TrackedChannel[]>;
35
+ };
36
+ /** The scheduler's settle cadence — what a wedge's `nextRetry` is derived from. */
37
+ settleIntervalMs: number;
38
+ listWedgedRefunds?: () => Promise<X402WedgedRefund[]>;
39
+ }
40
+ /** The two manager verbs the tracker wraps. */
41
+ interface X402TrackedManager {
42
+ claim(...args: never[]): Promise<{
43
+ vouchers: number;
44
+ transaction: string;
45
+ }[]>;
46
+ settle(): Promise<{
47
+ transaction: string;
48
+ }>;
49
+ }
50
+ /**
51
+ * The batch-settlement server's view of the reporter: attach once with the
52
+ * scope, then route every claim and settle through the tracked manager so
53
+ * the open batch is kept and the settle emits the payout.
54
+ */
55
+ interface X402PayoutObserver {
56
+ attach(ctx: X402PayoutContext): void;
57
+ trackManager<M extends X402TrackedManager>(manager: M): M;
58
+ /** A settle that landed outside the tracked manager — the manual claim-and-settle's own retry loop. */
59
+ recordSettle(transaction: string): Promise<void>;
60
+ /** A settle attempt that failed outside the tracked manager. One call per attempt a builder would count as one. */
61
+ recordSettleFailure(error: unknown): Promise<void>;
62
+ }
63
+
64
+ declare const lockPubkeyBrand: unique symbol;
65
+ /**
66
+ * NUT-11 P2PK lock pubkey, lowercase-by-construction. Hex-encoded compressed
67
+ * secp256k1 pubkey (66 chars starting with `02` / `03`), normalised via
68
+ * `toLockPubkey`. The brand exists so any in-memory site that compares
69
+ * lock-pubkeys against each other — receive matching, monitor orphan/retired
70
+ * lookups, melt-pending keypair maps — gets a compile-time guarantee that
71
+ * both sides have been normalised. Raw strings off the wire or out of the DB
72
+ * must pass through `toLockPubkey` before they're treated as one.
73
+ */
74
+ type LockPubkey = string & {
75
+ readonly [lockPubkeyBrand]: never;
76
+ };
77
+
78
+ /** Pool-shaped handle (`query` + `connect`). Required for the transactional insert path. */
79
+ type AccumulatorPool = Pick<Pool, "query" | "connect">;
80
+
81
+ /** Cashu accumulator opts resolved from environment variables. */
82
+ interface ResolvedCashuOpts {
83
+ cashuMode: CashuMode | undefined;
84
+ lockPubkey: string | undefined;
85
+ lockPubkeyGraceSeconds: number;
86
+ db: Pool | undefined;
87
+ dvmId: string | undefined;
88
+ canonicalDvmId: string | undefined;
89
+ }
90
+ /**
91
+ * Resolves cashu accumulator opts from environment variables, initializing DB
92
+ * tables when all prerequisites are met. Extracted from serve() so DVMs that
93
+ * call createDVMServer directly (cast, scrape) can wire cashu without using
94
+ * the serve() wrapper (internal-review).
95
+ *
96
+ * @param pool - existing Postgres pool to reuse; pass undefined when no DB is available
97
+ * @param cashuModeHint - programmatic override for cashu mode (serve()'s opts.cashuMode)
98
+ */
99
+ declare function resolveCashuOptsFromEnv(env: Record<string, string>, pool?: Pool, cashuModeHint?: CashuMode): Promise<ResolvedCashuOpts>;
100
+
101
+ /**
102
+ * Tracks MPP challenge ids that have already been consumed by an upfront-flow
103
+ * request, scoped per-DVM realm. Entries auto-expire after a TTL and the store
104
+ * is bounded so a long-running DVM doesn't accumulate state without bound.
105
+ *
106
+ * Used by `verifyUpfrontPayment` to reject replay of an MPP credential whose
107
+ * `(realm, challenge.id)` was already accepted within the current TTL window
108
+ * (internal-review). Mid-job replay is handled separately via per-job
109
+ * `pendingMppChallengeIds` (internal-review).
110
+ */
111
+ interface ConsumedCredentialStore {
112
+ /** True iff `(realm, challengeId)` was marked and the entry has not expired. */
113
+ has(realm: string, challengeId: string): Promise<boolean>;
114
+ /** Mark `(realm, challengeId)` as consumed for `ttlSeconds`. */
115
+ mark(realm: string, challengeId: string, ttlSeconds: number): Promise<void>;
116
+ }
117
+ /** Options for `MemoryConsumedCredentialStore`. */
118
+ interface MemoryConsumedCredentialStoreOpts {
119
+ /** FIFO eviction kicks in when entry count would exceed this. Default: 10_000. */
120
+ maxEntries?: number;
121
+ }
122
+ /**
123
+ * In-memory `ConsumedCredentialStore`. Insertion-ordered Map provides FIFO
124
+ * eviction at the configured cap; TTL is enforced lazily on `has()` (same
125
+ * pattern as `MemoryKVStore`).
126
+ *
127
+ * Per-process and not multi-instance-safe — each replica tracks its own
128
+ * consumed set, so a credential redirected to a different replica still works
129
+ * once. Acceptable for first-party single-instance Fly DVMs (internal-review open
130
+ * question); swap to a shared backend before deploying multiple replicas.
131
+ */
132
+ declare class MemoryConsumedCredentialStore implements ConsumedCredentialStore {
133
+ private readonly maxEntries;
134
+ private readonly data;
135
+ constructor(opts?: MemoryConsumedCredentialStoreOpts);
136
+ has(realm: string, challengeId: string): Promise<boolean>;
137
+ mark(realm: string, challengeId: string, ttlSeconds: number): Promise<void>;
138
+ }
139
+
140
+ /**
141
+ * Credit fields riding the signed request body (internal-review, spec §2).
142
+ *
143
+ * An explicit draw references the credit INSIDE the secp256k1/Schnorr-signed
144
+ * `body.data` — no new caller crypto. Wire field names (snake_case, matching
145
+ * the rest of the wire surface):
146
+ *
147
+ * - `credit_id` — the credit to draw against.
148
+ * - `draw_id` — client-generated draw id; the ledger's idempotency key.
149
+ * - `fund` — optional `{ amount_micro, commitment }` funding commitment:
150
+ * present iff a funding artifact (X-Cashu / X-PAYMENT / mpp credential)
151
+ * rides the same request. `commitment` is the SHA-256 hex over the raw
152
+ * artifact bytes, binding the header-borne proof — which sits OUTSIDE the
153
+ * signed body — to the credit its sender intended (spec §2 condition 3).
154
+ *
155
+ * These are envelope-level fields, not capability input: **the SDK** removes
156
+ * them before any builder-owned schema parses ({@link stripCreditEnvelope} in
157
+ * `JobManager.parseInput` and on the quote path; {@link
158
+ * creditEnvelopeIgnoreFields} feeding the auth verifier's own schema check), so
159
+ * handlers never see them and a top-level `.strict()` capability schema is
160
+ * fine (internal-review). Relying on Zod's default strip mode instead was the trap:
161
+ * `.strict()` rejects unknown keys, so an explicit draw 400'd at parse before
162
+ * the envelope was ever extracted.
163
+ *
164
+ * The strip never touches the canonical signing bytes. Every auth call site
165
+ * verifies the raw wire body (`signedRequestInput`, internal-review), so the credit
166
+ * fields are in the verified bytes because they were never taken out of them —
167
+ * the strip is only ever applied to what a schema is about to parse.
168
+ */
169
+ interface CreditEnvelope {
170
+ creditId: string;
171
+ drawId: string;
172
+ fund?: CreditFundCommitment;
173
+ }
174
+ /** The signed funding commitment for a fund-and-draw request. */
175
+ interface CreditFundCommitment {
176
+ /** Funding amount in 1e-6 units of the credit's currency. */
177
+ amountMicro: number;
178
+ /** SHA-256 hex over the raw funding artifact bytes. */
179
+ commitment: string;
180
+ }
181
+ /** Wire keys reserved for the credit envelope inside `body.data`. */
182
+ declare const CREDIT_ENVELOPE_KEYS: readonly ["credit_id", "draw_id", "fund"];
183
+ /**
184
+ * Thrown by {@link extractCreditEnvelope} on a structurally invalid credit
185
+ * envelope — half a pair, a `fund` block without credit fields, wrong types.
186
+ * Carries a caller-facing message; the route maps it to a 400.
187
+ */
188
+ declare class CreditEnvelopeError extends Error {
189
+ constructor(message: string);
190
+ }
191
+ /**
192
+ * Extract the credit envelope from the raw wire `body.data`. Returns
193
+ * `undefined` when no credit field is present (the implicit / legacy shape).
194
+ * Throws {@link CreditEnvelopeError} on partial or malformed fields — a
195
+ * request that names half a draw is a caller bug, not an implicit payment.
196
+ */
197
+ declare function extractCreditEnvelope(data: unknown): CreditEnvelope | undefined;
198
+ /**
199
+ * The reserved keys `schema` does not itself declare — the ones the SDK is
200
+ * free to remove before handing it an input (internal-review).
201
+ *
202
+ * A capability schema declares none of them, so all three are removable. The
203
+ * one exception this exists for is `/v1/credit`'s own `CREDIT_REQUEST_SCHEMA`,
204
+ * where `credit_id` and `fund` are real fields on a different wire shape and
205
+ * removing them would empty the request. It is not an escape hatch for a
206
+ * capability: the three names stay reserved on `body.data`, and declaring one
207
+ * doesn't win a builder their own value — `app.ts` runs
208
+ * {@link extractCreditEnvelope} over the raw body on every submit, so a
209
+ * capability that declares `fund` answers `invalid_credit_fields` instead.
210
+ */
211
+ declare function creditEnvelopeIgnoreFields(schema: unknown): readonly string[];
212
+ /**
213
+ * Remove the reserved credit keys from `data` before `schema` parses it, so a
214
+ * strict schema doesn't reject the protocol's own envelope (internal-review). Keys
215
+ * `schema` declares are left alone — see {@link creditEnvelopeIgnoreFields}.
216
+ *
217
+ * Returns `data` unchanged when it isn't a plain object or carries none of the
218
+ * removable keys, so the ordinary no-credit request allocates nothing. Never
219
+ * use the result to derive canonical signing bytes: those cover the wire form,
220
+ * credit fields included (`signedRequestInput`).
221
+ */
222
+ declare function stripCreditEnvelope(schema: unknown, data: unknown): unknown;
223
+
224
+ /**
225
+ * The public prepaid-credit terms a DVM advertises before a caller identifies
226
+ * itself. Snake_case: this is the wire shape.
227
+ *
228
+ * This type deliberately excludes the per-caller echo. `/v1/info` is public
229
+ * and cacheable, so its type must make a caller credit ID or balance
230
+ * unrepresentable rather than relying on the current handler not to ask for
231
+ * one.
232
+ */
233
+ interface CreditTerms {
234
+ /** Smallest funding accepted, in 1e-6 units of `currency`. */
235
+ min_micro: number;
236
+ /** Largest residual balance a caller may hold, in 1e-6 units of `currency`. */
237
+ max_micro: number;
238
+ /** Credit lifetime from the most recent funding, in ms. */
239
+ ttl_ms: number;
240
+ /** Currency the whole block and resulting credit are denominated in. */
241
+ currency: string;
242
+ /** Rails a top-up may be funded over. */
243
+ funding: FundingMethod[];
244
+ /** MPP instruments offered or temporarily withheld for credit funding. */
245
+ tempo?: {
246
+ methods: {
247
+ method: string;
248
+ intent: "charge" | "session";
249
+ }[];
250
+ withheld?: {
251
+ method: string;
252
+ intent: "charge" | "session";
253
+ reason: string;
254
+ }[];
255
+ };
256
+ /** x402 flavours accepted for credit funding. */
257
+ x402?: {
258
+ schemes: {
259
+ scheme: string;
260
+ network: string;
261
+ }[];
262
+ };
263
+ /** Smallest Lightning funding accepted, in 1e-6 units of `currency`. */
264
+ lightning_min_micro?: number;
265
+ }
266
+ /**
267
+ * The **funding menu** a DVM advertises on `/v1/quote` and in every 402
268
+ * (internal-review, credits spec §4/§6). Snake_case: this is the wire shape.
269
+ *
270
+ * `min_micro` / `max_micro` / `ttl_ms` are the DVM's sizing terms — `max_micro`
271
+ * binds the **residual balance**, not a funding amount, so a job priced above
272
+ * it still clears as fund-and-immediately-draw. `funding` is the rail list a
273
+ * top-up may arrive on — a {@link FundingMethod}, not a `PaymentMethod`, since
274
+ * `lightning` funds a credit without ever being an attached-proof wire method
275
+ * (internal-review). It degrades per spec §4: a rail whose backing wallet is
276
+ * unreachable drops out and the sale survives on the others.
277
+ *
278
+ * The `credit_id` / `balance_micro` / `remaining_micro` / `expiry_ms` echo
279
+ * appears only for an authenticated caller who already holds a live credit —
280
+ * so one round trip answers "what does this cost, and what do I have left?"
281
+ * (spec §7).
282
+ */
283
+ interface CreditMenu extends CreditTerms {
284
+ /**
285
+ * MPP instruments accepted for this credit, reusable sessions first when
286
+ * available.
287
+ *
288
+ * `withheld` names an instrument this DVM is configured for and is not
289
+ * offering at this instant, with the reason (internal-review). Two audiences read
290
+ * it. An agent gets to relay *why* `tempo/session` vanished instead of
291
+ * inferring the DVM never had it. And the platform's deploy probe reads
292
+ * capability from `methods ∪ withheld`, so a deploy landing during an
293
+ * observer blip no longer stamps `tempo_session_advertised_at` NULL and
294
+ * drops the DVM out of the close observer's payee fallback permanently.
295
+ * `methods` stays the list a caller acts on — never widen it to include a
296
+ * withheld instrument.
297
+ */
298
+ /**
299
+ * Which x402 flavours a top-up may arrive on, and the chain each rides
300
+ * (internal-review). Present exactly when `funding` lists `x402`. Reusable
301
+ * `batch-settlement` is admitted by default; one-payment `exact` appears
302
+ * only when the builder explicitly accepts its manual refund obligation.
303
+ */
304
+ /**
305
+ * Smallest funding the `lightning` rail accepts, in 1e-6 units of
306
+ * `currency`. Present only when `funding` lists `lightning`. This is the
307
+ * deployment's sats-physical receive floor (internal-review) rendered into fiat
308
+ * with a ×{@link LIGHTNING_FLOOR_FX_MARGIN} margin absorbing rate drift
309
+ * between menu fetch and funding — issuance enforces the *raw* floor at
310
+ * request-time rates, so funding exactly this figure always clears. Rails
311
+ * without an entry (cashu) have no floor beyond `min_micro`.
312
+ */
313
+ /** The caller's credit this echo describes. Present only with the echo. */
314
+ credit_id?: string;
315
+ /** Funded balance, pending holds NOT subtracted. */
316
+ balance_micro?: number;
317
+ /** Spendable balance — what a draw is actually checked against. */
318
+ remaining_micro?: number;
319
+ /** Absolute expiry of the echoed credit, ms since epoch. */
320
+ expiry_ms?: number;
321
+ }
322
+ /** Everything {@link buildCreditMenu} needs, assembled per request. */
323
+ interface BuildCreditMenuArgs {
324
+ /**
325
+ * Which runtime is answering. `"isolate"` is refused unconditionally — see
326
+ * the gate in {@link buildCreditMenu}.
327
+ */
328
+ runtime: "container" | "isolate";
329
+ /** The DVM's resolved `credit` block; `undefined` means it didn't opt in. */
330
+ config: ResolvedCreditConfig | undefined;
331
+ /**
332
+ * Currency the menu — and the credit it funds — denominates in: the DVM's
333
+ * declared `DVMConfig.currency`, which every call site reads off the single
334
+ * `DVMServer.creditCurrency()` (internal-review). Deliberately *not* the currency of
335
+ * the response this menu rides on: a quote priced elsewhere is refused before
336
+ * it reaches here, so the two agree, and sourcing it from the response is
337
+ * what left `POST /v1/credit` — which has no response to read — funding in
338
+ * hardwired USD at a DVM whose jobs draw in something else.
339
+ */
340
+ currency: string;
341
+ /** Rails this DVM settles over — the menu's funding options. */
342
+ funding: FundingMethod[];
343
+ /** Tempo methods wired for credit funding; builder policy filters the actionable menu. */
344
+ tempoMethods?: {
345
+ method: string;
346
+ intent: "charge" | "session";
347
+ }[];
348
+ /** Tempo methods this DVM is configured for but is not offering right now. */
349
+ tempoWithheld?: {
350
+ method: string;
351
+ intent: "charge" | "session";
352
+ reason: string;
353
+ }[];
354
+ /**
355
+ * x402 flavours this DVM can actually accept a top-up on (internal-review) — one
356
+ * server read, so what the sub-block advertises is what `/v1/credit` will
357
+ * answer a 402 with.
358
+ */
359
+ x402Schemes?: {
360
+ scheme: string;
361
+ network: string;
362
+ }[];
363
+ /**
364
+ * The deployment's Lightning receive floor in sats, when `lightning` is on
365
+ * the menu — rendered into `lightning_min_micro`. An fx failure during that
366
+ * rendering drops `lightning` from `funding` rather than suppressing the
367
+ * menu: a rail whose minimum we can't state honestly is a rail we must not
368
+ * advertise, but the other rails' sale survives.
369
+ */
370
+ lightningFundingMinSats?: number;
371
+ /**
372
+ * Whether this DVM has at least one accepted Cashu mint configured
373
+ * (internal-review). Without one, no Bitcoin funding rail may be advertised — see
374
+ * the filter in {@link buildCreditMenu}.
375
+ *
376
+ * Read off the **configured** mint list, never the health-filtered
377
+ * advertised one: a mint blip must not make a funding rail appear and
378
+ * disappear between quotes.
379
+ */
380
+ hasAcceptedMint?: boolean;
381
+ fxFetcher: FxFetcher;
382
+ /** True when the descriptor declares an auth scheme (see the gate). */
383
+ hasAuth: boolean;
384
+ ledger?: CreditLedgerLike;
385
+ /** devMode tolerates a non-durable ledger; production does not. */
386
+ devMode?: boolean;
387
+ /** Verified caller pubkey — required for the balance echo, never a fallback id. */
388
+ callerPubkey?: string;
389
+ }
390
+ /**
391
+ * Assemble the funding menu, or return `undefined` when this DVM must not
392
+ * advertise credit. **The refusal ladder is the feature**; each rung exists
393
+ * because advertising anyway would misadvertise:
394
+ *
395
+ * 1. **Isolate runtime** (credits spec §3) — checked first and unconditionally,
396
+ * before config is even read. Isolate DVMs have no Postgres of their own;
397
+ * their only DVM-side persistence is the last-write-wins callback KV, which
398
+ * spec §2 condition 2 prohibits for credit state. Until [internal-review] resolves
399
+ * the gate they run implicit N=1 only, and their quote simply never offers
400
+ * credit so nothing is misadvertised.
401
+ * 2. **Not opted in** — no `credit` block on `configureDVM`.
402
+ * 3. **No descriptor auth** — a credit belongs to a verified secp256k1 pubkey.
403
+ * Without auth the ledger's funder identity degrades to the requester id or
404
+ * `"anonymous"` (see `resolveLedgerContext`), which would pool every
405
+ * anonymous caller's money into one balance. Advertising a per-caller
406
+ * balance on such a DVM would be a lie at best and a disclosure at worst.
407
+ * 4. **Non-durable ledger outside devMode** — a `MemoryCreditLedger` is
408
+ * per-process, so a caller funding on machine A and drawing on machine B
409
+ * gets `credit_not_found`. Advertise only what survives the fleet.
410
+ * 5. **Bounds not expressible in `currency`** — an fx outage, or a currency the
411
+ * snapshot doesn't carry. Omit rather than quote a number we can't state.
412
+ *
413
+ * The balance echo is added only when a verified `callerPubkey` holds a live,
414
+ * same-currency credit.
415
+ *
416
+ * Rungs 1–4 live in {@link creditMenuConfigured}, which the capability refresh
417
+ * reads too.
418
+ */
419
+ declare function buildCreditMenu(args: BuildCreditMenuArgs): Promise<CreditMenu | undefined>;
420
+ /**
421
+ * The one credit a caller's quote echoes, from the several they may hold —
422
+ * every implicit N=1 payment mints its own `imp:<rail>:<paymentId>` credit, so
423
+ * this list grows with each paid job.
424
+ *
425
+ * Live, same-currency credits with something left in them; largest spendable
426
+ * balance wins, newest breaks a tie. That is the credit a caller would in fact
427
+ * draw against next. A failed job's released hold leaves a positive implicit
428
+ * balance, which is real caller money (spec §1, no debit on failure) and is
429
+ * echoed like any other.
430
+ *
431
+ * **Requires a verified pubkey.** Never call this with a requester id or
432
+ * `"anonymous"`: on an auth-less DVM the ledger pools every anonymous caller
433
+ * under one `caller_pubkey`, so echoing it would show one caller another's
434
+ * balance.
435
+ */
436
+ declare function selectPrimaryCredit(args: {
437
+ ledger: CreditLedgerLike;
438
+ callerPubkey?: string;
439
+ currency: string;
440
+ nowMs?: number;
441
+ }): Promise<CreditSnapshot | undefined>;
442
+ /** Narrow a ledger snapshot to the read-only shape a quote handler receives. */
443
+ declare function toCreditView(snapshot: CreditSnapshot): CreditView;
444
+ /**
445
+ * Attach a menu to a response body when there is one. Keeps the "omit the key
446
+ * entirely rather than emit `credit: null`" rule in one place — every consumer
447
+ * checks presence, and the isolate tier depends on the key never appearing.
448
+ */
449
+ declare function attachCreditMenu(body: Record<string, unknown>, menu: CreditMenu | undefined): Record<string, unknown>;
450
+
451
+ /**
452
+ * Canonical deploy-time attestation payload (internal-review). One signature serves
453
+ * both auth-on-deploy and the `/v1/info#builder` attestation — the bytes the
454
+ * builder signs are the canonical JSON of this object. Making a field required
455
+ * also requires the coordinated fleet-before-caller release note in
456
+ * `public compatibility guide`.
457
+ */
458
+ interface AttestationPayload {
459
+ /** Immutable platform or self-hosted DVM identifier this attestation applies to. */
460
+ dvm_id: string;
461
+ /** DVM slug the attestation applies to (must match the deploy request's slug). */
462
+ slug: string;
463
+ /** sha256(`canonicaliseForSigning(capabilities)`) hex. Binds the attestation
464
+ * to the capability shape declared at deploy time. */
465
+ capabilities_hash: string;
466
+ /** Builder identity x-only secp256k1 pubkey, hex (32 bytes / 64 chars). */
467
+ builder_pubkey: string;
468
+ /**
469
+ * Per-DVM receipt-signing x-only pubkey, hex (internal-review). Binds the key the
470
+ * deployed DVM signs job receipts with to the cold builder identity: the
471
+ * builder signs this payload, so a receipt verifying under
472
+ * `receipt_pubkey` inherits the builder's authority.
473
+ *
474
+ * Optional: attestations signed before receipts shipped omit it and still
475
+ * verify byte-for-byte (canonical JSON drops absent keys), and deploy paths
476
+ * that can't provision the matching secret leave it unset rather than
477
+ * attesting a key the running process doesn't hold.
478
+ */
479
+ receipt_pubkey?: string;
480
+ /** Unix seconds at which the attestation was signed. */
481
+ deployed_at: number;
482
+ }
483
+
484
+ /** A Lightning invoice created by the backend. */
485
+ interface CreatedInvoice {
486
+ /** BOLT11 payment request string. */
487
+ bolt11: string;
488
+ /** Payment hash (hex) — primary key for looking up settlement. */
489
+ paymentHash: string;
490
+ /** Amount in millisatoshis. */
491
+ amountMsats: number;
492
+ /** Unix milliseconds when the invoice expires. */
493
+ expiresAt: number;
494
+ }
495
+ /** Result of looking up an invoice's settlement status. */
496
+ interface InvoiceStatus {
497
+ /** True once the invoice has been paid and preimage is known. */
498
+ settled: boolean;
499
+ /** Preimage (hex) — present only after settlement. */
500
+ preimage?: string;
501
+ /** Unix milliseconds when the invoice settled (if settled). */
502
+ settledAt?: number;
503
+ /** Unix milliseconds when the invoice expires. */
504
+ expiresAt?: number;
505
+ }
506
+ /** How a payment reached a settled state — see `payInvoiceReconciling`. */
507
+ type LightningPayOutcome = "paid" | "reconciled" | "republished";
508
+ /** Settled payment plus the audit trail of how it got there. */
509
+ interface LightningPayment {
510
+ /** Payment preimage (hex) — proof the invoice was paid. */
511
+ preimage: string;
512
+ /** Routing fee reported by the wallet, in millisatoshis. Absent means unknown. */
513
+ feesPaidMsats?: number;
514
+ /** Whether the wallet answered directly, or the outcome had to be reconciled. */
515
+ outcome: LightningPayOutcome;
516
+ }
517
+ /** What a backend can tell us about the wallet behind it. */
518
+ interface LightningWalletInfo {
519
+ /** Human-readable wallet name (e.g. "Alby Hub"). */
520
+ alias?: string;
521
+ /** Operations the wallet supports (e.g. `pay_invoice`, `lookup_invoice`). */
522
+ methods: string[];
523
+ /**
524
+ * Budget fields the wallet volunteered, if any.
525
+ *
526
+ * No rail behind this interface is required to expose a connection's
527
+ * spending cap, and NWC — today's only implementation — has no method that
528
+ * returns one. Present it when a wallet offers it, never infer it.
529
+ */
530
+ walletReportedBudget?: Record<string, unknown>;
531
+ }
532
+ /** Abstraction over Lightning wallet backends (NWC, future). */
533
+ interface LightningBackend {
534
+ /**
535
+ * Pay a bolt11 invoice exactly once. Returns the preimage on success.
536
+ *
537
+ * Implementations must never re-send a payment on an unknown outcome without
538
+ * first reconciling against the wallet — the rails behind this interface have
539
+ * no idempotency key, so a blind retry is a second payment.
540
+ *
541
+ * `amountMsats` is passed when known (callers always have it from the
542
+ * provider's payment-request). Backends that don't need it may ignore the
543
+ * param.
544
+ */
545
+ payInvoice(bolt11: string, amountMsats?: number): Promise<LightningPayment>;
546
+ /** Get wallet balance in millisatoshis. */
547
+ getBalance(): Promise<number>;
548
+ /** Get wallet info (name, supported methods, any volunteered budget fields). */
549
+ getInfo(): Promise<LightningWalletInfo>;
550
+ /** Create a new incoming invoice for the given amount and description. */
551
+ createInvoice(params: {
552
+ amountMsats: number;
553
+ description?: string;
554
+ expirySeconds?: number;
555
+ }): Promise<CreatedInvoice>;
556
+ /** Look up an invoice's settlement status by payment hash. */
557
+ lookupInvoice(paymentHash: string): Promise<InvoiceStatus>;
558
+ }
559
+
560
+ /** Cached liveness signal for a platform-hosted DVM's Lightning receive rail. */
561
+ interface LightningRailHealth {
562
+ /** Whether the receive rail may be advertised at this instant. */
563
+ available(): boolean;
564
+ /** Prime the cached signal at boot when the source supports it. */
565
+ refresh?(): Promise<void>;
566
+ }
567
+
568
+ /** Builder-facing wiring for the Lightning receive leg (internal-review). */
569
+ interface LightningReceiveConfig {
570
+ /**
571
+ * Receive-only NWC connection URI (`DVMKIT_NWC_RECEIVE_URI`). Validated at
572
+ * boot: a connection that can *spend* is refused outright.
573
+ */
574
+ uri?: string;
575
+ /**
576
+ * Pre-built backend, bypassing URI parsing and the connect-time probe. Test
577
+ * seam only — production always goes through `uri` so the receive-only
578
+ * property is actually checked.
579
+ */
580
+ backend?: LightningBackend;
581
+ /** Invoice lifetime in seconds. Defaults to {@link DEFAULT_INVOICE_TTL_SECONDS}. */
582
+ invoiceTtlSeconds?: number;
583
+ /**
584
+ * Smallest funding this deployment can receive over Lightning, in sats
585
+ * (`DVMKIT_LIGHTNING_FUNDING_MIN_SATS`). The floor is a property of the
586
+ * receive wallet's channel policy (`htlc_minimum_msat` upstream), so it is
587
+ * per-deployment and sats-physical — the menu renders it into fiat with a
588
+ * margin, but everything that *enforces* it compares raw sats. Defaults to
589
+ * {@link DEFAULT_LIGHTNING_FUNDING_MIN_SATS}.
590
+ */
591
+ fundingMinSats?: number;
592
+ /**
593
+ * Optional platform-derived channel-liveness gate. Platform-hosted DVMs use
594
+ * it alongside their NWC reachability probe; self-hosted DVMs omit it.
595
+ */
596
+ railHealth?: LightningRailHealth;
597
+ }
598
+ /** Default bolt11 lifetime — long enough to pay by hand, short enough to retire. */
599
+ declare const DEFAULT_INVOICE_TTL_SECONDS = 900;
600
+ /**
601
+ * Floor on the invoice lifetime: twice the signed-request drift window, so the
602
+ * bolt11 always outlives the funding negotiation that produced it (the issue's
603
+ * "invoice expiry ≥ the funding-negotiation window"). A shorter one would
604
+ * expire inside the caller's own retry budget and strand them mid-top-up.
605
+ */
606
+ declare const MIN_INVOICE_TTL_SECONDS = 600;
607
+ /**
608
+ * The builder-side Lightning receive leg (internal-review, credits spec §4).
609
+ *
610
+ * Issues a bolt11 over a **receive-only** NWC connection while the DVM is
611
+ * awake serving the 402, and credits the ledger when a later request observes
612
+ * settlement. Two properties are load-bearing:
613
+ *
614
+ * - **It never holds a send credential.** `pay_invoice` on this connection is
615
+ * refused at construction, so a compromised DVM can mint invoices and
616
+ * nothing else. The drain/refund sender (internal-review) is a separate, budgeted
617
+ * connection by rule.
618
+ * - **Crediting is pull-based — there is no settlement watcher.** The caller's
619
+ * next request drives `lookup_invoice`, which is what makes this work on a
620
+ * suspend-to-zero fleet: a machine that is asleep has nothing to miss.
621
+ * Wallet downtime at that moment delays crediting; it never loses money,
622
+ * because the invoice→credit binding is a durable row.
623
+ *
624
+ * Reachability is **cached**, never probed per request: the funding menu is
625
+ * assembled on every quote and every 402, so a live NIP-47 round trip there
626
+ * would put a nostr relay in the latency path of every priced call.
627
+ */
628
+ declare class LightningReceive {
629
+ private readonly source;
630
+ readonly invoiceTtlSeconds: number;
631
+ readonly fundingMinSats: number;
632
+ private health;
633
+ private checkedAtMs;
634
+ private refreshing;
635
+ private permissionRefusal;
636
+ private constructor();
637
+ private readonly railHealth;
638
+ /**
639
+ * Validate the configured connection and build the receive leg, or return
640
+ * `undefined` when this DVM didn't configure one.
641
+ *
642
+ * **Throws on a credential that is wrong, degrades on one that is merely
643
+ * unreachable.** A wallet outage at boot must not stop a DVM whose other
644
+ * rails are fine — the menu simply omits `lightning` until a later probe
645
+ * succeeds. A connection that can spend, or one that can't state what it can
646
+ * do, is a different thing entirely: it is a standing money risk that no
647
+ * amount of retrying fixes, so it fails the boot loudly with the fix in the
648
+ * message.
649
+ */
650
+ static create(config: LightningReceiveConfig): Promise<LightningReceive | undefined>;
651
+ /**
652
+ * Whether `lightning` belongs on the funding menu right now.
653
+ *
654
+ * Reads the cached observation and never blocks; a stale one kicks off a
655
+ * background refresh and answers with what we last knew. Advertising a rail
656
+ * whose wallet is down would hand the caller an option that 402s on use —
657
+ * the §4 posture is to drop it and let the sale survive on the others.
658
+ */
659
+ available(): boolean;
660
+ /**
661
+ * Issue (or re-issue) the bolt11 funding `(creditId, fundId)`.
662
+ *
663
+ * The invoice is minted **for** the credit and amount named in the caller's
664
+ * signed body — that binding is the internal-review condition-3 commitment on this
665
+ * rail, and it is why no artifact hash rides the request: there is no
666
+ * caller-supplied artifact to hash. A re-poll returns the stored row rather
667
+ * than minting again; a second bolt11 for one `fund_id` would leave two
668
+ * payable invoices against a funding that can only be credited once.
669
+ */
670
+ issue(ledger: CreditLedgerLike, args: {
671
+ creditId: string;
672
+ fundId: string;
673
+ callerPubkey: string;
674
+ currency: string;
675
+ amountMicro: number;
676
+ amountMsats: number;
677
+ description?: string;
678
+ }): Promise<CreditInvoiceRecord>;
679
+ /**
680
+ * Ask the wallet whether one payment hash is paid, and when (internal-review).
681
+ *
682
+ * The operator's reconcile verb runs this before it credits anything. A
683
+ * `blocked` row implies payment — {@link applyOne} returns above the block
684
+ * classification when the lookup says unpaid — but that is *this fleet's
685
+ * belief, recorded possibly weeks ago*, and the verb it gates mints balance
686
+ * against it. One round trip turns the belief into a fact and recovers the
687
+ * true settlement instant for `settled_at`, which a blocked row never got to
688
+ * write.
689
+ *
690
+ * Unlike {@link settlePending} this **throws**: there is no request whose
691
+ * latency it protects, and crediting on an unverifiable wallet is exactly
692
+ * what it exists to prevent. `NOT_FOUND` is the one exception, and it is not
693
+ * an outage — it is the wallet saying it has never seen this hash, the same
694
+ * reading {@link applyOne} and the caller-side reconcile in `src/lib/nwc.ts`
695
+ * take. It comes back as `known: false` because the operator's repair for it
696
+ * ("you are pointed at a different wallet than the one that minted this")
697
+ * is not the repair for an unpaid invoice, and certainly not for a retry.
698
+ */
699
+ lookupSettlement(paymentHash: string): Promise<{
700
+ settled: boolean;
701
+ settledAt?: number;
702
+ known: boolean;
703
+ }>;
704
+ /**
705
+ * Consult the wallet about this caller's outstanding invoices and credit the
706
+ * ones that settled — the pull half of pull-based crediting.
707
+ *
708
+ * The pending rows are read locally first, so the common case (nothing
709
+ * outstanding) costs one indexed SELECT and no network at all. Bounded to
710
+ * {@link MAX_SETTLE_CHECKS} invoices on a short deadline, and **never throws
711
+ * into the request**: a wallet failure here means the caller's balance is
712
+ * merely not updated yet, which the ordinary `insufficient_credit` path
713
+ * already states honestly.
714
+ *
715
+ * **Each invoice gets its own failure boundary.** The sweep window is a few
716
+ * rows wide, so a row that fails on its own terms — a `fund_id` the caller
717
+ * reused on another rail, a credit someone else opened first — must not take
718
+ * the rest of the pass down with it: the next invoice in the window may be
719
+ * the paid one, and it would never be looked up. Only a transport failure
720
+ * ends the pass early, because there the wallet itself is gone and the
721
+ * remaining lookups would just spend the caller's latency confirming it.
722
+ */
723
+ settlePending(ledger: CreditLedgerLike, args: {
724
+ callerPubkey: string;
725
+ creditId?: string;
726
+ creditTtlMs: number;
727
+ dvmId?: string;
728
+ enqueueCreditDeposit?: CreditDepositEnqueue;
729
+ nowMs?: number;
730
+ }): Promise<InvoiceSettlement[]>;
731
+ /**
732
+ * One invoice's settlement check. Returns the applied settlement, or
733
+ * `undefined` when nothing changed (still unpaid, or retired unpaid).
734
+ *
735
+ * Throws whatever the wallet or the ledger threw — classifying that is
736
+ * {@link retireOrLog}'s job, so this stays one invoice's happy path.
737
+ */
738
+ private applyOne;
739
+ /**
740
+ * Decide what one invoice's failure means, and retire the invoice when the
741
+ * answer is "this can never succeed".
742
+ *
743
+ * The split that matters is permanent-versus-transient, because it decides
744
+ * whether the row keeps a slot in the sweep window. A transient failure — the
745
+ * database blinked, the wallet answered oddly — leaves it `pending` and the
746
+ * caller's next request retries it. A **permanent** one means the ledger will
747
+ * refuse this invoice identically forever, so leaving it pending costs a
748
+ * `lookup_invoice` on every subsequent request and, at
749
+ * {@link MAX_SETTLE_CHECKS} of them, fills the window so a genuinely payable
750
+ * invoice behind them is never even looked up. Those get `blocked`, which
751
+ * drops them out of the sweep and hands the operator the payment hash.
752
+ */
753
+ private retireOrLog;
754
+ /**
755
+ * The reason this invoice can never be credited, or `undefined` if the
756
+ * failure was transient.
757
+ *
758
+ * Every code here is decided by state a retry cannot move — a row the ledger
759
+ * has already committed (a `credit_fundings` entry at this
760
+ * `(credit_id, fund_id)`, or a `credits` row whose owner, denomination or
761
+ * rail disagrees with the invoice), or the basis this module rebuilds
762
+ * identically from the invoice row on every pass. None of them is a race.
763
+ *
764
+ * `funding_replayed` is the one that needs a second read to classify.
765
+ * Usually it *is* benign — a concurrent check won the race and the money is
766
+ * credited either way — but the key is `(credit_id, fund_id)` and `fund_id`
767
+ * is **caller-chosen**: reuse it on cashu or x402 after this bolt11 was
768
+ * minted and the row that collided is a different payment entirely, so the
769
+ * sats this invoice received have nowhere to land.
770
+ */
771
+ private blockingReason;
772
+ private withBackend;
773
+ /** Record activity without letting it promote an unvalidated connection. */
774
+ private noteOperationOk;
775
+ private notePermissionsOk;
776
+ private notePermissionRefused;
777
+ /**
778
+ * Only a *transport* failure from an ordinary invoice operation is a health
779
+ * signal. Permission-probe refusals are classified by {@link refresh}.
780
+ */
781
+ private noteFailure;
782
+ private refresh;
783
+ }
784
+
785
+ /** Tunables for `checkMintHealth`. */
786
+ interface CheckMintHealthOptions {
787
+ /** Per-attempt timeout in ms. Default: 5000. */
788
+ timeoutMs?: number;
789
+ /** Total attempts including the first. Default: 3. */
790
+ retries?: number;
791
+ /** Inter-attempt backoff schedule in ms. Default: [200, 500, 1000]. */
792
+ backoffMs?: number[];
793
+ /** Injected `fetch` for tests. Default: global fetch. */
794
+ fetchImpl?: typeof fetch;
795
+ }
796
+
797
+ /**
798
+ * Per-DVM runtime mint-health tracker (internal-review). Alongside the platform's
799
+ * durable mint-health cron, probes every configured
800
+ * mint on a timer, tracks consecutive failures, and flips a mint to `sick`
801
+ * once the threshold is crossed. The SDK uses the resulting view to:
802
+ *
803
+ * 1. Filter `/v1/info`'s advertised `mints` to currently-healthy ones.
804
+ * 2. Reject new accumulator receives at sick mints before they touch the
805
+ * DB (`cashu_mint_sick` 503 from `verifyAccumulatorReceipt`).
806
+ *
807
+ * Since internal-review this tracker is the sole owner of mint health: boot no longer
808
+ * runs a fail-fast check before binding the port. To preserve what boot used
809
+ * to guarantee it (a) treats a mint as healthy only when it's reachable AND
810
+ * NUT-compliant (reproducing `assertNutSupport` via `nutCompliant`), and (b)
811
+ * exposes `initialProbeComplete()` so the paid path holds calls at
812
+ * `mint_health_pending` until the first probe validates ≥1 mint.
813
+ *
814
+ * Existing accumulator entries are not touched — they're P2PK-locked to the
815
+ * original mint and stay put until `dvmctl melt-pending` drains them. The
816
+ * accumulator monitor's `wallet_accumulator_batch_expiring_soon` already
817
+ * pages the builder when a stuck batch nears `t_expire`.
818
+ *
819
+ * In-memory only — no DB persistence. The platform cron keeps the durable
820
+ * record + email path (internal-review); the SDK only needs fresh in-process state
821
+ * to make routing decisions.
822
+ */
823
+
824
+ /** Per-mint runtime view exposed by `snapshot()`. */
825
+ interface MintHealthSnapshot {
826
+ /** Last-observed wall-clock latency of the probe in ms. */
827
+ latencyMs?: number;
828
+ /** Consecutive failures up to the most recent tick. Reset on any success. */
829
+ consecutiveFailures: number;
830
+ /**
831
+ * True once the mint is considered unhealthy for routing: either
832
+ * `consecutiveFailures` crossed the threshold, or the last probe reached the
833
+ * mint but it fails the NUT gate (a deterministic, permanent config error
834
+ * that flips `sick` immediately — internal-review).
835
+ */
836
+ sick: boolean;
837
+ /**
838
+ * True when the last successful probe reached the mint but it fails the NUT
839
+ * capability gate ({@link assertNutSupport}'s required NUTs + bolt11/sat).
840
+ * Distinct from a plain outage: NUT-incompatibility is deterministic and
841
+ * never resolves without a config/mint change.
842
+ */
843
+ nutIncompatible?: boolean;
844
+ /** Mint version reported by the last successful probe. */
845
+ version?: string;
846
+ /** Error category from the most recent failed probe. */
847
+ errorCode?: string;
848
+ }
849
+ /** Tunables for {@link MintHealthTracker}. */
850
+ interface MintHealthTrackerOptions {
851
+ /** Cashu mint URLs the DVM accepts. The tracker probes every entry. */
852
+ mints: string[];
853
+ /**
854
+ * Tick cadence in ms. Defaults to {@link DEFAULT_TICK_INTERVAL_MS} or the
855
+ * `DVMKIT_MINT_HEALTH_TICK_MS` env var when set.
856
+ */
857
+ tickIntervalMs?: number;
858
+ /**
859
+ * Consecutive failed probes before flipping a mint to `sick`. Defaults to
860
+ * {@link DEFAULT_FAILURES_TO_SICK} or `DVMKIT_MINT_FAILURES_TO_SICK` env.
861
+ */
862
+ consecutiveFailuresToSick?: number;
863
+ /** Injected `fetch` for tests. */
864
+ fetchImpl?: typeof fetch;
865
+ /** Injected `Date.now` for tests. */
866
+ now?: () => number;
867
+ /**
868
+ * Per-probe timeout / retry / backoff, forwarded to {@link checkMintHealth}.
869
+ * Defaults to that function's own defaults — 3 attempts with `[200, 500]`
870
+ * backoff, which is what a real tick should do.
871
+ *
872
+ * Tests that drive several failing ticks set `backoffMs: [0]`: a failing
873
+ * probe otherwise costs 700ms of real `setTimeout`, and four of them put a
874
+ * unit test within a loaded CI runner's reach of the 5s default timeout.
875
+ * `fetchImpl` is set on the tracker, not here.
876
+ */
877
+ probe?: Omit<CheckMintHealthOptions, "fetchImpl">;
878
+ }
879
+ /**
880
+ * Runtime mint-health tracker. Owns its own timer; `start()` runs an initial
881
+ * probe immediately so the first received call doesn't see a stale snapshot.
882
+ */
883
+ declare class MintHealthTracker {
884
+ private readonly mints;
885
+ private readonly tickIntervalMs;
886
+ private readonly failuresToSick;
887
+ private readonly fetchImpl?;
888
+ private readonly probeOpts;
889
+ private readonly nowFn;
890
+ private readonly state;
891
+ private timer?;
892
+ private stopped;
893
+ private inFlight?;
894
+ private firstProbeComplete;
895
+ constructor(opts: MintHealthTrackerOptions);
896
+ /** Kick off the periodic probe. Runs one tick immediately. */
897
+ start(): void;
898
+ /** Stop the timer. Safe to call multiple times. */
899
+ stop(): void;
900
+ /**
901
+ * Probe every configured mint once. Sequential per mint to bound concurrent
902
+ * outbound load; the tick cadence makes parallelism unnecessary.
903
+ */
904
+ runOnce(): Promise<void>;
905
+ /**
906
+ * True once the first full probe tick has landed. Before this, paid calls
907
+ * are held at `503 mint_health_pending` rather than accepted against
908
+ * unvalidated mints (internal-review).
909
+ */
910
+ initialProbeComplete(): boolean;
911
+ /**
912
+ * True when the mint is either healthy or unknown to the tracker. Unknown
913
+ * mints (e.g. a value coming in via an externally-supplied tracker) get the
914
+ * benefit of the doubt — the receive path's mint-allowlist check is the
915
+ * authoritative gate on which mints the DVM accepts.
916
+ */
917
+ isHealthy(mintUrl: string): boolean;
918
+ /** Currently-healthy subset of the configured mints, preserving config order. */
919
+ healthyMints(): string[];
920
+ /**
921
+ * True when the last probe reached the mint but it failed the NUT capability
922
+ * gate — a deterministic, permanent config error, unlike a transient outage.
923
+ * Unknown mints return false. `advertisedMints()` uses this to keep a
924
+ * permanently-unsettleable mint off `/v1/info` even in the all-sick fallback,
925
+ * where a merely-flapping mint is still published (internal-review).
926
+ */
927
+ isNutIncompatible(mintUrl: string): boolean;
928
+ /** Plain-object snapshot keyed by mint URL — for tests + observability. */
929
+ snapshot(): Record<string, MintHealthSnapshot>;
930
+ private applyResult;
931
+ /**
932
+ * One-shot structured summary emitted after the first probe tick. Replaces
933
+ * the boot check's per-mint `cashu_mint_startup_health` lines and surfaces an
934
+ * all-mints-unhealthy warning — the operator alert that the Fly restart-loop
935
+ * used to be before boot stopped refusing to start (internal-review).
936
+ */
937
+ private logBootHealth;
938
+ }
939
+
940
+ /**
941
+ * Owner display identity surfaced on `/v1/info#owner` (internal-review). Personal orgs
942
+ * resolve to the owner builder's profile; shared orgs resolve to the org's own
943
+ * profile. Container DVMs read from env vars; isolate DVMs resolve from Postgres.
944
+ */
945
+ interface OwnerDisplay {
946
+ handle: string;
947
+ displayName?: string | null;
948
+ avatarUrl?: string | null;
949
+ type: "builder" | "org";
950
+ }
951
+ /** Optional builder identity surfaced on `/v1/info` (forward-compatible stub). */
952
+ interface BuilderIdentity {
953
+ id?: string;
954
+ name?: string;
955
+ url?: string;
956
+ /**
957
+ * Builder identity x-only secp256k1 pubkey (internal-review). When populated, the
958
+ * `/v1/info#builder` block also carries `attestation` + `signature` so
959
+ * consumers can verify the deploy was signed by the holder of this key.
960
+ * Populated at deploy time from `DVMKIT_BUILDER_PUBKEY` (Fly secret); the
961
+ * SDK never re-signs at runtime.
962
+ */
963
+ pubkey?: string;
964
+ /** Canonical deploy-time attestation payload (internal-review). Served verbatim. */
965
+ attestation?: AttestationPayload;
966
+ /** Schnorr signature over `canonicaliseForSigning(attestation)` (internal-review). */
967
+ signature?: string;
968
+ }
969
+ /** Platform reporter overrides — the host falls back to env when omitted. */
970
+ interface PlatformReporterOpts {
971
+ /** Bearer token. Defaults to `DVMKIT_PLATFORM_TOKEN` env. */
972
+ token?: string;
973
+ /** Platform internal URL. Defaults to `DVMKIT_PLATFORM_URL` env. */
974
+ url?: string;
975
+ }
976
+ /** Options for {@link createDVMHost}. */
977
+ interface DVMHostOpts {
978
+ /**
979
+ * Postgres connection string. Defaults to `DATABASE_URL` env.
980
+ * Required at runtime (internal-review). Boot fails when missing unless `jobStore`
981
+ * is explicitly supplied or `devMode` is true (test/dev escape hatches).
982
+ */
983
+ database?: string;
984
+ /** Listen port. Defaults to `PORT` env or 8080. */
985
+ port?: number;
986
+ /** Environment variables. Defaults to `process.env`. */
987
+ env?: Record<string, string>;
988
+ /** Builder identity for `/v1/info` (forward-compatible). */
989
+ builder?: BuilderIdentity;
990
+ /** Owner display identity for `/v1/info#owner` (internal-review). Falls back to env vars. */
991
+ owner?: OwnerDisplay;
992
+ /** Platform reporter overrides. Defaults to env-derived values. */
993
+ platformReporter?: PlatformReporterOpts;
994
+ /** Override the SDK's fx fetcher. Defaults to env-configured CoinGecko. */
995
+ fx?: FxFetcher;
996
+ /**
997
+ * Custom `/health` handler. When set, the SDK installs this instead of the
998
+ * default `{ status: "ok" }` responder — useful for runtime-specific health
999
+ * (pool depth, draining state, 503 while draining).
1000
+ */
1001
+ healthHandler?: (c: Context) => Response | Promise<Response>;
1002
+ /** KV store override. Defaults to PostgresKVStore (or MemoryKVStore in dev). */
1003
+ store?: KVStore;
1004
+ /** JobStore override. Takes precedence over `database`. */
1005
+ jobStore?: JobStore;
1006
+ /**
1007
+ * Existing Postgres pool to reuse instead of opening one from `database`
1008
+ * (internal-review test seam). When set, the host builds its JobStore / KVStore /
1009
+ * cashu accumulator on this pool and leaves it open at shutdown — the caller
1010
+ * owns it. Lets the e2e harness run DVMs in `p2pk-accumulator` mode against
1011
+ * its single shared pool rather than spawning a pool per DVM.
1012
+ */
1013
+ pool?: Pool;
1014
+ /** Cashu mints accepted. Defaults to env `DVMKIT_CASHU_MINTS`. */
1015
+ mints?: string[];
1016
+ /** Cashu receive mode override. */
1017
+ cashuMode?: CashuMode;
1018
+ /** MPP handle override. Defaults to env-resolved via `createMppFromOpts`. */
1019
+ mpp?: MppxServer;
1020
+ /** x402 stablecoin payment configuration. Defaults to env-resolved. */
1021
+ x402?: X402Config;
1022
+ /** Payment methods override. Defaults to derived from configured rails. */
1023
+ paymentMethods?: PaymentMethod[];
1024
+ /**
1025
+ * Lightning receive leg for credit funding (internal-review). Defaults to the
1026
+ * env-resolved `DVMKIT_NWC_RECEIVE_URI` /
1027
+ * `DVMKIT_NWC_RECEIVE_INVOICE_TTL_SECONDS`. Pass `backend` to inject a
1028
+ * wallet directly — a test seam that skips the connect-time probe, so
1029
+ * production must always come through the URI.
1030
+ */
1031
+ lightningReceive?: LightningReceiveConfig;
1032
+ /** When true, payment is skipped if no mints are configured (dev/test). */
1033
+ devMode?: boolean;
1034
+ /** Consumed-credential store override (test seam). */
1035
+ consumedCredentialStore?: ConsumedCredentialStore;
1036
+ /** Mint-health tracker override (test seam). */
1037
+ mintHealthTracker?: MintHealthTracker;
1038
+ /**
1039
+ * Wrap the SDK compatibility gate before it is exposed to descriptor custom
1040
+ * routes. Platform hosts use this to enforce their independently released
1041
+ * caller rollout policy; self-hosted SDK consumers receive the default gate.
1042
+ */
1043
+ wrapRouteClientCompatibility?: (sdkGate: ClientCompatibilityGate) => ClientCompatibilityGate;
1044
+ }
1045
+ /** Per-mount options. Currently only `prefix` is supported. */
1046
+ interface MountOpts {
1047
+ /** Path prefix for this DVM's protocol routes (e.g. `/delete-feed`). */
1048
+ prefix?: string;
1049
+ }
1050
+ /** Live DVM host. Single wiring locus for the SDK (internal-review). */
1051
+ interface DVMHost {
1052
+ /**
1053
+ * Underlying Hono app for custom non-protocol routes that belong to the
1054
+ * host (cross-DVM `/admin`, cron callbacks, etc.). For DVM-scoped routes,
1055
+ * declare `routes(app)` on the descriptor.
1056
+ */
1057
+ readonly app: Hono;
1058
+ /**
1059
+ * Resolved Postgres pool — undefined before `host.serve()` completes
1060
+ * database resolution, set thereafter. Exposed so DVM-local Postgres
1061
+ * consumers (e.g. scrape's `ScrapeDb` per-fetch event store, internal-review)
1062
+ * can reuse the host's pool rather than constructing their own. Wire
1063
+ * inside descriptor `onBoot` (fires after pool init, before listener opens).
1064
+ */
1065
+ readonly pool: Pool | undefined;
1066
+ /**
1067
+ * Money-safe credit ledger (internal-review) — undefined before `host.serve()`
1068
+ * resolves stores, set thereafter. Postgres-backed when a pool exists;
1069
+ * pool-less hosts get a shared in-memory ledger so the internal-review fund+draw
1070
+ * semantics hold everywhere. Same wiring window as `pool` (descriptor
1071
+ * `onBoot` fires after init, before the listener opens).
1072
+ */
1073
+ readonly creditLedger: CreditLedgerLike | undefined;
1074
+ /**
1075
+ * Add a DVM to this host. Multiple mounts compose at distinct prefixes;
1076
+ * a single mount with no prefix attaches at root.
1077
+ */
1078
+ mount<S, I extends ZodLike | undefined>(descriptor: DVMDescriptor<S, I>, opts?: MountOpts): void;
1079
+ /** Start listening. Returns the live server handle. */
1080
+ serve(opts?: {
1081
+ port?: number;
1082
+ }): Promise<{
1083
+ url: string;
1084
+ close: () => Promise<void>;
1085
+ }>;
1086
+ /** Graceful shutdown. Runs descriptor `onShutdown` hooks in reverse mount order. */
1087
+ shutdown(): Promise<void>;
1088
+ }
1089
+ /**
1090
+ * Single wiring locus for SDK-side construction (internal-review). Replaces both the
1091
+ * `serve()` wrapper and the `createDVMServer` direct path. Reads env once,
1092
+ * resolves payment rails, owns the Postgres pool, and mounts each DVM
1093
+ * descriptor as a Hono sub-app on `host.app`.
1094
+ */
1095
+ declare function createDVMHost(opts?: DVMHostOpts): DVMHost;
1096
+
1097
+ /** Promise resolver for a pending client prompt response. */
1098
+ interface PromptResolver {
1099
+ resolve: (value: ResponseContent) => void;
1100
+ reject: (reason: Error) => void;
1101
+ }
1102
+ /** Promise resolver for a pending client payment. */
1103
+ interface PaymentResolver {
1104
+ resolve: (value: PaymentContent) => void;
1105
+ reject: (reason: Error) => void;
1106
+ }
1107
+ /** In-memory representation of a running job. */
1108
+ interface ServerJob {
1109
+ id: string;
1110
+ tags: string[];
1111
+ /**
1112
+ * Capability name this job belongs to (internal-review). Set at submission from
1113
+ * `POST /v1/job` body's `capability` field; routes the SDK runtime to the
1114
+ * right `onJob` / `onResponse` / `onPayment` handler and the right input
1115
+ * schema. Required — the wire layer rejects requests that omit it.
1116
+ */
1117
+ capability: string;
1118
+ input: string;
1119
+ params: Record<string, string>;
1120
+ requesterId: string;
1121
+ /**
1122
+ * SHA-256 hex of the per-job opaque `job_token` minted at creation for
1123
+ * anonymous callers (internal-review). Mirror of `JobRecord.requesterTokenHash`;
1124
+ * see that field for the wire-level gating semantics.
1125
+ */
1126
+ requesterTokenHash?: string;
1127
+ /**
1128
+ * Submission provenance (internal-review) — mirrors `JobRecord.requesterToken` /
1129
+ * `requestFingerprint` / `requesterPubkey`. Carried on the job purely so
1130
+ * `toJobRecord` persists it; nothing in the runtime reads it. It's the
1131
+ * replay gate: a retried paid submit that reproduces the fingerprint (and,
1132
+ * on a signed DVM, the pubkey) is handed this token back.
1133
+ */
1134
+ requesterToken?: string;
1135
+ requestFingerprint?: string;
1136
+ requesterPubkey?: string;
1137
+ requestId?: string;
1138
+ /** HTTP path independently recorded when the caller proof was accepted. */
1139
+ authRequestPath?: string;
1140
+ status: "processing" | "completed" | "failed" | "awaiting-input" | "cancelled" | "working";
1141
+ summary?: string;
1142
+ messages: Message[];
1143
+ seq: number;
1144
+ paidMsats: number;
1145
+ paymentMint?: string;
1146
+ /** Rail of the most recent successful credit. Set on each credit branch in payment processing. */
1147
+ paymentRail?: FundingMethod;
1148
+ /**
1149
+ * Settlement reference for the most-recent successful credit. Threaded
1150
+ * into `onJobCompleted` so the platform revenue ledger can populate
1151
+ * `revenue_events.tx_hash`. mpp/x402 carry the credential's `challenge.id`
1152
+ * / EVM tx hash from the facilitator (internal-review); cashu accumulator carries
1153
+ * the `X-Cashu-Request-Id` UUID (internal-review). Updated on every credit
1154
+ * (upfront or mid-job via `processIncomingPayment`) — internal-review.
1155
+ * Single-row-per-job accounting keeps the most recent credit's hash,
1156
+ * mirroring `paymentRail`.
1157
+ */
1158
+ paymentTxHash?: string;
1159
+ /** Chain transaction returned to callers recovering x402 or Tempo acceptance. */
1160
+ paymentTransactionHash?: string;
1161
+ /**
1162
+ * Rail-native amount accumulated across all successful credits on this
1163
+ * job (sats for mpp, USDC microunits for x402). The single
1164
+ * `revenue_events` row written at completion uses this as `gross_native`,
1165
+ * so multi-credit jobs sum the per-credit native amounts (internal-review).
1166
+ * internal-review wired the upfront credit; mid-job credits accumulate same-rail
1167
+ * and reset on a cross-rail switch (see `processIncomingPayment`).
1168
+ */
1169
+ nativeAmount?: number;
1170
+ /** Native asset tag — paired with `nativeAmount`. internal-review. */
1171
+ nativeAsset?: "sats" | "usdc" | "usdc.e" | "usd-cents";
1172
+ /** Cashu flow discriminator written into `revenue_events.metadata.cashu_flow` (internal-review). */
1173
+ cashuFlow?: "p2pk_accumulator";
1174
+ /** Credit the upfront payment funded/drew (internal-review). See `JobRecord.creditId`. */
1175
+ creditId?: string;
1176
+ /** The draw placed for this job on `creditId` (internal-review). */
1177
+ drawId?: string;
1178
+ /** Original funding artifact returned by an explicit fund-and-draw. */
1179
+ fundingReceipt?: FundingReceipt;
1180
+ fundingCredit?: CreditSnapshot;
1181
+ receivedProofs: ProofLike[];
1182
+ pendingPaymentMsats?: number;
1183
+ /**
1184
+ * Per-job binding for MPP credentials. Contains the `challenge.id` of every
1185
+ * mppx challenge issued in the most-recent `requestPayment` yield. mppx HMAC
1186
+ * binds challenges to realm/method/amount/expiry, not to the dvmkit job-id;
1187
+ * verifying the incoming credential's `challenge.id` is in this set blocks
1188
+ * a credential lifted from a sibling job that requested the same fields.
1189
+ * Cleared on successful credit (internal-review).
1190
+ */
1191
+ pendingMppChallengeIds?: string[];
1192
+ /**
1193
+ * Per-job binding for x402 mid-job credentials (internal-review). 32-byte 0x-hex
1194
+ * nonce issued by `requestPayment` and stamped onto the outbound x402
1195
+ * envelope; the caller signs `TransferWithAuthorization` against this exact
1196
+ * `bytes32`. Verification rejects an incoming `x402_payment` whose decoded
1197
+ * authorization nonce differs, blocking a cross-job replay before the
1198
+ * facilitator settles. Cleared on successful credit.
1199
+ */
1200
+ pendingX402Nonce?: string;
1201
+ /**
1202
+ * Exact USDC microunit amount paired with `pendingX402Nonce`. Kept as a
1203
+ * decimal string so the EIP-3009 uint256 value remains lossless.
1204
+ */
1205
+ pendingX402AmountUsdcMicro?: string;
1206
+ /**
1207
+ * Fiat micro-units the outstanding `requestPayment` ask is worth, pinned when
1208
+ * the payment-request was emitted (internal-review). See `JobRecord`.
1209
+ */
1210
+ pendingPaymentFiatMicro?: number;
1211
+ /** Currency of {@link pendingPaymentFiatMicro} (lowercase ISO-4217). */
1212
+ pendingPaymentFiatCurrency?: string;
1213
+ /**
1214
+ * Cumulative fiat micro this job has asked for mid-job, the ceiling on its
1215
+ * draw growth (internal-review). See `JobRecord.askedTopUpMicro` for the sentinel.
1216
+ */
1217
+ askedTopUpMicro?: number;
1218
+ /**
1219
+ * Currency of {@link askedTopUpMicro} — and this job's sticky ask
1220
+ * denomination (internal-review). See `JobRecord.askedTopUpCurrency`.
1221
+ */
1222
+ askedTopUpCurrency?: string;
1223
+ /**
1224
+ * Why this job's draw growth is not capped, when it isn't (internal-review). Mirror
1225
+ * of `JobRecord.topUpCapUnenforcedReason`.
1226
+ */
1227
+ topUpCapUnenforcedReason?: TopUpCapUnenforcedReason;
1228
+ /** Price the job was charged at (msats). Used to detect overpayment for change/melt gating. */
1229
+ requiredMsats?: number;
1230
+ /**
1231
+ * The signed receipt for this job's terminal outcome (internal-review). Mirror of
1232
+ * `JobRecord.receipt`, set once the terminal funnel has signed and persisted
1233
+ * it so the local read paths (which serve from `activeJobs` during the
1234
+ * cleanup window) hand back the same bytes as a cross-machine re-read.
1235
+ */
1236
+ receipt?: JobReceipt;
1237
+ /**
1238
+ * The in-flight terminal chain — persist, then sign + store the receipt
1239
+ * (internal-review). Assigned synchronously by `ctx.complete()` / `ctx.fail()` at
1240
+ * the instant the job goes terminal, so the `POST /v1/job` fast path can
1241
+ * await it (bounded) and put the receipt on the synchronous 200 rather than
1242
+ * making the caller poll for it. Never rejects — the chain swallows its own
1243
+ * failures.
1244
+ */
1245
+ terminalWork?: Promise<void>;
1246
+ listeners: Set<(msg: Message) => void>;
1247
+ /**
1248
+ * Cancellation signal for the running handler (internal-review). Aborted whenever
1249
+ * the job is cancelled on this process — caller cancel, idle timeout,
1250
+ * stale-sweep teardown, supersession. Surfaced to builders as `ctx.signal`
1251
+ * and auto-threaded into `ctx.fetch`, so an in-flight provider call
1252
+ * (ElevenLabs, Scrapfly, …) is torn down instead of billing the caller for
1253
+ * work they just cancelled. Per-process runtime state like `listeners` —
1254
+ * never persisted, and re-created fresh on reactivation.
1255
+ */
1256
+ abort: AbortController;
1257
+ /**
1258
+ * Hook for cross-machine message persistence (Tx A, internal-review). Set by
1259
+ * `JobManager` when the store is streamable. The appender runs the
1260
+ * outgoing-message tx atomically with any payment-request counter bump so
1261
+ * `(message, pending_payment_msats)` land together — never separately.
1262
+ *
1263
+ * Per-job serialised (internal-review): each call chains onto `messageAppenderTail`
1264
+ * so two synchronous `providerMessage` calls (e.g. `artifact` followed by
1265
+ * `complete`) can't race for the DB-side seq allocation.
1266
+ */
1267
+ messageAppender?: (msg: Message, opts?: AppendOutgoingOptions) => void;
1268
+ /**
1269
+ * Tail of the per-job appender chain (internal-review). `wireMessageAppender` updates
1270
+ * this on every call so `persistJob` can await it before the terminal snapshot
1271
+ * save runs `DELETE FROM job_messages`.
1272
+ */
1273
+ messageAppenderTail?: Promise<unknown>;
1274
+ sdkCtx?: SDKJobContext<unknown, unknown>;
1275
+ stepCache: StepCache;
1276
+ state: unknown;
1277
+ pendingPrompts: Map<string, PromptResolver>;
1278
+ pendingPayment: PaymentResolver | null;
1279
+ /** When not null, indicates replay mode — prompt/payment scan message history for cached responses. */
1280
+ replayHighSeq: number | null;
1281
+ /** Number of provider messages to suppress during replay. Cleared on handler error. */
1282
+ replayProviderSkip: number;
1283
+ /** Index of the next payment to match during replay (for multi-payment handlers). */
1284
+ replayPaymentIndex: number;
1285
+ /**
1286
+ * Set once Tx C applies the current payment ask during this reactivation.
1287
+ * Runtime-only: replayed prompts may mutate `status`, but never clear this
1288
+ * marker before the matching payment is consumed.
1289
+ */
1290
+ reactivationPaymentApplied?: true;
1291
+ /** Unix ms timestamp of job creation. */
1292
+ createdAt: number;
1293
+ /** Unix ms timestamp of last activity (message, yield, terminal). */
1294
+ lastActivityAt: number;
1295
+ }
1296
+
1297
+ /** Lifetime of a per-call implicit credit before the ledger refuses new draws. */
1298
+ declare const IMPLICIT_CREDIT_TTL_MS: number;
1299
+
1300
+ /**
1301
+ * Fiat denomination of the job price, pinned per request (internal-review).
1302
+ *
1303
+ * The credit ledger is fiat-micro denominated (internal-review baked decision:
1304
+ * 1e-6 of the DVM's pricing currency, never msats). The conversion pins at
1305
+ * quote time: the verify path already fixes both the fiat price and its msat
1306
+ * equivalent, so the implicit N=1 fund credits the QUOTED fiat amount when
1307
+ * the attached token satisfies the quoted msats — no fresh BTC/USD lookup in
1308
+ * the rail verify hot path, and fund = draw = quoted price means zero
1309
+ * rounding drift at N=1.
1310
+ */
1311
+ interface PriceFiat {
1312
+ /** Lowercase currency code, e.g. `"usd"`. */
1313
+ currency: string;
1314
+ /** Job price in 1e-6 currency units. */
1315
+ amountMicro: number;
1316
+ }
1317
+
1318
+ /**
1319
+ * SHA-256 hex over the raw funding artifact — the value an explicit
1320
+ * fund-and-draw request must sign as `fund.commitment` (spec §2 condition 3).
1321
+ * The artifact is the exact header-borne string: the `X-Cashu` header value,
1322
+ * the `X-PAYMENT` header value, or the extracted `Payment <…>` mpp scheme
1323
+ * string. Hashing the verbatim wire string keeps the commitment computable by
1324
+ * the client without any decode step.
1325
+ */
1326
+ declare function fundingCommitment(artifact: string): string;
1327
+ /**
1328
+ * Settlement reference stamped on the revenue event of an **explicit** draw
1329
+ * (internal-review). One funding backs N draws, so the funding's own rail reference
1330
+ * cannot key them: `revenue_events` carries a global `UNIQUE (rail, tx_hash)`
1331
+ * that deliberately drops cross-job reuse of a settlement reference, and
1332
+ * reusing it would silently discard every draw but the first.
1333
+ *
1334
+ * Hashed rather than concatenated because `draw_id` is caller-generated: the
1335
+ * DVM id is mixed in so two DVMs whose callers both pick `draw_id: "1"` can't
1336
+ * collide, and the hash keeps caller-controlled bytes out of a column ops
1337
+ * reads. Deterministic, so a replayed report lands on the same row.
1338
+ */
1339
+ declare function drawSettlementRef(dvmId: string, creditId: string, drawId: string): string;
1340
+
1341
+ /**
1342
+ * Durable marker for a rail payment that already funded the credit ledger
1343
+ * (internal-review, spec §2 condition 3). The x402 and mpp rails have no dvmkit-side
1344
+ * commit point of their own — the money moves at the facilitator / the payer's
1345
+ * mpp server — so the marker row, inserted in the SAME transaction as
1346
+ * `CreditLedger.fund` + `draw`, is what makes a crash-replayed credential
1347
+ * unable to fund twice: the second attempt collides on the primary key and
1348
+ * surfaces as {@link ProcessedPaymentReplayError}, which the verifier routes
1349
+ * into the internal-review recovery gates instead of a second fund.
1350
+ *
1351
+ * The Cashu rail does not use this store — its accumulator insert is already
1352
+ * the durable commit point, and its replay key (the `X-Cashu-Request-Id`
1353
+ * regime) must stay exactly as-is (dual-idempotency rule, spec §2).
1354
+ */
1355
+ interface ProcessedPaymentStore {
1356
+ /**
1357
+ * Record that `paymentId` on `rail` funded a credit. Throws
1358
+ * {@link ProcessedPaymentReplayError} when the `(dvmId, rail, paymentId)`
1359
+ * marker already exists. `tx` (Postgres implementation only) joins the
1360
+ * caller-owned transaction so marker + fund + draw commit atomically.
1361
+ */
1362
+ record(args: {
1363
+ dvmId: string;
1364
+ rail: ProcessedPaymentRail;
1365
+ paymentId: string;
1366
+ creditId: string;
1367
+ drawId: string;
1368
+ jobId?: string;
1369
+ tx?: ProcessedPaymentQuerier;
1370
+ nowMs?: number;
1371
+ }): Promise<void>;
1372
+ /** Read one marker, `undefined` when absent. */
1373
+ get(args: {
1374
+ dvmId: string;
1375
+ rail: ProcessedPaymentRail;
1376
+ paymentId: string;
1377
+ }): Promise<ProcessedPaymentRecord | undefined>;
1378
+ /**
1379
+ * Run `fn` inside one transaction (Postgres: BEGIN → fn → COMMIT, rollback
1380
+ * on throw). The verifier funds and draws the ledger inside `fn`, passing
1381
+ * the handle through to `record` / `CreditLedger.fund` / `draw` so all
1382
+ * three commit atomically. The memory implementation invokes `fn` with no
1383
+ * handle — its operations are synchronous-atomic in-process.
1384
+ */
1385
+ withTx<T>(fn: (tx?: ProcessedPaymentQuerier) => Promise<T>): Promise<T>;
1386
+ }
1387
+ /** Rails that commit through the marker (Cashu commits via the accumulator). */
1388
+ type ProcessedPaymentRail = "x402" | "tempo";
1389
+ /** One recorded funding marker. */
1390
+ interface ProcessedPaymentRecord {
1391
+ dvmId: string;
1392
+ rail: ProcessedPaymentRail;
1393
+ paymentId: string;
1394
+ creditId: string;
1395
+ drawId: string;
1396
+ jobId: string | null;
1397
+ createdAt: number;
1398
+ }
1399
+ /** Subset of `pg.Pool` the store queries through; `PoolClient` satisfies it too. */
1400
+ interface ProcessedPaymentQuerier {
1401
+ query: Pool["query"];
1402
+ }
1403
+ /** Thrown by `record` when the payment id was already processed on this rail. */
1404
+ declare class ProcessedPaymentReplayError extends Error {
1405
+ readonly rail: ProcessedPaymentRail;
1406
+ readonly paymentId: string;
1407
+ constructor(rail: ProcessedPaymentRail, paymentId: string);
1408
+ }
1409
+ /** Postgres-backed {@link ProcessedPaymentStore} — the production implementation. */
1410
+ declare class PostgresProcessedPaymentStore implements ProcessedPaymentStore {
1411
+ private readonly pool;
1412
+ constructor(pool: Pick<Pool, "query" | "connect">);
1413
+ withTx<T>(fn: (tx?: ProcessedPaymentQuerier) => Promise<T>): Promise<T>;
1414
+ /** Create the `processed_payments` table if absent. Call once at SDK boot. */
1415
+ init(): Promise<void>;
1416
+ /** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
1417
+ private createTables;
1418
+ record(args: {
1419
+ dvmId: string;
1420
+ rail: ProcessedPaymentRail;
1421
+ paymentId: string;
1422
+ creditId: string;
1423
+ drawId: string;
1424
+ jobId?: string;
1425
+ tx?: ProcessedPaymentQuerier;
1426
+ nowMs?: number;
1427
+ }): Promise<void>;
1428
+ get(args: {
1429
+ dvmId: string;
1430
+ rail: ProcessedPaymentRail;
1431
+ paymentId: string;
1432
+ }): Promise<ProcessedPaymentRecord | undefined>;
1433
+ }
1434
+ /** In-memory {@link ProcessedPaymentStore} for pool-less setups. Per-process. */
1435
+ declare class MemoryProcessedPaymentStore implements ProcessedPaymentStore {
1436
+ private readonly markers;
1437
+ withTx<T>(fn: (tx?: ProcessedPaymentQuerier) => Promise<T>): Promise<T>;
1438
+ record(args: {
1439
+ dvmId: string;
1440
+ rail: ProcessedPaymentRail;
1441
+ paymentId: string;
1442
+ creditId: string;
1443
+ drawId: string;
1444
+ jobId?: string;
1445
+ tx?: ProcessedPaymentQuerier;
1446
+ nowMs?: number;
1447
+ }): Promise<void>;
1448
+ get(args: {
1449
+ dvmId: string;
1450
+ rail: ProcessedPaymentRail;
1451
+ paymentId: string;
1452
+ }): Promise<ProcessedPaymentRecord | undefined>;
1453
+ }
1454
+
1455
+ /** Durable state of one exact x402 authorization. */
1456
+ type X402ExactSettlementStatus = "prepared" | "submitting" | "settled" | "local_committed" | "ambiguous" | "rejected" | "complete";
1457
+ /** Serializable local economic effect that follows an exact settlement. */
1458
+ interface X402ExactSettlementEffect {
1459
+ priceFiat: {
1460
+ currency: string;
1461
+ amountMicro: number;
1462
+ };
1463
+ requiredMsats: number;
1464
+ callerId: string;
1465
+ dvmId: string;
1466
+ jobId?: string;
1467
+ /** Upfront job request bound to this payment; omitted for credit-only and mid-job effects. */
1468
+ jobAdmission?: {
1469
+ requestFingerprint: string;
1470
+ authFingerprint?: string;
1471
+ };
1472
+ creditEnvelope?: CreditEnvelope;
1473
+ fundOnly?: {
1474
+ creditId: string;
1475
+ fundId: string;
1476
+ commitment: string;
1477
+ allowOneShotStablecoin?: boolean;
1478
+ };
1479
+ topUp?: {
1480
+ creditId: string;
1481
+ drawId: string;
1482
+ };
1483
+ topUpCapMicro?: number;
1484
+ askSatisfied?: true;
1485
+ midJob?: true;
1486
+ drawBasis: boolean;
1487
+ ttlMs: number;
1488
+ paidMsats: number;
1489
+ fundedMicro: number;
1490
+ nativeAmount: number;
1491
+ }
1492
+ /** Canonical authorization facts retained without the bearer signature. */
1493
+ interface X402ExactAuthorizationFacts {
1494
+ network: string;
1495
+ asset: string;
1496
+ from: string;
1497
+ to: string;
1498
+ value: string;
1499
+ validAfter: string;
1500
+ validBefore: string;
1501
+ nonce: string;
1502
+ fingerprint: string;
1503
+ }
1504
+ /** One durable exact-x402 settlement intent. */
1505
+ interface X402ExactSettlementIntent extends X402ExactAuthorizationFacts {
1506
+ id: string;
1507
+ dvmId: string;
1508
+ resource: string;
1509
+ status: X402ExactSettlementStatus;
1510
+ effect?: X402ExactSettlementEffect;
1511
+ submissionBlock?: string;
1512
+ transaction?: string;
1513
+ settleResponse?: Record<string, unknown>;
1514
+ lastOutcome?: string;
1515
+ createdAt: number;
1516
+ updatedAt: number;
1517
+ resolvedAt?: number;
1518
+ }
1519
+ /** Evidence recorded around one facilitator settlement call. */
1520
+ interface X402ExactSettlementAttempt {
1521
+ id: string;
1522
+ intentId: string;
1523
+ outcome: "submitting" | "success" | "rejected" | "timeout" | "missing_reference";
1524
+ detail?: string;
1525
+ transaction?: string;
1526
+ createdAt: number;
1527
+ }
1528
+ /** Persistence contract for exact x402 settlement recovery. */
1529
+ interface X402ExactSettlementStore {
1530
+ prepare(intent: Omit<X402ExactSettlementIntent, "status" | "createdAt" | "updatedAt" | "resolvedAt">): Promise<X402ExactSettlementIntent>;
1531
+ get(dvmId: string, intentId: string): Promise<X402ExactSettlementIntent | undefined>;
1532
+ markSubmitting(args: {
1533
+ dvmId: string;
1534
+ intentId: string;
1535
+ submissionBlock?: string;
1536
+ }): Promise<boolean>;
1537
+ recordOutcome(args: {
1538
+ dvmId: string;
1539
+ intentId: string;
1540
+ status: Extract<X402ExactSettlementStatus, "settled" | "ambiguous" | "rejected">;
1541
+ outcome: X402ExactSettlementAttempt["outcome"];
1542
+ detail?: string;
1543
+ transaction?: string;
1544
+ settleResponse?: Record<string, unknown>;
1545
+ }): Promise<X402ExactSettlementIntent>;
1546
+ finalize<T>(dvmId: string, intentId: string, fn: (intent: X402ExactSettlementIntent, tx?: ProcessedPaymentQuerier) => Promise<T>): Promise<{
1547
+ intent: X402ExactSettlementIntent;
1548
+ value?: T;
1549
+ replayed: boolean;
1550
+ }>;
1551
+ markAdmissionComplete(dvmId: string, intentId: string): Promise<X402ExactSettlementIntent>;
1552
+ list(args: {
1553
+ dvmId: string;
1554
+ limit: number;
1555
+ includeResolved?: boolean;
1556
+ }): Promise<X402ExactSettlementIntent[]>;
1557
+ attempts(dvmId: string, intentId: string): Promise<X402ExactSettlementAttempt[]>;
1558
+ }
1559
+ /** Postgres-backed exact settlement intent store. */
1560
+ declare class PostgresX402ExactSettlementStore implements X402ExactSettlementStore {
1561
+ private readonly pool;
1562
+ constructor(pool: Pick<Pool, "query" | "connect">);
1563
+ /** Create the exact settlement tables. */
1564
+ init(): Promise<void>;
1565
+ prepare(intent: Omit<X402ExactSettlementIntent, "status" | "createdAt" | "updatedAt" | "resolvedAt">): Promise<X402ExactSettlementIntent>;
1566
+ get(dvmId: string, intentId: string): Promise<X402ExactSettlementIntent | undefined>;
1567
+ markSubmitting(args: {
1568
+ dvmId: string;
1569
+ intentId: string;
1570
+ submissionBlock?: string;
1571
+ }): Promise<boolean>;
1572
+ recordOutcome(args: {
1573
+ dvmId: string;
1574
+ intentId: string;
1575
+ status: Extract<X402ExactSettlementStatus, "settled" | "ambiguous" | "rejected">;
1576
+ outcome: X402ExactSettlementAttempt["outcome"];
1577
+ detail?: string;
1578
+ transaction?: string;
1579
+ settleResponse?: Record<string, unknown>;
1580
+ }): Promise<X402ExactSettlementIntent>;
1581
+ finalize<T>(dvmId: string, intentId: string, fn: (intent: X402ExactSettlementIntent, tx?: ProcessedPaymentQuerier) => Promise<T>): Promise<{
1582
+ intent: X402ExactSettlementIntent;
1583
+ value?: T;
1584
+ replayed: boolean;
1585
+ }>;
1586
+ markAdmissionComplete(dvmId: string, intentId: string): Promise<X402ExactSettlementIntent>;
1587
+ list(args: {
1588
+ dvmId: string;
1589
+ limit: number;
1590
+ includeResolved?: boolean;
1591
+ }): Promise<X402ExactSettlementIntent[]>;
1592
+ attempts(dvmId: string, intentId: string): Promise<X402ExactSettlementAttempt[]>;
1593
+ private insertAttempt;
1594
+ }
1595
+ /** In-memory exact settlement store used by dev and unit tests. */
1596
+ declare class MemoryX402ExactSettlementStore implements X402ExactSettlementStore {
1597
+ private readonly intents;
1598
+ private readonly evidence;
1599
+ private readonly authBindings;
1600
+ private readonly locks;
1601
+ prepare(intent: Omit<X402ExactSettlementIntent, "status" | "createdAt" | "updatedAt" | "resolvedAt">): Promise<X402ExactSettlementIntent>;
1602
+ get(dvmId: string, intentId: string): Promise<X402ExactSettlementIntent | undefined>;
1603
+ markSubmitting(args: {
1604
+ dvmId: string;
1605
+ intentId: string;
1606
+ submissionBlock?: string;
1607
+ }): Promise<boolean>;
1608
+ recordOutcome(args: {
1609
+ dvmId: string;
1610
+ intentId: string;
1611
+ status: Extract<X402ExactSettlementStatus, "settled" | "ambiguous" | "rejected">;
1612
+ outcome: X402ExactSettlementAttempt["outcome"];
1613
+ detail?: string;
1614
+ transaction?: string;
1615
+ settleResponse?: Record<string, unknown>;
1616
+ }): Promise<X402ExactSettlementIntent>;
1617
+ finalize<T>(dvmId: string, intentId: string, fn: (intent: X402ExactSettlementIntent, tx?: ProcessedPaymentQuerier) => Promise<T>): Promise<{
1618
+ intent: X402ExactSettlementIntent;
1619
+ value?: T;
1620
+ replayed: boolean;
1621
+ }>;
1622
+ markAdmissionComplete(dvmId: string, intentId: string): Promise<X402ExactSettlementIntent>;
1623
+ list(args: {
1624
+ dvmId: string;
1625
+ limit: number;
1626
+ includeResolved?: boolean;
1627
+ }): Promise<X402ExactSettlementIntent[]>;
1628
+ attempts(dvmId: string, intentId: string): Promise<X402ExactSettlementAttempt[]>;
1629
+ private addAttempt;
1630
+ private withLock;
1631
+ }
1632
+ /** Raised when the same on-chain authorization is presented for a different local effect. */
1633
+ declare class X402ExactIntentConflictError extends Error {
1634
+ readonly intentId: string;
1635
+ constructor(intentId: string);
1636
+ }
1637
+ /** Raised when local finalization is attempted before settlement is evidenced. */
1638
+ declare class X402ExactSettlementNotReadyError extends Error {
1639
+ readonly intentId: string;
1640
+ readonly status: X402ExactSettlementStatus;
1641
+ constructor(intentId: string, status: X402ExactSettlementStatus);
1642
+ }
1643
+
1644
+ /** Chain evidence recovered for one exact x402 authorization. */
1645
+ interface X402ExactSettlementChainEvidence {
1646
+ transaction: string;
1647
+ amount: string;
1648
+ }
1649
+ /** Chain boundary used by exact-settlement recovery. */
1650
+ interface X402ExactSettlementEvidenceReader {
1651
+ getBlockNumber(): Promise<bigint>;
1652
+ findExactPayment?(intent: X402ExactSettlementIntent): Promise<X402ExactSettlementChainEvidence | undefined>;
1653
+ }
1654
+ /** Result of accepting or recovering one exact x402 authorization. */
1655
+ type X402ExactAcceptance<T> = {
1656
+ outcome: "accepted";
1657
+ intentId: string;
1658
+ receipt: X402Receipt;
1659
+ committed?: T;
1660
+ replayed: boolean;
1661
+ admissionPending: boolean;
1662
+ effect?: X402ExactSettlementEffect;
1663
+ } | {
1664
+ outcome: "rejected";
1665
+ intentId?: string;
1666
+ reason: string;
1667
+ } | {
1668
+ outcome: "ambiguous";
1669
+ intentId: string;
1670
+ reason: string;
1671
+ };
1672
+ /** Dependencies for the durable exact x402 settlement coordinator. */
1673
+ interface X402ExactSettlementServerOpts {
1674
+ store: X402ExactSettlementStore;
1675
+ evidence?: X402ExactSettlementEvidenceReader;
1676
+ repair?: (effect: X402ExactSettlementEffect | undefined, receipt: X402Receipt, tx?: ProcessedPaymentQuerier) => Promise<unknown>;
1677
+ }
1678
+ /** Durable coordinator for irreversible exact x402 settlement. */
1679
+ declare class X402ExactSettlementServer {
1680
+ private readonly opts;
1681
+ private readonly activeSubmissions;
1682
+ constructor(opts: X402ExactSettlementServerOpts);
1683
+ /** Verify, settle, and atomically finalize one exact authorization. */
1684
+ accept<T>(args: {
1685
+ paymentHeader: string;
1686
+ config: X402Config;
1687
+ requiredUsdcMicro: bigint;
1688
+ resource: string;
1689
+ btcUsdRate: number;
1690
+ exactVersions?: X402ExactVersionSupport;
1691
+ dvmId: string;
1692
+ effect?: X402ExactSettlementEffect;
1693
+ commit: (receipt: X402Receipt, tx?: ProcessedPaymentQuerier, effect?: X402ExactSettlementEffect) => Promise<T>;
1694
+ }): Promise<X402ExactAcceptance<T>>;
1695
+ /** Read the operator queue. */
1696
+ list(args: {
1697
+ dvmId: string;
1698
+ limit: number;
1699
+ includeResolved?: boolean;
1700
+ }): Promise<X402ExactSettlementIntent[]>;
1701
+ /** Read durable attempt evidence for one intent. */
1702
+ attempts(dvmId: string, intentId: string): Promise<X402ExactSettlementAttempt[]>;
1703
+ /** Reconcile and finish one intent from chain evidence. */
1704
+ reconcile(dvmId: string, intentId: string): Promise<{
1705
+ intent: X402ExactSettlementIntent;
1706
+ replayed: boolean;
1707
+ }>;
1708
+ /** Mark a locally committed upfront settlement complete after its job row lands. */
1709
+ completeAdmission(dvmId: string, intentId: string): Promise<X402ExactSettlementIntent>;
1710
+ private finalize;
1711
+ private recoverEvidence;
1712
+ private requiredIntent;
1713
+ }
1714
+ /** Raised when operator repair cannot prove the payment on chain. */
1715
+ declare class X402ExactSettlementEvidenceMissingError extends Error {
1716
+ readonly intentId: string;
1717
+ constructor(intentId: string);
1718
+ }
1719
+
1720
+ /** Exact successful chain action recovered for one ambiguous settlement attempt. */
1721
+ interface X402SettlementChainEvidence {
1722
+ transaction: string;
1723
+ amount: string;
1724
+ }
1725
+ /** Chain boundary used to bookmark and reconcile irreversible x402 submissions. */
1726
+ interface X402SettlementEvidenceReader extends X402ExactSettlementEvidenceReader {
1727
+ getBlockNumber(): Promise<bigint>;
1728
+ findAppliedSettlement(intent: X402SettlementIntent): Promise<X402SettlementChainEvidence | undefined>;
1729
+ }
1730
+
1731
+ /** Hosted-facilitator channel network retained for the existing testnet path. */
1732
+ declare const X402_BATCH_SETTLEMENT_NETWORK = "eip155:84532";
1733
+ /** Policy floor advertised to callers and enforced at boot. */
1734
+ declare const X402_BATCH_MIN_WITHDRAW_DELAY_SECONDS = 86400;
1735
+ /** Default claim and transfer cadence; comfortably inside the 24-hour exit window. */
1736
+ declare const X402_BATCH_AUTO_SETTLEMENT: AutoSettlementConfig;
1737
+ /** Natural identity and incremental value carried by an accepted channel voucher. */
1738
+ interface X402BatchFunding {
1739
+ channelId: string;
1740
+ maxClaimableAmount: string;
1741
+ paymentId: string;
1742
+ amount: string;
1743
+ }
1744
+ /** Successful batch-settlement result plus the caller-owned ledger result. */
1745
+ interface X402BatchAcceptance<T> {
1746
+ funding: X402BatchFunding;
1747
+ settlement: SettleResponse;
1748
+ committed: T;
1749
+ }
1750
+ /** Authoritative contract views needed to refresh one stored batch channel. */
1751
+ interface X402BatchChannelObservation {
1752
+ balance: string;
1753
+ totalClaimed: string;
1754
+ withdrawRequestedAt: number;
1755
+ refundNonce: number;
1756
+ }
1757
+ /**
1758
+ * The chain submission behind an accepted voucher did not return success (internal-review).
1759
+ *
1760
+ * Typed and `retryable` because it is a service-availability fault, not a fault
1761
+ * in the credential: the voucher verified, the ledger leg rolled back, and the
1762
+ * same request first reconciles the durable submission bookmark before it can
1763
+ * submit again. Without this, the branch threw a bare `Error`, which
1764
+ * `mapLedgerCommitError` could not recognise
1765
+ * and `verifyUpfrontPayment` answered as a 402 `payment_invalid` — copy that
1766
+ * tells an agent its perfectly good payment can never be reused.
1767
+ *
1768
+ * It is deliberately *not* the relay submission lock's own error class. Upstream's
1769
+ * `BatchSettlementEvmScheme` catches everything the signer throws and returns
1770
+ * `{success: false, errorReason}`, so {@link X402RelaySubmissionLockError} never
1771
+ * reaches this layer — it arrives as `settle_transaction_failed` with its message
1772
+ * in `errorMessage`. Every sibling reason (RPC read, simulation, a reverted
1773
+ * submission) is the same class of fault and had the same wrong copy, so the
1774
+ * classification is made where upstream draws it rather than by re-deriving the
1775
+ * one cause from a string.
1776
+ *
1777
+ * `errorReason` is upstream's enum and is safe to hand an unauthenticated caller.
1778
+ * `errorMessage` — and therefore `.message` — is not: upstream builds it from
1779
+ * whatever the signer threw, so it can carry a relay lock holder's backend pid
1780
+ * and `client_addr`, or a viem error quoting the RPC URL. The self-relay path
1781
+ * drops it before constructing this error; hosted facilitators retain their
1782
+ * provider message for server logs. The wire gets the reason in either case.
1783
+ */
1784
+ declare class X402SettlementSubmissionError extends Error {
1785
+ readonly errorReason: string;
1786
+ readonly errorMessage?: string | undefined;
1787
+ readonly code = "x402_settlement_submission_failed";
1788
+ readonly retryable = true;
1789
+ constructor(errorReason: string, errorMessage?: string | undefined);
1790
+ }
1791
+ /** Structured refusal that can be turned back into a corrective 402. */
1792
+ interface X402BatchRefusal {
1793
+ ok: false;
1794
+ reason: string;
1795
+ requirements: PaymentRequirementsV2[];
1796
+ /**
1797
+ * `false` when re-signing cannot fix this: the caller must be answered with a
1798
+ * terminal error rather than a 402 carrying fresh requirements, because an
1799
+ * agent reads a corrective 402 as "sign this and resubmit" and lands straight
1800
+ * back on the same refusal (internal-review). Omitted means corrective.
1801
+ */
1802
+ corrective?: boolean;
1803
+ }
1804
+ /**
1805
+ * What the chain says about one wedged settlement (internal-review).
1806
+ *
1807
+ * The four answers are kept apart because their repairs differ and because an
1808
+ * operator re-pointing an RPC needs a definitive answer to read as definitive
1809
+ * — the same split `LightningReceive.lookupSettlement` draws between
1810
+ * `invoice_unknown_to_wallet` and `wallet_unreachable`. `findAppliedSettlement`
1811
+ * collapses all of them (`undefined` for absent *and* unbookmarked, a throw for
1812
+ * transport), so this is where they are separated.
1813
+ */
1814
+ type X402SettlementEvidenceOutcome =
1815
+ /** The chain carries this exact settlement, at this exact amount. */
1816
+ {
1817
+ chain: "found";
1818
+ transaction: string;
1819
+ amount: string;
1820
+ }
1821
+ /** Scanned from the submission bookmark to head and found nothing. */
1822
+ | {
1823
+ chain: "absent";
1824
+ }
1825
+ /** The RPC could not be read — says nothing about whether the money moved. */
1826
+ | {
1827
+ chain: "unreachable";
1828
+ error: string;
1829
+ }
1830
+ /** No submission bookmark, so there is no range to scan and no answer to give. */
1831
+ | {
1832
+ chain: "unbookmarked";
1833
+ };
1834
+ /** One row of the operator's repair queue: the durable intent plus what the chain says. */
1835
+ interface X402WedgedSettlement {
1836
+ intent: X402SettlementIntent;
1837
+ evidence?: X402SettlementEvidenceOutcome;
1838
+ }
1839
+ /** Why {@link X402SettlementRepair.reconcile} could not complete a settlement. */
1840
+ type X402SettlementRepairRefusal = {
1841
+ refusal: "settlement_not_found";
1842
+ }
1843
+ /** `prepared` — nothing was ever submitted, so there is nothing to repair. */
1844
+ | {
1845
+ refusal: "settlement_not_wedged";
1846
+ status: X402SettlementStatus;
1847
+ }
1848
+ /** A wedged deposit; the caller's own retry recovers it. See {@link X402SettlementRepair}. */
1849
+ | {
1850
+ refusal: "settlement_not_repairable";
1851
+ operation: "fund";
1852
+ } | {
1853
+ refusal: "settlement_unbookmarked";
1854
+ } | {
1855
+ refusal: "settlement_not_on_chain";
1856
+ } | {
1857
+ refusal: "chain_unreachable";
1858
+ error: string;
1859
+ }
1860
+ /** A concurrent repair or a caller retry moved the row out from under this one. */
1861
+ | {
1862
+ refusal: "settlement_raced";
1863
+ error: string;
1864
+ };
1865
+ /** One page of the repair queue, each row carrying what the chain says about it. */
1866
+ interface X402WedgedSettlementPage {
1867
+ settlements: X402WedgedSettlement[];
1868
+ /** Absent when this page was not full — there is nothing behind it. */
1869
+ nextAfter?: X402SettlementCursor;
1870
+ }
1871
+ /** A settlement the repair completed — or found already complete. */
1872
+ interface X402SettlementRepaired<T> {
1873
+ intent: X402SettlementIntent;
1874
+ /** The chain figure the ledger leg was completed against; absent on a replay. */
1875
+ evidence?: X402SettlementEvidenceOutcome & {
1876
+ chain: "found";
1877
+ };
1878
+ committed?: T;
1879
+ /** True when the settlement was already `complete` and nothing was booked again. */
1880
+ replayed: boolean;
1881
+ }
1882
+ /**
1883
+ * The operator's exit from a settlement stuck between "chain paid" and "ledger
1884
+ * booked" (internal-review).
1885
+ *
1886
+ * `reconcile` is the ordinary recovery path with the chain's figure substituted
1887
+ * for the facilitator's: the amount is re-derived from
1888
+ * `x402-settlement-evidence.ts` and never supplied by the operator, which is
1889
+ * what keeps this door from extinguishing a different figure than the chain
1890
+ * actually paid. The ledger legs stay outside this module — they arrive as the
1891
+ * same `commit`/`complete` callbacks {@link X402BatchSettlementServer.accept}
1892
+ * takes, so the live path and the repair cannot drift about what completing a
1893
+ * drain means.
1894
+ *
1895
+ * Both write verbs are `refund`-only. A wedged `fund` is recovered by the
1896
+ * caller's own retry (that leg has no amount guard to wedge on), and its ledger
1897
+ * effect is `payment.ts`'s fund-and-draw, which an admin route has no way to
1898
+ * reconstruct.
1899
+ */
1900
+ interface X402SettlementRepair {
1901
+ /** One settlement by its own id, with no chain lookup — the repair's own read. */
1902
+ get(settlementId: string): Promise<X402SettlementIntent | undefined>;
1903
+ list(args: {
1904
+ limit: number;
1905
+ after?: X402SettlementCursor;
1906
+ /** Staleness floor — a settlement younger than this is in flight, not wedged. */
1907
+ updatedBeforeMs: number;
1908
+ includeResolved?: boolean;
1909
+ /** Skip the per-row chain lookups; the queue itself needs no RPC. */
1910
+ evidence?: boolean;
1911
+ }): Promise<X402WedgedSettlementPage>;
1912
+ reconcile<T>(args: {
1913
+ settlementId: string;
1914
+ commit: (intent: X402SettlementIntent, tx: CreditLedgerQuerier) => Promise<T>;
1915
+ complete: (evidence: {
1916
+ transaction: string;
1917
+ amount: string;
1918
+ }, committed: T, tx: CreditLedgerQuerier) => Promise<T>;
1919
+ nowMs?: number;
1920
+ }): Promise<X402SettlementRepaired<T> | X402SettlementRepairRefusal>;
1921
+ writeOff(args: {
1922
+ settlementId: string;
1923
+ note?: string;
1924
+ nowMs?: number;
1925
+ }): Promise<X402SettlementWriteOff | X402SettlementRepairRefusal>;
1926
+ }
1927
+ /** Runtime server handle used only by `/v1/credit`. */
1928
+ interface X402BatchSettlementServer {
1929
+ buildRequirements(amount: string): Promise<PaymentRequirementsV2[]>;
1930
+ accept<T>(args: {
1931
+ paymentHeader: string;
1932
+ amount: string;
1933
+ effectId: string;
1934
+ /**
1935
+ * Required on `refund`: the credit's unspent balance in the channel token's
1936
+ * own units. It is the ceiling the on-chain payout is sized to, so that a
1937
+ * drain returns what the caller is owed and nothing the DVM has earned
1938
+ * (internal-review). `commit` must re-assert it against the authoritative ledger
1939
+ * figure, since this one is read before the channel lock is taken.
1940
+ */
1941
+ refundBoundNative?: number;
1942
+ commit: (funding: X402BatchFunding, tx?: CreditLedgerQuerier) => Promise<T>;
1943
+ complete?: (settlement: SettleResponse, committed: T, tx?: CreditLedgerQuerier) => Promise<T>;
1944
+ } & ({
1945
+ operation: "fund";
1946
+ expectedChannelId?: undefined;
1947
+ } | {
1948
+ operation: "refund";
1949
+ /**
1950
+ * The channel the credit being drained is bound to. Required, and
1951
+ * required in the *type*, because the payload the payer presents is
1952
+ * what revokes, settles and pays: a refund voucher naming another
1953
+ * channel would extinguish this credit's balance out of that channel's
1954
+ * escrow (internal-review). The fund path binds in the ledger
1955
+ * (`credit-ledger.ts`'s `rail_mismatch`) and the Tempo close binds in
1956
+ * its credential verifier; this is x402's.
1957
+ */
1958
+ expectedChannelId: string;
1959
+ })): Promise<{
1960
+ ok: true;
1961
+ value: X402BatchAcceptance<T>;
1962
+ } | X402BatchRefusal>;
1963
+ getChannel(channelId: string): Promise<Channel | undefined>;
1964
+ /** Refresh one stored channel from the facilitator without settling or broadcasting anything. */
1965
+ observeChannel(channelId: string): Promise<Channel | undefined>;
1966
+ claimAndSettle(): Promise<void>;
1967
+ /**
1968
+ * Operator repair for a settlement wedged between chain and ledger
1969
+ * (internal-review). Absent without durable storage: with no settlement rows there
1970
+ * is no queue and nothing a repair could act on.
1971
+ */
1972
+ repair?: X402SettlementRepair;
1973
+ /**
1974
+ * The durable settlement read the credit ledger gates spending on
1975
+ * (internal-review). Absent without durable storage, on the same reasoning as
1976
+ * {@link repair}: with no settlement rows nothing can be wedged, so there is
1977
+ * nothing to gate.
1978
+ */
1979
+ settlementGate?: X402RefundSettlementGate;
1980
+ stop(): Promise<void>;
1981
+ }
1982
+ /** Options for constructing one DVM's batch-settlement server. */
1983
+ interface CreateX402BatchSettlementServerOpts {
1984
+ network: string;
1985
+ payTo: string;
1986
+ facilitator?: string;
1987
+ facilitatorAuth?: X402FacilitatorAuth;
1988
+ config: X402BatchSettlementConfig;
1989
+ /**
1990
+ * Chain RPC endpoint for settlement reconciliation; defaults to viem's bundled public one.
1991
+ *
1992
+ * Takes precedence over {@link X402BatchSettlementConfig.rpcUrl}, which is
1993
+ * where a builder configuring the rail declares it.
1994
+ */
1995
+ rpcUrl?: string;
1996
+ db?: Pool;
1997
+ /**
1998
+ * Durable store to adopt instead of building one from {@link db} (internal-review).
1999
+ *
2000
+ * `createDVMServer` builds it at ledger construction, because the credit
2001
+ * ledger's spend gate has to exist whether or not this server is ever
2002
+ * constructed. Adopting it here keeps one `PostgresX402ChannelStorage` — and
2003
+ * so one settlement pool — per mount, which is what the internal-review reasoning on
2004
+ * {@link PostgresX402ChannelStorage} rests on. Ignored when the config
2005
+ * supplies its own {@link X402BatchSettlementConfig.storage}.
2006
+ */
2007
+ channelStorage?: PostgresX402ChannelStorage;
2008
+ allowInMemory?: boolean;
2009
+ /**
2010
+ * Rail-native value the credit a channel funded has earned, in the channel
2011
+ * token's atomic units — `CreditLedgerLike.earnedNativeForX402Channel`.
2012
+ * `undefined` means the channel's credit could not be resolved.
2013
+ *
2014
+ * Required, not optional: it is the ceiling every claim is capped at
2015
+ * (internal-review), and a server that cannot derive one must claim nothing rather
2016
+ * than fall back to the deposit.
2017
+ */
2018
+ earnedNative: (channelId: string) => Promise<number | undefined>;
2019
+ /** Bounded host-probe result reused during resource initialization. */
2020
+ supportedResponse?: SupportedResponse;
2021
+ /** Exact-chain evidence override for deterministic tests and custom RPC infrastructure. */
2022
+ settlementEvidenceReader?: X402SettlementEvidenceReader;
2023
+ /** Authoritative channel-view override for deterministic tests. */
2024
+ channelStateReader?: (channelId: string) => Promise<X402BatchChannelObservation>;
2025
+ /**
2026
+ * The payout tracker (internal-review). Attached with this server's settle scope
2027
+ * and routed every claim and settle, so the batch that lands at `pay_to` is
2028
+ * reported with the value claimed into it, and the pending snapshot can say
2029
+ * what is still on the channels and whether the settle is wedged.
2030
+ *
2031
+ * @internal Wired by `createDVMServer` from the platform reporter; not a
2032
+ * builder surface, and not part of the extractable SDK contract (internal-review).
2033
+ */
2034
+ payout?: X402PayoutObserver;
2035
+ }
2036
+ /**
2037
+ * Initialize upstream's scheme and channel manager against dvmkit storage.
2038
+ * `resource.initialize()` performs the facilitator capability check and is
2039
+ * deliberately part of boot, so x402.org's missing authorizer refuses a
2040
+ * misconfigured server before it advertises a channel callers could fund.
2041
+ */
2042
+ declare function createX402BatchSettlementServer(opts: CreateX402BatchSettlementServerOpts): Promise<X402BatchSettlementServer>;
2043
+
2044
+ /** Payment state from upfront token verification. */
2045
+ interface PaymentInfo {
2046
+ /**
2047
+ * Rail value this payment contributes to the job, in millisatoshis.
2048
+ *
2049
+ * For a credit-drawn job (internal-review) this is the **draw's** allocated share of
2050
+ * the credit's rail value, not the amount the caller handed over on this
2051
+ * request — under an explicit fund-and-draw the caller may have funded twenty
2052
+ * jobs' worth. That makes it the revenue basis for this job: mid-job top-ups
2053
+ * accumulate onto it, and the terminal books exactly what the job consumed.
2054
+ */
2055
+ paidMsats: number;
2056
+ paymentMint?: string;
2057
+ /**
2058
+ * Rail that satisfied the upfront payment, if any. A {@link FundingMethod}
2059
+ * rather than a `PaymentMethod`: a draw against a Lightning-funded credit
2060
+ * reports `"lightning"` here (internal-review), which is what lets the revenue
2061
+ * ledger book it instead of silently dropping the row.
2062
+ */
2063
+ paymentRail?: FundingMethod;
2064
+ receivedProofs: ProofLike[];
2065
+ /**
2066
+ * Settlement reference for the credit. x402 carries the EVM tx hash from
2067
+ * the facilitator (internal-review); mpp carries the credential's `challenge.id`,
2068
+ * HMAC-bound to the realm so cross-DVM collisions are impossible (internal-review);
2069
+ * cashu accumulator (internal-review) sets it to the `X-Cashu-Request-Id` UUID
2070
+ * (internal-review). Required by the platform revenue ledger for `tempo`/`x402`
2071
+ * rails; nullable for cashu's `tx_hash` column.
2072
+ */
2073
+ paymentTxHash?: string;
2074
+ /** Chain transaction returned to a caller recovering this accepted request. */
2075
+ paymentTransactionHash?: string;
2076
+ /**
2077
+ * Rail-native payment amount in atomic units (internal-review). Sats for mpp,
2078
+ * USDC microunits for x402. Cashu leaves it unset — the existing
2079
+ * `paidMsats / 1000` derivation in the platform callback covers it.
2080
+ */
2081
+ nativeAmount?: number;
2082
+ /** Native asset tag matching `revenue_events.native_asset` (internal-review). */
2083
+ nativeAsset?: "sats" | "usdc" | "usdc.e" | "usd-cents";
2084
+ /**
2085
+ * Discriminator written into `revenue_events.metadata.cashu_flow` so ops
2086
+ * queries can split per-call cashu rows by source path (internal-review).
2087
+ */
2088
+ cashuFlow?: "p2pk_accumulator";
2089
+ /**
2090
+ * Base64-encoded x402 `SettleResponse` for the `X-PAYMENT-RESPONSE`
2091
+ * response header (internal-review). Set on the upfront-flow x402 success path;
2092
+ * `app.ts` attaches it to the 2xx response per x402 spec.
2093
+ */
2094
+ x402SettleResponseHeader?: string;
2095
+ /** Exact-settlement admission retained until the matching upfront job row is durable. */
2096
+ x402ExactAdmission?: {
2097
+ intentId: string;
2098
+ jobId: string;
2099
+ replayed: boolean;
2100
+ };
2101
+ /**
2102
+ * Credit-ledger linkage (internal-review). Set whenever the payment funded and/or
2103
+ * drew the ledger — every paid job with a ledger wired. Persisted on the
2104
+ * job so the terminal funnel can settle/release the draw and the receipt
2105
+ * can countersign the `ReceiptCredit` block.
2106
+ */
2107
+ creditId?: string;
2108
+ /** The draw placed for this job on `creditId` (internal-review). */
2109
+ drawId?: string;
2110
+ /** The draw's fiat amount, 1e-6 of {@link creditCurrency} (internal-review). */
2111
+ drawAmountMicro?: number;
2112
+ /** Currency the credit — and therefore the draw — is denominated in (internal-review). */
2113
+ creditCurrency?: string;
2114
+ /**
2115
+ * The funding leg, when this request actually moved money onto a credit
2116
+ * (internal-review). Reported to the platform as a **deposit**: a liability until
2117
+ * drawn, never summed into revenue. Absent on a pure draw against an
2118
+ * existing balance.
2119
+ */
2120
+ funding?: CreditFundingReport;
2121
+ /** Signed proof and snapshot for an explicit fund-and-draw funding leg. */
2122
+ fundingReceipt?: FundingReceipt;
2123
+ fundingCredit?: CreditSnapshot;
2124
+ /**
2125
+ * Fiat micro of {@link creditCurrency} this payment funded that bought the
2126
+ * job **nothing** (internal-review) — a second payment for an ask the job's draw
2127
+ * already covers, or the overpaid tail of an oversized one. It is available
2128
+ * balance on {@link creditId}, drainable at once, and the caller has to be
2129
+ * told or they will re-send. Absent on every ordinary payment.
2130
+ */
2131
+ unappliedMicro?: number;
2132
+ }
2133
+ /**
2134
+ * A funding event as the platform books it — one deposit row (internal-review, spec
2135
+ * §10). Keyed on `(dvmId, creditId, fundingId)`, so a retried report is a
2136
+ * no-op rather than a double-count.
2137
+ */
2138
+ interface CreditFundingReport {
2139
+ creditId: string;
2140
+ /** The rail's own settlement reference — the deposit's idempotency key. */
2141
+ fundingId: string;
2142
+ rail: FundingMethod;
2143
+ /** Rail value received, in millisatoshis. */
2144
+ paidMsats: number;
2145
+ /** Gross Cashu proof face before its receiver input fee; Cashu only. */
2146
+ grossPaidMsats?: number;
2147
+ /** Standalone NUT-02 fee reserved by the received Cashu proofs; Cashu only. */
2148
+ cashuInputFeeMsats?: number;
2149
+ /** Rail-native atomic units; omitted for cashu (the platform derives from msats). */
2150
+ nativeAmount?: number;
2151
+ nativeAsset?: "sats" | "usdc" | "usdc.e" | "usd-cents";
2152
+ mint?: string;
2153
+ /** Fiat micro credited to the ledger, and the currency it's denominated in. */
2154
+ amountMicro: number;
2155
+ currency: string;
2156
+ fundedAt: number;
2157
+ /** Current credit expiry after this funding committed (epoch ms). */
2158
+ expiryMs: number;
2159
+ /** TIP-1034 voucher identity, present only for session-funded credit. */
2160
+ tempoSession?: {
2161
+ channelId: string;
2162
+ cumulativeAmount: string;
2163
+ };
2164
+ /** TIP-1034 chain binding, reported so the platform can route a close (internal-review). */
2165
+ tempoChannel?: TempoChannelReport;
2166
+ }
2167
+ /**
2168
+ * The chain identity of a TIP-1034 channel, as reported to the platform
2169
+ * alongside its deposit (internal-review). Read from the DVM's own durable channel
2170
+ * store, never from the caller's credential.
2171
+ */
2172
+ interface TempoChannelReport {
2173
+ chainId: number;
2174
+ escrow: string;
2175
+ payee?: string;
2176
+ }
2177
+ /** Options for verifying an upfront payment on job submission. */
2178
+ interface UpfrontPaymentOpts {
2179
+ cashuToken?: string;
2180
+ requiredMsats?: number;
2181
+ mints?: string[];
2182
+ /** Base64-encoded x402 `PaymentPayload` from the `X-PAYMENT` header. */
2183
+ x402Payment?: string;
2184
+ x402Config?: X402Config;
2185
+ /** Durable exact-settlement coordinator. Production hosts wire this automatically. */
2186
+ x402ExactSettlement?: X402ExactSettlementServer;
2187
+ /** Exact generations the facilitator currently advertises. Defaults to both. */
2188
+ x402ExactVersions?: X402ExactVersionSupport;
2189
+ /** Reusable channel acceptor. Only consulted for fund-only `/v1/credit` requests. */
2190
+ x402BatchSettlement?: X402BatchSettlementServer;
2191
+ /**
2192
+ * Resource URL the client retried against. Bound into the rebuilt
2193
+ * `PaymentRequirements` when verifying x402, and surfaced on the 402 body
2194
+ * `accepts` array for x402 dual-serve (internal-review).
2195
+ */
2196
+ x402Resource?: string;
2197
+ /** Either the structured credential object or its serialized `Authorization: Payment <…>` value. */
2198
+ mppCredential?: MppxCredential | string;
2199
+ mpp?: MppxServer;
2200
+ /**
2201
+ * Request path of the resource being paid for (e.g. `/v1/job`). Bound into
2202
+ * each issued mppx challenge's `meta.path` so a credential can't be replayed
2203
+ * against a different paid endpoint of the same DVM (internal-review).
2204
+ */
2205
+ resourcePath?: string;
2206
+ /**
2207
+ * Whether `tempo/session` may be issued and newly accepted right now — the
2208
+ * platform close-observer and hosted settlement-readiness gates (internal-review,
2209
+ * internal-review), resolved per request by `DVMServer`. Absent means offered: a
2210
+ * self-hosted DVM operates these responsibilities itself.
2211
+ *
2212
+ * `false` refuses a session credential that would open a channel the platform
2213
+ * cannot safely protect or settle (internal-review, internal-review), and withholds session
2214
+ * challenges from every 402 this call emits, *except* on a credit already
2215
+ * bound to a TIP-1034 channel. That deposit is already exposed, so refusing
2216
+ * its vouchers only strands it; and since a channel-bound credit can be funded
2217
+ * no other way, withholding the challenge as well would have made the
2218
+ * acceptance carve-out unreachable through ordinary discovery.
2219
+ */
2220
+ tempoSessionOffered?: boolean;
2221
+ /** Machine-readable cause when {@link tempoSessionOffered} is false. */
2222
+ tempoSessionUnavailableReason?: "observer_unhealthy" | "settlement_unhealthy";
2223
+ /**
2224
+ * Tracks consumed `(realm, challenge.id)` pairs so a credential that was
2225
+ * already accepted on the upfront path can't be replayed within its
2226
+ * `expires` window (internal-review). Optional: when absent, the upfront flow has
2227
+ * no per-process replay protection beyond mppx's own expiry check.
2228
+ */
2229
+ consumedCredentialStore?: ConsumedCredentialStore;
2230
+ /** Cashu receive mode (internal-review / internal-review). Routes the receive path. */
2231
+ cashuMode?: CashuMode;
2232
+ /** Per-DVM Postgres pool — required for the `p2pk-accumulator` path. */
2233
+ accumulatorDb?: AccumulatorPool;
2234
+ /** DVM identifier — required for the `p2pk-accumulator` path. */
2235
+ dvmId?: string;
2236
+ /**
2237
+ * Active NUT-11 P2PK lock pubkeys — required for the `p2pk-accumulator`
2238
+ * path. Includes the current pubkey followed by retired pubkeys still
2239
+ * within the rotation grace window (internal-review). Caller resolves via
2240
+ * `loadLockPubkeyState` per-request so rotations propagate without a
2241
+ * server restart.
2242
+ */
2243
+ lockPubkeys?: LockPubkey[];
2244
+ /** UUIDv4 from `X-Cashu-Request-Id` header. */
2245
+ requestId?: string;
2246
+ /**
2247
+ * Runtime mint-health tracker (internal-review). Receive path short-circuits with
2248
+ * `cashu_mint_sick` when the token's mint is currently flagged sick, so
2249
+ * agents retry against a healthy alternative without waiting for the
2250
+ * per-call mint timeout to fire inside `acceptAccumulatorPayment`.
2251
+ */
2252
+ mintHealthTracker?: MintHealthTracker;
2253
+ /**
2254
+ * Host fx fetcher used to price the non-sat rails (x402 USDC, mppx fiat)
2255
+ * through the same BTC/USD source the quote path uses (internal-review). On a hosted
2256
+ * DVM this is the platform-served snapshot; self-hosted defaults to
2257
+ * CoinGecko-direct. Optional: a lazy default fetcher stands in when unset, so
2258
+ * the payment path never reaches `fetchBtcUsdRate`.
2259
+ */
2260
+ fxFetcher?: FxFetcher;
2261
+ /**
2262
+ * Credit ledger every payment funds and draws through (internal-review, spec §1):
2263
+ * an attached rail payment is an implicit N=1 funding — `fund` the paid
2264
+ * amount + `draw` it, atomically with the rail commit. When absent (or
2265
+ * when the price can't be fiat-denominated), the payment is accepted with
2266
+ * legacy semantics and a structured warning — money is never refused
2267
+ * because the ledger is unavailable.
2268
+ */
2269
+ creditLedger?: CreditLedgerLike;
2270
+ /**
2271
+ * Durable processed-payment markers for the x402/mpp rails (internal-review).
2272
+ * Those rails commit money outside dvmkit, so the marker row — written in
2273
+ * the same transaction as the ledger fund — is what makes a crash-replayed
2274
+ * credential unable to fund twice.
2275
+ */
2276
+ processedPayments?: ProcessedPaymentStore;
2277
+ /**
2278
+ * Reporter-owned transactional outbox enqueue (internal-review). When present,
2279
+ * every funding awaits this inside the rail's commit transaction, so money
2280
+ * and its durable deposit report land together.
2281
+ */
2282
+ enqueueCreditDeposit?: CreditDepositEnqueue;
2283
+ /**
2284
+ * The job price's fiat denomination, pinned by the caller from the same
2285
+ * quote surface that produced `requiredMsats` (internal-review). The ledger is
2286
+ * fiat-micro denominated; fund = draw = this amount in the exact-payment
2287
+ * case, so no fresh fx conversion happens on the verify hot path.
2288
+ */
2289
+ priceFiat?: PriceFiat;
2290
+ /** Pre-allocated job id — recorded on the draw so replays can recover the job. */
2291
+ jobId?: string;
2292
+ /** Durable submit fingerprint paired with {@link jobId} for exact-x402 job admission. */
2293
+ requestFingerprint?: string;
2294
+ /** Fingerprint including the caller-auth envelope, reserved to this exact intent. */
2295
+ requestAuthFingerprint?: string;
2296
+ /** Verified caller pubkey from the signed-request envelope, when the DVM has auth. */
2297
+ callerPubkey?: string;
2298
+ /** Requester id — funder identity fallback for DVMs without descriptor auth. */
2299
+ requesterId?: string;
2300
+ /**
2301
+ * Explicit credit fields extracted from the signed body (spec §2). Their
2302
+ * signature coverage and structural validation happen in `app.ts` before
2303
+ * payment verification; this layer enforces ownership, the funding
2304
+ * commitment, and the draw itself.
2305
+ */
2306
+ creditEnvelope?: CreditEnvelope;
2307
+ /**
2308
+ * Turn this verification into a **fund-only top-up** (internal-review): the rails
2309
+ * verify and commit exactly as they do for a job, but the ledger leg funds
2310
+ * without drawing — there is no job to pay for. Set by `POST /v1/credit`;
2311
+ * `requiredMsats` / `priceFiat` carry the requested top-up amount rather
2312
+ * than a job price.
2313
+ */
2314
+ fundOnly?: FundOnlyRequest;
2315
+ /**
2316
+ * Credit lifetime to mint, in ms — the DVM's advertised `credit.ttl`.
2317
+ * Defaults to {@link IMPLICIT_CREDIT_TTL_MS}. Threading it keeps the
2318
+ * funding menu honest: a DVM advertising a 7-day TTL must not mint 30-day
2319
+ * credits.
2320
+ */
2321
+ creditTtlMs?: number;
2322
+ }
2323
+ /** A fund-only top-up's identity: which credit, under which client-generated id. */
2324
+ interface FundOnlyRequest {
2325
+ /**
2326
+ * Target credit. `/v1/credit` always supplies one — either the caller's
2327
+ * `credit_id` or the deterministic id derived from `(pubkey, fund_id)` — so
2328
+ * a retry lands on the same credit and hits the `credit_fundings` key.
2329
+ */
2330
+ creditId?: string;
2331
+ /**
2332
+ * Builder opt-in for one-payment stablecoin credit. Absent fails closed;
2333
+ * only `POST /v1/credit` supplies this policy input.
2334
+ */
2335
+ allowOneShotStablecoin?: boolean;
2336
+ /** Client-generated idempotency key for this deposit (`credit_fundings` PK). */
2337
+ fundId: string;
2338
+ /**
2339
+ * SHA-256 hex over the raw funding artifact, signed by the caller (spec §2
2340
+ * condition 3). Checked before any rail work: it is what binds the artifact
2341
+ * presented on the header to the deposit named in the signed body.
2342
+ */
2343
+ commitment: string;
2344
+ }
2345
+
2346
+ /** Everything the issuer needs to sign for one DVM. */
2347
+ interface ReceiptIssuerOpts {
2348
+ /** 32-byte hex receipt secret — `DVMKIT_RECEIPT_KEY`, or an ephemeral dev key. */
2349
+ secret: string;
2350
+ /** DVM slug written into every receipt's `dvm` field. */
2351
+ dvm: string;
2352
+ /** Immutable DVM identity written into every signed receipt. */
2353
+ dvmId: string;
2354
+ }
2355
+ /**
2356
+ * Signs job receipts for one DVM (internal-review).
2357
+ *
2358
+ * Constructed only when a receipt key is wired, so the presence of an issuer
2359
+ * *is* the "this DVM emits receipts" signal that `/v1/info` advertises. Pure:
2360
+ * it holds the key and the slug and turns a terminal `JobRecord` plus an
2361
+ * allocated sequence number into signed bytes — sequence allocation and
2362
+ * persistence belong to the store, which is what makes issuance idempotent
2363
+ * across machines.
2364
+ */
2365
+ declare class ReceiptIssuer {
2366
+ /** x-only pubkey this issuer signs under; the attested `receipt_pubkey`. */
2367
+ readonly pubkey: string;
2368
+ private readonly secret;
2369
+ private readonly dvm;
2370
+ private readonly dvmId;
2371
+ constructor(opts: ReceiptIssuerOpts);
2372
+ /**
2373
+ * Build and sign the receipt for a terminal job. `seq` comes from
2374
+ * `JobStore.claimReceiptSeq`, `issuedAt` defaults to now (tests pin it).
2375
+ *
2376
+ * Free jobs get receipts too, with `paid.msats: 0` — they still consume a
2377
+ * sequence number and still attest an outcome. Whether feedback is
2378
+ * paid-only is the feedback layer's call, not this one's.
2379
+ *
2380
+ * `credit` is the resolved draw block (internal-review) — present on every job
2381
+ * whose payment funded/drew the ledger, absent otherwise. Signature-
2382
+ * compatible either way: `canonicalize` omits absent keys.
2383
+ */
2384
+ issue(record: JobRecord, seq: number, issuedAt?: number, credit?: ReceiptCredit): JobReceipt;
2385
+ /**
2386
+ * Countersign one reclaim event (internal-review). Unlike job receipts there is
2387
+ * no store-allocated sequence — the drain's own `ledger_seq` (taken under
2388
+ * the credit row lock, shared with draws) already orders it in the
2389
+ * per-credit evidence chain.
2390
+ */
2391
+ issueDrain(args: {
2392
+ creditId: string;
2393
+ drainId: string;
2394
+ event: DrainReceiptEvent;
2395
+ method: string;
2396
+ amountMicro: number;
2397
+ balanceAfterMicro: number;
2398
+ ledgerSeq: number;
2399
+ callerPubkey: string;
2400
+ issuedAt?: number;
2401
+ }): DrainReceipt;
2402
+ /** Countersign the immutable tuple captured when a credit funding committed. */
2403
+ issueFunding(args: {
2404
+ creditId: string;
2405
+ fundId: string;
2406
+ fundedMicro: number;
2407
+ balanceAfterMicro: number;
2408
+ ledgerSeq: number;
2409
+ callerPubkey: string;
2410
+ issuedAt?: number;
2411
+ }): FundingReceipt;
2412
+ }
2413
+
2414
+ /** Stable reason why the configured x402 exact rail cannot be advertised. */
2415
+ type X402FacilitatorUnavailableReason = "probe_failed" | "unsupported_exact";
2416
+ /** Versioned settlement capabilities advertised by one facilitator/network pair. */
2417
+ interface X402FacilitatorSupport {
2418
+ /** True when at least one exact generation can settle. */
2419
+ available: boolean;
2420
+ exact: X402ExactVersionSupport;
2421
+ batchSettlement: boolean;
2422
+ facilitator: string;
2423
+ network: string;
2424
+ reason?: X402FacilitatorUnavailableReason;
2425
+ error?: string;
2426
+ }
2427
+ /** Live, synchronous capability view consumed while payment responses are rendered. */
2428
+ interface X402FacilitatorHealthLike {
2429
+ /** Prime or refresh the capability observation. */
2430
+ refresh(): Promise<void>;
2431
+ /** Return current support and begin a background refresh when its TTL has elapsed. */
2432
+ support(): X402FacilitatorSupport;
2433
+ /** Last successful raw response, reused by batch initialization without another request. */
2434
+ supportedResponse?(): SupportedResponse | undefined;
2435
+ }
2436
+
2437
+ /** Options for creating a JobManager. */
2438
+ interface JobManagerOpts {
2439
+ /** Environment variables for ctx.env. */
2440
+ env: Record<string, string>;
2441
+ /** KV store for ctx.store. */
2442
+ store: KVStore;
2443
+ /** Cashu mints accepted for payment. */
2444
+ mints?: string[];
2445
+ /** Persistent backing store for job records. Default: in-memory. */
2446
+ jobStore?: JobStore;
2447
+ /** When true, payment messages auto-credit without verification. */
2448
+ devMode?: boolean;
2449
+ /** Payment methods this DVM accepts. Default: derived by the server from configured rails. */
2450
+ paymentMethods?: PaymentMethod[];
2451
+ /** x402 stablecoin payment configuration. */
2452
+ x402?: X402Config;
2453
+ /** Durable exact-settlement coordinator shared with upfront verification. */
2454
+ x402ExactSettlement?: X402ExactSettlementServer;
2455
+ /** Live facilitator capabilities for version-specific mid-job payment handling. */
2456
+ x402FacilitatorHealth?: X402FacilitatorHealthLike;
2457
+ /** MPP multi-rail payment handle (built via `createMppFromOpts`). */
2458
+ mpp?: MppxServer;
2459
+ /** Cashu receive mode (internal-review). */
2460
+ cashuMode?: CashuMode;
2461
+ /** Builder's NUT-11 P2PK lock pubkey (internal-review). Required for accumulator mode. */
2462
+ lockPubkey?: string;
2463
+ /** Postgres pool for wallet accumulator persistence. */
2464
+ db?: Pool;
2465
+ /**
2466
+ * Agent-wallet (internal-review): per-DVM identifier used for the scheduler advisory
2467
+ * lock and the `(dvm_id, request_id)` replay key. Defaults to `DVMKIT_DVM_ID`
2468
+ * env at boot in `createDVMHost()`.
2469
+ */
2470
+ dvmId?: string;
2471
+ /** Audience expected by persisted v2 caller statements. */
2472
+ authAudience?: SignedRequestAudience;
2473
+ /**
2474
+ * Shared fx fetcher used to convert USD-string descriptor prices and
2475
+ * fiat-form `requestPayment` calls into sats (internal-review). The SDK server owns
2476
+ * one per process so concurrent quotes share the 60s cache window.
2477
+ */
2478
+ fxFetcher: FxFetcher;
2479
+ /**
2480
+ * Signs a receipt at every terminal transition (internal-review). Absent when the
2481
+ * DVM has no receipt key wired — jobs then terminate exactly as before and
2482
+ * `/v1/info` doesn't advertise `receipts`.
2483
+ */
2484
+ receiptIssuer?: ReceiptIssuer;
2485
+ /**
2486
+ * Credit ledger for the terminal funnel (internal-review): a job carrying a
2487
+ * `creditId`/`drawId` settles its draw on success and releases it on
2488
+ * failure/cancel — this is how "no debit on job failure" is mechanically
2489
+ * real. The resolution runs before receipt issuance so the receipt's
2490
+ * `credit` block countersigns the post-resolution balance.
2491
+ */
2492
+ creditLedger?: CreditLedgerLike;
2493
+ /**
2494
+ * Durable processed-payment markers (internal-review), threaded through to the
2495
+ * mid-job top-up path (internal-review) so an x402/mpp payment's marker, `fund`, and
2496
+ * `growDraw` commit together.
2497
+ */
2498
+ processedPayments?: ProcessedPaymentStore;
2499
+ /**
2500
+ * Reporter-owned transactional deposit enqueue threaded into mid-job rail
2501
+ * commits (internal-review).
2502
+ */
2503
+ enqueueCreditDeposit?: CreditDepositEnqueue;
2504
+ /** Reporter outbox inserted atomically with a terminal draw release (internal-review). */
2505
+ enqueueCreditDrawRelease?: CreditDrawReleaseEnqueue;
2506
+ /** Credit lifetime a top-up mints with, from the DVM's advertised `credit.ttl`. */
2507
+ creditTtlMs?: number;
2508
+ /**
2509
+ * The DVM's declared pricing currency (internal-review), which is the currency of
2510
+ * every credit it opens. Threaded into the job context so a mid-job ask on a
2511
+ * job that holds no credit yet is still pinned in the denomination its first
2512
+ * payment will open the credit in (internal-review). `DVMServer` passes its resolved
2513
+ * `pricingCurrency`; a host building a `JobManager` directly falls back to the
2514
+ * descriptor, and then to `"usd"`.
2515
+ */
2516
+ pricingCurrency?: string;
2517
+ /**
2518
+ * Callback fired when a mid-job top-up moves money onto a credit (internal-review).
2519
+ * The platform books it as a **deposit** — a liability until drawn, never
2520
+ * summed into revenue — so a top-up that skipped this would leave the
2521
+ * outstanding-liability view short. Mirrors `DVMServer.reportCreditDeposit`,
2522
+ * which handles the upfront and `/v1/credit` legs.
2523
+ */
2524
+ onCreditFunded?: (info: CreditDepositPayload) => void | Promise<void>;
2525
+ /**
2526
+ * Callback fired when a paid job completes (internal-review). Container-runtime DVMs
2527
+ * use this to report revenue to the platform via `RevenueReporter`. Mirrors
2528
+ * the isolate path's `IsolateJobManager.opts.onJobCompleted`.
2529
+ */
2530
+ onJobCompleted?: (info: {
2531
+ dvmId: string;
2532
+ jobId: string;
2533
+ paidMsats: number;
2534
+ paymentMint?: string;
2535
+ rail: string;
2536
+ paymentTxHash?: string;
2537
+ nativeAmount?: number;
2538
+ nativeAsset?: string;
2539
+ cashuFlow?: string;
2540
+ creditId?: string;
2541
+ drawId?: string;
2542
+ drawAmountMicro?: number;
2543
+ creditCurrency?: string;
2544
+ settledAt?: number;
2545
+ fundingRef?: string;
2546
+ kind?: string;
2547
+ }) => void | Promise<void>;
2548
+ /**
2549
+ * Callback fired when the stale-job reaper force-fails a *paid* job — its
2550
+ * pending credit draw is released rather than settled, because only a
2551
+ * completed job settles a draw (internal-review). Container-runtime DVMs wire this
2552
+ * to a paid-job-death report → operator `notice` (internal-review). Never fires for
2553
+ * free jobs (`paidMsats === 0`). Fired fire-and-forget so a throw or a slow
2554
+ * report can't wedge the sweep.
2555
+ */
2556
+ onPaidJobDeath?: (info: {
2557
+ dvmId: string;
2558
+ jobId: string;
2559
+ capability: string;
2560
+ paidMsats: number;
2561
+ rail: string;
2562
+ reason: "worker_died_mid_job" | "stale_no_terminal_status";
2563
+ nativeAmount?: number;
2564
+ nativeAsset?: string;
2565
+ paymentMint?: string;
2566
+ }) => void | Promise<void>;
2567
+ /**
2568
+ * Callback fired when a settled credit draw cannot book a revenue event
2569
+ * because no usable payment rail is available (internal-review). Container-runtime
2570
+ * DVMs auto-wire this to the platform's deduped operator notice. Fired
2571
+ * fire-and-forget so alert delivery cannot block terminal handling or draw
2572
+ * reconciliation.
2573
+ */
2574
+ onRevenueSkippedNoRail?: (info: RevenueSkippedNoRailPayload) => void | Promise<void>;
2575
+ /**
2576
+ * Stale-job sweep threshold in ms (internal-review). Jobs in a non-terminal status
2577
+ * (`processing` / `working` / `awaiting-input`) whose `lastActivityAt` is
2578
+ * older than this are force-cancelled with reason `stale_no_terminal_status`,
2579
+ * so callers always reach a terminal status instead of polling forever after
2580
+ * a worker crash, OOM, or silent `messageAppender` failure. Default:
2581
+ * `2 × descriptor.idleTimeout + STALE_JOB_WATCHDOG_HEADROOM_MS`, matching
2582
+ * the internal-review acceptance criterion against scrape's `JOB_TIMEOUT_MS`.
2583
+ * Builders whose per-handler wall-clock bound exceeds 90s must override
2584
+ * this — otherwise the watchdog fires on a legitimate in-flight handler.
2585
+ * Set to `0` to disable (test seam).
2586
+ */
2587
+ staleJobTimeoutMs?: number;
2588
+ /**
2589
+ * Sweep interval for the stale-job watchdog. Default: `min(60s, max(15s,
2590
+ * tightestActiveTimeout / 4))`. Tests override to fire deterministically.
2591
+ */
2592
+ staleJobSweepIntervalMs?: number;
2593
+ /**
2594
+ * Worker-liveness watchdog in ms (internal-review). Jobs in `processing`/`working`
2595
+ * whose `lastActivityAt` is older than this are marked `failed` (dead
2596
+ * worker), separately from the longer `awaiting-input` idle timeout above.
2597
+ * Default: `descriptor.processingWatchdog * 1000` or
2598
+ * `DEFAULT_PROCESSING_WATCHDOG_MS`. Set to `0` to disable (test seam).
2599
+ */
2600
+ processingWatchdogMs?: number;
2601
+ /**
2602
+ * Interval at which locally-active `processing`/`working` jobs have their
2603
+ * `lastActivityAt` bumped so the processing watchdog only fires on a truly
2604
+ * dead worker (internal-review). Default: `DEFAULT_HEARTBEAT_INTERVAL_MS`. Set to
2605
+ * `0` to disable (test seam). Also floors how far the internal-review clock guard
2606
+ * may compensate a backwards step on the processing arm — a cadence close to
2607
+ * `processingWatchdogMs` leaves it no room and it compensates nothing.
2608
+ */
2609
+ heartbeatIntervalMs?: number;
2610
+ /**
2611
+ * Age in ms at which a `pending` credit draw whose `job_id` matches no row in
2612
+ * the job store is released as orphaned (internal-review). The draw commits before
2613
+ * the job row is persisted, so a crash or a fail-closed refusal in that
2614
+ * window strands a hold no terminal path can ever reach. Must stay
2615
+ * comfortably above the submit path's draw→persist latency. Set to `0` to
2616
+ * disable the watchdog entirely — stranded holds then stay stranded, so
2617
+ * treat it as a money knob, not a tuning one. Default:
2618
+ * {@link DEFAULT_ORPHAN_DRAW_AGE_MS}.
2619
+ */
2620
+ orphanDrawAgeMs?: number;
2621
+ /**
2622
+ * Age in ms at which a `pending` credit draw whose job row **is** terminal is
2623
+ * reconciled against that row's outcome (internal-review) — settled for a
2624
+ * `completed` job, released for a `failed`/`cancelled` one. Covers the hole
2625
+ * `orphanDrawAgeMs` cannot: `resolveCreditDraw` throwing on a ledger error
2626
+ * leaves a terminal job whose hold no later path retries.
2627
+ *
2628
+ * Rides the same scan as the orphan arm, which has two consequences for the
2629
+ * value you set here. Setting `orphanDrawAgeMs` to `0` disables this arm too.
2630
+ * And the effective gate is `max(orphanDrawAgeMs, terminalDrawReconcileAgeMs)`
2631
+ * — the scan never returns a row younger than its own cutoff, so anything
2632
+ * below `orphanDrawAgeMs` is silently clamped up to it. That floor is
2633
+ * deliberate rather than a wart: honouring a narrower value would mean
2634
+ * widening the scan, which hands the orphan arm rows younger than
2635
+ * `orphanDrawAgeMs` that it must not release. It can only ever delay a
2636
+ * settle, never advance one, which is the safe direction for a debit.
2637
+ *
2638
+ * Set to `0` to disable this arm alone — a money knob, not a tuning one.
2639
+ * Default: {@link DEFAULT_TERMINAL_DRAW_RECONCILE_AGE_MS}.
2640
+ */
2641
+ terminalDrawReconcileAgeMs?: number;
2642
+ /**
2643
+ * Sweep interval for the orphan-draw watchdog. Default:
2644
+ * {@link DEFAULT_ORPHAN_DRAW_SWEEP_INTERVAL_MS}. Set to `0` to disable — the
2645
+ * test seam, since suites drive `sweepOrphanDraws()` directly.
2646
+ */
2647
+ orphanDrawSweepIntervalMs?: number;
2648
+ /**
2649
+ * Grace window in ms a reactivating machine waits for a live original handler
2650
+ * to win the single-execution claim before replaying an `awaiting-input` job
2651
+ * itself (internal-review). Default: `DEFAULT_REACTIVATION_CLAIM_GRACE_MS`. Widen if
2652
+ * cross-machine NOTIFY latency causes spurious double executions; lower in
2653
+ * tests for speed.
2654
+ */
2655
+ reactivationClaimGraceMs?: number;
2656
+ }
2657
+ type ResolvedInput<I extends ZodLike | undefined> = I extends ZodLike<infer O> ? O : string;
2658
+ /**
2659
+ * A deposit this payment funded that the job row did **not** take (internal-review).
2660
+ *
2661
+ * Reached when two mid-job top-ups race on a free-then-paid job: each opens its
2662
+ * own implicit credit, and `verifyAndCredit`'s set-if-null bind keeps one. The
2663
+ * loser's money is real, owned by the caller, and reclaimable — but nothing
2664
+ * named it, so the caller would have to derive `imp:<rail>:<payment_id>` to
2665
+ * reach their own balance. This rides the 201 so they don't have to.
2666
+ *
2667
+ * The response is the fast surface, not the only one: the credit is opened
2668
+ * under the caller's verified pubkey, so `POST /v1/credit` op `balance` lists
2669
+ * it and op `drain` reclaims it once the hold resolves.
2670
+ */
2671
+ interface UnappliedCredit {
2672
+ credit_id: string;
2673
+ credited_micro: number;
2674
+ credit_currency: string;
2675
+ display: string;
2676
+ hint: string;
2677
+ }
2678
+ /**
2679
+ * What {@link JobManager.processPayment} tells the route. `error` is a refusal
2680
+ * to serialise verbatim; otherwise the payment was accepted, optionally
2681
+ * carrying an {@link UnappliedCredit} to fold into the 201.
2682
+ */
2683
+ type ProcessPaymentOutcome = {
2684
+ error: {
2685
+ body: Record<string, unknown>;
2686
+ status: number;
2687
+ };
2688
+ unappliedCredit?: undefined;
2689
+ } | {
2690
+ error?: undefined;
2691
+ unappliedCredit?: UnappliedCredit;
2692
+ };
2693
+ /**
2694
+ * Transport-agnostic job lifecycle manager.
2695
+ *
2696
+ * Handles job creation, handler invocation, message dispatch, persistence,
2697
+ * idle timeout, and durable replay. Shared by both the HTTP server and
2698
+ * relay provider.
2699
+ */
2700
+ declare class JobManager<State, InputSchema extends ZodLike | undefined> {
2701
+ private readonly descriptor;
2702
+ private readonly opts;
2703
+ private readonly _activeJobs;
2704
+ private readonly jobStore;
2705
+ private readonly idleTimers;
2706
+ private readonly cleanupTimers;
2707
+ /** Per-job NOTIFY unsubscribe handles for the durable-status watch (internal-review). */
2708
+ private readonly statusWatchers;
2709
+ /** Job ids whose durable-status re-read is in flight — coalesces notify storms (internal-review). */
2710
+ private readonly statusChecksInFlight;
2711
+ /** Job ids that were notified mid-re-read and must be re-checked (internal-review). */
2712
+ private readonly statusChecksQueued;
2713
+ private readonly sessionId;
2714
+ private nextJobId;
2715
+ private accumulatorMonitor?;
2716
+ private readonly staleJobTimeoutMs;
2717
+ private readonly processingWatchdogMs;
2718
+ private readonly reactivationClaimGraceMs;
2719
+ /**
2720
+ * Worker-heartbeat cadence (internal-review). Also floors the internal-review clock
2721
+ * guard's compensation on the processing arm — see
2722
+ * {@link staleSweepCompensationCapMs}.
2723
+ */
2724
+ private readonly heartbeatIntervalMs;
2725
+ private readonly orphanDrawAgeMs;
2726
+ private readonly terminalDrawReconcileAgeMs;
2727
+ private staleSweepTimer?;
2728
+ private heartbeatTimer?;
2729
+ private orphanDrawSweepTimer?;
2730
+ /** This manager holds {@link ORPHAN_SWEEP_CLAIMS} for its ledger. */
2731
+ private orphanSweepClaimed;
2732
+ /** Where a page-capped tick left off; cleared once a tick reaches the end. */
2733
+ private orphanSweepResumeFrom?;
2734
+ /**
2735
+ * A `Date.now()` reading that cannot regress within this manager's lifetime
2736
+ * (internal-review). Anchored at construction, so a manager already running when
2737
+ * the clock stepped is covered. The stale-job sweep's two cutoffs, the
2738
+ * orphan-draw age gate and the reactivation claim grace all read it; see
2739
+ * {@link createMonotonicClock} for what it costs and where it is clamped.
2740
+ */
2741
+ private readonly monotonicNowMs;
2742
+ private sweepInFlight;
2743
+ private orphanSweepInFlight;
2744
+ private heartbeatInFlight;
2745
+ /**
2746
+ * The denomination of every credit this DVM opens (internal-review). Resolved once,
2747
+ * with the same boundary fallback `DVMServer` applies for direct JavaScript
2748
+ * callers that force an incomplete value past the descriptor contract.
2749
+ */
2750
+ private readonly pricingCurrency;
2751
+ constructor(descriptor: DVMDescriptor<State, InputSchema>, opts: JobManagerOpts);
2752
+ /** Active in-memory jobs. */
2753
+ get activeJobs(): Map<string, ServerJob>;
2754
+ /** The backing job store. */
2755
+ get store(): JobStore;
2756
+ /**
2757
+ * True when this DVM will actually issue receipts (internal-review) — a key *and*
2758
+ * a store that can allocate sequence numbers and persist bytes write-once.
2759
+ * `/v1/info#receipts` reads this rather than the key alone, so the flag can
2760
+ * never promise something `issueReceipt` silently declines to do.
2761
+ */
2762
+ get receiptsEnabled(): boolean;
2763
+ /** True when the named capability exists on this descriptor. */
2764
+ hasCapability(name: string): boolean;
2765
+ /** Names of every capability this descriptor exposes — used for error envelopes. */
2766
+ capabilityNames(): string[];
2767
+ /**
2768
+ * Resolve a capability's static `price` to msats (internal-review).
2769
+ *
2770
+ * `"$X.XX"` is parsed as USD and converted via the SDK's shared fx fetcher.
2771
+ * Returns `undefined` for dynamic-priced capabilities (those that declare
2772
+ * `onQuote`) — the caller falls back to `computeDynamicPrice` in that path.
2773
+ */
2774
+ currentPriceMsats(capability: string): Promise<number | undefined>;
2775
+ /**
2776
+ * Translate a capability's static `price` into the fiat envelope used by
2777
+ * the per-capability `pricing.max` advertised on `/v1/info` (internal-review).
2778
+ * Returns `undefined` for dynamic-priced capabilities and for unknown
2779
+ * names (the route handler validates the name before invoking this).
2780
+ */
2781
+ currentPricingMax(capability: string): {
2782
+ amount: number;
2783
+ currency: string;
2784
+ } | undefined;
2785
+ /**
2786
+ * Parse a request body through a capability's input schema (internal-review).
2787
+ *
2788
+ * Resolution order when a capability declares an `input` schema:
2789
+ * 1. `body.data` is preferred — agents pass structured fields directly
2790
+ * (CLI builds this from `--param k=v`).
2791
+ * 2. Fallback: `JSON.parse(body.input)` for clients still on the legacy
2792
+ * JSON-string contract.
2793
+ * 3. Neither usable → throw `MissingStructuredInputError` so the route can
2794
+ * return `invalid_input` with a hint pointing at `/v1/info`.
2795
+ *
2796
+ * When the capability has no `input` schema, returns `body.input` raw
2797
+ * (primitive path).
2798
+ *
2799
+ * The internal-review credit envelope is removed before the schema runs, on both
2800
+ * branches (internal-review) — it rides the signed body but is protocol-level, so a
2801
+ * top-level `.strict()` capability schema would otherwise reject every
2802
+ * explicit draw as an unknown key, before the envelope was even extracted.
2803
+ * The persisted `record.input` the internal-review reactivation path re-reads is the
2804
+ * pre-Zod wire form, so it comes through the second branch carrying them too.
2805
+ */
2806
+ parseInput(body: {
2807
+ input?: string;
2808
+ data?: unknown;
2809
+ }, capability: string): unknown;
2810
+ /**
2811
+ * Look up the per-capability descriptor by name (internal-review). Throws when the
2812
+ * name doesn't exist — the route layer validates body.capability first, so
2813
+ * reaching this with an unknown name is a programmer error.
2814
+ */
2815
+ private requireCapability;
2816
+ /**
2817
+ * Soft capability lookup — `undefined` when the name isn't defined. The
2818
+ * single place the `descriptor.capabilities` index cast lives; callers that
2819
+ * tolerate a missing capability (the route layer pre-validates, or the cap
2820
+ * was deleted from the descriptor after a job was persisted) route through
2821
+ * here instead of re-casting inline.
2822
+ */
2823
+ private getCapability;
2824
+ /**
2825
+ * Allocate the id the next job will be created under (internal-review). The
2826
+ * submit path calls this BEFORE payment verification so the ledger draw
2827
+ * commits with its job linkage, then passes the id back through
2828
+ * `createJob`'s provenance. Ids allocated for requests whose payment is
2829
+ * refused are simply never used — the sequence has gaps, which nothing
2830
+ * reads meaning into.
2831
+ */
2832
+ allocateJobId(): string;
2833
+ /**
2834
+ * Create a new job from a request body. `provenance` (internal-review) is what the
2835
+ * idempotent-replay path on `POST /v1/job` reads back: the raw `job_token`
2836
+ * to re-issue, and the fingerprint + caller pubkey a retry must reproduce
2837
+ * to be given it.
2838
+ */
2839
+ createJob(body: {
2840
+ input: string;
2841
+ params?: Record<string, string>;
2842
+ }, requesterId: string, payment: PaymentInfo, requiredMsats: number | undefined, capability: string, requesterTokenHash?: string, provenance?: {
2843
+ requesterToken?: string;
2844
+ requestFingerprint?: string;
2845
+ requesterPubkey?: string;
2846
+ requestId?: string;
2847
+ authRequestPath?: string;
2848
+ /**
2849
+ * Pre-allocated id from {@link allocateJobId} (internal-review) — the submit
2850
+ * path allocates before payment verification so the ledger draw can
2851
+ * record the job it pays for.
2852
+ */
2853
+ jobId?: string;
2854
+ }): ServerJob;
2855
+ /** Build an SDKJobContext and attach it to the job. */
2856
+ buildAndAttachContext(job: ServerJob, parsedInput: unknown): SDKJobContext<State, ResolvedInput<InputSchema>>;
2857
+ /** Start the handler for a job. */
2858
+ runHandler(job: ServerJob, sdkCtx: SDKJobContext<unknown, unknown>): void;
2859
+ /** Dispatch a validated incoming message to the appropriate handler or pending resolver. */
2860
+ dispatchMessage(job: ServerJob, type: MessageType, content: unknown): Promise<void>;
2861
+ /**
2862
+ * Persist a job to the backing store.
2863
+ *
2864
+ * Awaits the per-job appender tail (internal-review) so that:
2865
+ * 1. The snapshot's `messages` is consistent with the `job_messages` table
2866
+ * — no row is still in flight at snapshot time.
2867
+ * 2. For terminal saves, the `DELETE FROM job_messages` inside `save()`
2868
+ * can't race a still-pending `appendOutgoing` for the final yield
2869
+ * message (which would otherwise wipe the row before an in-flight SSE
2870
+ * subscriber's NOTIFY-driven fetch can see it).
2871
+ */
2872
+ persistJob(job: ServerJob): Promise<void>;
2873
+ /**
2874
+ * Cross-machine terminal guard (internal-review). `buildContext`'s guard reads the
2875
+ * in-memory `job.status`, so on its own it only protects the machine running
2876
+ * the handler. On a multi-machine DVM a `DELETE /v1/job/:id` routinely lands
2877
+ * on a machine that isn't running the job — `app.ts` cancels it in the store
2878
+ * and this process learns of it only through the durable row.
2879
+ *
2880
+ * Adopting the durable status blocks the rest of the handler's writes via the
2881
+ * local guard, makes `handleJobTerminal` see a non-completed job so it skips
2882
+ * the revenue report, rejects a suspended handler's prompt/payment yields, and
2883
+ * fires `job.abort` so in-flight `ctx.fetch` calls tear down. Returns true when
2884
+ * the durable state won and the caller must skip its save.
2885
+ *
2886
+ * Two triggers: `watchDurableStatus`'s NOTIFY subscription (internal-review) fires
2887
+ * this within a round-trip of the remote cancel committing, which is what
2888
+ * actually stops the provider spend; `persistJob` calls it again on every save
2889
+ * as the backstop for the window where a cancel commits between a handler's
2890
+ * read and its write (the check-then-write it can still lose is covered by the
2891
+ * stores' terminal-sticky `save`, which keeps the row itself correct).
2892
+ */
2893
+ private adoptDurableTerminal;
2894
+ /**
2895
+ * Watch the durable status of a locally-active job (internal-review).
2896
+ *
2897
+ * `ctx.signal` is raised by `dispatchMessage`, which only runs on the machine
2898
+ * holding the job in `activeJobs`. On a multi-machine DVM the `DELETE` usually
2899
+ * lands somewhere else, and that machine's store-only cancel (`cancelJob`)
2900
+ * commits the terminal status, appends the `cancel` message, and `pg_notify`s
2901
+ * in one transaction. This subscription is how the handler's machine hears it:
2902
+ * on each notify, re-read the durable status and adopt it if it went terminal.
2903
+ * Without it, the handler only notices at its next store write, and the
2904
+ * in-flight provider call — the spend the cancel was meant to stop — runs to
2905
+ * completion.
2906
+ *
2907
+ * Every locally-emitted message notifies this machine too, so the re-read is
2908
+ * coalesced: at most one `getCounters` in flight per job, and none once the
2909
+ * job is locally terminal.
2910
+ */
2911
+ private watchDurableStatus;
2912
+ /**
2913
+ * Tear down a job's durable-status subscription (internal-review). Must be called
2914
+ * wherever a job leaves `activeJobs` — a leaked subscriber outlives the job on
2915
+ * the store's shared LISTEN connection. Idempotent.
2916
+ */
2917
+ unwatchDurableStatus(jobId: string): void;
2918
+ /**
2919
+ * NOTIFY-driven durable-status re-read, coalesced per job (internal-review).
2920
+ *
2921
+ * A notify that arrives while a re-read is in flight is queued rather than
2922
+ * dropped: the in-flight read may have observed the row a moment *before* the
2923
+ * cancel committed, and dropping its notify would put the abort back where
2924
+ * this issue found it — waiting for the handler's next store write.
2925
+ */
2926
+ private checkDurableTerminal;
2927
+ /**
2928
+ * Wire the Tx A appender when the store supports streaming.
2929
+ *
2930
+ * Tx A (internal-review): outgoing message + `pending_payment_msats` bump land in
2931
+ * the same transaction. The DB allocates the row's seq via
2932
+ * `UPDATE jobs SET next_seq = next_seq + 1 RETURNING` so concurrent inbound
2933
+ * traffic on different machines can never collide on the `(job_id, seq)` PK.
2934
+ * In-memory `job.seq` keeps its own counter for same-machine SSE listeners.
2935
+ *
2936
+ * Per-job serialisation (internal-review): chained through `job.messageAppenderTail`
2937
+ * so two synchronous `providerMessage` calls (e.g. `artifact` followed by
2938
+ * `complete`) commit their `appendOutgoing` transactions in call order.
2939
+ * Without this, the two transactions race for the `jobs` row lock and the
2940
+ * DB-side seq can be allocated in the opposite order — which both renumbers
2941
+ * the persisted messages and reorders the NOTIFY events feeding the
2942
+ * cross-machine SSE subscriber. The subscriber closes the stream on the
2943
+ * first yield message it sees, so a NOTIFY for `complete` arriving before
2944
+ * `artifact`'s NOTIFY silently drops the artifact.
2945
+ */
2946
+ private wireMessageAppender;
2947
+ /** Start (or restart) the idle timer for a job. */
2948
+ startIdleTimer(job: ServerJob): void;
2949
+ /** Clear the idle timer for a job. */
2950
+ clearIdleTimer(id: string): void;
2951
+ /** Cancel a job due to idle timeout. Exposed for the reactivation path's idle-expiry guard (internal-review). */
2952
+ cancelJobIdle(job: ServerJob): Promise<void>;
2953
+ /**
2954
+ * Force-terminate stale non-terminal jobs. Two status-aware arms:
2955
+ * • `awaiting-input` past `staleJobTimeoutMs` → `cancelled`
2956
+ * (`stale_no_terminal_status`) — the caller never paid / responded.
2957
+ * • `processing`/`working` past `processingWatchdogMs` → `failed`
2958
+ * (`worker_died_mid_job`) — the worker died mid-job. The heartbeat keeps
2959
+ * live workers fresh, so a stale row here means a dead process.
2960
+ *
2961
+ * Fires on a timer (wired in the constructor) and also callable on demand
2962
+ * (tests, ops). The CAS in `cancelStaleJob` makes this safe to run
2963
+ * concurrently from multiple DVM machines. Scoped to capabilities this
2964
+ * JobManager owns so multi-mount hosts don't steal stuck rows from sibling
2965
+ * descriptors — the local activeJobs teardown only makes sense on the
2966
+ * JobManager that actually hosted the zombie handler.
2967
+ *
2968
+ * **Both cutoffs float off the wall clock (internal-review).** Each is one
2969
+ * `Date.now()` sample compared against `last_activity_at`, stamped from a
2970
+ * different sample at a different moment, so a backwards step between the
2971
+ * two makes every row look more recently active than it is: the query
2972
+ * matches nothing and both arms go silent for the length of the skew. The
2973
+ * dead worker's job keeps its `processing` status, its credit hold stays
2974
+ * `pending`, and the internal-review paid-job-death alert — wired to this reaper —
2975
+ * never fires. Same defect, same host class, as internal-review's orphan sweep.
2976
+ *
2977
+ * **The compensation is capped** — see {@link staleSweepCompensationCapMs}
2978
+ * for where each arm's cap comes from — which internal-review did not need to do. Its eagerness costs at worst an early release
2979
+ * of an unclaimed hold, refused outright for any live job row; ours
2980
+ * force-*fails* a running job. `cancelStaleJob`'s CAS does not cover that:
2981
+ * `expectedActivityBefore` is the same compensated threshold the query used,
2982
+ * and a live worker's post-step heartbeat writes the low, post-step
2983
+ * `Date.now()`, so `last_activity_at <= expectedActivityBefore` still holds.
2984
+ * The CAS guards a job checking in *between* the find and the claim with a
2985
+ * stamp above the cutoff — a race, not a clock.
2986
+ *
2987
+ * **Some blindness is inherent, and no cap choice removes it.** A backwards
2988
+ * step of `S` inverts the stamp ordering: everything stamped after it reads
2989
+ * `S` older than everything stamped before. A live worker heartbeating right
2990
+ * now stamps `wall - S`; a job genuinely idle for `age` stamps `wall - age`.
2991
+ * The stale row is the lower of the two — the one a cutoff reaches first —
2992
+ * only once `age` exceeds `S`. Below that the reaper cannot tell them apart,
2993
+ * so any cutoff that claimed the stale job would force-fail every live
2994
+ * worker on the host with it. The cap picks where on that curve to sit: on
2995
+ * the defaults a row must have been silent for `S + window / 2`, and a live
2996
+ * worker keeps five missed heartbeats of margin. The
2997
+ * alternative to a missed reap is mild and self-healing — the orphan sweep
2998
+ * backstops the hold, and the arm recovers when the wall catches up. The
2999
+ * alternative to a false reap is an irreversible force-fail of paid work.
3000
+ *
3001
+ * The stamps themselves stay wall-clock epoch-ms on purpose — they are
3002
+ * written by whichever machine touched the job and read by every other one,
3003
+ * so only the reading side can float.
3004
+ */
3005
+ sweepStaleJobs(): Promise<{
3006
+ swept: number;
3007
+ }>;
3008
+ /**
3009
+ * How far one watchdog arm may lean on {@link monotonicNowMs} past the wall
3010
+ * clock to cover a backwards step (internal-review). The compensation is capped,
3011
+ * not free: a claimed row must still have been silent for
3012
+ * `windowMs - cap`, and unlike the orphan sweep, claiming eagerly here
3013
+ * force-fails a job that may well be alive.
3014
+ *
3015
+ * **Half the window** is the floor that governs a wide window — five missed
3016
+ * heartbeats on the processing defaults. On the `awaiting-input` arm it also
3017
+ * preserves a real invariant: with the derived
3018
+ * `2 x idleTimeout + headroom`, half is `idleTimeout + 45s`, so the sweeper
3019
+ * still cannot fire before the in-process idle timer would have. (A builder
3020
+ * who overrides `staleJobTimeoutMs` below `2 x idleTimeout` has already
3021
+ * opted out of that, guard or no guard.)
3022
+ *
3023
+ * **Two heartbeat intervals** is the floor that governs a window configured
3024
+ * close to the beat cadence — `processingWatchdog: 40` against the 30s
3025
+ * default beat, where half the window is less than one beat and a worker
3026
+ * heartbeating exactly on schedule would be reaped the moment the host's
3027
+ * clock stepped. That pair is marginal already; the guard must not make it
3028
+ * deterministic. The cap collapses to zero there, which is today's
3029
+ * behaviour: late, never wrong. The `awaiting-input` arm takes no such term
3030
+ * — nothing heartbeats it by design, and its refresh is an inbound caller
3031
+ * message on no cadence at all.
3032
+ *
3033
+ * **Why a magnitude and not a predicate.** The tempting sharper rule is to
3034
+ * compensate per row, fully, for any stamp that reads ahead of our wall —
3035
+ * one our own post-step heartbeat could not have written. It is unsafe: a
3036
+ * future-stamped row is equally what a live job heartbeated by a peer whose
3037
+ * clock runs fast looks like, and from the stamp alone the two are
3038
+ * indistinguishable. The same reasoning bounds the cap from the other side —
3039
+ * while our clock is stepped, the guard force-fails the live jobs of any
3040
+ * peer slower than `windowMs - cap`, which is 2.5 minutes of tolerated
3041
+ * disagreement on the defaults, far outside anything NTP-managed hosts show.
3042
+ */
3043
+ private staleSweepCompensationCapMs;
3044
+ /**
3045
+ * Resolve `pending` credit draws the terminal funnel can no longer reach.
3046
+ * Two arms over one scan, distinguished by whether the draw's job row exists.
3047
+ *
3048
+ * **No job row (internal-review) — release.** internal-review allocates the job id and
3049
+ * commits the ledger draw, with its `job_id` linkage, before `recordReplay`
3050
+ * and `persistJob` write the job row. A crash, a `draw_conflict` 409, or a
3051
+ * fail-closed `replay_detected` 401 in that window leaves a committed hold
3052
+ * that **no** terminal path can ever reach: `issueReceipt` →
3053
+ * `resolveCreditDraw` keys off the `creditId`/`drawId` persisted on the job
3054
+ * row, so with no row there is no release path at all and the caller's
3055
+ * available balance stays reduced forever. Cosmetic for an implicit N=1
3056
+ * credit, a silent balance shrink for an explicit N>1 one.
3057
+ *
3058
+ * **Terminal job row (internal-review) — reconcile to its outcome.** The funnel
3059
+ * demonstrably fails: `resolveCreditDraw` throws on a ledger/DB error,
3060
+ * `issueReceipt` catches it, logs `credit_resolve_failed` and returns
3061
+ * nothing, and until now nothing retried. Same stranded hold, reached through
3062
+ * a different door — and for a `completed` job it strands the platform's
3063
+ * books too, since draws *are* the revenue events (internal-review): the caller is
3064
+ * charged nothing, the balance never moves, and outstanding liability
3065
+ * (`deposits − draw revenue`) overstates forever. So a late settle books, off
3066
+ * the committed draw row, through the ordinary reporter path.
3067
+ *
3068
+ * **A non-terminal job row is still never touched** — the stale-job sweep
3069
+ * above drives those to terminal first, and its `issueReceiptForStoredJob`
3070
+ * resolves the draw on the way. Those skipped holds are why the scan
3071
+ * keyset-paginates rather than re-reading one batch: they stay `pending`
3072
+ * indefinitely (an `awaiting-input` job on a 30-minute idle timeout sits well
3073
+ * past the orphan cutoff), so a fixed first page of them would starve every
3074
+ * real orphan behind it, on every tick, forever. The cursor advances over
3075
+ * every row examined; a tick that hits {@link ORPHAN_DRAW_SWEEP_MAX_PAGES}
3076
+ * logs `orphan_draw_sweep_truncated` and the next one resumes where it
3077
+ * stopped, so progress is bounded per tick but never blocked.
3078
+ *
3079
+ * Fires on a timer (wired in the constructor) and also callable on demand
3080
+ * (tests, ops). Both `settle` and `release` are idempotent under the credit
3081
+ * row's `FOR UPDATE`, so concurrent sweepers on several machines — and a
3082
+ * sweeper racing the in-line funnel — resolve the hold exactly once; the
3083
+ * losers see `replayed: true` and book nothing.
3084
+ */
3085
+ sweepOrphanDraws(): Promise<{
3086
+ released: number;
3087
+ reconciled: number;
3088
+ }>;
3089
+ /**
3090
+ * The end-of-scan line for a tick that resolved nothing (internal-review, extended
3091
+ * by internal-review). Two shapes are worth a warning, and neither is the ordinary
3092
+ * one — the timer runs every 5 minutes per ledger in production.
3093
+ *
3094
+ * `examined > 0`: aged candidates were looked at and none moved. Usually
3095
+ * legitimate (their jobs are still running, or terminal inside the reconcile
3096
+ * grace period), but it is also what a silently skipped hold looks like.
3097
+ *
3098
+ * `examined === 0`: the query returned nothing. Ordinarily that means there
3099
+ * is nothing to do — but it is equally what a blinded scan looks like, which
3100
+ * is the case the internal-review gate could not reach. Probe for the oldest
3101
+ * `pending` hold at any age and speak up when one exists the scan should
3102
+ * have seen and didn't: already past the age gate outright, stamped ahead of
3103
+ * our clock (some other process's clock is fast), or hidden while our own
3104
+ * clock is held forward over a backwards step. A ledger holding nothing but
3105
+ * young draws stays quiet.
3106
+ *
3107
+ * The probe is diagnostics, so it is wrapped: a tick that swept cleanly must
3108
+ * never report failure because the extra read fell over. A probe that throws
3109
+ * says so on the same line rather than replacing the tick's outcome.
3110
+ */
3111
+ private reportSweepOutcome;
3112
+ /**
3113
+ * Resolve one aged `pending` draw whose job row reached terminal (internal-review),
3114
+ * mirroring what `resolveCreditDraw` would have done in line. Returns true
3115
+ * when this call is the one that moved the draw — the caller counts it, and
3116
+ * only it books.
3117
+ *
3118
+ * **The settle is gated on the job row naming _this_ draw.** The scan finds
3119
+ * the draw by `credit_draws.job_id`, but the job's own `credit_id`/`draw_id`
3120
+ * are written by a *later* transaction than the one that committed the draw
3121
+ * (`verifyIncomingPayment` commits fund+draw; `verifyAndCredit` binds the
3122
+ * row), and a free-then-paid job binds set-if-null. A store failure or a
3123
+ * second concurrent top-up therefore leaves a hold tagged with the job whose
3124
+ * row points somewhere else, or nowhere. Settling that would debit the caller
3125
+ * a second time for one job — and invisibly, since `revenue_events`'
3126
+ * `UNIQUE (dvm_id, job_id, rail)` would drop the booking. So an unbound hold
3127
+ * is **released**, never settled: the job's payment was the draw it is bound
3128
+ * to, and this one bought nothing.
3129
+ *
3130
+ * Concurrency needs no guard of its own. `settle`/`release` run under the
3131
+ * credit row's `FOR UPDATE`, so exactly one caller — a sibling reconciler,
3132
+ * or the in-line funnel arriving late — sees `replayed: false`, which makes
3133
+ * the booking gate a true mutex rather than a hopeful one. The one race left
3134
+ * open is another machine's `handleJobTerminal` stalled between resolving the
3135
+ * draw and booking it, which would double-report; the platform's
3136
+ * `(dvm_id, credit_id, draw_id)` and `(dvm_id, job_id, rail)` unique indexes
3137
+ * absorb that.
3138
+ *
3139
+ * `invalid_draw_state` — the funnel having already resolved the draw to the
3140
+ * other state — is the expected loss, logged and swallowed like the orphan
3141
+ * arm's, so one contended hold can't strand the rest of the batch.
3142
+ */
3143
+ private reconcileTerminalDraw;
3144
+ /**
3145
+ * Refresh `lastActivityAt` for jobs this process is actively running
3146
+ * (`processing`/`working`) so the processing watchdog only fires once the
3147
+ * worker is genuinely dead (internal-review). `awaiting-input` jobs are
3148
+ * deliberately excluded — their idle timeout must still elapse. Fires on the
3149
+ * heartbeat timer; also callable on demand for tests.
3150
+ */
3151
+ heartbeatActiveJobs(): Promise<{
3152
+ beat: number;
3153
+ }>;
3154
+ /**
3155
+ * Single-execution claim for a reactivating machine (internal-review). Before
3156
+ * replaying an `awaiting-input` job, wait a bounded grace window for a live
3157
+ * original handler to win the `awaiting-input → processing` CAS via its
3158
+ * NOTIFY wake. Resolves:
3159
+ * • `false` — the row left `awaiting-input` during the window (the original
3160
+ * handler claimed it / drove it terminal). Stand down; do not replay.
3161
+ * • `true` — the window elapsed still `awaiting-input` (the original worker
3162
+ * is gone or has no live resolver) AND this machine won the claim CAS.
3163
+ * Replay for dead-worker recovery.
3164
+ *
3165
+ * Biasing the original handler to win kills the double execution (double
3166
+ * substrate spend, clobbered artifact) without heartbeating `awaiting-input`
3167
+ * (which would break the caller-input idle timeout). Non-streamable stores
3168
+ * have no cross-machine race, so the claim is a no-op `true`.
3169
+ */
3170
+ claimAwaitingInputForReactivation(jobId: string): Promise<boolean>;
3171
+ /**
3172
+ * Record an inbound client message durably via Tx B (internal-review) and append
3173
+ * it to the in-memory `job.messages` array. Returns the DB-allocated seq
3174
+ * (`null` when the store isn't streamable — non-Postgres test fallback).
3175
+ *
3176
+ * Status starts as `pending-verification`; a follow-up Tx C call
3177
+ * (`processPayment` for payments, `markInboundVerified` for everything
3178
+ * else) transitions it to `verified` once the inbound is accepted.
3179
+ */
3180
+ recordInboundPending(job: ServerJob, msgType: MessageType, msgContent: Record<string, unknown>): Promise<number | null>;
3181
+ /**
3182
+ * Tx C complement for non-payment inbound messages — flip the row to
3183
+ * `verified` so cross-machine readers see it via `subscribeMessages`.
3184
+ */
3185
+ markInboundVerified(jobId: string, inboundSeq: number | null): Promise<void>;
3186
+ /**
3187
+ * Reactivate a suspended/event-driven job from the store. Builds the
3188
+ * in-memory `ServerJob`, wires the appender, builds the context, starts
3189
+ * the idle timer. The caller has already recorded the inbound via Tx B
3190
+ * and run Tx C (verify + credit); reactivation is now decoupled from
3191
+ * credit (internal-review, internal-review). The caller decides whether to run the
3192
+ * handler (Pattern A: status was `awaiting-input`) or to dispatch the
3193
+ * message to a custom handler (Pattern B).
3194
+ *
3195
+ * For DVMs that declare descriptor-level auth, the persisted
3196
+ * `record.input` carries the original signed envelope. We re-verify the
3197
+ * signature here (internal-review) so a DB-layer tamper — compromised admin, SQL
3198
+ * injection, malicious operator with DB access — can't silently feed an
3199
+ * attacker-supplied pubkey/envelope into the handler. The drift window
3200
+ * and replay store are deliberately skipped: the persisted timestamp is
3201
+ * from the original signing instant (long past the 5-min drift bound for
3202
+ * any long-running job), and the nonce was already committed at submission.
3203
+ * On failure: throws `SignedRequestError` ({@link AuthSchemaError} when
3204
+ * the capability has no input schema to verify against); the caller is
3205
+ * expected to mark the row failed and surface a 401 / 500 to the inbound
3206
+ * caller.
3207
+ *
3208
+ * What gets re-verified is the persisted row read back through
3209
+ * {@link signedRequestInput} — `app.ts` stores the pre-Zod wire form, so
3210
+ * those are the caller's own signed bytes, the same ones `/v1/quote` and the
3211
+ * submit checked (internal-review). That makes the tamper check strictly stronger
3212
+ * than the parsed form it replaced: an injected key the capability schema
3213
+ * doesn't declare used to be stripped before the signature was checked, so
3214
+ * it re-verified clean.
3215
+ */
3216
+ reactivateJob(record: JobRecord): {
3217
+ job: ServerJob;
3218
+ sdkCtx: SDKJobContext<unknown, unknown>;
3219
+ };
3220
+ /**
3221
+ * Verify an inbound payment and apply the credit via Tx C (internal-review).
3222
+ * The inbound message must have been recorded via Tx B already
3223
+ * (`recordInboundPending` returned `inboundSeq`). For non-streamable
3224
+ * stores (`inboundSeq === null`), falls back to in-memory mutation via
3225
+ * `applyPaymentInfoToJob`.
3226
+ *
3227
+ * Dev-mode short-circuit: auto-credits the outstanding
3228
+ * `pendingPaymentMsats` without external verification. Gated on
3229
+ * `devModeSkipsPaymentVerification` — the same predicate the upfront path
3230
+ * reads, so a dev server wired to a mint verifies mid-job payments for real
3231
+ * (internal-review) — plus the explicit `dev_auto` opt-out the dev console uses,
3232
+ * which is the one caller that has no wallet to pay from. `dev_auto` loses to
3233
+ * any proof riding the same message: money on the wire always takes the rail,
3234
+ * so the flag can never leave a real token unspent against a credited job.
3235
+ *
3236
+ * Tx C is not atomic with the rail commit (internal-review). `verifyIncomingPayment`
3237
+ * commits the proofs *and* the ledger leg in one transaction and returns;
3238
+ * only then does `verifyAndCredit` write the job row, on its own connection.
3239
+ * Everything after that call therefore runs with the caller's money already
3240
+ * moved, which is why the write is retried rather than left to throw, and why
3241
+ * an exhausted retry answers with the credit it landed on instead of a bare
3242
+ * 500. See `creditInbound`.
3243
+ */
3244
+ processPayment(job: ServerJob, body: {
3245
+ type: MessageType;
3246
+ content: Record<string, unknown>;
3247
+ }, inboundSeq: number | null, extra?: {
3248
+ lockPubkeys?: LockPubkey[];
3249
+ mintHealthTracker?: MintHealthTracker;
3250
+ }): Promise<ProcessPaymentOutcome>;
3251
+ /**
3252
+ * Tx C with a bounded retry (internal-review).
3253
+ *
3254
+ * By the time this runs on the verified path, the rail commit and the ledger
3255
+ * leg have already committed in a transaction this call is not part of — so a
3256
+ * throw here is money moved against a job row that records none of it, and
3257
+ * letting it propagate was the bug. A transient store error is the ordinary
3258
+ * cause and a second attempt clears it.
3259
+ *
3260
+ * The retry is safe by construction, not by convention: `verifyAndCredit`
3261
+ * CASes on `job_messages.status = 'pending-verification'` inside its own
3262
+ * transaction, so a call whose COMMIT actually landed before the ack was lost
3263
+ * finds the row already `verified` and returns `alreadyVerified: true`
3264
+ * carrying the counters and binding that first attempt committed. Re-applying
3265
+ * the delta is impossible either way.
3266
+ *
3267
+ * That argument covers every attempt but the last, whose error nothing
3268
+ * re-checks — and a connection-level fault is exactly the shape that loses
3269
+ * the ack on *all* of them, idempotent replays included (the no-op path
3270
+ * COMMITs too). So the exhausted path asks the row before reporting a
3271
+ * failure: `getVerifiedInbound` reads, transaction-free, whether a Tx C for
3272
+ * this seq committed. Without it a fully credited, fully bound job answers
3273
+ * `payment_unapplied` and invites a retry, and a caller who takes it pays
3274
+ * twice for one ask — the second payment growing the now-bound draw and
3275
+ * settling as revenue.
3276
+ *
3277
+ * The read runs on the pool that just failed those COMMITs, so it is retried
3278
+ * too, and a read that fails all the way through is reported as *unknown*
3279
+ * rather than as nothing: `CreditInboundExhaustedError.readError` is what
3280
+ * separates "checked the row, nothing landed" from "couldn't check", both on
3281
+ * the operator's log line and in the copy the caller acts on.
3282
+ */
3283
+ private creditInbound;
3284
+ /**
3285
+ * Answer a payment whose rail and ledger legs committed but whose job row
3286
+ * could not be written (internal-review) — every retry spent, the money real.
3287
+ *
3288
+ * The refusal names the credit the money landed on so the caller can act on
3289
+ * it directly, mirroring the short mid-job pay's 402 (`payment.ts`). It is a
3290
+ * 500 rather than a 402 on purpose: the payment was valid and was accepted,
3291
+ * so telling the caller it was insufficient would be a lie, and this is a
3292
+ * fault an operator should see in their 5xx rate. `retryable` is true because
3293
+ * the ask is genuinely still outstanding.
3294
+ *
3295
+ * Nothing is released here. The hold is left to the internal-review reconciler,
3296
+ * which makes the same decision — unbound draw ⇒ release, never settle — but
3297
+ * under the credit row's `FOR UPDATE`, after the job is terminal, where it
3298
+ * cannot race an in-flight COMMIT. Releasing from here would also be
3299
+ * irreversible: a released draw can be neither re-grown nor re-opened, so one
3300
+ * bad call would silently de-ledger every later top-up on the job.
3301
+ *
3302
+ * Whether the row was *read* is load-bearing on both surfaces. When the
3303
+ * post-exhaustion read-back failed as well, `credit_job_row_unwritten` says so
3304
+ * (`row_checked: false` plus the read's own error) instead of implying an
3305
+ * operator can trust the row is clean, and the caller gets the `"unconfirmed"`
3306
+ * copy rather than a flat "pay it again" it could be charged twice for.
3307
+ *
3308
+ * That copy overrides `display` as well as `hint`, on every branch including
3309
+ * the ledger-less one. `display` is the sentence an agent relays to its human
3310
+ * (repo convention: JSON carries the words, not just the facts), so a body
3311
+ * whose `hint` says the outcome is unknown while its `display` still asserts
3312
+ * the ask is outstanding is read as "pay again" by the audience that acts on
3313
+ * it — and a payment that did land is charged on top.
3314
+ */
3315
+ private reportUnappliedPayment;
3316
+ /**
3317
+ * Describe a deposit the job row never took, in the shape both the 201 and
3318
+ * the `payment_unapplied` 500 carry (internal-review). `undefined` when the payment
3319
+ * ran ledger-less (no credit ledger wired, or the top-up preflight skipped) —
3320
+ * there is no credit to name, so the 201 carries no `credit` block and the
3321
+ * 500 takes its words from `unappliedCopy` directly.
3322
+ */
3323
+ private unappliedCredit;
3324
+ /** Book a mid-job top-up's funding as a deposit (internal-review, spec §10). */
3325
+ private reportCreditDeposit;
3326
+ /**
3327
+ * Internal: build the BuildContextOpts shared between `createJob` and
3328
+ * `reactivateJob`. Centralised so persistence/terminal callbacks and the
3329
+ * internal-review NOTIFY-driven cross-machine wake stay in one place.
3330
+ */
3331
+ private buildContextOpts;
3332
+ /**
3333
+ * Internal: what unit `ctx.requestPayment`'s auto-credit gate may measure this
3334
+ * job's remaining pool in (internal-review).
3335
+ *
3336
+ * The regime is read off the DRAW, never off the job. Implicit N=1 is exactly
3337
+ * `credit_id === imp:<rail>:<draw_id>`, because `fundAndDraw` derives both
3338
+ * from the same rail payment id — a fact about a row that was written once
3339
+ * and never moves. The tempting alternative, comparing `job.payment_tx_hash`
3340
+ * against `drawSettlementRef` (what the mid-job preflight does to inherit
3341
+ * `drawBasis`), silently rots: Tx C overwrites that column on every mid-job
3342
+ * credit, and a payment whose ledger leg was skipped — a cross-rail top-up,
3343
+ * an fx outage, a draw already resolved — leaves the rail's own reference
3344
+ * there, so an explicit-draw job would read as implicit from then on.
3345
+ *
3346
+ * Never throws, and never answers `rail` on a guess. `rail` is claimed only
3347
+ * where `withDrawBasis` demonstrably left `PaymentInfo.paidMsats` alone — no
3348
+ * draw at all, or a draw carrying no rail value to overlay it with. A job that
3349
+ * drew and whose pool this can't read or denominate answers `unpriced`, which
3350
+ * the gate declines to auto-credit from: the msat figure it would otherwise
3351
+ * fall back on is a slice of the credit's rail value at a ratio pinned
3352
+ * whenever that credit was funded, which is the exact figure this issue exists
3353
+ * to stop spending against.
3354
+ */
3355
+ private resolveDrawPool;
3356
+ /** Internal: structured warn for the one path that can't price a drawn job's pool. */
3357
+ private warnDrawPool;
3358
+ /**
3359
+ * Issue the signed receipt for a job that has reached a terminal status
3360
+ * (internal-review). Idempotent and safe to call from every terminal path — the
3361
+ * store owns both the sequence allocation and the write-once persist, so
3362
+ * two machines racing on the same job converge on identical bytes and burn
3363
+ * exactly one sequence number.
3364
+ *
3365
+ * Never throws. A receipt is evidence about a job, not part of delivering
3366
+ * it: if signing or persistence fails we log structured and serve the
3367
+ * response without one (same posture as the `cashu_refund_failed` branch in
3368
+ * `context.ts`). The gap is visible to callers — the `seq` series skips a
3369
+ * number — which is precisely the completeness signal receipts exist for.
3370
+ */
3371
+ issueReceipt(record: JobRecord): Promise<JobReceipt | undefined>;
3372
+ /** Build the common settle/release args, adding the hosted release outbox when wired. */
3373
+ private drawResolutionArgs;
3374
+ /**
3375
+ * Settle or release a terminal job's credit draw (internal-review): `completed`
3376
+ * settles (the hold becomes a real debit); `failed`/`cancelled` releases
3377
+ * (the hold evaporates — "no debit on job failure", mechanically
3378
+ * superseding the internal-review/974 no-op refund for credit-paid jobs, including
3379
+ * the `ctx.fail` path and the stale-sweeper reap). Idempotent — a replayed
3380
+ * resolution returns the recorded state. Returns the `ReceiptCredit` block
3381
+ * for the receipt: `balance_after` is the recorded draw trajectory for a
3382
+ * settled draw, and the restored available balance for a released one.
3383
+ */
3384
+ private resolveCreditDraw;
3385
+ /**
3386
+ * The one place a terminal job's status becomes a ledger verb (internal-review):
3387
+ * `completed` settles the hold into a real debit, `failed`/`cancelled`
3388
+ * release it. Read by the in-line funnel (`resolveCreditDraw`) and by the
3389
+ * late reconciler (`reconcileTerminalDraw`) — two paths that must never
3390
+ * disagree about what an outcome means for the caller's money.
3391
+ */
3392
+ private settlesDraw;
3393
+ /**
3394
+ * Sign a locally-held terminal job's receipt and mirror it onto the
3395
+ * in-memory job, so the read paths served out of `activeJobs` during the
3396
+ * cleanup window hand back the same bytes as a cross-machine re-read.
3397
+ */
3398
+ private attachReceipt;
3399
+ /**
3400
+ * Read-path repair for a terminal job carrying no receipt (internal-review), for
3401
+ * either an in-memory job or a store record. Free in the steady state — it
3402
+ * returns on the `receipt` check without touching the store — so it costs
3403
+ * only on the cases it exists for:
3404
+ *
3405
+ * - **A crash between the two store calls.** `claimReceiptSeq` commits the
3406
+ * sequence number to the job row before `saveReceipt` writes the bytes; a
3407
+ * process death in that window would otherwise strand that number
3408
+ * forever, and a permanent gap is indistinguishable from the deliberate
3409
+ * suppression `seq` exists to expose. Re-issuing here reuses the already
3410
+ * committed number (the claim is idempotent per job) rather than
3411
+ * allocating a second one.
3412
+ * - **A transient store failure** at the terminal: `issueReceipt` logs
3413
+ * `receipt_issue_failed` and returns nothing rather than failing the job,
3414
+ * so the next read retries.
3415
+ * - **Jobs that terminated before the DVM had a receipt key.** They pick one
3416
+ * up on first read, with `issued_at` reflecting when it was signed.
3417
+ */
3418
+ ensureReceipt(target: ServerJob | JobRecord): Promise<void>;
3419
+ /**
3420
+ * Close out a terminal job whose caller hasn't already persisted it: sign
3421
+ * the receipt, then save the snapshot. The snapshot save never writes the
3422
+ * receipt column, so ordering only affects how soon a reader sees the
3423
+ * receipt — this way a caller polling immediately after the terminal
3424
+ * already finds it.
3425
+ */
3426
+ private finalizeTerminal;
3427
+ /**
3428
+ * Issue a receipt for a job this process doesn't hold in `activeJobs` — a
3429
+ * cross-machine cancel, or a row the stale sweeper just reaped. Re-reads the
3430
+ * record so the receipt is built from the committed terminal row rather than
3431
+ * from whatever the caller happened to have in hand.
3432
+ */
3433
+ issueReceiptForStoredJob(jobId: string): Promise<JobReceipt | undefined>;
3434
+ /** Handle job reaching terminal state (completed, failed, cancelled). */
3435
+ private handleJobTerminal;
3436
+ /**
3437
+ * Report a completed paid job's revenue (internal-review, re-keyed by internal-review).
3438
+ * Fire-and-forget — the `RevenueReporter` owns persistence and retry.
3439
+ *
3440
+ * For a **credit-backed** job the revenue event is the *settled draw* (spec
3441
+ * §10), and since internal-review the sats figure corrects the job's mirror of that
3442
+ * draw against the draw itself: `withDrawBasis` stamps the job when the
3443
+ * payment lands, but a Bitcoin credit's settle re-prices the draw against
3444
+ * the funding lots it consumed. The correction is a delta, because a job's
3445
+ * counter can carry legs no draw ever absorbed — except where the draw's
3446
+ * forecast was zero and the counter therefore never held a component to
3447
+ * correct. The event is dated at settlement rather than at report time. A
3448
+ * released draw — the failed/cancelled path — is deliberately unreachable
3449
+ * here, which is what makes "no debit on job failure" true in the books as
3450
+ * well as the ledger.
3451
+ *
3452
+ * For everything else (free-then-paid jobs, isolate-shaped flows, a DVM
3453
+ * whose price couldn't be fiat-denominated) nothing changes: the pre-credits
3454
+ * payload is reported verbatim.
3455
+ *
3456
+ * Takes the fields rather than a `ServerJob` so the internal-review reconciler — a
3457
+ * background sweep that holds no in-process job — books through this exact
3458
+ * path off the row it read. `paymentTxHash` is passed **verbatim**, never
3459
+ * recomputed as a `drawSettlementRef`: it is the same column the in-line
3460
+ * funnel reads, so the two bookings are byte-identical by construction and
3461
+ * the platform's `UNIQUE (rail, tx_hash)` dedupes them. Deriving it instead
3462
+ * would diverge on an implicit N=1 job (whose row carries the rail's own
3463
+ * reference) and on any job whose column a mid-job top-up overwrote — the
3464
+ * same rot `resolveDrawPool` documents as the reason not to read regime off
3465
+ * this column.
3466
+ *
3467
+ * Returns whether the report was handed to `onJobCompleted`, so a caller
3468
+ * that logs the booking says what actually happened rather than what it
3469
+ * assumed — the guards below still drop legitimate shapes (a free job, a
3470
+ * DVM with no reporter). Every drop that costs a booking logs first.
3471
+ *
3472
+ * The rail check sits **below** the draw re-read, not in the entry guard
3473
+ * (internal-review): a settled draw the terminal can't report under is a real debit
3474
+ * with no revenue event, permanently overstating outstanding liability
3475
+ * (`deposits − draw revenue`), and it used to return here in silence. Placed
3476
+ * after the re-read, `revenue_skipped_no_rail` can name the draw and its
3477
+ * amount, and can distinguish the four causes — see its `reason` below.
3478
+ */
3479
+ private bookRevenue;
3480
+ /** Schedule job removal from activeJobs after a delay so clients can still poll final status. */
3481
+ scheduleCleanup(job: ServerJob): void;
3482
+ /** Clear all timers. Called on server shutdown to allow clean exit. */
3483
+ shutdown(): void;
3484
+ }
3485
+
3486
+ /** A configured Tempo operator fee-token balance reading. */
3487
+ interface TempoSettlementBalanceObservation {
3488
+ address: `0x${string}`;
3489
+ network: string;
3490
+ token: `0x${string}`;
3491
+ balanceMicro: bigint;
3492
+ lowBalanceMicro: bigint;
3493
+ checkedAt: number;
3494
+ /** Whether balance and any fee-rejection latch both permit new sessions. */
3495
+ ready: boolean;
3496
+ }
3497
+ /** A redacted operator-funded transaction rejection. */
3498
+ interface TempoSettlementFailureObservation {
3499
+ address: `0x${string}`;
3500
+ network: string;
3501
+ token: `0x${string}`;
3502
+ operation: "settle" | "close";
3503
+ errorClass: "InsufficientFundsError";
3504
+ checkedAt: number;
3505
+ balanceMicro?: bigint;
3506
+ channelId?: string;
3507
+ creditId?: string;
3508
+ drainId?: string;
3509
+ }
3510
+ /** Listener for platform reporting and builder-supplied observability. */
3511
+ interface TempoSettlementReadinessListener {
3512
+ onBalance?(observation: TempoSettlementBalanceObservation): Promise<void> | undefined;
3513
+ onFailure?(observation: TempoSettlementFailureObservation): Promise<void> | undefined;
3514
+ }
3515
+ /** Fleet-wide persisted state for one DVM/operator/token readiness epoch. */
3516
+ interface TempoSettlementFailureState {
3517
+ latched: boolean;
3518
+ version?: string;
3519
+ balanceMicro?: bigint;
3520
+ }
3521
+ /** Durable latch operations shared by every Machine serving a hosted DVM. */
3522
+ interface TempoSettlementReadinessStateStore {
3523
+ readFailure(): Promise<TempoSettlementFailureState>;
3524
+ latchFailure(): Promise<TempoSettlementFailureState>;
3525
+ captureFailureBalance(version: string, balanceMicro: bigint): Promise<boolean>;
3526
+ clearFailureIfRefilled(version: string, balanceMicro: bigint, lowBalanceMicro: bigint): Promise<boolean>;
3527
+ clearFailureAfterSuccessfulTransaction(version: string): Promise<boolean>;
3528
+ }
3529
+ /** Construction inputs for {@link TempoSettlementReadiness}. */
3530
+ interface TempoSettlementReadinessOptions {
3531
+ address: `0x${string}`;
3532
+ network: string;
3533
+ token: `0x${string}`;
3534
+ lowBalanceMicro?: bigint;
3535
+ balanceCheckIntervalMs?: number;
3536
+ readBalance(): Promise<bigint>;
3537
+ stateStore?: TempoSettlementReadinessStateStore;
3538
+ now?: () => number;
3539
+ }
3540
+ /**
3541
+ * Readiness gate for the account that funds Tempo session settlement.
3542
+ *
3543
+ * The timer is deliberately unref'd: it reports only while the Fly Machine is
3544
+ * awake and can never keep one awake or cause an external wake. An explicit
3545
+ * fee rejection is stronger evidence than a balance reading, so hosted DVMs
3546
+ * persist that latch in their shared database until a later successful operator
3547
+ * transaction or an observed refill clears it for every Machine.
3548
+ */
3549
+ declare class TempoSettlementReadiness {
3550
+ private readonly opts;
3551
+ private readonly lowBalanceMicro;
3552
+ private readonly intervalMs;
3553
+ private readonly now;
3554
+ private readonly listeners;
3555
+ private timer;
3556
+ private inFlight;
3557
+ private lastBalanceMicro;
3558
+ private failureBalanceMicro;
3559
+ private failureVersion;
3560
+ private failureLatched;
3561
+ private failureStateKnown;
3562
+ private localFailureGeneration;
3563
+ private pendingFailureGeneration;
3564
+ private failurePersistence;
3565
+ constructor(opts: TempoSettlementReadinessOptions);
3566
+ /** Whether new Tempo session channels may be offered at this instant. */
3567
+ available(): boolean;
3568
+ /** Refresh shared failure state and hold the first balance read before gating. */
3569
+ ensureFresh(): Promise<void>;
3570
+ /** Add a reporting listener; the disposer removes only that listener. */
3571
+ subscribe(listener: TempoSettlementReadinessListener): () => void;
3572
+ /** Start the boot reading and in-process cadence. Idempotent. */
3573
+ start(): void;
3574
+ /** Stop the cadence without affecting any in-flight reading. */
3575
+ stop(): void;
3576
+ /** Read and report the configured fee-token balance now. */
3577
+ checkBalance(opts?: {
3578
+ successfulTransaction?: boolean;
3579
+ recoveryVersion?: string;
3580
+ }): Promise<TempoSettlementBalanceObservation>;
3581
+ /** Latch fleet-wide and report every redacted fee-funding rejection. */
3582
+ recordInsufficientFunds(failure: Omit<TempoSettlementFailureObservation, "address" | "network" | "token" | "errorClass" | "checkedAt" | "balanceMicro">): Promise<void>;
3583
+ /** Snapshot the failure version an operator-funded transaction may recover. */
3584
+ captureRecoveryVersion(): Promise<string | undefined>;
3585
+ /** Re-check after a landed transaction and recover only its preflight version. */
3586
+ recordSuccessfulTransaction(recoveryVersion?: string): Promise<void>;
3587
+ /** Exposed for timer-lifecycle assertions without reaching into Node internals. */
3588
+ timerHasRef(): boolean | undefined;
3589
+ private checkBalanceOnce;
3590
+ private captureFailureBalance;
3591
+ private refreshFailureState;
3592
+ private persistPendingFailure;
3593
+ private applyFailureState;
3594
+ private currentFailureState;
3595
+ private reportBalance;
3596
+ private notify;
3597
+ }
3598
+
3599
+ /**
3600
+ * Hono env for the SDK's DVM app. `unknownRoute` is set by
3601
+ * {@link unknownRouteNotFound} so the `dvm.request` span can classify a 404 as
3602
+ * scanner-probe noise vs a registered route's own 404 (internal-review).
3603
+ *
3604
+ * Exported as a type only, for route modules split out of this file
3605
+ * (`credit-routes.ts`) — a type-only import erases at compile time, so it
3606
+ * introduces no runtime cycle back into `app.ts`.
3607
+ */
3608
+ interface AppEnv {
3609
+ Variables: {
3610
+ requesterId: string;
3611
+ unknownRoute?: boolean;
3612
+ clientCompatibility?: ClientCompatibility;
3613
+ };
3614
+ }
3615
+
3616
+ /**
3617
+ * What a terminal drain transition needs in order to book its platform drain
3618
+ * report (internal-review). Every field is optional because a DVM can legitimately
3619
+ * run without platform reporting — a `dvmctl dev` server, a self-hosted
3620
+ * builder, a test host — and a drain must still be fulfillable there.
3621
+ */
3622
+ interface DrainReportWiring {
3623
+ /** The SDK's own Postgres, which holds both the ledger and the reporter's outbox. */
3624
+ db?: Pool;
3625
+ /** Platform DVM record ID, bound into every report. */
3626
+ dvmId?: string;
3627
+ /** Reporter-owned outbox write, run inside the transition's transaction. */
3628
+ enqueueCreditDrain?: CreditDrainEnqueue;
3629
+ }
3630
+
3631
+ /**
3632
+ * Signed body of a `POST /v1/credit` request (internal-review). The op discriminator
3633
+ * lives **inside** the signed bytes, so a network attacker can't rewrite a
3634
+ * `balance` read into a `drain`.
3635
+ *
3636
+ * Deliberately independent of `credit-envelope.ts`, which parses the credit
3637
+ * fields riding a `/v1/job` body: that shape pairs `credit_id` with a
3638
+ * mandatory `draw_id`, and a fund-only top-up has no draw. Two routes, two
3639
+ * schemas — sharing one would force a top-up to invent a draw id.
3640
+ */
3641
+ declare const CREDIT_REQUEST_SCHEMA: z.ZodObject<{
3642
+ op: z.ZodEnum<{
3643
+ drain: "drain";
3644
+ balance: "balance";
3645
+ fund: "fund";
3646
+ }>;
3647
+ credit_id: z.ZodOptional<z.ZodString>;
3648
+ fund: z.ZodOptional<z.ZodObject<{
3649
+ amount_micro: z.ZodNumber;
3650
+ fund_id: z.ZodString;
3651
+ commitment: z.ZodOptional<z.ZodString>;
3652
+ method: z.ZodOptional<z.ZodEnum<{
3653
+ tempo: "tempo";
3654
+ lightning: "lightning";
3655
+ }>>;
3656
+ intent: z.ZodOptional<z.ZodEnum<{
3657
+ charge: "charge";
3658
+ session: "session";
3659
+ }>>;
3660
+ }, z.core.$strip>>;
3661
+ drain: z.ZodOptional<z.ZodObject<{
3662
+ drain_id: z.ZodString;
3663
+ method: z.ZodOptional<z.ZodString>;
3664
+ payout: z.ZodOptional<z.ZodObject<{
3665
+ refund_pubkey: z.ZodOptional<z.ZodString>;
3666
+ address: z.ZodOptional<z.ZodString>;
3667
+ }, z.core.$strip>>;
3668
+ }, z.core.$strip>>;
3669
+ pubkey: z.ZodString;
3670
+ signature: z.ZodString;
3671
+ timestamp: z.ZodNumber;
3672
+ nonce: z.ZodString;
3673
+ }, z.core.$strip>;
3674
+ /** Parsed `/v1/credit` body. */
3675
+ type CreditRequest = z.infer<typeof CREDIT_REQUEST_SCHEMA>;
3676
+ /** Everything the `/v1/credit` handler needs from the server that mounts it. */
3677
+ interface CreditRouteDeps {
3678
+ /** Resolved credit sizing, or `undefined` when this DVM didn't opt in. */
3679
+ menu: () => Promise<CreditMenu | undefined>;
3680
+ /** Resolved builder policy; absent and older config fail closed. */
3681
+ creditConfig: ResolvedCreditConfig | undefined;
3682
+ auth: DVMAuthScheme | undefined;
3683
+ /** Exact `/v1/credit` audience and operation expected by the auth gate. */
3684
+ authDomain?: SignedRequestDomain;
3685
+ ledger: CreditLedgerLike;
3686
+ fxFetcher: FxFetcher;
3687
+ /** Per-DVM + per-request rail wiring, shared with `/v1/job` (see `railVerifyOpts`). */
3688
+ railOpts: (c: Context<AppEnv>) => Promise<UpfrontPaymentOpts>;
3689
+ /**
3690
+ * Book a committed top-up as a platform deposit (internal-review). A fund-only
3691
+ * top-up moves real money onto a credit whose draws will each book revenue
3692
+ * later, so skipping this would report the spending without the money that
3693
+ * backed it — the DVM's outstanding liability would go negative.
3694
+ */
3695
+ onDeposit: (funding: NonNullable<PaymentInfo["funding"]>) => void;
3696
+ /**
3697
+ * Platform drain-report wiring (internal-review), used by the cashu pickup — the
3698
+ * one terminal drain transition that lands on this caller-facing route.
3699
+ * Empty on a DVM with no platform reporting, which changes nothing about
3700
+ * the reclaim itself.
3701
+ */
3702
+ drainReport: DrainReportWiring;
3703
+ /**
3704
+ * Receipt signer, when this DVM has a receipt key (internal-review). Drain ops
3705
+ * countersign every reclaim event with it (internal-review); absent, drains still
3706
+ * work — the ledger stays authoritative, the caller just gets no portable
3707
+ * evidence artifact.
3708
+ */
3709
+ receiptIssuer: ReceiptIssuer | undefined;
3710
+ /**
3711
+ * The Lightning receive leg, when this DVM configured one (internal-review).
3712
+ * Absent ⇒ `method: "lightning"` is refused with `lightning_unavailable`
3713
+ * and the funding menu never advertised it in the first place.
3714
+ *
3715
+ * Only the *issue* path reaches through this. Settling goes via
3716
+ * {@link settleLightning}, which is where a settlement books its deposit.
3717
+ */
3718
+ lightning: {
3719
+ receive: LightningReceive;
3720
+ } | undefined;
3721
+ /**
3722
+ * Apply any Lightning invoice this caller has paid since the last check, and
3723
+ * book the deposit for each one that credits (internal-review). A no-op when the
3724
+ * rail isn't configured. Runs before the read ops so a balance reflects a
3725
+ * top-up that settled out of band, and behind the fund poll so a sibling
3726
+ * invoice on the same credit is swept with it.
3727
+ */
3728
+ settleLightning: (callerPubkey: string, creditId?: string) => Promise<void>;
3729
+ /** Hosted operator fee-token gate; absent on self-hosted DVMs. */
3730
+ tempoSettlementReadiness?: TempoSettlementReadiness;
3731
+ }
3732
+ /**
3733
+ * `POST /v1/credit` — the caller-facing surface of the credits settlement
3734
+ * layer (internal-review, spec §4/§7). Three ops on one signed, replay-protected
3735
+ * envelope, mirroring `/v1/job`'s auth discipline exactly:
3736
+ *
3737
+ * - **fund** — a top-up with no job attached. The rail artifact rides the same
3738
+ * headers as `/v1/job` (`X-Cashu` + `X-Cashu-Request-Id`, `X-PAYMENT`,
3739
+ * `Authorization: Payment …`) and a missing or bad one comes back as the
3740
+ * same 402 envelope, which is what lets a caller's existing resumable-submit
3741
+ * machinery drive a top-up with no new client code.
3742
+ * - **balance** — what do I hold here, including expired credits (their value
3743
+ * is still the caller's; spec §5), plus the funding menu, so the same read
3744
+ * also answers what may be added (internal-review).
3745
+ * - **drain** — reclaim the unspent balance (internal-review, spec §5 tier 3). One
3746
+ * op is both the request and the poll/pickup: the first call debits the
3747
+ * available balance into a `pending` liability; re-polling the same
3748
+ * `drain_id` returns the current state, delivers the parked Cashu token
3749
+ * when the builder's batch job has parked it, and is idempotent throughout.
3750
+ */
3751
+ declare function handleCreditRequest(c: Context<AppEnv>, deps: CreditRouteDeps): Promise<Response>;
3752
+
3753
+ /**
3754
+ * Pool-shaped handle the lock needs — `connect`, since the lock and the unlock
3755
+ * must ride one pinned client, plus `pg`'s resolved config when it's there (a
3756
+ * hand-rolled pool shape won't have it) so the size check below can run.
3757
+ * Declared here rather than reusing `AccumulatorPool` so the stores can depend
3758
+ * on this module without an import cycle; every SDK pool type (`Pool`,
3759
+ * `AccumulatorPool`, `CreditLedgerPool`) structurally satisfies it.
3760
+ */
3761
+ type InitLockPool = Pick<Pool, "connect"> & {
3762
+ readonly options?: {
3763
+ readonly max?: number;
3764
+ };
3765
+ };
3766
+ /**
3767
+ * The single session advisory-lock key serializing **all** SDK boot-time
3768
+ * schema work against concurrent creators — two machines cold-booting one DVM
3769
+ * against a fresh database, or parallel test files sharing one DB.
3770
+ *
3771
+ * Postgres' `IF NOT EXISTS` existence check is not atomic against a concurrent
3772
+ * creator: two sessions running the same `CREATE TABLE IF NOT EXISTS` at once
3773
+ * can raise `duplicate key value violates unique constraint
3774
+ * "pg_type_typname_nsp_index"` (23505) in the loser, which at SDK boot is an
3775
+ * unhandled throw that kills the machine (internal-review).
3776
+ *
3777
+ * Deliberately distinct from `PLATFORM_INIT_ADVISORY_LOCK` (17320917) — a
3778
+ * DVM's own database is not the platform database — and from the first-party
3779
+ * DVMs' own keys (cast 17320918, scrape 17320926, scribe 24700919). **A new
3780
+ * SDK store reuses this key rather than inventing a second one**: one key means
3781
+ * there is only ever one lock order, so the per-store lock-order inversion that
3782
+ * deadlocked internal-review can't recur.
3783
+ */
3784
+ declare const SDK_INIT_ADVISORY_LOCK = 17320927;
3785
+ /**
3786
+ * Run `fn` holding {@link SDK_INIT_ADVISORY_LOCK} on `db`. Every SDK
3787
+ * boot-path `CREATE TABLE IF NOT EXISTS` goes through here.
3788
+ *
3789
+ * Overlapping callers on the same pool share **one** acquisition: the first
3790
+ * connects and locks, concurrent and nested callers join the held lock, and
3791
+ * the last one out unlocks and releases the client. That keeps internal-review's
3792
+ * deliberately-parallel store-init batch parallel — six `init()`s racing each
3793
+ * other would otherwise queue behind one another — and keeps the uncontended
3794
+ * cost at one extra round trip per boot phase rather than one per store.
3795
+ * Sharing is safe for the DDL, which is what this lock is for: it excludes
3796
+ * *other machines*, and within one process no two inits create the same table.
3797
+ * It is not by itself a mutex between same-pool callers — two of them
3798
+ * concurrently running one non-idempotent statement still race, so anything
3799
+ * riding this lock that isn't `CREATE … IF NOT EXISTS` must carry its own
3800
+ * conflict handling (`seedLockPubkeyState`'s `ON CONFLICT DO NOTHING`).
3801
+ *
3802
+ * Session-level (not `pg_advisory_xact_lock`) because the DDL runs in
3803
+ * autocommit with no enclosing transaction, so the lock and the unlock ride a
3804
+ * dedicated pinned client — advisory locks are session-scoped and the unlock
3805
+ * must land on the backend that holds it. Released in `finally` on every path
3806
+ * including throws: an SDK boot that died holding this lock would block every
3807
+ * sibling until its connection closed.
3808
+ *
3809
+ * The guarded DDL runs on the pool's *other* connections, so the pool must
3810
+ * allow at least two — `pg`'s default `max` is 10 and the host never narrows
3811
+ * it, but a builder-supplied `Pool({ max: 1 })` would starve boot. That one
3812
+ * deadlocks with no error and reads as a dead database, so it is refused up
3813
+ * front instead.
3814
+ */
3815
+ declare function withSdkInitLock<T>(db: InitLockPool, fn: () => Promise<T>): Promise<T>;
3816
+
3817
+ /**
3818
+ * In-memory {@link CreditLedgerLike} for pool-less setups — devMode, direct
3819
+ * `createDVMServer` tests, and the in-process x402 money loop — mirroring the
3820
+ * `MemoryJobStore` precedent. Same semantics as the Postgres `CreditLedger`
3821
+ * (two-phase draws, replay-before-expiry, `draw_conflict`, gap-free
3822
+ * `ledger_seq`), minus durability and cross-machine safety: per-process only,
3823
+ * so production DVMs must use the Postgres ledger (spec §2 condition 2).
3824
+ * Every mutating verb is synchronous between awaits, so operations are
3825
+ * atomic in-process; the `tx` handles are accepted and ignored.
3826
+ */
3827
+ declare class MemoryCreditLedger implements CreditLedgerLike {
3828
+ /**
3829
+ * Per-process and lost on restart, so a DVM backed by this ledger never
3830
+ * advertises the funding menu outside devMode (internal-review) — funding on one
3831
+ * machine and drawing on another would `credit_not_found`.
3832
+ */
3833
+ readonly durable = false;
3834
+ private readonly credits;
3835
+ private readonly fundings;
3836
+ private readonly invoices;
3837
+ private readonly tempoLosses;
3838
+ private readonly x402Losses;
3839
+ private x402Settlements?;
3840
+ /** See {@link CreditLedger.useX402SettlementGate} — same contract (internal-review). */
3841
+ useX402SettlementGate(gate: X402RefundSettlementGate): void;
3842
+ fund(args: Parameters<CreditLedgerLike["fund"]>[0]): Promise<CreditSnapshot>;
3843
+ draw(args: Parameters<CreditLedgerLike["draw"]>[0]): Promise<DrawResult>;
3844
+ /** See {@link CreditLedger.blockingX402Refund} — same contract (internal-review). */
3845
+ blockingX402Refund(channelId: string | null | undefined, statuses: X402SettlementStatus[]): Promise<X402UnresolvedRefund | undefined>;
3846
+ /** The unresolved x402 refund that must stop an effect on `creditId` (internal-review). */
3847
+ private blockingRefundForCredit;
3848
+ growDraw(args: Parameters<CreditLedgerLike["growDraw"]>[0]): Promise<GrownDrawResult>;
3849
+ settle(args: Parameters<CreditLedgerLike["settle"]>[0]): Promise<DrawResolution>;
3850
+ release(args: Parameters<CreditLedgerLike["release"]>[0]): Promise<DrawResolution>;
3851
+ recordFunding(args: Parameters<CreditLedgerLike["recordFunding"]>[0]): Promise<FundingRecord>;
3852
+ getFunding(args: {
3853
+ creditId: string;
3854
+ fundId: string;
3855
+ }): Promise<FundingRecord | undefined>;
3856
+ completeFunding(args: Parameters<CreditLedgerLike["completeFunding"]>[0]): Promise<FundingRecord>;
3857
+ saveFundingReceipt(args: Parameters<CreditLedgerLike["saveFundingReceipt"]>[0]): Promise<FundingReceipt>;
3858
+ recordInvoice(args: Parameters<CreditLedgerLike["recordInvoice"]>[0]): Promise<CreditInvoiceRecord>;
3859
+ getInvoice(args: {
3860
+ creditId: string;
3861
+ fundId: string;
3862
+ }): Promise<CreditInvoiceRecord | undefined>;
3863
+ listPendingInvoices(args: {
3864
+ callerPubkey: string;
3865
+ creditId?: string;
3866
+ limit: number;
3867
+ }): Promise<CreditInvoiceRecord[]>;
3868
+ settleInvoice(args: Parameters<CreditLedgerLike["settleInvoice"]>[0]): Promise<InvoiceSettlement>;
3869
+ markInvoiceExpired(args: {
3870
+ creditId: string;
3871
+ fundId: string;
3872
+ }): Promise<CreditInvoiceRecord | undefined>;
3873
+ markInvoiceBlocked(args: {
3874
+ creditId: string;
3875
+ fundId: string;
3876
+ reason: string;
3877
+ }): Promise<CreditInvoiceRecord | undefined>;
3878
+ getInvoiceByPaymentHash(paymentHash: string): Promise<CreditInvoiceRecord | undefined>;
3879
+ listBlockedInvoices(args: {
3880
+ limit: number;
3881
+ after?: BlockedInvoiceCursor;
3882
+ includeResolved?: boolean;
3883
+ }): Promise<CreditInvoiceRecord[]>;
3884
+ reconcileBlockedInvoice(args: Parameters<CreditLedgerLike["reconcileBlockedInvoice"]>[0]): Promise<InvoiceReconciliation>;
3885
+ writeOffBlockedInvoice(args: {
3886
+ paymentHash: string;
3887
+ note?: string;
3888
+ nowMs?: number;
3889
+ }): Promise<InvoiceWriteOff | undefined>;
3890
+ private findByPaymentHash;
3891
+ private fundSync;
3892
+ private drawSync;
3893
+ /** Mid-job top-up growth (internal-review) — see `CreditLedger.growDraw` for the semantics. */
3894
+ private growDrawSync;
3895
+ get(creditId: string, opts?: {
3896
+ nowMs?: number;
3897
+ }): Promise<CreditSnapshot | undefined>;
3898
+ getForCaller(callerPubkey: string, opts?: {
3899
+ nowMs?: number;
3900
+ }): Promise<CreditSnapshot[]>;
3901
+ getDraw(args: {
3902
+ creditId: string;
3903
+ drawId: string;
3904
+ }): Promise<DrawRecord | undefined>;
3905
+ getDrawByJobId(jobId: string): Promise<DrawRecord | undefined>;
3906
+ listPendingDraws(creditId: string): Promise<DrawRecord[]>;
3907
+ listStalePendingDraws(args: {
3908
+ createdBeforeMs: number;
3909
+ limit: number;
3910
+ after?: StalePendingDrawCursor;
3911
+ }): Promise<DrawRecord[]>;
3912
+ /** Settled draws' native value for the credit an x402 channel funded (internal-review). */
3913
+ earnedNativeForX402Channel(channelId: string): Promise<number | undefined>;
3914
+ getCreditByX402Channel(channelId: string): Promise<CreditSnapshot | undefined>;
3915
+ terminalizeTempoCredits(evidence: TempoCreditLossEvidence, _tx?: CreditLedgerQuerier): Promise<TempoCreditLoss[]>;
3916
+ listTempoCreditLosses(args?: {
3917
+ limit?: number;
3918
+ }): Promise<TempoCreditLoss[]>;
3919
+ terminalizeX402Credit(evidence: X402CreditLossEvidence, _tx?: CreditLedgerQuerier): Promise<X402CreditLoss[]>;
3920
+ listX402CreditLosses(args?: {
3921
+ limit?: number;
3922
+ }): Promise<X402CreditLoss[]>;
3923
+ requestDrain(args: {
3924
+ creditId: string;
3925
+ drainId: string;
3926
+ callerPubkey: string;
3927
+ method: DrainMethod;
3928
+ payout: Record<string, string>;
3929
+ requireNoPending?: boolean;
3930
+ tx?: CreditLedgerQuerier;
3931
+ nowMs?: number;
3932
+ }): Promise<DrainRequestResult>;
3933
+ bitcoinDepositLiability(): Promise<BitcoinDepositLiability>;
3934
+ listFundingLots(creditId: string): Promise<FundingLot[]>;
3935
+ getDrain(args: {
3936
+ creditId: string;
3937
+ drainId: string;
3938
+ }): Promise<CreditDrainRecord | undefined>;
3939
+ listPendingDrains(): Promise<CreditDrainRecord[]>;
3940
+ listChannelDrains(args: {
3941
+ limit: number;
3942
+ after?: ChannelDrainCursor;
3943
+ createdBeforeMs: number;
3944
+ rail: "tempo";
3945
+ includeResolved?: boolean;
3946
+ }): Promise<CreditDrainRecord[]>;
3947
+ writeOffDrain(args: {
3948
+ creditId: string;
3949
+ drainId: string;
3950
+ note?: string;
3951
+ nowMs?: number;
3952
+ }): Promise<DrainWriteOff | undefined>;
3953
+ clearDrainWriteOff(args: {
3954
+ creditId: string;
3955
+ drainId: string;
3956
+ }): Promise<void>;
3957
+ releaseDrain(args: {
3958
+ creditId: string;
3959
+ drainId: string;
3960
+ tx?: CreditLedgerQuerier;
3961
+ nowMs?: number;
3962
+ }): Promise<DrainReleaseResult>;
3963
+ parkDrain(args: {
3964
+ creditId: string;
3965
+ drainId: string;
3966
+ token: string;
3967
+ fulfilment?: DrainFulfilment;
3968
+ tx?: CreditLedgerQuerier;
3969
+ nowMs?: number;
3970
+ }): Promise<CreditDrainRecord>;
3971
+ markDrainPickedUp(args: {
3972
+ creditId: string;
3973
+ drainId: string;
3974
+ tx?: CreditLedgerQuerier;
3975
+ nowMs?: number;
3976
+ }): Promise<DrainTransitionResult>;
3977
+ markDrainSent(args: {
3978
+ creditId: string;
3979
+ drainId: string;
3980
+ sentRef: Record<string, unknown>;
3981
+ tx?: CreditLedgerQuerier;
3982
+ nowMs?: number;
3983
+ }): Promise<DrainTransitionResult>;
3984
+ appendDrainReceipt(args: {
3985
+ creditId: string;
3986
+ drainId: string;
3987
+ receipt: Record<string, unknown>;
3988
+ tx?: CreditLedgerQuerier;
3989
+ }): Promise<void>;
3990
+ private requireDrain;
3991
+ private transitionRefusal;
3992
+ private resolve;
3993
+ private requireCredit;
3994
+ private snapshot;
3995
+ }
3996
+
3997
+ /** Postgres-backed JobStore with cross-machine message streaming via LISTEN/NOTIFY. */
3998
+ declare class PostgresJobStore implements StreamableJobStore, ReceiptIssuingStore {
3999
+ private pool;
4000
+ private listenChannel;
4001
+ private messageSubscribers;
4002
+ private notifySubscribers;
4003
+ private requestIdClaimLocks;
4004
+ constructor(pool: Pool);
4005
+ /** Run the CREATE TABLE migration. Call once at startup. */
4006
+ init(): Promise<void>;
4007
+ /** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
4008
+ private createTables;
4009
+ get(id: string): Promise<JobRecord | undefined>;
4010
+ /**
4011
+ * internal-review. Indexed by `idx_jobs_payment_tx_hash`. `paymentTxHash` is unique
4012
+ * per settlement in practice — the accumulator's
4013
+ * `UNIQUE(dvm_id, request_id, proof_secret)` is what makes a second job under
4014
+ * the same request id impossible — but order by `created_at` so a
4015
+ * hypothetical duplicate resolves to the job the caller actually paid for.
4016
+ */
4017
+ findJobByPaymentTxHash(txHash: string): Promise<JobRecord | undefined>;
4018
+ findJobByRequestId(requestId: string, requesterPubkey: string): Promise<JobRecord | undefined>;
4019
+ claimRequestId(claim: RequestIdClaim): Promise<RequestIdClaimResult | undefined>;
4020
+ resumeRequestId(claim: RequestIdClaim): Promise<RequestIdClaimResult | undefined>;
4021
+ private acquireRequestIdClaim;
4022
+ releaseRequestIdClaim(claim: RequestIdClaim): Promise<void>;
4023
+ save(record: JobRecord): Promise<void>;
4024
+ delete(id: string): Promise<void>;
4025
+ claimReceiptSeq(jobId: string): Promise<number>;
4026
+ saveReceipt(jobId: string, receipt: JobReceipt): Promise<JobReceipt | undefined>;
4027
+ appendOutgoing(jobId: string, message: OutgoingMessage, opts?: AppendOutgoingOptions): Promise<number>;
4028
+ recordInbound(jobId: string, message: OutgoingMessage): Promise<number>;
4029
+ verifyAndCredit(jobId: string, seq: number, credit: PaymentCreditDelta | null): Promise<VerifyAndCreditResult>;
4030
+ getVerifiedInbound(jobId: string, seq: number): Promise<VerifyAndCreditResult | undefined>;
4031
+ markInboundFailed(jobId: string, seq: number, _reason: string): Promise<void>;
4032
+ findStaleJobs(processingThresholdMs: number, awaitingThresholdMs: number, limit: number, capabilities: string[] | null): Promise<JobRecord[]>;
4033
+ heartbeatActiveJobs(jobIds: string[], now: number): Promise<void>;
4034
+ cancelStaleJob(jobId: string, expectedActivityBefore: number, reason: string, terminalStatus: "failed" | "cancelled"): Promise<boolean>;
4035
+ cancelJob(jobId: string, reason: string): Promise<boolean>;
4036
+ getCounters(jobId: string): Promise<JobCounters | undefined>;
4037
+ claimForProcessing(jobId: string): Promise<boolean>;
4038
+ tryReactivationLock<T>(jobId: string, fn: () => Promise<T>): Promise<{
4039
+ acquired: true;
4040
+ result: T;
4041
+ } | {
4042
+ acquired: false;
4043
+ }>;
4044
+ subscribeMessages(jobId: string, afterSeq: number, onMessage: (msg: Message) => void): Promise<() => void>;
4045
+ subscribeNotifications(jobId: string, onNotify: () => void): Promise<() => void>;
4046
+ getMessages(jobId: string, afterSeq: number): Promise<Message[]>;
4047
+ initStreaming(): Promise<void>;
4048
+ shutdownStreaming(): Promise<void>;
4049
+ private dispatch;
4050
+ /**
4051
+ * Drive a non-terminal job to `terminalStatus` and append its final `cancel`
4052
+ * message in one transaction. Backs both `cancelStaleJob` (which passes an
4053
+ * `expectedActivityBefore` cutoff, so the flip only lands while the row is
4054
+ * still stale — a live worker that heartbeats in between keeps its job) and
4055
+ * `cancelJob` (which passes `null`: an operator/caller cancel is
4056
+ * unconditional, gated only on the row still being non-terminal).
4057
+ *
4058
+ * The `SELECT … FOR UPDATE` makes the whole thing a CAS: two machines racing
4059
+ * to terminate the same job serialise on the row lock, and the loser's
4060
+ * status filter no longer matches. `pg_notify` fires inside the tx so the
4061
+ * machine running the handler wakes as soon as the terminal status is
4062
+ * visible.
4063
+ */
4064
+ private terminateJob;
4065
+ private fetchCountersTx;
4066
+ /**
4067
+ * The credit binding on the job row, read inside Tx C (internal-review). Used on the
4068
+ * paths that don't get it from a `RETURNING` — the `alreadyVerified` skip and
4069
+ * a non-payment inbound.
4070
+ */
4071
+ private fetchCreditBindingTx;
4072
+ }
4073
+
4074
+ /**
4075
+ * Postgres-backed KVStore. Requires the `pg` package as a peer dependency.
4076
+ *
4077
+ * Expired rows are filtered on read but never deleted. A periodic cleanup
4078
+ * mechanism (e.g. `DELETE FROM kv_store WHERE expires_at IS NOT NULL AND
4079
+ * expires_at < now()`) should be added before production use to prevent
4080
+ * unbounded table growth.
4081
+ */
4082
+ declare class PostgresKVStore implements KVStore {
4083
+ private pool;
4084
+ constructor(pool: Pool);
4085
+ /** Run the CREATE TABLE migration. Call once at startup. */
4086
+ init(): Promise<void>;
4087
+ /** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
4088
+ private createTables;
4089
+ get<T = unknown>(key: string): Promise<T | undefined>;
4090
+ set(key: string, value: unknown, opts?: {
4091
+ ttl?: number;
4092
+ }): Promise<void>;
4093
+ delete(key: string): Promise<void>;
4094
+ list(prefix?: string): Promise<string[]>;
4095
+ }
4096
+
4097
+ /** Options for `PostgresReplayStore`. */
4098
+ interface PostgresReplayStoreOpts {
4099
+ /** Table name. Defaults to `"signed_request_replays"`. Must match `/^[a-zA-Z_][a-zA-Z0-9_]*$/`. */
4100
+ tableName?: string;
4101
+ /**
4102
+ * Retention window in seconds — rows older than `now - windowSeconds` are
4103
+ * eligible for the sampled-in-line GC sweep. Defaults to 600 (10 minutes),
4104
+ * matching the SDK's signed-request retention default.
4105
+ */
4106
+ windowSeconds?: number;
4107
+ /**
4108
+ * Probability (per `checkAndRecord` call) of firing a fire-and-forget GC
4109
+ * sweep. Defaults to 0.01 — every ~100 writes runs one DELETE. Set to 0 to
4110
+ * disable GC entirely (rely on an external sweeper).
4111
+ */
4112
+ gcSampleRate?: number;
4113
+ }
4114
+ /**
4115
+ * Postgres-backed replay store implementing {@link SignedRequestReplayStore}.
4116
+ *
4117
+ * Cross-machine safe: relies on the PK on `(pubkey, timestamp, nonce)` with
4118
+ * `INSERT ... ON CONFLICT DO NOTHING` to detect duplicates atomically across
4119
+ * concurrent inserts from multiple machines. The consumer hands in a `Pool`
4120
+ * (matching `PostgresJobStore` / `PostgresKVStore`) and manages its lifecycle.
4121
+ *
4122
+ * Multi-machine deploys of secp256k1-auth'd DVMs SHOULD use this store to
4123
+ * close the per-process replay window — see {@link SignedRequestReplayStore}
4124
+ * for the threat model.
4125
+ */
4126
+ declare class PostgresReplayStore implements SignedRequestReplayStore {
4127
+ private pool;
4128
+ private tableName;
4129
+ private windowSeconds;
4130
+ private gcSampleRate;
4131
+ constructor(pool: Pool, opts?: PostgresReplayStoreOpts);
4132
+ /** Run the CREATE TABLE migration. Idempotent — safe to call repeatedly. */
4133
+ init(): Promise<void>;
4134
+ /** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
4135
+ private createTables;
4136
+ checkAndRecord(pubkey: string, timestamp: number, nonce: string, now: number): Promise<boolean>;
4137
+ private gcSweep;
4138
+ }
4139
+
4140
+ /** Protocol-owned request field carried inside a signed request body. */
4141
+ declare const REQUEST_ID_FIELD = "request_id";
4142
+ /** Auth-schema fields the protocol owns rather than the builder input schema. */
4143
+ declare function protocolEnvelopeIgnoreFields(schema: unknown): readonly string[];
4144
+ /** Remove protocol-owned credit and recovery fields before builder input parsing. */
4145
+ declare function stripProtocolEnvelope(schema: unknown, data: unknown): unknown;
4146
+ /** Read and validate the optional recovery id from a signed request body. */
4147
+ declare function requestIdFromEnvelope(data: unknown): string | undefined;
4148
+ /** True when the request body carries a request id field but its value is invalid. */
4149
+ declare function hasInvalidRequestId(data: unknown): boolean;
4150
+
4151
+ /**
4152
+ * Convenience one-liner over {@link createDVMHost} (internal-review). For
4153
+ * single-DVM hosts, `serve(dvm, opts)` is equivalent to
4154
+ * `createDVMHost(opts).mount(dvm).serve()`. Multi-DVM hosts go through
4155
+ * `createDVMHost` directly so each mount can carry its own prefix.
4156
+ *
4157
+ * This is **shorthand-over-host**, not a parallel code path — every wiring
4158
+ * step (Cashu/MPP/x402 envelope resolution, platform reporter, db, fx) flows
4159
+ * through `createDVMHost`. First-party DVMs that use `serve(...)` are NOT
4160
+ * exposed to the wiring-drift bug class fixed in internal-review/485/511, which was
4161
+ * about bypassing the host entirely. There is no migration to do.
4162
+ */
4163
+ declare function serve<State, InputSchema extends ZodLike | undefined = undefined>(descriptor: DVMDescriptor<State, InputSchema>, opts?: DVMHostOpts): Promise<{
4164
+ url: string;
4165
+ close: () => Promise<void>;
4166
+ }>;
4167
+
4168
+ export { BlockedInvoiceCursor, type BuildCreditMenuArgs, type BuilderIdentity, CREDIT_ENVELOPE_KEYS, CREDIT_REQUEST_SCHEMA, ClientCompatibility, ClientCompatibilityGate, type ConsumedCredentialStore, type CreateX402BatchSettlementServerOpts, type CreditEnvelope, CreditEnvelopeError, type CreditFundCommitment, CreditInvoiceRecord, CreditLedgerLike, CreditLedgerQuerier, type CreditMenu, type CreditRequest, type CreditRouteDeps, CreditSnapshot, type CreditTerms, DEFAULT_INVOICE_TTL_SECONDS, DVMAuthScheme, type DVMHost, type DVMHostOpts, DrawRecord, DrawResolution, DrawResult, FundingRecord, GrownDrawResult, IMPLICIT_CREDIT_TTL_MS, type InitLockPool, InvoiceReconciliation, InvoiceSettlement, InvoiceWriteOff, JobManager, type JobManagerOpts, JobRecord, JobStore, LightningReceive, type LightningReceiveConfig, MIN_INVOICE_TTL_SECONDS, MemoryConsumedCredentialStore, type MemoryConsumedCredentialStoreOpts, MemoryCreditLedger, MemoryProcessedPaymentStore, MemoryX402ExactSettlementStore, type MountOpts, type OwnerDisplay, type PlatformReporterOpts, PostgresJobStore, PostgresKVStore, PostgresProcessedPaymentStore, PostgresReplayStore, type PostgresReplayStoreOpts, PostgresX402ChannelStorage, PostgresX402ExactSettlementStore, type PriceFiat, type ProcessedPaymentQuerier, type ProcessedPaymentRail, type ProcessedPaymentRecord, ProcessedPaymentReplayError, type ProcessedPaymentStore, REQUEST_ID_FIELD, type ResolvedCashuOpts, RevenueSkippedNoRailPayload, SDK_INIT_ADVISORY_LOCK, SignedRequestAudience, SignedRequestDomain, SignedRequestReplayStore, StalePendingDrawCursor, StreamableJobStore, type X402BatchAcceptance, type X402BatchFunding, type X402BatchRefusal, type X402BatchSettlementServer, type X402ExactAcceptance, X402ExactIntentConflictError, type X402ExactSettlementAttempt, type X402ExactSettlementChainEvidence, type X402ExactSettlementEffect, X402ExactSettlementEvidenceMissingError, type X402ExactSettlementEvidenceReader, type X402ExactSettlementIntent, X402ExactSettlementNotReadyError, X402ExactSettlementServer, type X402ExactSettlementServerOpts, type X402ExactSettlementStatus, type X402ExactSettlementStore, type X402SettlementChainEvidence, type X402SettlementEvidenceReader, X402SettlementSubmissionError, X402_BATCH_AUTO_SETTLEMENT, X402_BATCH_MIN_WITHDRAW_DELAY_SECONDS, X402_BATCH_SETTLEMENT_NETWORK, attachCreditMenu, buildCreditMenu, createDVMHost, createX402BatchSettlementServer, creditEnvelopeIgnoreFields, drawSettlementRef, extractCreditEnvelope, fundingCommitment, handleCreditRequest, hasInvalidRequestId, protocolEnvelopeIgnoreFields, requestIdFromEnvelope, resolveCashuOptsFromEnv, selectPrimaryCredit, serve, stripCreditEnvelope, stripProtocolEnvelope, toCreditView, withSdkInitLock };