draft
optional
author:jb55
author:kieran
This NIP defines two new event types for recording lightning payments between users. 9734
is a zap request
, representing a payer's request to a recipient's lightning wallet for an invoice. 9735
is a zap receipt
, representing the confirmation by the recipient's lightning wallet that the invoice issued in response to a zap request
has been paid.
Having lightning receipts on nostr allows clients to display lightning payments from entities on the network. These can be used for fun or for spam deterrence.
- Client calculates a recipient's lnurl pay request url from the
zap
tag on the event being zapped (see Appendix G), or by decoding their lud06 or lud16 field on their profile according to the lnurl specifications. The client MUST send a GET request to this url and parse the response. IfallowsNostr
exists and it istrue
, and ifnostrPubkey
exists 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
, andmaxSendable
values. - Clients may choose to display a lightning zap button on each post or on a user's profile. If the user's lnurl pay request endpoint supports nostr, the client SHOULD use this NIP to request a
zap receipt
rather than a normal lnurl invoice. - When a user (the "sender") indicates they want to send a zap to another user (the "recipient"), the client should create a
zap request
event as described in Appendix A of this NIP and sign it. - Instead of publishing the
zap request
, the9734
event should instead be sent to thecallback
url received from the lnurl pay endpoint for the recipient using a GET request. See Appendix B for details and an example. - The recipient's lnurl server will receive this
zap request
and validate it. See Appendix C for details on how to properly configure an lnurl server to support zaps, and Appendix D for details on how to validate thenostr
query parameter. - If the
zap request
is valid, the server should fetch a description hash invoice where the description is thiszap request
note and this note only. No additional lnurl metadata is included in the description. This will be returned in the response according to LUD06. - On receiving the invoice, the client MAY pay it or pass it to an app that can pay the invoice.
- Once the invoice is paid, the recipient's lnurl server MUST generate a
zap receipt
as described in Appendix E, and publish it to therelays
specified in thezap request
. - Clients MAY fetch
zap receipt
s on posts and profiles, but MUST authorize their validity as described in Appendix F. If thezap request
note contains a non-emptycontent
, it may display a zap comment. Generally clients should show users thezap request
note, and use thezap receipt
to show "zap authorized by ..." but this is optional.
A zap request
is an event of kind 9734
that is not published to relays, but is instead sent to a recipient's lnurl pay callback
url. This event's content
MAY be an optional message to send along with the payment. The event MUST include the following tags:
relays
is a list of relays the recipient's wallet should publish itszap receipt
to. Note that relays should not be nested in an additional list, but should be included as shown in the example below.amount
is the amount in millisats the sender intends to pay, formatted as a string. This is recommended, but optional.lnurl
is the lnurl pay url of the recipient, encoded using bech32 with the prefixlnurl
. This is recommended, but optional.p
is the hex-encoded pubkey of the recipient.
In addition, the event MAY include the following tags:
e
is an optional hex-encoded event id. Clients MUST include this if zapping an event rather than a person.a
is an optional NIP-33 event coordinate that allows tipping parameterized replaceable events such as NIP-23 long-form notes.
Example:
{
"kind": 9734,
"content": "Zap!",
"tags": [
["relays", "wss://nostr-pub.wellorder.com"],
["amount", "21000"],
["lnurl", "lnurl1dp68gurn8ghj7um5v93kketj9ehx2amn9uh8wetvdskkkmn0wahz7mrww4excup0dajx2mrv92x9xp"],
["p", "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9"],
["e", "9ae37aa68f48645127299e9453eb5d908a0cbb6058ff340d528ed4d37c8994fb"]
],
"pubkey": "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322",
"created_at": 1679673265,
"id": "30efed56a035b2549fcaeec0bf2c1595f9a9b3bb4b1a38abaf8ee9041c4b7d93",
"sig": "f2cb581a84ed10e4dc84937bd98e27acac71ab057255f6aa8dfa561808c981fe8870f4a03c1e3666784d82a9c802d3704e174371aa13d63e2aeaf24ff5374d9d"
}
A signed zap request
event is not published, but is instead sent using a HTTP GET request to the recipient's callback
url, which was provided by the recipient's lnurl pay endpoint. This request should have the following query parameters defined:
amount
is the amount in millisats the sender intends to paynostr
is the9734
zap request
event, JSON encoded then URI encodedlnurl
is the lnurl pay url of the recipient, encoded using bech32 with the prefixlnurl
This request should return a JSON response with a pr
key, which is the invoice the sender must pay to finalize his zap. Here is an example flow:
const senderPubkey // The sender's pubkey
const recipientPubkey = // The recipient's pubkey
const callback = // The callback received from the recipients lnurl pay endpoint
const 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}`)
The lnurl server will need some additional pieces of information so that clients can know that zap invoices are supported:
- Add a
nostrPubkey
to the lnurl-pay static endpoint/.well-known/lnurlp/<user>
, wherenostrPubkey
is the nostr pubkey your server will use to signzap receipt
events. Clients will use this to validatezap receipt
s. - Add an
allowsNostr
field and set it to true.
When a client sends a zap request
event to a server's lnurl-pay callback URL, there will be a nostr
query parameter whose value is that event which is URI- and JSON-encoded. If present, the zap request
event must be validated in the following ways:
- It MUST have a valid nostr signature
- It MUST have tags
- It MUST have only one
p
tag - It MUST have 0 or 1
e
tags - There should be a
relays
tag with the relays to send thezap receipt
to. - If there is an
amount
tag, it MUST be equal to theamount
query parameter. - If there is an
a
tag, it MUST be a valid NIP-33 event coordinate
The event MUST then be stored for use later, when the invoice is paid.
A zap receipt
is created by a lightning node when an invoice generated by a zap request
is paid. Zap receipt
s are only created when the invoice description (committed to the description hash) contains a zap request
note.
When receiving a payment, the following steps are executed:
- Get the description for the invoice. This needs to be saved somewhere during the generation of the description hash invoice. It is saved automatically for you with CLN, which is the reference implementation used here.
- Parse the bolt11 description as a JSON nostr event. This SHOULD be validated based on the requirements in Appendix D, either when it is received, or before the invoice is paid.
- Create a nostr event of kind
9735
as described below, and publish it to therelays
declared in thezap request
.
The following should be true of the zap receipt
event:
- The
content
SHOULD be empty. - The
created_at
date SHOULD be set to the invoicepaid_at
date for idempotency. tags
MUST include thep
tag AND optionale
tag from thezap request
.- The
zap receipt
MUST have abolt11
tag containing the description hash bolt11 invoice. - The
zap receipt
MUST contain adescription
tag which is the JSON-encoded invoice description. SHA256(description)
MUST match the description hash in the bolt11 invoice.- The
zap receipt
MAY contain apreimage
tag to match against the payment hash of the bolt11 invoice. This isn't really a payment proof, there is no real way to prove that the invoice is real or has been paid. You are trusting the author of thezap receipt
for the legitimacy of the payment.
The zap receipt
is not a proof of payment, all it proves is that some nostr user fetched an invoice. The existence of the zap receipt
implies the invoice as paid, but it could be a lie given a rogue implementation.
A reference implementation for a zap-enabled lnurl server can be found here.
Example zap receipt
:
{
"id": "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446",
"pubkey": "9630f464cca6a5147aa8a35f0bcdd3ce485324e732fd39e09233b1d848238f31",
"created_at": 1674164545,
"kind": 9735,
"tags": [
["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"],
["e", "3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8"],
["bolt11", "lnbc10u1p3unwfusp5t9r3yymhpfqculx78u027lxspgxcr2n2987mx2j55nnfs95nxnzqpp5jmrh92pfld78spqs78v9euf2385t83uvpwk9ldrlvf6ch7tpascqhp5zvkrmemgth3tufcvflmzjzfvjt023nazlhljz2n9hattj4f8jq8qxqyjw5qcqpjrzjqtc4fc44feggv7065fqe5m4ytjarg3repr5j9el35xhmtfexc42yczarjuqqfzqqqqqqqqlgqqqqqqgq9q9qxpqysgq079nkq507a5tw7xgttmj4u990j7wfggtrasah5gd4ywfr2pjcn29383tphp4t48gquelz9z78p4cq7ml3nrrphw5w6eckhjwmhezhnqpy6gyf0"],
["description", "{\"pubkey\":\"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\",\"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": "",
"sig": "b0a3c5c984ceb777ac455b2f659505df51585d5fd97a0ec1fdb5f3347d392080d4b420240434a3afd909207195dac1e2f7e3df26ba862a45afd8bfe101c2b1cc"
}
A client can retrieve zap receipt
s on events and pubkeys using a NIP-01 filter, for example {"kinds": [9735], "#e": [...]}
. Zaps MUST be validated using the following steps:
- The
zap receipt
event'spubkey
MUST be the same as the recipient's lnurl provider'snostrPubkey
(retrieved in step 1 of the protocol flow). - The
invoiceAmount
contained in thebolt11
tag of thezap receipt
MUST equal theamount
tag of thezap request
(if present). - The
lnurl
tag of thezap request
(if present) SHOULD equal the recipient'slnurl
.
When an event includes one or more zap
tags, clients wishing to zap it SHOULD calculate the lnurl pay request based on the tags value instead of the event author's profile field. The tag's second argument is the hex
string of the receiver's pub key and the third argument is the relay to download the receiver's metadata (Kind-0). An optional fourth parameter specifies the weight (a generalization of a percentage) assigned to the respective receiver. Clients should parse all weights, calculate a sum, and then a percentage to each receiver. If weights are not present, CLIENTS should equally divide the zap amount to all receivers. If weights are only partially present, receivers without a weight should not be zapped (weight = 0
).
{
"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.
Zaps can be extended to be more private by encrypting zap request
notes to the target user, but for simplicity it has been left out of this initial draft.