1 Overview
A zap is a Lightning payment that leaves a public trace on Nostr. This NIP defines two event kinds for that trace. Kind 9734 is a zap request. It represents a payer's request to a recipient's Lightning wallet for an invoice. Kind 9735 is a zap receipt. It represents the confirmation, by the recipient's Lightning wallet, that the invoice issued for a zap request has been paid.
The sender is the user who pays. The recipient is the user who gets paid. The recipient's wallet sits behind an LNURL server: an HTTP service that issues Lightning invoices per the lnurl specifications.
With zap receipts on Nostr, clients can display Lightning payments from entities on the network. These can be used for fun or for spam deterrence.
A zap receipt is not a proof of payment. It only proves that some Nostr user fetched an invoice. The receipt implies the invoice was paid, but a rogue implementation could lie. You trust the author of the zap receipt for the legitimacy of the payment.
2 Protocol Flow
A zap runs through nine steps. HTTP carries the request and the invoice. Lightning carries the money. Nostr relays carry the receipt.
- The client finds the recipient's lnurl pay request URL. If the target event has a
zaptag, calculate it from that tag (see §9). Otherwise decode thelud16field on the recipient's profile per the lnurl specifications. The client MUST send a GET request to this URL and parse the response. IfallowsNostrexists and istrue, and ifnostrPubkeyexists and is a valid BIP-340 public key in hex, the client should associate this information with the user, along with the response'scallback,minSendable, andmaxSendablevalues. - Clients may display a zap button on each post or on a user's profile. If the recipient's lnurl endpoint supports Nostr, the client SHOULD use this NIP to request a zap receipt instead of a normal lnurl invoice.
- When the sender starts a zap, the client creates a kind
9734zap request event as described in §3 and signs it. - The client does not publish the zap request. It sends the event to the
callbackURL from the lnurl pay endpoint, using a GET request. See §4. - The recipient's LNURL server receives the zap request and validates it. See §5 for server configuration and §6 for the validation rules.
- If the zap request is valid, the server fetches a description hash invoice. The description is this zap request note, and this note only. No additional lnurl metadata goes into the description. The server returns the invoice per LUD06.
- On receiving the invoice, the client MAY pay it or pass it to an app that can pay it.
- Once the invoice is paid, the recipient's LNURL server MUST generate a zap receipt as described in §7. It publishes the receipt to the
relayslisted in the zap request. - Clients MAY fetch zap receipts on posts and profiles, but MUST authorize their validity as described in §8. If the zap request has a non-empty
content, the client may display it as a zap comment. Generally clients should show users the zap request note, and use the zap receipt to show "zap authorized by ...". This is optional.
3 The Zap Request (kind 9734)
A zap request is an event of kind 9734. It is not published to relays. The client sends it to the recipient's lnurl pay callback URL instead. The content MAY be an optional message to send along with the payment.
The event MUST include these tags:
| Tag | Value | Rule |
|---|---|---|
| relays | one or more relay URLs | The relays the recipient's wallet should publish its zap receipt to. Put each URL as its own element of the tag. Do not nest the relays in an additional list. |
| amount | string, millisats | The amount the sender intends to pay, in millisats, formatted as a string. Recommended, but optional. |
| lnurl | bech32 string | The lnurl pay URL of the recipient, encoded with bech32 and the prefix lnurl. Recommended, but optional. |
| p | hex pubkey | The hex-encoded pubkey of the recipient. |
The event MAY include these tags:
| Tag | Value | Rule |
|---|---|---|
| e | hex event id | Optional. Clients MUST include it when they zap an event rather than a person. |
| a | event coordinate | Optional. Allows tipping addressable events, such as NIP-23 long-form notes. |
| k | string | The stringified kind of the target event. |
{
"kind": 9734,
"content": "Zap!",
"tags": [
["relays", "wss://nostr-pub.wellorder.com", "wss://anotherrelay.example.com"],
["amount", "21000"],
["lnurl", "lnurl1dp68gurn8ghj7um5v93kketj9ehx2amn9uh8wetvdskkkmn0wahz7mrww4excup0dajx2mrv92x9xp"],
["p", "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9"],
["e", "9ae37aa68f48645127299e9453eb5d908a0cbb6058ff340d528ed4d37c8994fb"],
["k", "1"]
],
"pubkey": "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322",
"created_at": 1679673265,
"id": "30efed56a035b2549fcaeec0bf2c1595f9a9b3bb4b1a38abaf8ee9041c4b7d93",
"sig": "f2cb581a84ed10e4dc84937bd98e27acac71ab057255f6aa8dfa561808c981fe8870f4a03c1e3666784d82a9c802d3704e174371aa13d63e2aeaf24ff5374d9d"
}
4 Sending the Request over HTTP
The client sends the signed zap request with an HTTP GET request to the recipient's callback URL. The lnurl pay endpoint provided that URL in step 1. The request should define these query parameters:
| Parameter | Content |
|---|---|
| amount | The amount in millisats the sender intends to pay. |
| nostr | The kind 9734 zap request event, JSON encoded then URI encoded. |
| lnurl | The lnurl pay URL of the recipient, encoded with bech32 and the prefix lnurl. |
The response is a JSON object with a pr key. The pr value is the invoice the sender must pay to finalize the zap. This flow looks like this in JavaScript:
// senderPubkey: the sender's pubkey // recipientPubkey: the recipient's pubkey // callback: from the recipient's lnurl pay endpoint // lnurl: the recipient's lightning address, encoded as a lnurl const sats = 21 const amount = sats * 1000 const relays = ['wss://nostr-pub.wellorder.net'] const event = encodeURI(JSON.stringify(await signEvent({ kind: 9734, content: "", pubkey: senderPubkey, created_at: Math.round(Date.now() / 1000), tags: [ ["relays", ...relays], ["amount", amount.toString()], ["lnurl", lnurl], ["p", recipientPubkey], ], }))) const {pr: invoice} = await fetchJson(`${callback}?amount=${amount}&nostr=${event}&lnurl=${lnurl}`)
5 LNURL Server Configuration
An LNURL server needs two extra fields so that clients know it supports zap invoices. Add them to the lnurl-pay static endpoint /.well-known/lnurlp/<user>:
- Add a
nostrPubkeyfield. This is the Nostr pubkey your server uses to sign zap receipt events. Clients use it to validate zap receipts. - Add an
allowsNostrfield and set it totrue.
The nostrPubkey belongs to the server, not to the user. One zap-enabled server signs every receipt it publishes with this one key. Clients compare a receipt's pubkey against it in §8.
6 Server-Side Validation
When a client calls the lnurl-pay callback URL, the nostr query parameter holds the zap request event, URI- and JSON-encoded. If the parameter is present, the server must validate the event with these rules:
- It MUST have a valid Nostr signature.
- It MUST have tags.
- It MUST have only one
ptag. - It MUST have 0 or 1
etags. - There should be a
relaystag with the relays to send the zap receipt to. - If there is an
amounttag, it MUST equal theamountquery parameter. - If there is an
atag, it MUST be a valid event coordinate. - There MUST be 0 or 1
Ptags. If there is one, it MUST equal the zap receipt'spubkey.
The server MUST then store the event for later use, when the invoice is paid.
Rule 8 is inconsistent with §7. Here, a P tag in the zap request must equal the zap receipt's pubkey — the LNURL server's nostrPubkey. In §7, the P tag of the zap receipt holds the zap sender's pubkey instead. The source spec defines no sender-side meaning for a P tag in a zap request. Implementations should treat rule 8 with care.
7 The Zap Receipt (kind 9735)
A zap receipt is an event of kind 9735. A Lightning node creates it when an invoice generated by a zap request is paid. Zap receipts are only created when the invoice description — committed to the description hash — contains a zap request note.
When the payment arrives, the server executes these steps:
- Get the description for the invoice. Save it somewhere during the generation of the description hash invoice. CLN, the reference implementation here, saves it automatically.
- Parse the bolt11 description as a JSON Nostr event. This SHOULD be validated against the rules in §6, either when it is received, or before the invoice is paid.
- Create a Nostr event of kind
9735as described below. Publish it to therelaysdeclared in the zap request.
The following should be true of the zap receipt event:
- The
contentSHOULD be empty. - The
created_atdate SHOULD be set to the invoicepaid_atdate, for idempotency. tagsMUST include theptag (zap recipient) AND the optionaletag from the zap request AND the optionalatag from the zap request AND the optionalPtag from the pubkey of the zap request (zap sender).- The receipt MUST have a
bolt11tag that contains the description hash bolt11 invoice. - The receipt MUST contain a
descriptiontag: the JSON-encoded zap request. SHA256(description)SHOULD match the description hash in the bolt11 invoice.- The receipt MAY contain a
preimagetag to match against the payment hash of the bolt11 invoice. This is not really a payment proof. There is no real way to prove that the invoice is real or has been paid. You trust the author of the zap receipt for the legitimacy of the payment.
A reference implementation for a zap-enabled LNURL server is cln-nostr-zapper.
{
"id": "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446",
"pubkey": "9630f464cca6a5147aa8a35f0bcdd3ce485324e732fd39e09233b1d848238f31",
"created_at": 1674164545,
"kind": 9735,
"tags": [
["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"],
["P", "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322"],
["e", "3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8"],
["k", "1"],
["bolt11", "lnbc10u1p3unwfusp5t9r3yymhpfqculx78u027lxspgxcr2n2987mx2j55nnfs95nxnzqpp5jmrh92pfld78spqs78v9euf2385t83uvpwk9ldrlvf6ch7tpascqhp5zvkrmemgth3tufcvflmzjzfvjt023nazlhljz2n9hattj4f8jq8qxqyjw5qcqpjrzjqtc4fc44feggv7065fqe5m4ytjarg3repr5j9el35xhmtfexc42yczarjuqqfzqqqqqqqqlgqqqqqqgq9q9qxpqysgq079nkq507a5tw7xgttmj4u990j7wfggtrasah5gd4ywfr2pjcn29383tphp4t48gquelz9z78p4cq7ml3nrrphw5w6eckhjwmhezhnqpy6gyf0"],
["description", "{\"pubkey\":\"97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322\",\"content\":\"\",\"id\":\"d9cc14d50fcb8c27539aacf776882942c1a11ea4472f8cdec1dea82fab66279d\",\"created_at\":1674164539,\"sig\":\"77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d\",\"kind\":9734,\"tags\":[[\"e\",\"3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8\"],[\"p\",\"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\"],[\"relays\",\"wss://relay.damus.io\",\"wss://nostr-relay.wlvs.space\",\"wss://nostr.fmt.wiz.biz\",\"wss://relay.nostr.bg\",\"wss://nostr.oxtr.dev\",\"wss://nostr.v0l.io\",\"wss://brb.io\",\"wss://nostr.bitcoiner.social\",\"ws://monad.jb55.com:8080\",\"wss://relay.snort.social\"]]}"],
["preimage", "5d006d2cf1e73c7148e7519a4c68adc81642ce0e25a432b2434c99f97344c15f"]
],
"content": ""
}
The source spec's receipt example is not valid JSON. It has a trailing comma after content and omits the sig field. A real kind 9735 event must carry a valid sig per NIP-01. The example above fixes the comma but keeps the source's field set. The source also mentions the lud16 profile field only in the flow; older lud06 lookup was removed from the spec in 2025.
8 Client-Side Receipt Validation
A client can retrieve zap receipts on events and pubkeys with a NIP-01 filter, for example {"kinds": [9735], "#e": [...]}. Zaps MUST be validated with these steps:
- The zap receipt's
pubkeyMUST be the same as the recipient's lnurl provider'snostrPubkey, retrieved in step 1 of the protocol flow. - The
invoiceAmountin the receipt'sbolt11tag MUST equal theamounttag of the zap request, if present. - The
lnurltag of the zap request, if present, SHOULD equal the recipient'slnurl.
9 Zap Splits: the zap Tag
An event can carry one or more zap tags. Clients that want to zap such an event SHOULD calculate the lnurl pay request from these tags, not from the author's profile field. Each zap tag has this shape: the second element is the hex pubkey of a receiver. The third element is the relay to download the receiver's kind-0 metadata from. The optional fourth element is a weight: a generalization of a percentage.
Clients should parse all weights, calculate their sum, and then a percentage for each receiver. If no weights are present, clients should divide the zap amount equally across all receivers. If weights are only partially present, receivers without a weight get weight = 0 and are not zapped.
{
"tags": [
["zap", "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2", "wss://nostr.oxtr.dev", "1"], // 25%
["zap", "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", "wss://nostr.wine/", "1"], // 25%
["zap", "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", "wss://nos.lol/", "2"] // 50%
]
}
Clients MAY display the zap split configuration in the note.
10 Worked Example
Alice zaps 21 sats to Bob's note. Her client already fetched Bob's lnurl endpoint and saw allowsNostr: true. The transcript shows the HTTP leg first, then the relay leg. Orange lines go out from a client. Blue lines come back.
-- 1. Alice's client asks Bob's LNURL server for an invoice -- → GET https://pay.bob.example/callback?amount=21000 &nostr=%7B%22kind%22%3A9734%2C%22content%22%3A%22Zap!%22%2C...%7D &lnurl=lnurl1dp68gurn8ghj7... ← {"pr": "lnbc210n1p3unwfu...qy9qxpq"} -- 2. Alice pays the bolt11 invoice with her Lightning wallet -- -- 3. Bob's LNURL server publishes the receipt to the request's relays -- → ["EVENT", {"kind":9735, "pubkey":"9630f464cca6…", "created_at":1674164545, // = invoice paid_at "tags":[["p","32e18276…"], ["P","97c70a44…"], ["e","3624762a…"], ["bolt11","lnbc210n1p3unwfu…"], ["description","{\"kind\":9734,…}"], ["preimage","5d006d2c…"]], "content":"", "sig":"c2e9f3a1…"}] ← ["OK", "67b48a14fb…", true, ""] -- 4. Carol's client fetches and validates zaps on Bob's note -- → ["REQ", "zaps", {"kinds":[9735], "#e":["3624762a1274dd96…"]}] ← ["EVENT", "zaps", {"kind":9735, "pubkey":"9630f464cca6…", …}] ← ["EOSE", "zaps"] -- Carol checks: pubkey == Bob's nostrPubkey; bolt11 amount == 21000 msat --
Zaps can become more private in the future. Encrypt the zap request note to the target user, and only the recipient can read who paid. The spec leaves this out of the initial draft for simplicity.
11 Version History
William Casarin proposed zaps in February 2023. Damus and Snort shipped them within weeks. Notable spec changes, newest first:
- 20252025-09Removed the
lud06profile field from the lookup flow. Clients now decodelud16only. - 20252025-02Relaxed the description hash check:
SHA256(description)SHOULD (no longer MUST) match the bolt11 invoice. - 20242024-01Added the
Ptag so zap receipts index the zap sender. - 20232023-09Zap receipts must copy the optional
atag from the zap request. - 20232023-05Added zap splits with weighted
zaptags. Renamed "zap note" to "zap receipt". - 20232023-03Full rewrite: consolidated the protocol flow and the appendix structure, clarified terminology.
- 20232023-02Added the
amounttag and theatag for addressable event coordinates. - Origin2023-02-13William Casarin published NIP-57 (#224).
Original Proposal - Credits
| Author | William Casarin (jb55) |
| First commit | 2023-02-13 — “NIP-57: Lightning Zaps (#224)” |
| Pull request | nostr-protocol/nips#224 |
| Status | draft - optional — clients and LNURL servers may implement it |
| Source | 57.md on GitHub - full change history |
12 See Also
- NIP-01 — event format, filters, and the relay messages used to fetch receipts.
- NIP-23 — long-form notes; zap them through the
acoordinate tag. - NIP-47 — Nostr Wallet Connect; lets a client pay the zap invoice through a remote wallet.
- NIP-19 — bech32 encodings; the
lnurltag uses the same bech32 alphabet with anlnurlprefix.