Skip to content

Self-custody payouts + audit hardening (re-land of #26 remainder)#27

Merged
comwanga merged 9 commits into
mainfrom
fix/self-custody-payouts
Jun 20, 2026
Merged

Self-custody payouts + audit hardening (re-land of #26 remainder)#27
comwanga merged 9 commits into
mainfrom
fix/self-custody-payouts

Conversation

@comwanga

Copy link
Copy Markdown
Owner

PR #26 was merged at a stale head and only captured the first two commits (preimage verification + idempotency). This re-lands the remaining 8 commits that didn't make it into main.

All developed under TDD; this is the exact tree that was green before (backend 156, frontend 58, typecheck/lint/build).

Self-custody payouts (architectural)

Design: docs/superpowers/specs/2026-06-20-self-custody-payouts-design.md.

  • The NWC spend credential never reaches the backend. The browser (lib/lightning.ts) resolves invoices (LUD-16) and performs the NIP-47 pay_invoice over a WebSocket; the server creates pending items and verifies each reported preimage before recording paid.
  • The server-side spend path and the nwc request field are removed, so the backend cannot spend — self-custody is enforced. This also eliminates the old blocking/timeout-prone server payment loop.

Also included

  • Spend ceiling — configurable PAYOUT_MAX_SATS, enforced pre-flight.
  • Retry with corrected addresses — recover a payout that failed on a bad address (now client-driven).
  • bech32 checksum validation (BIP-173) — a mistyped npub no longer decodes to a wrong key (also fixed a latent nsec test-vector typo).
  • Batched AI categorization — the max_tokens=1024 truncation cliff that silently dropped large events to fallback; now batched with per-batch isolation, a cacheable system block, and abstention.
  • Removed the unused requests dependency.

comwanga added 8 commits June 20, 2026 16:12
The hand-rolled decoder dropped the 6-char checksum without verifying it, so a
single mistyped character in an npub decoded to a different (wrong) 32-byte key
and was silently accepted — a feedback DM or future zap would then go nowhere.

Add the standard BIP-173 polymod checksum verification. This also surfaced a
long-standing typo in the nsec test vector (...nlfe9 → ...nlfe5): its body
always decoded correctly, so the missing checksum check hid the bad tail.
A single Claude call with max_tokens=1024 truncated its tool output past ~14
"Other" registrants; the partial JSON failed to parse and the whole event
silently dropped to the per-slug deterministic fallback — exactly at the scale
where AI normalization matters most.

Process participants in capped batches with raised max_tokens, so output can't
truncate and one failing batch only forces its own members to fallback. Move
the static taxonomy + instructions into a cacheable system block, and let the
model OMIT a participant it can't confidently place (handled by fallback /
manual override) rather than forcing a guess. Prompt-building and response
parsing are now pure helpers with direct unit tests.
total_sats was bounded only by gt=0, so one extra zero could fire off a huge
payout with a single click. Add a configurable PAYOUT_MAX_SATS ceiling
(default 5,000,000) enforced before a wallet is ever touched.

Also remove the unused `requests` dependency (only httpx is used).
A payout that failed because an address was wrong had no recovery path: the
unique-per-team constraint blocks a new payout, and retry reused the stored
(bad) address. Let retry accept an optional address-correction map, applied to
failed items before re-resolving. The modal re-enables the address fields when
items can be retried and sends the corrections.
Architecture decision record for moving the Lightning send into the organizer's
browser: server computes the split and verifies reported preimages, but the NWC
spend credential never reaches the backend. Resolves the self-custody and
blocking/timeout audit findings together.
…sends)

Adds the server side of client-side NWC: when create_payout is called without
an nwc credential, the server creates pending items and returns them for the
browser to pay. Two new organizer-authenticated endpoints record each result —
/items/{id}/result verifies the reported preimage against the invoice before
marking paid (mismatch -> unverified), /items/{id}/failed records a no-preimage
failure. Both roll the payout status up and are idempotent on already-paid items.

The legacy server-side path still runs when nwc is supplied, so nothing breaks;
the frontend switch and removal of the server-side credential path follow.
New lib/lightning.ts resolves invoices (LUD-16) and performs the NIP-47
pay_invoice over a WebSocket entirely in the browser; the NWC credential never
leaves the client. The payout modal now creates a payout without an nwc, pays
each item locally, and reports each result for the server to verify. Retry
re-drives only the failed items (using any corrected addresses).
…tody)

The create endpoint no longer accepts an nwc credential and the server-side
execute/retry path is gone, so the backend has no way to spend from a wallet —
self-custody is now enforced, not merely the frontend's default. Payment happens
in the browser and the server only verifies reported preimages. The NIP-47/LNURL
utility modules and their unit tests remain as secret-free, reusable helpers.
@vercel

vercel Bot commented Jun 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
squadsync Ready Ready Preview, Comment Jun 20, 2026 7:39pm

@comwanga comwanga merged commit ffe4e83 into main Jun 20, 2026
6 checks passed
@comwanga comwanga deleted the fix/self-custody-payouts branch June 21, 2026 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant