@dvmkit/sdk 0.0.0 → 0.1.0-rc.1

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