Skip to content

feat: Lightning zaps v1 (NIP-57) — static zap links + server-side receipt counts - #14

Merged
sovITxyz merged 2 commits into
mainfrom
feat/zaps-v1
Jul 19, 2026
Merged

feat: Lightning zaps v1 (NIP-57) — static zap links + server-side receipt counts#14
sovITxyz merged 2 commits into
mainfrom
feat/zaps-v1

Conversation

@sovITxyz

Copy link
Copy Markdown
Owner

Closes #12 (v1 scope).

What

JS-free zap affordance on every post (blog pages keep BLOG_CSP's zero-script contract):

Server-side receipt ingestion (src/services/zaps.ts, run per claimed user from the cron loop with independent error isolation):

  • Fetch kind 9735 (#p = author) from the user's relays + defaults — never the first-party relay (its kind allowlist can't hold receipts)
  • Trust chain per the NIP-57 appendix: receipt signature schnorr-verified; receipt pubkey must equal the author's LNURL-pay nostrPubkey (resolved from lud16, cached 24h in D1, byte-capped + timeout + shape-validated fetch — SSRF-guarded); embedded 9734 must target the author (p) and the post (a); bolt11 HRP amount is authoritative, 9734 amount equality enforced when present
  • Deduped by receipt id into zaps; zap_totals rebuilt idempotently per address inside the same atomic batch (migration 0007)
  • Watermark discipline mirrors cron/refresh.ts: only verified stored receipts advance it; deferred receipts (post not mirrored yet) hold it below themselves; persists only over a closed relay window; verification capped per run (ZAP_VERIFY_CAP)

Review

The adversarial multi-agent review ran partially (spend limit killed 19/23 agents mid-run); every finding — including ones the harness mislabeled as refuted because their verify votes errored — was re-verified manually. Four confirmed defects are fixed in the second commit, the critical one being watermark suppression: one spam 9735 per cron run with a near-future created_at could permanently outrun all real receipts. The watermark now only ever advances over verified stored receipts.

Known v1 bounds (documented in code)

  • No relay paging for receipts: a backlog beyond one 60-event page older than the newest is not recovered (same class of bound the issue's open questions flag; paging is a follow-up)
  • e-tag-only receipts are not counted (our posts are addressable; clients zap them via a)
  • npub (unclaimed) pages show the zap link when a lud16 exists, but the cron only ingests receipts for claimed users, so counts populate on claim

Tests

894 passing (21 new): lud16/bolt11/receipt validation matrix, cron pass end-to-end over the mock relay (LNURL cache, dedup, junk-suppression guard, deferred-receipt hold + later landing), post-page/feed rendering incl. hostile-lud16 and zero-script assertions.

sovITxyz added 2 commits July 19, 2026 07:27
…eipt counts (#12)

- post pages get a JS-free zap footer (author lud16 required): njump naddr
  hand-off for attributed zaps, lightning: plain-tip fallback, and
  '⚡ N sats · M zaps' totals; discover/search feed rows show counts too
- cron zap pass per claimed user: fetch kind 9735 (#p) from the user's
  relays + defaults, bind receipts to the author's LNURL-pay nostrPubkey
  (resolved from lud16, cached 24h in D1, byte-capped SSRF-guarded fetch),
  validate the embedded 9734 (p/a/amount vs bolt11 HRP), schnorr-verify,
  dedup by receipt id — watermarked and verification-capped like the post
  refresh, with an independent try so zap trouble never sinks post sync
- migration 0007: zaps + zap_totals rollup (recomputed idempotently per
  address in the ingest batch) + lnurl_cache; render path is a PK lookup
- BlogProfile carries lud16; safeLud16 shape-gates every href
… amount

Adversarial review (partial — spend-limit-degraded; findings re-verified
manually) confirmed a critical watermark defect plus three related holes:

- only VERIFIED receipts advance the zap watermark now (stored rows and
  stored-dedup hits). The empty-candidates branch advanced it over
  arbitrary unverified junk — one spam 9735 per run with a near-future
  created_at would permanently outrun every real receipt; the parse-fail
  advance had the same hole (the pubkey field is attacker-claimable).
  Bounded junk is refetched each run instead, costing no crypto
- a verified receipt deferred because its post is not mirrored yet holds
  the watermark below itself, so a newer receipt stored in the same run
  can no longer advance past it (it counts once the post lands)
- the watermark persists only over a CLOSED relay window; cap-out no
  longer persists over a full page whose limit may have truncated older
  receipts
- the 9734 amount tag is optional per NIP-57: the bolt11 invoice amount
  is authoritative, equality enforced only when both exist — wallets
  omitting the tag now count
@sovITxyz
sovITxyz merged commit 13b84fe into main Jul 19, 2026
1 check passed
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.

Lightning Zaps (NIP-57): static zap links on JS-free blogs + server-side receipt counts

1 participant