Lightning Zaps on Nostr (NIP-57)

Value-for-value payments: zap requests, receipts, split zaps, LNURL, and validation

Lightning Zaps (NIP-57)

Value-for-value payments on Nostr. Bitcoin Lightning integrated directly into the social protocol.

The Flow

   SENDER                    LNURL PROVIDER            RECEIVER
┌──────────┐               ┌──────────────┐         ┌──────────┐
│  Alice   │               │   Wallet/    │         │   Bob    │
│          │──① fetch LN──▶│   LN node    │         │          │
│          │   URL          │              │         │          │
│          │◄──────────────│              │         │          │
│          │   callback URL │              │         │          │
│          │                │              │         │          │
│          │──② Zap Req────▶│              │         │          │
│  Kind    │   (kind 9734)  │              │         │          │
│  9734    │                │──③ pay──────▶│         │          │
│          │                │   invoice    │         │          │
│          │                │◄─────────────│         │          │
│          │                │   preimage   │         │          │
│          │                │              │         │          │
│   Kind   │                │──④ Zap Rcpt─▶│ relays  │──▶Bob    │
│   9735   │                │   (kind 9735)│◄────────│   sees   │
│          │                │              │          │   zap    │
└──────────┘               └──────────────┘         └──────────┘

Event Types

Kind 9734 — Zap Request

The payment intent. Sent to the LNURL-pay endpoint.

{
  "kind": 9734,
  "content": "",
  "tags": [
    ["relays", "wss://relay.damus.io", "wss://nos.lol"],
    ["amount", "21000"],
    ["lnurl", "lnurl1d..."],
    ["p", "<recipient-pubkey>"],
    ["e", "<optional-event-id>"],
    ["a", "<optional-addressable-event>"],
    ["P", "<optional-zapper-pubkey>"]
  ]
}

amount is in millisatoshis: 21000 = 21 sats.

Kind 9735 — Zap Receipt

Published by the LNURL provider after payment succeeds. This is the public proof of payment.

{
  "kind": 9735,
  "pubkey": "<provider's pubkey>",
  "content": "",
  "tags": [
    ["p", "<recipient-pubkey>"],
    ["P", "<sender-pubkey>"],
    ["e", "<event-id>"],
    ["bolt11", "<invoice>"],
    ["description", "<JSON-encoded zap request>"],
    ["preimage", "<payment preimage>"]
  ]
}

Validation

  1. zap receipt pubkey MUST match the LNURL provider’s nostrPubkey
  2. invoiceAmount in bolt11 MUST equal amount tag in zap request
  3. SHA256(description) MUST match bolt11 description hash

LNURL Provider Setup

The recipient’s Lightning node must expose:

GET /.well-known/lnurlp/<username>
→ {
    "callback": "https://ln.example.com/callback",
    "maxSendable": 100000000,
    "minSendable": 1000,
    "metadata": "[...]",
    "nostrPubkey": "<provider's nostr pubkey>",
    "allowsNostr": true,
    "tag": "payRequest"
  }

allowsNostr: true signals zap compatibility to clients.

Split Zaps

Send to multiple recipients in one zap:

{
  "tags": [
    ["zap", "<pubkey1>", "wss://relay1.com", "1"],
    ["zap", "<pubkey2>", "wss://relay2.com", "1"],
    ["zap", "<pubkey3>", "wss://relay3.com", "2"]
  ]
}

Weight calculation: Bob gets 25%, Carol gets 25%, Dave gets 50%.

Without weights: equal split. Missing weight: that recipient gets 0.

Anonymous Zaps

  • No P tag in zap request = anonymous
  • Recipient sees only that someone zapped, not who
  • Provider still knows (payment is real), but recipient doesn’t

Zap Goals (NIP-75)

Crowdfunding targets on Nostr.

Kind 9041:

{
  "kind": 9041,
  "content": "Help me buy a new laptop!",
  "tags": [
    ["amount", "100000000"],      // target: 100k sats
    ["relays", "wss://..."],
    ["closed_at", "1700000000"]   // deadline
  ]
}

The sum of all zap receipts for this goal is compared against the target.

Fee Model

Layer Fee Who Pays
LNURL provider Configurable Provider or sender
Lightning routing Per-hop fees Sender
On-chain (if needed) Market rate Whoever closes channel
Nostr relay Free N/A

Typical total: 0–2 sats for a 21 sat zap.

Why Zaps Beat Tips

Aspect Tip (Kind 1 reply) Zap (Kind 9735)
Verifiable ❌ Trust-me-bro text ✅ Cryptographic proof
Amount visible Only if claimed ✅ In bolt11 invoice
Sender visible Only if claimed ✅ In zap receipt
LN address Static Dynamic callback
Split support ✅ Weight-based
Anonymity ✅ Omit P tag

Client Display

Clients show zaps as:

⚡ Alice zapped 21 sats
⚡ Bob zapped 1000 sats to you
⚡ 3 people zapped 42 sats each

Sorting by total zapped amount is the defacto trending algorithm.

Implementation Notes

Creating a Zap (Client-Side)

// 1. Fetch LNURL from recipient's profile
const lnurl = await fetchLnurl(recipientPubkey)

// 2. Create Zap Request event
const zapRequest = await signEvent({
  kind: 9734,
  content: "",
  tags: [
    ["relays", ...relays],
    ["amount", sats * 1000],
    ["lnurl", lnurl],
    ["p", recipientPubkey],
  ]
})

// 3. Send to LNURL callback
const {pr: invoice} = await fetch(`${callback}?amount=${amount}&nostr=${encodeURI(JSON.stringify(zapRequest))}`)

// 4. Pay invoice
await payInvoice(invoice)

Verifying a Zap Receipt

// 1. Get LNURL provider's nostrPubkey
const providerPubkey = await getProviderPubkey(lnurl)

// 2. Verify zap receipt pubkey matches provider
if (zapReceipt.pubkey !== providerPubkey) throw "Invalid provider"

// 3. Verify invoice amount matches request amount
if (decodeInvoice(zapReceipt.bolt11).amount !== zapRequest.amount) throw "Amount mismatch"

// 4. Verify description hash
if (sha256(zapReceipt.description) !== decodeInvoice(zapReceipt.bolt11).descriptionHash) throw "Hash mismatch"

Quick Reference

What Kind Key Tags Direction
Zap Request 9734 p, amount, lnurl, relays Client → LNURL
Zap Receipt 9735 p, P, bolt11, description, preimage LNURL → Relays
Zap Goal 9041 amount, relays, closed_at User → Relays

Write a comment
No comments yet.