@dvmkit/sdk 0.1.0-rc.1 → 0.1.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,9 @@ var ENDPOINTS = {
8
8
  revenue: "/_internal/job-revenue",
9
9
  deposit: "/_internal/credit-deposit",
10
10
  release: "/_internal/credit-draw-release",
11
- drain: "/_internal/credit-drain"
11
+ drain: "/_internal/credit-drain",
12
+ payout: "/_internal/payout",
13
+ expiry_release: "/_internal/credit-expiry-release"
12
14
  };
13
15
  var RevenueReporter = class _RevenueReporter {
14
16
  // 24h of failed retries
@@ -83,6 +85,79 @@ var RevenueReporter = class _RevenueReporter {
83
85
  async enqueueCreditDrain(tx, payload) {
84
86
  await this.insertPending(tx, "drain", payload);
85
87
  }
88
+ /**
89
+ * Join a payout to the transaction that commits the movement it reports
90
+ * (internal-review) — the batch close, the accumulator mark, the drain booking.
91
+ * No network I/O, only the durable queue row through `tx`, the discipline
92
+ * {@link enqueueCreditDrain} set and for the same reason: the money has
93
+ * already moved on a rail this ledger does not own, so the platform row is
94
+ * the only record the builder's dashboard has of it, and a row lost between
95
+ * the fact and the queue would understate "paid out" forever. Without `tx`
96
+ * the row is queued on the pool, for callers with no fact of their own to
97
+ * commit. Idempotent on `(dvmId, payoutId)` platform-side, so the retry
98
+ * loop redelivers freely.
99
+ */
100
+ async enqueuePayout(payload, tx) {
101
+ await this.insertPending(tx ?? this.db, "payout", payload);
102
+ }
103
+ /**
104
+ * Report one landed payout on its own and try to deliver it at once — the
105
+ * live Tempo settlement, which has no dvmkit transaction to join. Durable
106
+ * like {@link report}.
107
+ */
108
+ async reportPayout(payload) {
109
+ return this.enqueue("payout", payload, payload.payoutId);
110
+ }
111
+ /**
112
+ * Report one rail's pending-pool snapshot (internal-review). Best-effort and
113
+ * fire-and-forget, deliberately unlike {@link reportPayout}: a snapshot is
114
+ * replaced by the next tick, so queueing a stale one behind an outage would
115
+ * only deliver figures the platform already has newer ones for.
116
+ */
117
+ async reportPayoutPending(payload) {
118
+ const url = `${this.platformUrl}/_internal/payout-pending`;
119
+ try {
120
+ const res = await fetch(url, {
121
+ method: "POST",
122
+ headers: {
123
+ "Content-Type": "application/json",
124
+ Authorization: `Bearer ${this.platformToken}`
125
+ },
126
+ body: JSON.stringify(payload),
127
+ signal: AbortSignal.timeout(1e4)
128
+ });
129
+ if (!res.ok) {
130
+ console.warn(
131
+ JSON.stringify({
132
+ level: "payout_pending_report_failed",
133
+ rail: payload.rail,
134
+ status: res.status
135
+ })
136
+ );
137
+ }
138
+ } catch (err) {
139
+ console.warn(
140
+ JSON.stringify({
141
+ level: "payout_pending_report_failed",
142
+ rail: payload.rail,
143
+ error: err instanceof Error ? err.message : String(err)
144
+ })
145
+ );
146
+ }
147
+ }
148
+ /**
149
+ * Join a credit-expiry release — or the revival that reverses one — to the
150
+ * transaction that made it true (internal-review).
151
+ *
152
+ * Same discipline as {@link enqueueCreditDrain}: no network I/O, only the
153
+ * durable queue row written through `tx`. A release whose row was lost after
154
+ * the sweep committed would understate committed value forever; a lost
155
+ * reversal would overstate it, which is the worse direction — the builder
156
+ * would see money as theirs that a revived credit can still buy work with.
157
+ */
158
+ async enqueueCreditExpiryRelease(tx, payload) {
159
+ await this.insertPending(tx, "expiry_release", payload);
160
+ }
86
161
  /**
87
162
  * Report a reaper-force-failed paid job to the platform (internal-review) so the
88
163
  * operator receives an ambient notice that a DVM died or wedged mid-job.
@@ -325,7 +400,7 @@ var RevenueReporter = class _RevenueReporter {
325
400
  [Date.now()]
326
401
  );
327
402
  for (const row of rows) {
328
- const label = row.payload.jobId ?? row.payload.creditId ?? row.id;
403
+ const label = row.payload.jobId ?? row.payload.creditId ?? row.payload.payoutId ?? row.id;
329
404
  try {
330
405
  await this.attemptDelivery(row.id, row.payload, row.kind, label);
331
406
  } catch {
@@ -1,8 +1,8 @@
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';
1
+ import { X as CashuMode, R as ResolvedCreditConfig, Y as FundingMethod, _ as CreditLedgerLike, $ as CreditSnapshot, g as CreditView, a0 as CreditInvoiceRecord, a1 as CreditDepositEnqueue, a2 as InvoiceSettlement, Z as ZodLike, a as DVMDescriptor, K as KVStore, o as JobStore, a3 as MppxServer, a4 as X402Config, p as PaymentMethod, a5 as ClientCompatibilityGate, a6 as Message, a7 as FundingReceipt, a8 as TopUpCapUnenforcedReason, a9 as JobReceipt, aa as AppendOutgoingOptions, S as SDKJobContext, ab as StepCache, v as ResponseContent, P as PaymentContent, ac as X402Receipt, ad as X402ExactVersionSupport, ae as X402SettlementIntent, af as X402FacilitatorAuth, ag as X402BatchSettlementConfig, ah as PostgresX402ChannelStorage, ai as PaymentRequirementsV2, aj as CreditLedgerQuerier, ak as X402SettlementCursor, al as X402SettlementStatus, am as X402SettlementWriteOff, an as X402RefundSettlementGate, ao as MppxCredential, J as JobRecord, ap as ReceiptCredit, aq as DrainReceiptEvent, ar as DrainReceipt, z as SignedRequestAudience, as as CreditDrawReleaseEnqueue, at as CreditDepositPayload, au as RevenueSkippedNoRailPayload, av as MessageType, aw as ClientCompatibility, ax as CreditDrainEnqueue, ay as DVMAuthScheme, B as SignedRequestDomain, az as DrawResult, aA as X402UnresolvedRefund, aB as GrownDrawResult, aC as DrawResolution, aD as FundingRecord, aE as BlockedInvoiceCursor, aF as InvoiceReconciliation, aG as InvoiceWriteOff, aH as DrawRecord, aI as StalePendingDrawCursor, aJ as TempoCreditLossEvidence, aK as TempoCreditLoss, aL as X402CreditLossEvidence, aM as X402CreditLoss, aN as DrainMethod, aO as DrainRequestResult, aP as BitcoinDepositLiability, aQ as FundingLot, aR as CreditDrainRecord, aS as ChannelDrainCursor, aT as DrainWriteOff, aU as DrainReleaseResult, aV as DrainFulfilment, aW as DrainTransitionResult, aX as StreamableJobStore, aY as ReceiptIssuingStore, aZ as RequestIdClaim, a_ as RequestIdClaimResult, a$ as OutgoingMessage, b0 as PaymentCreditDelta, b1 as VerifyAndCreditResult, b2 as JobCounters, G as SignedRequestReplayStore } from '../job-store-6gR4pZRP.js';
2
+ export { b3 as CLIENT_COMPATIBILITY_HEADERS, d as CanonicalEnvelope, b4 as ClientCompatibilityEnv, b5 as ClientCompatibilityRequirement, b6 as ClientSemVer, e as CreateSignedRequestVerifierOpts, b7 as CreditFundingBasis, b8 as CreditInvoiceStatus, b9 as CreditLedger, ba as CreditLedgerError, bb as CreditLedgerErrorCode, bc as CreditLedgerErrorDetails, bd as CreditLedgerPool, be as CreditStatus, bf as DRAIN_DELIVERY_RESERVE_SATS, bg as DVM_PROTOCOL_VERSION, bh as DrainConflictReason, bi as DrawRailValue, bj as DrawStatus, n as JobStatus, bk as ReplayStoreBackend, bl as RevenueSkippedNoRailReason, x as SIGNED_REQUEST_AUTH_ID, y as SIGNED_REQUEST_STATEMENT_VERSION, bm as Secp256k1AuthOpts, E as SignedRequestError, F as SignedRequestFailure, H as SignedRequestSignOpts, M as SignedRequestStatementHeader, N as SignedRequestVerifier, bn as X402ChannelStorageOpts, bo as X402RelayLockHolder, bp as X402RelaySubmissionLock, bq as X402RelaySubmissionLockError, br as allocateDrawValue, bs as clientCompatibilityAttributes, bt as clientCompatibilityMiddleware, bu as clientUpgradeRequired, O as createSignedRequestVerifier, bv as isStreamableJobStore, bw as parseClientCapabilities, bx as parseClientCompatibility, by as parseDvmClient, bz as parseProtocolVersion, bA as requireClientCompatibility, bB as secp256k1Auth, bC as signedRequestInput, V as signedRequestStatementHeader } from '../job-store-6gR4pZRP.js';
3
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';
4
+ import { F as FxFetcher } from '../ssrf-DZi-xJyn.js';
5
+ export { P as PinnedFetch, S as SSRFError, e as SSRFGuardOpts, f as SSRFReason, g as SSRFResolver, h as assertSafeUrl, j as createPinnedFetch } from '../ssrf-DZi-xJyn.js';
6
6
  import { Hono, Context } from 'hono';
7
7
  import { z } from 'zod';
8
8
  import { ProofLike } from '@cashu/cashu-ts';
@@ -11,6 +11,56 @@ import { Channel, AutoSettlementConfig } from '@x402/evm/batch-settlement/server
11
11
  import 'mppx';
12
12
  import '@x402/core/server';
13
13
 
14
+ /** The channel view the tracker sums claims over. */
15
+ interface X402TrackedChannel {
16
+ channelId: string;
17
+ chargedCumulativeAmount: string;
18
+ totalClaimed: string;
19
+ }
20
+ /** A refund settlement wedged between chain and ledger (internal-review), for the wedge list. */
21
+ interface X402WedgedRefund {
22
+ settlementId: string;
23
+ /** Epoch ms the settlement was first prepared. */
24
+ createdAt: number;
25
+ native?: number;
26
+ }
27
+ /** What the batch-settlement server hands the tracker once it knows its scope. */
28
+ interface X402PayoutContext {
29
+ /** `${network}|${payTo}|${token}` — the scope the settle-pending marker is keyed on. */
30
+ scope: string;
31
+ payTo: string;
32
+ network: string;
33
+ storage: {
34
+ list(): Promise<X402TrackedChannel[]>;
35
+ };
36
+ /** The scheduler's settle cadence — what a wedge's `nextRetry` is derived from. */
37
+ settleIntervalMs: number;
38
+ listWedgedRefunds?: () => Promise<X402WedgedRefund[]>;
39
+ }
40
+ /** The two manager verbs the tracker wraps. */
41
+ interface X402TrackedManager {
42
+ claim(...args: never[]): Promise<{
43
+ vouchers: number;
44
+ transaction: string;
45
+ }[]>;
46
+ settle(): Promise<{
47
+ transaction: string;
48
+ }>;
49
+ }
50
+ /**
51
+ * The batch-settlement server's view of the reporter: attach once with the
52
+ * scope, then route every claim and settle through the tracked manager so
53
+ * the open batch is kept and the settle emits the payout.
54
+ */
55
+ interface X402PayoutObserver {
56
+ attach(ctx: X402PayoutContext): void;
57
+ trackManager<M extends X402TrackedManager>(manager: M): M;
58
+ /** A settle that landed outside the tracked manager — the manual claim-and-settle's own retry loop. */
59
+ recordSettle(transaction: string): Promise<void>;
60
+ /** A settle attempt that failed outside the tracked manager. One call per attempt a builder would count as one. */
61
+ recordSettleFailure(error: unknown): Promise<void>;
62
+ }
63
+
14
64
  declare const lockPubkeyBrand: unique symbol;
15
65
  /**
16
66
  * NUT-11 P2PK lock pubkey, lowercase-by-construction. Hex-encoded compressed
@@ -1972,6 +2022,16 @@ interface CreateX402BatchSettlementServerOpts {
1972
2022
  settlementEvidenceReader?: X402SettlementEvidenceReader;
1973
2023
  /** Authoritative channel-view override for deterministic tests. */
1974
2024
  channelStateReader?: (channelId: string) => Promise<X402BatchChannelObservation>;
2025
+ /**
2026
+ * The payout tracker (internal-review). Attached with this server's settle scope
2027
+ * and routed every claim and settle, so the batch that lands at `pay_to` is
2028
+ * reported with the value claimed into it, and the pending snapshot can say
2029
+ * what is still on the channels and whether the settle is wedged.
2030
+ *
2031
+ * @internal Wired by `createDVMServer` from the platform reporter; not a
2032
+ * builder surface, and not part of the extractable SDK contract (internal-review).
2033
+ */
2034
+ payout?: X402PayoutObserver;
1975
2035
  }
1976
2036
  /**
1977
2037
  * Initialize upstream's scheme and channel manager against dvmkit storage.
@@ -1,15 +1,15 @@
1
+ import {
2
+ PostgresReplayStore
3
+ } from "../chunk-L4OYF4DQ.js";
1
4
  import {
2
5
  MemoryCreditLedger
3
- } from "../chunk-NTK5DJ6R.js";
6
+ } from "../chunk-OJ5WFIB2.js";
4
7
  import {
5
8
  PostgresJobStore
6
9
  } from "../chunk-6JZIX5WW.js";
7
10
  import {
8
11
  PostgresKVStore
9
12
  } from "../chunk-FROTD5XQ.js";
10
- import {
11
- PostgresReplayStore
12
- } from "../chunk-L4OYF4DQ.js";
13
13
  import {
14
14
  RAIL_REFUNDABLE,
15
15
  SIGNED_ENVELOPE_FIELDS,
@@ -82,7 +82,7 @@ import {
82
82
  isNonChannelBitcoinRail,
83
83
  netOwedSats,
84
84
  x402SettlementPending
85
- } from "../chunk-RPXHKMYE.js";
85
+ } from "../chunk-365P52XQ.js";
86
86
  import {
87
87
  applyRetryToPool,
88
88
  withPostgresRecoveryContext
@@ -3176,11 +3176,20 @@ async function createX402BatchSettlementServer(opts) {
3176
3176
  read: () => postgres.isSettlePending(settleScope),
3177
3177
  write: (pending) => postgres.setSettlePending(settleScope, pending)
3178
3178
  } : void 0;
3179
+ opts.payout?.attach({
3180
+ scope: settleScope,
3181
+ payTo: opts.payTo,
3182
+ network: opts.network,
3183
+ storage,
3184
+ settleIntervalMs: (settlementConfig.settleIntervalSecs ?? X402_BATCH_AUTO_SETTLEMENT.settleIntervalSecs ?? 300) * 1e3,
3185
+ ...postgres ? { listWedgedRefunds: () => listWedgedRefunds(postgres) } : {}
3186
+ });
3187
+ const tracked = opts.payout ? opts.payout.trackManager(manager) : manager;
3179
3188
  let scheduler;
3180
3189
  if (opts.config.autoSettlement !== false) {
3181
3190
  if (postgres && marker) {
3182
3191
  scheduler = createX402FleetSettlementScheduler({
3183
- manager,
3192
+ manager: tracked,
3184
3193
  storage,
3185
3194
  config: settlementConfig,
3186
3195
  withLock: (operation) => postgres.withFleetSettlementLock(operation),
@@ -3521,7 +3530,7 @@ async function createX402BatchSettlementServer(opts) {
3521
3530
  if ("withdrawalPending" in result) {
3522
3531
  try {
3523
3532
  const claim = async () => {
3524
- const results = await manager.claim({
3533
+ const results = await tracked.claim({
3525
3534
  maxClaimsPerBatch: 100,
3526
3535
  selectClaimChannels: (channels, context) => claimCeiling(
3527
3536
  channels.filter(
@@ -3567,13 +3576,19 @@ async function createX402BatchSettlementServer(opts) {
3567
3576
  } : {},
3568
3577
  async claimAndSettle() {
3569
3578
  const operation = async () => {
3570
- const claims = await manager.claim({
3579
+ const claims = await tracked.claim({
3571
3580
  maxClaimsPerBatch: 100,
3572
3581
  selectClaimChannels: claimCeiling
3573
3582
  });
3574
3583
  if (claims.length > 0) await marker?.write(true);
3575
3584
  if (claims.length === 0 && !await marker?.read()) return;
3576
- await settleOutstandingClaims(manager);
3585
+ try {
3586
+ const settled = await settleOutstandingClaims(manager);
3587
+ await opts.payout?.recordSettle(settled.transaction);
3588
+ } catch (error) {
3589
+ await opts.payout?.recordSettleFailure(error);
3590
+ throw error;
3591
+ }
3577
3592
  await marker?.write(false);
3578
3593
  };
3579
3594
  if (postgres) await postgres.withFleetSettlementLock(operation, true);
@@ -3588,12 +3603,27 @@ async function createX402BatchSettlementServer(opts) {
3588
3603
  }
3589
3604
  };
3590
3605
  }
3606
+ async function listWedgedRefunds(postgres) {
3607
+ const page = await postgres.listSettlements({
3608
+ statuses: X402_WEDGED_SETTLEMENT_STATUSES,
3609
+ updatedBeforeMs: Date.now() - WEDGED_REFUND_MIN_AGE_MS,
3610
+ limit: 50
3611
+ });
3612
+ return page.settlements.filter((intent) => intent.operation === "refund").map((intent) => {
3613
+ const amount = intent.response?.amount;
3614
+ return {
3615
+ settlementId: intent.settlementId,
3616
+ createdAt: intent.createdAt,
3617
+ ...typeof amount === "string" && /^\d{1,15}$/.test(amount) ? { native: Number(amount) } : {}
3618
+ };
3619
+ });
3620
+ }
3621
+ var WEDGED_REFUND_MIN_AGE_MS = 5 * 6e4;
3591
3622
  async function settleOutstandingClaims(manager) {
3592
3623
  const retryDelaysMs = [250, 500, 1e3, 2e3];
3593
3624
  for (let attempt = 0; ; attempt++) {
3594
3625
  try {
3595
- await manager.settle();
3596
- return;
3626
+ return await manager.settle();
3597
3627
  } catch (error) {
3598
3628
  const message = error instanceof Error ? error.message : String(error);
3599
3629
  if (!message.includes("invalid_batch_settlement_evm_nothing_to_settle") || attempt >= retryDelaysMs.length) {
@@ -9732,8 +9762,29 @@ async function handleMarkMelted(c, opts) {
9732
9762
  );
9733
9763
  }
9734
9764
  }
9735
- await recordMeltAudit(opts.db, rowIds, { meltQuoteId, paymentPreimage });
9736
- await markRowsMelted(opts.db, rowIds);
9765
+ const client = await opts.db.connect();
9766
+ try {
9767
+ await client.query("BEGIN");
9768
+ await recordMeltAudit(client, rowIds, { meltQuoteId, paymentPreimage });
9769
+ await markRowsMelted(client, rowIds);
9770
+ if (opts.onMelted) {
9771
+ await opts.onMelted.enqueue(
9772
+ {
9773
+ rows: rows.map((r) => ({ id: r.id, mintUrl: r.mintUrl, proofAmount: r.proofAmount })),
9774
+ meltQuoteId,
9775
+ paymentPreimage
9776
+ },
9777
+ client
9778
+ );
9779
+ }
9780
+ await client.query("COMMIT");
9781
+ } catch (err) {
9782
+ await client.query("ROLLBACK").catch(() => void 0);
9783
+ throw err;
9784
+ } finally {
9785
+ client.release();
9786
+ }
9787
+ opts.onMelted?.committed();
9737
9788
  return c.json({ melted: rowIds.length });
9738
9789
  }
9739
9790
  async function handleRestartFailed(c, opts) {
@@ -10877,7 +10928,11 @@ async function handleReconcileInvoice(c, opts) {
10877
10928
  amountMicro: invoice.amountMicro,
10878
10929
  creditCurrency: invoice.currency,
10879
10930
  fundedAt: settledAtMs ?? invoice.settledAt ?? nowMs,
10880
- expiryMs
10931
+ expiryMs,
10932
+ // The settlements table types this deposit `repair` off the
10933
+ // flag (internal-review): the sats landed at payment time and the
10934
+ // deposit is the one row they get.
10935
+ repaired: true
10881
10936
  }
10882
10937
  }
10883
10938
  } : {},
@@ -11228,8 +11283,10 @@ async function handleReconcileTempo(c, opts) {
11228
11283
  if (record.status === "sent") return tempoReconciledBody(c, record, channelId, true);
11229
11284
  const evidence = await classifyTempoCloseEvidence(rail.rail, channelId);
11230
11285
  if (evidence.chain !== "found") return tempoEvidenceRefusal(c, evidence);
11286
+ const client = await opts.db.connect();
11231
11287
  let booked;
11232
11288
  try {
11289
+ await client.query("BEGIN");
11233
11290
  booked = await completeTempoChannelDrain({
11234
11291
  deps: {
11235
11292
  ledger: opts.ledger,
@@ -11245,14 +11302,30 @@ async function handleReconcileTempo(c, opts) {
11245
11302
  refundedNative: evidence.refundedToPayer,
11246
11303
  debitedNative: record.drainedNative,
11247
11304
  reference: evidence.transaction,
11248
- repaired: true
11305
+ repaired: true,
11306
+ tx: client
11249
11307
  });
11308
+ if (opts.onTempoDrainReconciled && !booked.replayed) {
11309
+ await opts.onTempoDrainReconciled.enqueue(
11310
+ {
11311
+ channelId,
11312
+ txHash: evidence.transaction,
11313
+ settledToPayee: evidence.settledToPayee
11314
+ },
11315
+ client
11316
+ );
11317
+ }
11318
+ await client.query("COMMIT");
11250
11319
  } catch (err) {
11320
+ await client.query("ROLLBACK").catch(() => void 0);
11251
11321
  if (err instanceof CreditLedgerError) {
11252
11322
  return c.json({ error: err.code, message: err.message }, ledgerRefusalStatus(err.code));
11253
11323
  }
11254
11324
  throw err;
11325
+ } finally {
11326
+ client.release();
11255
11327
  }
11328
+ if (!booked.replayed) opts.onTempoDrainReconciled?.committed();
11256
11329
  if (opts.receiptIssuer && !booked.replayed) {
11257
11330
  const receipt = opts.receiptIssuer.issueDrain({
11258
11331
  creditId: booked.drain.creditId,
@@ -11922,6 +11995,58 @@ function parseSerializedDleq(raw) {
11922
11995
  return { e: dleq.e, s: dleq.s, ...dleq.r !== void 0 ? { r: dleq.r } : {} };
11923
11996
  }
11924
11997
 
11998
+ // src/sdk/server/credit-expiry-sweep.ts
11999
+ function startCreditExpirySweep(ledger, opts = {}) {
12000
+ const sweepExpiredCredits = ledger.sweepExpiredCredits?.bind(ledger);
12001
+ if (!sweepExpiredCredits) return void 0;
12002
+ const intervalMs = opts.intervalMs ?? DEFAULT_SWEEP_INTERVAL_MS;
12003
+ const bootDelayMs = opts.bootDelayMs ?? DEFAULT_BOOT_DELAY_MS;
12004
+ let stopped = false;
12005
+ const runOnce = () => sweepExpiredCredits({ limit: opts.limit });
12006
+ const pass = () => {
12007
+ if (stopped) return;
12008
+ void runOnce().then(({ released }) => {
12009
+ if (released.length === 0) return;
12010
+ console.log(
12011
+ JSON.stringify({
12012
+ level: "credit_expiry_released",
12013
+ count: released.length,
12014
+ credits: released.map((r) => r.creditId)
12015
+ })
12016
+ );
12017
+ }).catch((err) => {
12018
+ console.error(
12019
+ JSON.stringify({
12020
+ level: "credit_expiry_sweep_failed",
12021
+ error: err instanceof Error ? err.message : String(err)
12022
+ })
12023
+ );
12024
+ });
12025
+ };
12026
+ let bootTimer;
12027
+ if (bootDelayMs > 0) {
12028
+ bootTimer = setTimeout(pass, bootDelayMs);
12029
+ bootTimer.unref();
12030
+ }
12031
+ let intervalTimer;
12032
+ if (intervalMs > 0) {
12033
+ intervalTimer = setInterval(pass, intervalMs);
12034
+ intervalTimer.unref();
12035
+ }
12036
+ return {
12037
+ stop() {
12038
+ stopped = true;
12039
+ if (bootTimer) clearTimeout(bootTimer);
12040
+ if (intervalTimer) clearInterval(intervalTimer);
12041
+ bootTimer = void 0;
12042
+ intervalTimer = void 0;
12043
+ },
12044
+ runOnce
12045
+ };
12046
+ }
12047
+ var DEFAULT_SWEEP_INTERVAL_MS = 60 * 60 * 1e3;
12048
+ var DEFAULT_BOOT_DELAY_MS = 3e4;
12049
+
11925
12050
  // src/sdk/server/dev-page.ts
11926
12051
  function devPageHtml(dvmName) {
11927
12052
  const escaped = dvmName.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -12863,7 +12988,7 @@ function jobCredentialGate(opts) {
12863
12988
  // a human verbatim, so the conditional advice lives in `hint`, which
12864
12989
  // is what the agent acts on.
12865
12990
  display: "Couldn't retrieve your result \u2014 the request was missing its job credential.",
12866
- hint: `Send the job_token from the POST /v1/job response as the X-Job-Token header${alternative}. The token is minted once, at submit, and the DVM cannot reissue it \u2014 a job submitted from another machine, or by a caller that never stored it, can't be read here. If your caller never sends X-Job-Token at all, upgrade: npm i -g @dvmkit/cli@0.1.0.`
12991
+ hint: `Send the job_token from the POST /v1/job response as the X-Job-Token header${alternative}. The token is minted once, at submit, and the DVM cannot reissue it \u2014 a job submitted from another machine, or by a caller that never stored it, can't be read here. If your caller never sends X-Job-Token at all, upgrade: npm i -g @dvmkit/dvm-cli@0.1.4.`
12867
12992
  };
12868
12993
  }
12869
12994
  };
@@ -17116,12 +17241,24 @@ async function createDVMServer(descriptor, opts) {
17116
17241
  const platformUrl = opts.platformUrl ?? envMaybe.DVMKIT_PLATFORM_URL;
17117
17242
  const dvmId = opts.dvmId;
17118
17243
  let revenueReporter;
17244
+ let payoutReporter;
17119
17245
  if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
17120
- const { RevenueReporter } = await import("../revenue-reporter-M35KP6V7.js");
17246
+ const { RevenueReporter } = await import("../revenue-reporter-GB4WKLDC.js");
17121
17247
  const reporter = new RevenueReporter(opts.db, platformUrl, platformToken);
17122
17248
  await reporter.init();
17123
17249
  reporter.startRetryLoop();
17124
17250
  revenueReporter = reporter;
17251
+ const { MemoryX402BatchStore, PayoutReporter } = await import("../payout-reporter-4TNWRS5F.js");
17252
+ payoutReporter = new PayoutReporter({
17253
+ dvmId,
17254
+ db: opts.db,
17255
+ transport: reporter,
17256
+ // The same condition the admin routes and the accumulator monitor mount on.
17257
+ cashu: opts.cashuMode === "p2pk-accumulator" && !!opts.lockPubkey,
17258
+ ...typeof opts.db.connect === "function" ? {} : { batchStore: new MemoryX402BatchStore() }
17259
+ });
17260
+ await payoutReporter.init();
17261
+ payoutReporter.start();
17125
17262
  }
17126
17263
  assertRevenueReporterReady({
17127
17264
  failFast: opts.failFast ?? false,
@@ -17164,6 +17301,13 @@ async function createDVMServer(descriptor, opts) {
17164
17301
  });
17165
17302
  opts.tempoSettlementReadiness?.start();
17166
17303
  const creditLedger = opts.creditLedger ?? new MemoryCreditLedger();
17304
+ if (revenueReporter && dvmId) {
17305
+ creditLedger.useCreditExpiryReleaseOutbox?.({
17306
+ dvmId,
17307
+ enqueue: revenueReporter.enqueueCreditExpiryRelease.bind(revenueReporter)
17308
+ });
17309
+ }
17310
+ const creditExpirySweep = startCreditExpirySweep(creditLedger, opts.creditExpirySweep);
17167
17311
  const processedPayments = opts.processedPayments ?? new MemoryProcessedPaymentStore();
17168
17312
  const bootX402Support = opts.x402 ? opts.x402FacilitatorHealth?.support() ?? configuredX402FacilitatorSupport(opts.x402) : void 0;
17169
17313
  const x402Config = opts.x402;
@@ -17214,7 +17358,8 @@ async function createDVMServer(descriptor, opts) {
17214
17358
  db: opts.db,
17215
17359
  allowInMemory: opts.devMode,
17216
17360
  earnedNative: (channelId) => creditLedger.earnedNativeForX402Channel(channelId),
17217
- supportedResponse: selfRelayEnabled ? void 0 : opts.x402FacilitatorHealth?.supportedResponse?.()
17361
+ supportedResponse: selfRelayEnabled ? void 0 : opts.x402FacilitatorHealth?.supportedResponse?.(),
17362
+ payout: payoutReporter?.x402Observer()
17218
17363
  }) : void 0;
17219
17364
  const x402BatchSettlement = x402BatchSettlementFactory && (bootX402Support?.batchSettlement || selfRelayEnabled) ? await x402BatchSettlementFactory() : void 0;
17220
17365
  const x402ExactStore = x402Config && opts.db && typeof opts.db.connect === "function" ? new PostgresX402ExactSettlementStore(opts.db) : void 0;
@@ -17251,21 +17396,25 @@ async function createDVMServer(descriptor, opts) {
17251
17396
  enqueueCreditDrawRelease,
17252
17397
  x402BatchSettlement,
17253
17398
  x402BatchSettlementFactory,
17254
- x402ExactSettlement
17399
+ x402ExactSettlement,
17400
+ payoutReporter
17255
17401
  });
17256
17402
  return {
17257
17403
  app: server.app,
17258
17404
  authAudience: server.authAudience,
17259
17405
  shutdown() {
17260
17406
  const settlementStopped = server.shutdown();
17407
+ payoutReporter?.stop();
17261
17408
  revenueReporter?.stop();
17409
+ creditExpirySweep?.stop();
17262
17410
  unsubscribeTempoSettlementReadiness?.();
17263
17411
  if (x402ChannelStorage) {
17264
17412
  void settlementStopped.then(() => x402ChannelStorage.close()).catch(() => void 0);
17265
17413
  }
17266
17414
  },
17267
17415
  jobManager: server.jobManager,
17268
- revenueReporterActive: !!revenueReporter
17416
+ revenueReporterActive: !!revenueReporter,
17417
+ payoutReporter
17269
17418
  };
17270
17419
  }
17271
17420
  function unknownRouteNotFound(c) {
@@ -17540,12 +17689,16 @@ var DVMServer = class {
17540
17689
  if (this.opts.cashuMode === "p2pk-accumulator" && this.opts.db && this.opts.dvmId && this.opts.lockPubkey) {
17541
17690
  const db = this.opts.db;
17542
17691
  const dvmId = this.opts.dvmId;
17692
+ const payoutReporter = this.opts.payoutReporter;
17543
17693
  installAdminCashuRoutes(this.app, {
17544
17694
  db,
17545
17695
  dvmId,
17546
17696
  getLockPubkeyState: () => this.loadActiveLockPubkeyState(db, dvmId),
17547
17697
  graceSeconds: this.graceSeconds(),
17548
- nonceStore: this.adminCashuNonceStore
17698
+ nonceStore: this.adminCashuNonceStore,
17699
+ // internal-review: a completed melt is a landed payout, queued in the
17700
+ // transaction that marks it.
17701
+ ...payoutReporter ? { onMelted: payoutReporter.meltHook() } : {}
17549
17702
  });
17550
17703
  installAdminCreditRoutes(this.app, {
17551
17704
  db,
@@ -17575,7 +17728,10 @@ var DVMServer = class {
17575
17728
  // Health-filtered, and read per request rather than captured: the
17576
17729
  // servicer picking a mint to buy a refund at wants the list that
17577
17730
  // excludes a mint currently failing its probe (internal-review).
17578
- mints: () => this.advertisedMints()
17731
+ mints: () => this.advertisedMints(),
17732
+ // internal-review: a repaired Tempo close is a landed payout the agent
17733
+ // finished, queued in the transaction that books the drain.
17734
+ ...payoutReporter ? { onTempoDrainReconciled: payoutReporter.tempoRepairHook() } : {}
17579
17735
  });
17580
17736
  }
17581
17737
  }
@@ -21613,6 +21769,9 @@ function createDVMHost(opts = {}) {
21613
21769
  let pgPool;
21614
21770
  let creditLedger;
21615
21771
  let tempoSessionStore;
21772
+ const tempoPayoutReader = {};
21773
+ const payoutSinks = {};
21774
+ let tempoPayoutOwner;
21616
21775
  let tempoSettlementReadiness;
21617
21776
  let processedPayments;
21618
21777
  let pgStreamStore;
@@ -21764,6 +21923,7 @@ ${config.facilitatorAuth?.keyId ?? ""}`;
21764
21923
  ]);
21765
21924
  const sessionStore = new PostgresTempoSessionStore(pgPool);
21766
21925
  tempoSessionStore = sessionStore;
21926
+ tempoPayoutReader.listActive = (limit, cursor) => sessionStore.listActive(limit, cursor);
21767
21927
  pendingInits.push(sessionStore.init());
21768
21928
  const operator = privateKeyToAccount5(tempoOperatorKey);
21769
21929
  const tempoCurrency = envMaybe.DVMKIT_TEMPO_CURRENCY ?? TEMPO_USDC_MAINNET;
@@ -21826,6 +21986,13 @@ ${config.facilitatorAuth?.keyId ?? ""}`;
21826
21986
  delta: settlement.delta.toString()
21827
21987
  })
21828
21988
  );
21989
+ await payoutSinks.tempoSettled?.({
21990
+ txHash: settlement.txHash,
21991
+ channelId: settlement.channelId,
21992
+ trigger: settlement.trigger,
21993
+ amount: settlement.amount,
21994
+ delta: settlement.delta
21995
+ });
21829
21996
  }
21830
21997
  };
21831
21998
  }
@@ -21916,7 +22083,7 @@ ${config.facilitatorAuth?.keyId ?? ""}`;
21916
22083
  sharedPgConsumedStore = consumedStore;
21917
22084
  }
21918
22085
  if (pgPool) {
21919
- const { CreditLedger: CreditLedger2 } = await import("../credit-ledger-EDMEZSA2.js");
22086
+ const { CreditLedger: CreditLedger2 } = await import("../credit-ledger-RO4FGSHG.js");
21920
22087
  const pgLedger = new CreditLedger2(pgPool, tempoSessionStore);
21921
22088
  pendingInits.push(pgLedger.init());
21922
22089
  creditLedger = pgLedger;
@@ -21925,7 +22092,7 @@ ${config.facilitatorAuth?.keyId ?? ""}`;
21925
22092
  pendingInits.push(pgProcessed.init());
21926
22093
  processedPayments = pgProcessed;
21927
22094
  } else {
21928
- const { MemoryCreditLedger: MemoryCreditLedger2 } = await import("../memory-credit-ledger-7TTZDSRS.js");
22095
+ const { MemoryCreditLedger: MemoryCreditLedger2 } = await import("../memory-credit-ledger-I2G64DDK.js");
21929
22096
  creditLedger = new MemoryCreditLedger2();
21930
22097
  const { MemoryProcessedPaymentStore: MemoryProcessedPaymentStore2 } = await import("../processed-payment-store-HAA4SFNK.js");
21931
22098
  processedPayments = new MemoryProcessedPaymentStore2();
@@ -21999,7 +22166,8 @@ ${config.facilitatorAuth?.keyId ?? ""}`;
21999
22166
  const {
22000
22167
  app: subApp,
22001
22168
  shutdown,
22002
- authAudience
22169
+ authAudience,
22170
+ payoutReporter
22003
22171
  } = await createDVMServer(descriptor, {
22004
22172
  env,
22005
22173
  store,
@@ -22034,6 +22202,16 @@ ${config.facilitatorAuth?.keyId ?? ""}`;
22034
22202
  tempoSettlementReadiness
22035
22203
  });
22036
22204
  entry.shutdown = shutdown;
22205
+ if (payoutReporter && !tempoPayoutOwner) {
22206
+ tempoPayoutOwner = payoutReporter;
22207
+ const ledger = creditLedger;
22208
+ payoutReporter.attachTempo({
22209
+ ...tempoPayoutReader,
22210
+ ...mppTempoRecipient ? { recipient: mppTempoRecipient } : {},
22211
+ listChannelDrains: (args) => ledger.listChannelDrains(args)
22212
+ });
22213
+ payoutSinks.tempoSettled = (event) => payoutReporter.tempoSettled(event);
22214
+ }
22037
22215
  if (descriptor.config.routes) {
22038
22216
  await descriptor.config.routes(subApp, {
22039
22217
  authAudience,
@@ -1,4 +1,4 @@
1
- import { C as Currency } from './job-store-C5n6bhap.js';
1
+ import { C as Currency } from './job-store-6gR4pZRP.js';
2
2
 
3
3
  /**
4
4
  * Quote-time fx snapshot. Embedded in scribe's `lockedQuote` for within-job
@@ -1,4 +1,4 @@
1
- import { aX as StreamableJobStore, aY as ReceiptIssuingStore, J as JobRecord, aZ as RequestIdClaim, a_ as RequestIdClaimResult, a9 as JobReceipt, a$ as OutgoingMessage, aa as AppendOutgoingOptions, b0 as PaymentCreditDelta, b1 as VerifyAndCreditResult, b2 as JobCounters, a6 as Message, av as MessageType, K as KVStore, L as Logger, v as ResponseContent, P as PaymentContent, S as SDKJobContext } from '../job-store-C5n6bhap.js';
1
+ import { aX as StreamableJobStore, aY as ReceiptIssuingStore, J as JobRecord, aZ as RequestIdClaim, a_ as RequestIdClaimResult, a9 as JobReceipt, a$ as OutgoingMessage, aa as AppendOutgoingOptions, b0 as PaymentCreditDelta, b1 as VerifyAndCreditResult, b2 as JobCounters, a6 as Message, av as MessageType, K as KVStore, L as Logger, v as ResponseContent, P as PaymentContent, S as SDKJobContext } from '../job-store-6gR4pZRP.js';
2
2
  import '@cashu/cashu-ts';
3
3
  import 'mppx';
4
4
  import 'hono';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dvmkit/sdk",
3
- "version": "0.1.0-rc.1",
3
+ "version": "0.1.0-rc.2",
4
4
  "description": "SDK for building accountless, pay-per-use Digital Vending Machines",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {