Add zap support to link aggregator submissions and comments #17

Open
opened 2026-02-23 17:55:19 +00:00 by padreug · 0 comments
Owner

Summary

Add NIP-57 zap support to the Nostr feed / link aggregator module, letting users zap submissions and comments with sats. This builds on top of Lightning.Pub's existing /.well-known/lnurlp/:username endpoint and zap receipt infrastructure (see #16).

Motivation

The link aggregator currently has upvotes/downvotes (kind 7 reactions) but no way to tip or zap content creators. Zaps add a real economic signal — they're more meaningful than free votes and incentivize quality content. Since Lightning.Pub already handles zap requests (kind 9734), zap receipts (kind 9735), and Lightning Address resolution, the server-side work is done — this is purely a client-side feature.

Current State

  • Submissions are kind 1111 Nostr events (NIP-22)
  • Votes are kind 7 reactions (NIP-25): + for upvote, - for downvote
  • VoteControls.vue renders the upvote/downvote buttons
  • ReactionService tracks reaction counts and user vote state
  • SubmissionService manages feed subscriptions and event publishing
  • No payment integrationPAYMENT_SERVICE and INVOICE_SERVICE DI tokens exist but are unused by the nostr-feed module

Implementation

1. Zap button on submissions and comments

Modify: VoteControls.vue

  • Add a zap (lightning bolt) button alongside the existing upvote/downvote controls
  • On click, show a small amount picker (e.g., 21, 100, 500, 1000 sats — or custom)
  • Display cumulative zap amount received (sum of kind 9735 events referencing this submission)

Modify: SubmissionRow.vue and SubmissionDetail.vue

  • Show zap total next to the vote score
  • Highlight if the current user has already zapped this submission

