When a priced request arrives, the payment middlewares spawn detached background tasks that poll processor.verifyPayment until the payment verifies or verifyTimeoutMs (default ≤ 5 min) elapses:
- transparent:
src/payments/server-payments.ts — the inFlight verify loop inside request processing
- explicit gating:
src/payments/server-explicit-gating.ts — the detached verify task after -32042
NostrServerTransport.close() clears the correlation store, session store, and stream factories — but nothing signals these tasks to stop. rs-sdk cancels them via a cancellation token parented to server shutdown.
Observed effects (worst first)
- Paid tool executes after shutdown began (transparent mode): a verify that resolves after
close() proceeds to forward the invocation to the underlying MCP handler and publish payment_accepted. In-flight-at-close requests only, but it breaks rolling-deploy/graceful-shutdown expectations.
- Process exit delayed: the poll timers (
withTimeout's setTimeout) keep the event loop alive up to verifyTimeoutMs after close() — hangs shutdown scripts and test teardown.
- Wasted external polls: continues hitting the payment processor's API (LNbits/NWC/…) for up to 5 min after the server is gone.
- Grant written to dead state (explicit gating): verify success post-close writes into an
AuthorizationStore nobody reads. Harmless today (new withServerPayments = new store), noting for completeness.
Bounds / severity
Not a money bug: no double-charge, no unbounded growth, no cross-restart state leak. Everything is capped by pending count (≤ maxPendingPayments) and verifyTimeoutMs. Hygiene + graceful-shutdown correctness.
Proposed fix shape
withServerPayments creates an AbortController shared by both middlewares.
NostrServerTransport exposes a middleware-observable close hook (none exists for this purpose today).
- Verify/poll loops check
signal.aborted per iteration and bail (explicit: skip the grant; transparent: skip the forward — kills effect 1).
- On abort, treat as verify-failure cleanup (clear pending) so state stays coherent.
~30 lines across 4 files (server-payments.ts, server-explicit-gating.ts, server-transport-payments.ts, nostr-server-transport.ts), plus a test that closes mid-verify and asserts no forward and no lingering timer.
When a priced request arrives, the payment middlewares spawn detached background tasks that poll
processor.verifyPaymentuntil the payment verifies orverifyTimeoutMs(default ≤ 5 min) elapses:src/payments/server-payments.ts— theinFlightverify loop inside request processingsrc/payments/server-explicit-gating.ts— the detached verify task after-32042NostrServerTransport.close()clears the correlation store, session store, and stream factories — but nothing signals these tasks to stop. rs-sdk cancels them via a cancellation token parented to server shutdown.Observed effects (worst first)
close()proceeds to forward the invocation to the underlying MCP handler and publishpayment_accepted. In-flight-at-close requests only, but it breaks rolling-deploy/graceful-shutdown expectations.withTimeout'ssetTimeout) keep the event loop alive up toverifyTimeoutMsafterclose()— hangs shutdown scripts and test teardown.AuthorizationStorenobody reads. Harmless today (newwithServerPayments= new store), noting for completeness.Bounds / severity
Not a money bug: no double-charge, no unbounded growth, no cross-restart state leak. Everything is capped by pending count (≤
maxPendingPayments) andverifyTimeoutMs. Hygiene + graceful-shutdown correctness.Proposed fix shape
withServerPaymentscreates anAbortControllershared by both middlewares.NostrServerTransportexposes a middleware-observable close hook (none exists for this purpose today).signal.abortedper iteration and bail (explicit: skip the grant; transparent: skip the forward — kills effect 1).~30 lines across 4 files (
server-payments.ts,server-explicit-gating.ts,server-transport-payments.ts,nostr-server-transport.ts), plus a test that closes mid-verify and asserts no forward and no lingering timer.