Lightning Zaps on Nostr (NIP-57)
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
zap receiptpubkey MUST match the LNURL provider’snostrPubkeyinvoiceAmountin bolt11 MUST equalamounttag in zap requestSHA256(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
Ptag 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