Lightning Zaps
NIP-57 enables Lightning Network payments (zaps) on Nostr, allowing users to send Bitcoin tips to events and profiles using kind 9735 zap receipt events.
NIP-57: Lightning Zaps
Status: Final Authors: jb55, kieran Category: Monetization
Overview
NIP-57 defines Lightning Zaps — the ability to send Bitcoin payments directly to Nostr events and users via the Lightning Network.
What is a Zap?
- ✅ Bitcoin payment sent via Lightning Network
- ✅ Publicly visible on Nostr (kind 9735 events)
- ✅ Attached to specific events or profiles
- ✅ Displays sender, amount, and optional message
- ✅ Near-instant and low-fee
Zaps enable value-for-value monetization on Nostr — if you like content, zap it!
Why Zaps Matter
Value-for-Value Economy
Traditional platforms:
- Ads for revenue
- Algorithms for engagement
- Platform takes cut
Nostr with Zaps:
- Direct creator support
- No intermediaries (peer-to-peer)
- No platform fees
- No ads needed
Use Cases
- Tipping Content: Zap great posts, articles, artwork
- Supporting Creators: Recurring zaps to favorite creators
- Paywalls: Zap to unlock premium content
- Bounties: Offer zaps for solutions/answers
- Social Signaling: Large zaps show strong support
- Micropayments: Send tiny amounts (1 sat = $0.0003)
How It Works
The Zap Flow
1. User clicks "Zap" button
↓
2. Client fetches recipient's Lightning address (from profile)
↓
3. Client requests invoice from Lightning address service
↓
4. User's Lightning wallet pays invoice
↓
5. Lightning service creates zap receipt (kind 9735)
↓
6. Zap receipt published to Nostr relays
↓
7. Everyone sees the zap on the event
Event Types
Kind 9735: Zap Receipt
Published by Lightning services to prove a zap occurred:
{
"kind": 9735,
"created_at": 1673347337,
"content": "Great post! ⚡",
"tags": [
["bolt11", "lnbc1000n1p..."],
["description", "{\"kind\":9734,...}"],
["e", "zapped_event_id"],
["p", "recipient_pubkey"],
["amount", "1000"]
],
"pubkey": "lightning_service_pubkey",
"id": "...",
"sig": "..."
}
Key Tags:
- bolt11: Lightning invoice that was paid
- description: Original zap request (kind 9734)
- e: Event being zapped (if any)
- p: Recipient’s pubkey
- amount: Amount in millisats (1000 = 1 sat)
Kind 9734: Zap Request (ephemeral)
Created by the sender’s client, sent to Lightning service:
{
"kind": 9734,
"created_at": 1673347337,
"content": "Great post! ⚡",
"tags": [
["relays", "wss://relay.damus.io", "wss://relay.primal.net"],
["amount", "1000"],
["lnurl", "lnurl1dp68gurn8ghj..."],
["p", "recipient_pubkey"],
["e", "event_id"]
],
"pubkey": "sender_pubkey",
"id": "...",
"sig": "..."
}
Note: Kind 9734 is not published to Nostr relays directly—it’s included in the invoice request.
Setting Up Zaps (Recipients)
1. Get a Lightning Address
Lightning addresses look like email: alice@getalby.com
Popular Services:
- Alby (getalby.com) - Browser extension + Lightning address
- Strike (strike.me) - User-friendly wallet
- Wallet of Satoshi - Mobile wallet with Lightning address
- ZBD (zbd.gg) - Gaming-focused wallet
- Custom: Run your own Lightning node with LNURL service
2. Add Lightning Address to Profile
Update your kind 0 profile:
{
"kind": 0,
"content": "{
\"name\": \"Alice\",
\"about\": \"Nostr enthusiast\",
\"picture\": \"https://example.com/alice.jpg\",
\"lud16\": \"alice@getalby.com\"
}",
...
}
Field: lud16 (Lightning User Domain, version 16)
Alternative: lud06 for older LNURL format
3. Done!
Users can now zap your profile and your events.
Sending Zaps (Senders)
Prerequisites
- Lightning Wallet with funds
- Nostr Client that supports zaps (Damus, Primal, Amethyst, etc.)
- Recipient with Lightning address in their profile
Example: Send a Zap
import { getPublicKey, signEvent } from 'nostr-tools';
async function sendZap(recipientPubkey, eventId, amount, message, privateKey) {
// 1. Fetch recipient's profile to get Lightning address
const profile = await fetchProfile(recipientPubkey);
const lud16 = JSON.parse(profile.content).lud16;
if (!lud16) {
throw new Error("Recipient doesn't have a Lightning address");
}
// 2. Create zap request (kind 9734)
const zapRequest = {
kind: 9734,
created_at: Math.floor(Date.now() / 1000),
content: message || "",
tags: [
["relays", "wss://relay.damus.io", "wss://relay.primal.net"],
["amount", String(amount * 1000)], // Convert sats to millisats
["lnurl", await getLNURL(lud16)],
["p", recipientPubkey]
],
pubkey: getPublicKey(privateKey)
};
if (eventId) {
zapRequest.tags.push(["e", eventId]);
}
const signedRequest = signEvent(zapRequest, privateKey);
// 3. Get invoice from Lightning service
const invoice = await requestInvoice(lud16, amount, signedRequest);
// 4. Pay invoice with Lightning wallet
await payInvoice(invoice);
// 5. Wait for zap receipt to appear on relays
// (Lightning service will publish kind 9735 event)
}
// Usage
await sendZap(
"recipient_pubkey",
"event_id_to_zap",
100, // 100 sats
"Great post! ⚡",
privateKey
);
Fetching Zaps
Get Zaps for an Event
async function getEventZaps(eventId, relays) {
const filters = {
kinds: [9735],
"#e": [eventId]
};
const zaps = await Promise.all(
relays.map(relay => relay.list([filters]))
);
return zaps.flat();
}
// Usage
const zaps = await getEventZaps(postId, myRelays);
console.log(`${zaps.length} zaps received`);
Calculate Total Zapped Amount
function calculateZapTotal(zaps) {
return zaps.reduce((total, zap) => {
const amountTag = zap.tags.find(t => t[0] === "amount");
const millisats = amountTag ? parseInt(amountTag[1]) : 0;
return total + millisats;
}, 0) / 1000; // Convert millisats to sats
}
// Usage
const totalSats = calculateZapTotal(zaps);
console.log(`Total: ${totalSats} sats`);
Display Zap Count and Amount
function ZapDisplay({ event, zaps }) {
const totalSats = calculateZapTotal(zaps);
const zapCount = zaps.length;
return (
<div className="zap-display">
<span className="zap-icon">⚡</span>
<span className="zap-count">{zapCount}</span>
<span className="zap-amount">{totalSats.toLocaleString()} sats</span>
</div>
);
}
Lightning Address Discovery
Fetch Lightning Address from Profile
async function getLightningAddress(pubkey, relays) {
const profile = await relays[0].get({
kinds: [0],
authors: [pubkey],
limit: 1
});
if (!profile) return null;
const metadata = JSON.parse(profile.content);
return metadata.lud16 || metadata.lud06 || null;
}
// Usage
const lnAddress = await getLightningAddress(userPubkey, myRelays);
if (lnAddress) {
console.log(`Send zaps to: ${lnAddress}`);
}
LNURL to Lightning Address Conversion
function lud16ToLNURL(lud16) {
const [username, domain] = lud16.split('@');
const url = `https://${domain}/.well-known/lnurlp/${username}`;
// Encode as bech32 LNURL (for compatibility)
return bech32Encode('lnurl', url);
}
Zap Requests
Creating a Zap Request
function createZapRequest(recipientPubkey, eventId, amount, message, relays, privateKey) {
const zapRequest = {
kind: 9734,
created_at: Math.floor(Date.now() / 1000),
content: message || "",
tags: [
["p", recipientPubkey],
["amount", String(amount * 1000)], // sats to millisats
["relays", ...relays.map(r => r.url)]
],
pubkey: getPublicKey(privateKey)
};
if (eventId) {
zapRequest.tags.push(["e", eventId]);
}
return signEvent(zapRequest, privateKey);
}
Requesting Invoice
async function requestInvoice(lud16, amountSats, zapRequest) {
const [username, domain] = lud16.split('@');
const url = `https://${domain}/.well-known/lnurlp/${username}`;
// Get LNURL metadata
const metadata = await fetch(url).then(r => r.json());
// Request invoice
const invoiceUrl = metadata.callback;
const amount = amountSats * 1000; // millisats
const response = await fetch(
`${invoiceUrl}?amount=${amount}&nostr=${encodeURIComponent(JSON.stringify(zapRequest))}`
);
const data = await response.json();
if (data.status === 'ERROR') {
throw new Error(data.reason);
}
return data.pr; // Payment request (invoice)
}
Zap Receipts
Verifying Zap Receipts
function verifyZapReceipt(zapReceipt) {
// 1. Check signature is valid
if (!verifySignature(zapReceipt)) {
return false;
}
// 2. Verify bolt11 invoice was actually paid
const bolt11Tag = zapReceipt.tags.find(t => t[0] === "bolt11");
if (!bolt11Tag) return false;
// 3. Decode invoice and check amount
const invoice = decodeBolt11(bolt11Tag[1]);
// 4. Check description hash matches zap request
const descTag = zapReceipt.tags.find(t => t[0] === "description");
if (!descTag) return false;
const zapRequest = JSON.parse(descTag[1]);
// 5. Verify zap request is properly formed
if (zapRequest.kind !== 9734) return false;
return true;
}
UI Patterns
Zap Button
function ZapButton({ event, user }) {
const [amount, setAmount] = useState(21); // Default: 21 sats
const [showPicker, setShowPicker] = useState(false);
async function handleZap() {
try {
await sendZap(
event.pubkey,
event.id,
amount,
"", // No message
user.privateKey
);
toast.success(`Zapped ${amount} sats! ⚡`);
} catch (error) {
toast.error(`Zap failed: ${error.message}`);
}
}
return (
<div className="zap-button">
<button onClick={handleZap}>
⚡ Zap {amount} sats
</button>
{showPicker && (
<AmountPicker
value={amount}
onChange={setAmount}
onClose={() => setShowPicker(false)}
/>
)}
</div>
);
}
Amount Picker
function AmountPicker({ value, onChange, onClose }) {
const presets = [1, 21, 100, 1000, 10000];
return (
<div className="amount-picker">
<h3>How many sats?</h3>
<div className="presets">
{presets.map(amount => (
<button
key={amount}
onClick={() => { onChange(amount); onClose(); }}
className={value === amount ? 'selected' : ''}
>
{amount} sats
</button>
))}
</div>
<input
type="number"
value={value}
onChange={(e) => onChange(parseInt(e.target.value))}
placeholder="Custom amount"
/>
<button onClick={onClose}>Cancel</button>
</div>
);
}
Zap Leaderboard
function ZapLeaderboard({ event, zaps }) {
// Group zaps by sender
const zapsBySender = {};
zaps.forEach(zap => {
const descTag = zap.tags.find(t => t[0] === "description");
if (!descTag) return;
const zapRequest = JSON.parse(descTag[1]);
const sender = zapRequest.pubkey;
if (!zapsBySender[sender]) {
zapsBySender[sender] = { pubkey: sender, total: 0, count: 0 };
}
const amountTag = zap.tags.find(t => t[0] === "amount");
const millisats = amountTag ? parseInt(amountTag[1]) : 0;
zapsBySender[sender].total += millisats / 1000;
zapsBySender[sender].count += 1;
});
// Sort by total amount
const topZappers = Object.values(zapsBySender)
.sort((a, b) => b.total - a.total)
.slice(0, 10);
return (
<div className="zap-leaderboard">
<h3>Top Zappers ⚡</h3>
{topZappers.map((zapper, index) => (
<div key={zapper.pubkey} className="zapper-row">
<span className="rank">#{index + 1}</span>
<Avatar pubkey={zapper.pubkey} />
<span className="total">{zapper.total.toLocaleString()} sats</span>
<span className="count">({zapper.count} zaps)</span>
</div>
))}
</div>
);
}
Advanced Features
Zap Splits
Split zap amount between multiple recipients:
// Future NIP extension (not yet standardized)
const zapSplit = {
kind: 9734,
tags: [
["p", "recipient1", "0.5"], // 50%
["p", "recipient2", "0.3"], // 30%
["p", "recipient3", "0.2"] // 20%
],
...
};
Zap Goals
Set a fundraising goal:
function ZapGoal({ event, target }) {
const zaps = await getEventZaps(event.id);
const raised = calculateZapTotal(zaps);
const percentage = (raised / target) * 100;
return (
<div className="zap-goal">
<div className="progress-bar">
<div className="progress" style={{ width: `${percentage}%` }} />
</div>
<p>{raised.toLocaleString()} / {target.toLocaleString()} sats ({percentage.toFixed(1)}%)</p>
</div>
);
}
Anonymous Zaps
Some wallets support anonymous zaps (sender not revealed):
const anonZapRequest = {
kind: 9734,
tags: [
["p", recipientPubkey],
["anon", ""] // Anonymous zap flag
],
pubkey: "0000000000000000000000000000000000000000000000000000000000000000", // Zero pubkey
...
};
Note: Not all services support anonymous zaps.
Security Considerations
Verify Zap Receipts
Always verify zap receipts before displaying:
function displayZaps(zaps) {
return zaps
.filter(zap => verifyZapReceipt(zap))
.map(zap => <ZapDisplay zap={zap} />);
}
Check Invoice Paid
Zap receipt proves invoice exists, not that it was paid:
Best Practice: Trust reputable Lightning services (Alby, Strike, etc.)
Future: Proof-of-payment verification (Lightning payment hash)
Zap Spam
Fake zaps with tiny amounts to spam:
Mitigation:
- Minimum zap threshold (e.g., ignore <10 sats)
- Rate limit zap display
- Hide zaps from unknown pubkeys
Lightning Integration
Popular Lightning Wallets
Mobile:
- Alby (Browser extension + mobile)
- Wallet of Satoshi (iOS, Android)
- Zeus (Advanced, self-custodial)
- Muun (Beginner-friendly)
Desktop:
- Alby Extension (Chrome, Firefox)
- LNbits (Self-hosted)
- Thunderhub (Node management)
Nostr-Native:
- Damus Purple (Built into Damus iOS)
- Amethyst (Built-in zaps)
WebLN Integration
For browser-based zaps:
if (window.webln) {
await window.webln.enable();
const result = await window.webln.sendPayment(invoice);
console.log("Payment sent:", result);
} else {
alert("Please install Alby or another WebLN wallet");
}
Common Questions
What’s the minimum zap amount?
Technically 1 sat (1/100,000,000 BTC). Practically, Lightning routing may require ~10+ sats.
Do zaps cost fees?
Lightning fees are typically 1-3 sats per payment (negligible).
Can I zap without a Lightning wallet?
No. You need a Lightning-enabled Bitcoin wallet with funds.
Are zaps public?
Yes. Zap amounts, senders (unless anonymous), and recipients are all public via kind 9735 events.
Can I zap any event?
Only if the event author has a Lightning address in their profile.
What if my zap fails?
Invoice expires after ~10 minutes. Your wallet won’t send funds if payment fails.
Client Support
Full Zap Support
- Damus - Native zaps with Alby integration
- Primal - Built-in zaps with amounts display
- Amethyst - Comprehensive zap features
- Snort - Quick zaps with WebLN
- Iris - Basic zap support
- Nostrudel - Advanced zap analytics
Lightning Address Required
All clients require recipients to have:
lud16(Lightning address) orlud06(LNURL) in their kind 0 profile
Check our Client Directory for details.
Related NIPs
- NIP-01 - Basic protocol (event structure)
- NIP-25 - Reactions (zaps often paired with likes)
- NIP-05 - DNS verification (trust signal for zaps)
Technical Specification
For the complete technical specification, see NIP-57 on GitHub.
Summary
NIP-57 enables Bitcoin payments on Nostr:
✅ Lightning Network for instant, low-fee payments ✅ Kind 9735 zap receipt events (public record) ✅ Kind 9734 zap request events (ephemeral) ✅ Value-for-value monetization model
Setup:
- Get Lightning address (alice@getalby.com)
- Add
lud16to profile (kind 0) - Users can zap your events and profile
Flow:
- User clicks “Zap”
- Client requests invoice
- User pays via Lightning wallet
- Service publishes zap receipt
- Everyone sees the zap ⚡
Best practice: Verify zap receipts, support WebLN, display zap totals prominently.
Next Steps:
- Learn about lists in NIP-51
- Explore long-form content in NIP-23
- Understand relay lists in NIP-65
- Browse all NIPs in our reference
Last updated: January 2024 Official specification: GitHub
Client Support
This NIP is supported by the following clients: