Nostr Intermediate 6 min read
Lightning Zaps
Send and receive Lightning payments through Nostr with NIP-57 Zaps. Social micropayments for agents.
zaps lightning micropayments NIP-57 LNURL tips
Lightning Zaps
Zaps are Lightning payments sent through Nostr. They connect social interaction with value transfer—you can tip a note, support a creator, or receive payments for your agent’s services.
How Zaps Work
1. Alice wants to zap Bob's note
2. Alice's client fetches Bob's Lightning address (LNURL)
3. Alice creates a zap request (kind 9734)
4. Alice sends request to Bob's LNURL provider
5. Provider returns a Lightning invoice
6. Alice pays the invoice
7. Provider publishes a zap receipt (kind 9735)
8. Bob (and everyone) sees the zap
Zap Flow Diagram
┌─────────┐ ┌─────────────┐ ┌──────────────┐
│ Alice │ │ LNURL Server│ │ Relays │
└────┬────┘ └──────┬──────┘ └──────┬───────┘
│ │ │
│─── Zap Request ─►│ │
│ (kind 9734) │ │
│ │ │
│◄── Invoice ──────│ │
│ │ │
│─── Pay Invoice ──►│ │
│ │ │
│ │─── Zap Receipt ──►│
│ │ (kind 9735) │
│ │ │
Setting Up Zap Receiving
1. Configure Lightning Address
Add a Lightning address to your profile:
{
"kind": 0,
"content": "{\"name\":\"Agent\",\"lud16\":\"agent@getalby.com\",\"lud06\":\"LNURL...\"}"
}
| Field | Format | Example |
|---|---|---|
lud16 | Email-like | user@provider.com |
lud06 | LNURL string | lnurl1dp68... |
Either works; lud16 is more readable.
2. Use a Zap-Compatible Provider
| Provider | Setup | Notes |
|---|---|---|
| Alby | getalby.com | Easy, custodial option |
| Wallet of Satoshi | walletofsatoshi.com | Mobile-friendly |
| LNbits | Self-hosted | Full control |
| Zeus | zeusln.app | Self-custodial |
3. Enable Zap Receipts
Your LNURL provider must support NIP-57 to publish zap receipts.
Sending Zaps
Step 1: Get Recipient’s LNURL
import httpx
import json
async def get_lnurl_from_profile(pubkey: str, relays: list) -> str | None:
"""Fetch user's Lightning address from their profile."""
# Query for kind 0 (metadata)
profile = await query_profile(pubkey, relays)
if not profile:
return None
metadata = json.loads(profile["content"])
# Check for lud16 (Lightning Address) or lud06 (LNURL)
lud16 = metadata.get("lud16")
lud06 = metadata.get("lud06")
if lud16:
# Convert Lightning Address to LNURL endpoint
name, domain = lud16.split("@")
return f"https://{domain}/.well-known/lnurlp/{name}"
elif lud06:
# Decode LNURL
return decode_lnurl(lud06)
return None
Step 2: Create Zap Request
def create_zap_request(
sender_pubkey: str,
recipient_pubkey: str,
amount_msats: int,
relays: list,
event_id: str = None, # Optional: zap specific note
content: str = "" # Optional: zap comment
) -> dict:
"""Create a NIP-57 zap request (kind 9734)."""
tags = [
["relays", *relays],
["amount", str(amount_msats)],
["p", recipient_pubkey]
]
# If zapping a specific note
if event_id:
tags.append(["e", event_id])
return {
"kind": 9734,
"pubkey": sender_pubkey,
"created_at": int(time.time()),
"tags": tags,
"content": content
}
Step 3: Request Invoice
async def request_zap_invoice(
lnurl_endpoint: str,
zap_request: dict,
amount_msats: int
) -> str:
"""Request a Lightning invoice for a zap."""
# Sign the zap request
signed_request = sign_event(zap_request, private_key)
request_json = json.dumps(signed_request)
# URL-encode the request
encoded_request = urllib.parse.quote(request_json)
# Call LNURL callback
async with httpx.AsyncClient() as client:
# First, get the callback URL
lnurl_response = await client.get(lnurl_endpoint)
lnurl_data = lnurl_response.json()
callback = lnurl_data["callback"]
# Request invoice with zap request
params = {
"amount": amount_msats,
"nostr": encoded_request
}
invoice_response = await client.get(callback, params=params)
invoice_data = invoice_response.json()
return invoice_data["pr"] # BOLT11 invoice
Step 4: Pay Invoice
async def pay_zap(invoice: str, lnbits_url: str, admin_key: str):
"""Pay the zap 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 = ""
):
"""Send a complete zap."""
# 1. Get recipient's LNURL
lnurl = await get_lnurl_from_profile(recipient_pubkey, RELAYS)
if not lnurl:
raise ValueError("Recipient has no Lightning address")
# 2. Create zap request
amount_msats = amount_sats * 1000
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
)
# 3. Get invoice
invoice = await request_zap_invoice(lnurl, zap_request, amount_msats)
# 4. Pay invoice
payment = await pay_zap(invoice, LNBITS_URL, ADMIN_KEY)
return payment
Receiving Zaps
Monitor Zap Receipts
async def monitor_zaps(my_pubkey: str):
"""Subscribe to incoming zaps."""
filter = {
"kinds": [9735], # Zap receipts
"#p": [my_pubkey]
}
async for event in subscribe(RELAYS, filter):
zap = parse_zap_receipt(event)
print(f"Received {zap['amount']} sats from {zap['sender']}")
Parse Zap Receipt
def parse_zap_receipt(event: dict) -> dict:
"""Parse a zap receipt (kind 9735)."""
# Extract tags
tags = {t[0]: t[1:] for t in event["tags"]}
# Get the embedded zap request
description_tag = tags.get("description", [""])[0]
zap_request = json.loads(description_tag) if description_tag else None
# Get bolt11 to extract amount
bolt11 = tags.get("bolt11", [""])[0]
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": zap_request["pubkey"] if zap_request else None,
"recipient": tags.get("p", [None])[0],
"zapped_event": tags.get("e", [None])[0],
"comment": zap_request["content"] if zap_request else "",
"timestamp": event["created_at"]
}
Zap Splits
NIP-57 supports splitting zaps among multiple recipients:
{
"kind": 9734,
"tags": [
["p", "pubkey1", "wss://relay1", "0.7"],
["p", "pubkey2", "wss://relay2", "0.3"]
]
}
The LNURL provider handles splitting the payment.
Agent Use Cases
Accept Zaps for Services
# Monitor for zaps with specific content
async def handle_service_request(zap):
if zap["comment"].startswith("/query"):
query = zap["comment"][7:]
result = await process_query(query)
await post_reply(zap["zapped_event"], result)
Zap-Gated Content
# Check if user has zapped before revealing content
async def has_zapped(user_pubkey: str, my_pubkey: str, min_sats: int):
zaps = await query_zaps_from(user_pubkey, my_pubkey)
total = sum(z["amount_sats"] for z in zaps)
return total >= min_sats
Automated Tipping
# Auto-zap quality content
async def auto_zap_if_quality(event):
score = await analyze_content(event["content"])
if score > 0.8:
await send_zap(event["pubkey"], amount_sats=21)
Machine-Readable Summary
{
"topic": "nostr-zaps",
"audience": "ai-agents",
"prerequisites": ["lightning-basics", "nostr-events", "lnurl"],
"key_concepts": [
"zap-request-kind-9734",
"zap-receipt-kind-9735",
"lnurl-integration",
"zap-splits"
],
"code_examples": ["python"],
"event_kinds": {
"zap_request": 9734,
"zap_receipt": 9735
},
"related": [
"/learn/nostr/specs/nip-57",
"/learn/lightning/api-lnurl",
"/learn/lightning/invoices"
]
}