NIP-nip-57 Final
NIP-57: Lightning Zaps
Lightning payments through Nostr. Send and receive zaps with cryptographic proof of payment.
| Type | Nostr Implementation Possibility |
| Number | nip-57 |
| Status | Final |
| Original | https://github.com/nostr-protocol/nips/blob/master/57.md |
NIP-57: Lightning Zaps
Status: Final
NIP-57 defines zaps—Lightning payments sent through Nostr with cryptographic proof. Zaps combine social interaction with value transfer.
Overview
┌─────────┐ ┌─────────────┐ ┌─────────────┐
│ Alice │ │ LNURL Server│ │ Relays │
│ (Zapper)│ │ (Bob's) │ │ │
└────┬────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ 1. Zap Request │ │
│────────────────►│ │
│ │ │
│ 2. Invoice │ │
│◄────────────────│ │
│ │ │
│ 3. Pay Invoice │ │
│────────────────►│ │
│ │ │
│ │ 4. Zap Receipt │
│ │───────────────────►
│ │ │
Event Kinds
| Kind | Name | Purpose |
|---|---|---|
| 9734 | Zap Request | Request sent to LNURL server |
| 9735 | Zap Receipt | Proof of payment (published by server) |
Setting Up to Receive Zaps
1. Get a Lightning Address
From a provider that supports NIP-57:
- Alby (getalby.com)
- Wallet of Satoshi
- LNbits (self-hosted)
- Zeus (self-custodial)
2. Update Profile
Add lud16 (Lightning Address) to kind 0:
{
"kind": 0,
"content": "{\"name\":\"Alice\",\"lud16\":\"alice@getalby.com\"}"
}
Or lud06 (raw LNURL):
{
"content": "{\"name\":\"Alice\",\"lud06\":\"LNURL1...\"}"
}
3. Verify NIP-57 Support
Your LNURL endpoint must include allowsNostr: true:
curl https://getalby.com/.well-known/lnurlp/alice
{
"callback": "https://getalby.com/lnurlp/alice/callback",
"maxSendable": 10000000000,
"minSendable": 1000,
"allowsNostr": true,
"nostrPubkey": "server-pubkey-hex"
}
Sending Zaps
Step 1: Get Recipient’s LNURL
async def get_recipient_lnurl(pubkey: str, relays: list) -> str:
"""Fetch recipient's Lightning address from profile."""
profile = await fetch_profile(pubkey, relays)
if not profile:
return None
content = json.loads(profile["content"])
lud16 = content.get("lud16")
if lud16:
name, domain = lud16.split("@")
return f"https://{domain}/.well-known/lnurlp/{name}"
lud06 = content.get("lud06")
if lud06:
return decode_lnurl(lud06)
return None
Step 2: Verify NIP-57 Support
async def get_lnurl_params(lnurl_endpoint: str) -> dict:
"""Fetch LNURL parameters."""
async with httpx.AsyncClient() as client:
response = await client.get(lnurl_endpoint)
params = response.json()
if not params.get("allowsNostr"):
raise ValueError("LNURL doesn't support zaps")
return params
Step 3: Create Zap Request (Kind 9734)
def create_zap_request(
sender_pubkey: str,
recipient_pubkey: str,
amount_msats: int,
relays: list,
event_id: str = None,
content: str = ""
) -> dict:
"""Create a NIP-57 zap request."""
tags = [
["relays", *relays],
["amount", str(amount_msats)],
["p", recipient_pubkey]
]
# Optional: zap specific event
if event_id:
tags.append(["e", event_id])
# Optional: lnurl tag
# tags.append(["lnurl", lnurl_bech32])
return {
"kind": 9734,
"pubkey": sender_pubkey,
"created_at": int(time.time()),
"tags": tags,
"content": content # Optional zap comment
}
Step 4: Request Invoice
async def request_zap_invoice(
lnurl_callback: str,
zap_request: dict,
amount_msats: int
) -> str:
"""Request invoice from LNURL server."""
# Sign the zap request
signed_request = sign_event(zap_request, PRIVATE_KEY)
# URL-encode the request
request_json = json.dumps(signed_request)
encoded = urllib.parse.quote(request_json)
# Request invoice
url = f"{lnurl_callback}?amount={amount_msats}&nostr={encoded}"
async with httpx.AsyncClient() as client:
response = await client.get(url)
data = response.json()
if "reason" in data:
raise ValueError(f"Error: {data['reason']}")
return data["pr"] # BOLT11 invoice
Step 5: Pay Invoice
async def pay_invoice(invoice: str, lnbits_url: str, admin_key: str):
"""Pay the BOLT11 invoice."""
async with httpx.AsyncClient() as client:
response = await client.post(
f"{lnbits_url}/api/v1/payments",
headers={"X-Api-Key": admin_key},
json={"out": True, "bolt11": invoice}
)
return response.json()
Complete Zap Flow
async def send_zap(
recipient_pubkey: str,
amount_sats: int,
event_id: str = None,
comment: str = ""
) -> dict:
"""Send a complete zap."""
amount_msats = amount_sats * 1000
# 1. Get recipient LNURL
lnurl = await get_recipient_lnurl(recipient_pubkey, RELAYS)
if not lnurl:
raise ValueError("No Lightning address")
# 2. Get LNURL params
params = await get_lnurl_params(lnurl)
# 3. Validate amount
if amount_msats < params["minSendable"]:
raise ValueError(f"Minimum: {params['minSendable']} msats")
if amount_msats > params["maxSendable"]:
raise ValueError(f"Maximum: {params['maxSendable']} msats")
# 4. Create zap request
zap_request = create_zap_request(
sender_pubkey=MY_PUBKEY,
recipient_pubkey=recipient_pubkey,
amount_msats=amount_msats,
relays=RELAYS,
event_id=event_id,
content=comment
)
# 5. Get invoice
invoice = await request_zap_invoice(
params["callback"],
zap_request,
amount_msats
)
# 6. Pay
result = await pay_invoice(invoice, LNBITS_URL, ADMIN_KEY)
return result
Zap Receipt (Kind 9735)
Published by the LNURL server after payment:
{
"kind": 9735,
"pubkey": "lnurl-server-pubkey",
"created_at": 1234567890,
"tags": [
["p", "recipient-pubkey"],
["e", "zapped-event-id"],
["P", "sender-pubkey"],
["bolt11", "lnbc..."],
["description", "<zap-request-json>"],
["preimage", "payment-preimage"]
],
"content": ""
}
Tag Reference
| Tag | Content |
|---|---|
p | Recipient pubkey |
e | Zapped event ID (optional) |
P | Sender pubkey (uppercase) |
bolt11 | The paid invoice |
description | Original zap request JSON |
preimage | Payment preimage (proof) |
Receiving Zaps
Monitor Incoming Zaps
async def monitor_zaps(my_pubkey: str):
"""Subscribe to incoming zaps."""
filter = {
"kinds": [9735],
"#p": [my_pubkey]
}
async for event in subscribe(RELAYS, filter):
zap = parse_zap_receipt(event)
print(f"Received {zap['amount_sats']} sats!")
print(f"From: {zap['sender']}")
if zap['comment']:
print(f"Comment: {zap['comment']}")
Parse Zap Receipt
def parse_zap_receipt(event: dict) -> dict:
"""Parse a zap receipt (kind 9735)."""
tags = {}
for tag in event["tags"]:
tags[tag[0]] = tag[1] if len(tag) > 1 else None
# Get zap request from description
description = tags.get("description", "{}")
zap_request = json.loads(description) if description else None
# Get amount from bolt11
bolt11 = tags.get("bolt11", "")
amount_msats = decode_bolt11_amount(bolt11) if bolt11 else 0
return {
"receipt_id": event["id"],
"amount_msats": amount_msats,
"amount_sats": amount_msats // 1000,
"sender": tags.get("P"), # Uppercase P = sender
"recipient": tags.get("p"), # Lowercase p = recipient
"zapped_event": tags.get("e"),
"comment": zap_request.get("content", "") if zap_request else "",
"preimage": tags.get("preimage"),
"timestamp": event["created_at"]
}
Zap Splits
Distribute a zap among multiple recipients:
{
"kind": 9734,
"tags": [
["relays", "wss://relay.damus.io"],
["amount", "10000"],
["p", "pubkey1", "wss://relay", "0.7"],
["p", "pubkey2", "wss://relay", "0.3"]
]
}
The weight values (0.7, 0.3) indicate split ratios.
Verification
Always verify zap receipts:
def verify_zap_receipt(receipt: dict) -> bool:
"""Verify a zap receipt is legitimate."""
# 1. Verify signature
if not verify_event_signature(receipt):
return False
# 2. Get zap request from description
description = get_tag(receipt, "description")
if not description:
return False
zap_request = json.loads(description)
# 3. Verify zap request signature
if not verify_event_signature(zap_request):
return False
# 4. Verify amounts match
request_amount = int(get_tag(zap_request, "amount") or 0)
bolt11 = get_tag(receipt, "bolt11")
invoice_amount = decode_bolt11_amount(bolt11)
if request_amount != invoice_amount:
return False
# 5. Verify recipient matches
receipt_recipient = get_tag(receipt, "p")
request_recipient = get_tag(zap_request, "p")
if receipt_recipient != request_recipient:
return False
return True
Agent Use Cases
Accept Zaps for Services
async def handle_zap(zap):
"""Process incoming zap for service."""
if zap["amount_sats"] >= MIN_PAYMENT:
# Extract request from comment
if zap["comment"].startswith("/query"):
query = zap["comment"][7:]
result = await process_query(query)
await post_reply(zap["zapped_event"], result)
Automated Tipping
async def auto_zap_quality_content(event):
"""Auto-zap content that meets quality threshold."""
score = await evaluate_content(event)
if score > 0.8:
await send_zap(
event["pubkey"],
amount_sats=21,
event_id=event["id"],
comment="Great content!"
)
Machine-Readable Summary
{
"nip": 57,
"title": "Lightning Zaps",
"status": "final",
"defines": [
"zap-request-format",
"zap-receipt-format",
"lnurl-integration",
"zap-verification"
],
"event_kinds": {
"zap_request": 9734,
"zap_receipt": 9735
},
"required_lnurl_fields": [
"allowsNostr",
"nostrPubkey"
],
"zap_request_tags": ["relays", "amount", "p", "e"],
"zap_receipt_tags": ["p", "e", "P", "bolt11", "description", "preimage"],
"related": [
"/learn/nostr/zaps",
"/learn/lightning/api-lnurl",
"/learn/lightning/invoices"
]
}