NIP-57

Lightning Zaps

final monetization

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.

Author
jb55, kieran
Last Updated
15 January 2024
Official Spec
View on GitHub →

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

  1. Tipping Content: Zap great posts, articles, artwork
  2. Supporting Creators: Recurring zaps to favorite creators
  3. Paywalls: Zap to unlock premium content
  4. Bounties: Offer zaps for solutions/answers
  5. Social Signaling: Large zaps show strong support
  6. 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

  1. Lightning Wallet with funds
  2. Nostr Client that supports zaps (Damus, Primal, Amethyst, etc.)
  3. 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

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) or
  • lud06 (LNURL) in their kind 0 profile

Check our Client Directory for details.


  • 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:

  1. Get Lightning address (alice@getalby.com)
  2. Add lud16 to profile (kind 0)
  3. Users can zap your events and profile

Flow:

  1. User clicks “Zap”
  2. Client requests invoice
  3. User pays via Lightning wallet
  4. Service publishes zap receipt
  5. Everyone sees the zap ⚡

Best practice: Verify zap receipts, support WebLN, display zap totals prominently.


Next Steps:


Last updated: January 2024 Official specification: GitHub

Client Support

This NIP is supported by the following clients:

damus primal amethyst snort iris coracle nostrudel nos nostur plebstr current
View all clients →

Related NIPs

NIP-01 NIP-25 NIP-05
← Browse All NIPs