Modify: SubmissionComment.vue

  • Same zap button on comments (zap targets the comment's event ID)

2. Zap service

New file: src/modules/nostr-feed/services/ZapService.ts

  • Extends BaseService, registers with DI container
  • sendZap(targetEvent, amountSats):
    1. Look up target event author's NIP-05 or lud16 tag → resolve Lightning Address
    2. Fetch /.well-known/lnurlp/:username → get LNURL-pay info
    3. Construct kind 9734 zap request event with tags: e (target event), p (recipient pubkey), relays, amount
    4. Sign with user's Nostr key
    5. Call the LNURL-pay callback with amount + nostr (serialized zap request)
    6. Receive BOLT11 invoice → pay via PaymentService (wallet balance) or present QR for external wallet
  • getZapReceipts(eventIds):
    1. Subscribe to kind 9735 events with e tags matching the submission/comment IDs
    2. Parse bolt11 tag to extract amount
    3. Aggregate totals per event
  • getZapTotal(eventId): return cached total sats zapped to an event

3. Zap receipts in the feed subscription

Modify: SubmissionService.ts

  • When subscribing to a feed, also subscribe to kind 9735 (zap receipts) for the loaded submissions
  • Pass zap totals to the submission objects alongside vote counts

4. Author Lightning Address resolution

For zaps to work, we need the recipient's Lightning Address. Options (in priority order):

  1. Author's NIP-05 identifier (already in profile metadata) → resolves via /.well-known/lnurlp/:name
  2. Author's lud16 tag in kind 0 profile metadata (standard Nostr Lightning Address field)
  3. Author's lud06 tag (LNURL fallback)

ProfileService already resolves Nostr profiles — extend it to extract lud16/lud06 from kind 0 metadata.

UX Flow

User sees submission with [▲ 42 ▼] [⚡ 2,100 sats]
    ↓
Clicks ⚡ button
    ↓
Amount picker appears: [21] [100] [500] [1k] [custom]
    ↓
Selects 100 sats
    ↓
ZapService resolves author's Lightning Address
    ↓
Constructs kind 9734 zap request, calls LNURL-pay callback
    ↓
Receives BOLT11 invoice
    ↓
If wallet has balance → pays automatically via PaymentService
If no balance → shows QR code / opens external wallet
    ↓
Lightning.Pub settles payment, publishes kind 9735 zap receipt
    ↓
Feed subscription picks up zap receipt → updates total: [⚡ 2,200 sats]

Dependencies

  • #16 (Replace LNbits with Lightning.Pub) — zap payments need to go through Lightning.Pub, not LNbits
  • Alternatively, this can be implemented against LNbits first (it supports LNURL-pay) and migrated later

Files to create

  • src/modules/nostr-feed/services/ZapService.ts (~200 lines)

Files to modify

  • src/modules/nostr-feed/components/VoteControls.vue — add zap button + amount picker
  • src/modules/nostr-feed/components/SubmissionRow.vue — show zap total
  • src/modules/nostr-feed/components/SubmissionDetail.vue — show zap total
  • src/modules/nostr-feed/components/SubmissionComment.vue — add zap button
  • src/modules/nostr-feed/services/SubmissionService.ts — subscribe to kind 9735 events
  • src/modules/nostr-feed/services/ProfileService.ts — extract lud16/lud06 from profiles
  • src/modules/nostr-feed/index.ts — register ZapService in module plugin
  • src/modules/nostr-feed/types/submission.ts — add zapTotal field to submission type
## Summary Add NIP-57 zap support to the Nostr feed / link aggregator module, letting users zap submissions and comments with sats. This builds on top of Lightning.Pub's existing `/.well-known/lnurlp/:username` endpoint and zap receipt infrastructure (see #16). ## Motivation The link aggregator currently has upvotes/downvotes (kind 7 reactions) but no way to tip or zap content creators. Zaps add a real economic signal — they're more meaningful than free votes and incentivize quality content. Since Lightning.Pub already handles zap requests (kind 9734), zap receipts (kind 9735), and Lightning Address resolution, the server-side work is done — this is purely a client-side feature. ## Current State - Submissions are kind 1111 Nostr events (NIP-22) - Votes are kind 7 reactions (NIP-25): `+` for upvote, `-` for downvote - `VoteControls.vue` renders the upvote/downvote buttons - `ReactionService` tracks reaction counts and user vote state - `SubmissionService` manages feed subscriptions and event publishing - **No payment integration** — `PAYMENT_SERVICE` and `INVOICE_SERVICE` DI tokens exist but are unused by the nostr-feed module ## Implementation ### 1. Zap button on submissions and comments **Modify: `VoteControls.vue`** - Add a zap (lightning bolt) button alongside the existing upvote/downvote controls - On click, show a small amount picker (e.g., 21, 100, 500, 1000 sats — or custom) - Display cumulative zap amount received (sum of kind 9735 events referencing this submission) **Modify: `SubmissionRow.vue` and `SubmissionDetail.vue`** - Show zap total next to the vote score - Highlight if the current user has already zapped this submission **Modify: `SubmissionComment.vue`** - Same zap button on comments (zap targets the comment's event ID) ### 2. Zap service **New file: `src/modules/nostr-feed/services/ZapService.ts`** - Extends `BaseService`, registers with DI container - `sendZap(targetEvent, amountSats)`: 1. Look up target event author's NIP-05 or `lud16` tag → resolve Lightning Address 2. Fetch `/.well-known/lnurlp/:username` → get LNURL-pay info 3. Construct kind 9734 zap request event with tags: `e` (target event), `p` (recipient pubkey), `relays`, `amount` 4. Sign with user's Nostr key 5. Call the LNURL-pay callback with `amount` + `nostr` (serialized zap request) 6. Receive BOLT11 invoice → pay via `PaymentService` (wallet balance) or present QR for external wallet - `getZapReceipts(eventIds)`: 1. Subscribe to kind 9735 events with `e` tags matching the submission/comment IDs 2. Parse `bolt11` tag to extract amount 3. Aggregate totals per event - `getZapTotal(eventId)`: return cached total sats zapped to an event ### 3. Zap receipts in the feed subscription **Modify: `SubmissionService.ts`** - When subscribing to a feed, also subscribe to kind 9735 (zap receipts) for the loaded submissions - Pass zap totals to the submission objects alongside vote counts ### 4. Author Lightning Address resolution For zaps to work, we need the recipient's Lightning Address. Options (in priority order): 1. Author's NIP-05 identifier (already in profile metadata) → resolves via `/.well-known/lnurlp/:name` 2. Author's `lud16` tag in kind 0 profile metadata (standard Nostr Lightning Address field) 3. Author's `lud06` tag (LNURL fallback) `ProfileService` already resolves Nostr profiles — extend it to extract `lud16`/`lud06` from kind 0 metadata. ## UX Flow ``` User sees submission with [▲ 42 ▼] [⚡ 2,100 sats] ↓ Clicks ⚡ button ↓ Amount picker appears: [21] [100] [500] [1k] [custom] ↓ Selects 100 sats ↓ ZapService resolves author's Lightning Address ↓ Constructs kind 9734 zap request, calls LNURL-pay callback ↓ Receives BOLT11 invoice ↓ If wallet has balance → pays automatically via PaymentService If no balance → shows QR code / opens external wallet ↓ Lightning.Pub settles payment, publishes kind 9735 zap receipt ↓ Feed subscription picks up zap receipt → updates total: [⚡ 2,200 sats] ``` ## Dependencies - **#16** (Replace LNbits with Lightning.Pub) — zap payments need to go through Lightning.Pub, not LNbits - Alternatively, this can be implemented against LNbits first (it supports LNURL-pay) and migrated later ## Files to create - `src/modules/nostr-feed/services/ZapService.ts` (~200 lines) ## Files to modify - `src/modules/nostr-feed/components/VoteControls.vue` — add zap button + amount picker - `src/modules/nostr-feed/components/SubmissionRow.vue` — show zap total - `src/modules/nostr-feed/components/SubmissionDetail.vue` — show zap total - `src/modules/nostr-feed/components/SubmissionComment.vue` — add zap button - `src/modules/nostr-feed/services/SubmissionService.ts` — subscribe to kind 9735 events - `src/modules/nostr-feed/services/ProfileService.ts` — extract lud16/lud06 from profiles - `src/modules/nostr-feed/index.ts` — register ZapService in module plugin - `src/modules/nostr-feed/types/submission.ts` — add `zapTotal` field to submission type
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: aiolabs/webapp#17
No description provided.