The push notification's amtPaidSat was read from request.invoiceAmount,
which is the gross amount the payer pays over Lightning (e.g. 1003 sats).
The receiver actually claims response.onchainAmount, which is net of
Boltz fees (e.g. 1000 sats). Prefer onchainAmount so the notification
shows what was received; fall back to invoiceAmount only when the wallet
registered a swap without the Boltz response.
Tests now use distinct gross/net amounts to guard the distinction.
Co-authored-by: Claude <noreply@anthropic.com>
Addresses code-review findings on the push pipeline:
- Never lose the push (was: a transient ntfy failure on invoice.settled
dropped the notification permanently, since the SwapManager dedupes an
unchanged status and never re-emits). Delivery now retries with backoff and
a periodic reconciliation sweep re-attempts any settled-but-still-registered
swap, recovering from transient outages and from settling while down.
- Prevent double-send: a synchronous in-flight guard stops a re-entrant event
(e.g. two rapid /simulate calls) from sending twice before the first resolves.
- Prune terminal swaps: delivered and failed reverse swaps are removed from
both the registry and the manager, so neither grows unbounded (previously
failed swaps lingered and were re-subscribed on every restart).
- Crash-safe persistence: the registry writes a temp file then renames, so a
crash mid-write can't corrupt registrations.json. markStatus also skips the
write when the status is unchanged.
- Consistent register/delete ordering so the registry and SwapManager can't
diverge if one side errors.
- Drop the redundant top-level status/notifiedSettled fields; swap.status is
the single source of truth.
Adds test/deliveryRetry.test.ts proving sweep-based recovery after a failed
delivery; updates registry/paymentFlow tests for the prune-on-terminal model.
https://claude.ai/code/session_018HhVgswGG7LynM25qTpejq
A sample service that pushes a phone notification when a Bitcoin Lightning
payment is received in an Arkade-enabled wallet, built on the official
@arkade-os/sdk and @arkade-os/boltz-swap packages.
Receiving Lightning uses a Boltz reverse submarine swap; the wallet registers
each invoice's pending swap per-payment (opt-in, privacy-friendly), and the
service notifies when it reaches invoice.settled.
- swapWatcher.ts: monitor-only @arkade-os/boltz-swap SwapManager
(enableAutoActions: false) — needs no wallet keys. SwapManager owns the
multiplexed Boltz websocket, polling fallback, and reconnect/backoff.
- paymentService.ts: wires SwapManager events to pushes via
isReverseSuccessStatus (invoice.settled).
- registry.ts: persisted swapId -> {topic, swap} map, resubscribed on restart;
the wallet may redact the preimage since monitoring never claims.
- notifier/: pluggable Notifier interface with an ntfy.sh implementation.
- server.ts: POST/GET/DELETE /register, /health, /simulate (Fastify + zod).
- scripts/demo-receive.ts: wallet side — creates an invoice via
ArkadeSwaps.createLightningInvoice and registers the swap (no raw REST).
- Tests: registry persistence, plus a component test driving the REAL
SwapManager with a mocked globalThis.WebSocket through
register -> settle -> push.
https://claude.ai/code/session_018HhVgswGG7LynM25qTpejq