Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
286 changes: 286 additions & 0 deletions B1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
NIP-B1
======

BOLT12 Zaps
-----------

`draft` `optional`

This NIP defines BOLT12 zaps: public Nostr events proving that a BOLT12 payment was made to the author of a profile, event, or addressable event.

BOLT12 zaps add a BOLT12 payment flow alongside the LNURL callback flow used by NIP-57. No LNURL endpoint, zap callback server, separately relayed zap request event, or recipient-operated receipt publisher is required. Recipients announce their BOLT12 offers in a replaceable Nostr event.

## Motivation

NIP-57 made Lightning payments visible on Nostr, but it depends on LNURL pay servers. A sender asks the recipient's server for a BOLT11 invoice, pays it, and then trusts that server to publish a zap receipt.

With BOLT12, a recipient can publish a reusable offer. A payer can request an invoice over the Lightning network, pay it, receive an `lnp` payer proof, and publish a Nostr event proving the payment.

This NIP defines public zap proof events that allow clients to compute a verified cumulative zap amount for a profile, event, or addressable event.

## Event Kind

This draft uses three candidate event kinds:

| kind | name | use |
| --- | --- | --- |
| `9736` | `bolt12_zap` | public BOLT12 payment proof |
| `9737` | `bolt12_zap_intent` | signed pre-payment zap intent embedded in `9736` |
| `10058` | `bolt12_offer` | replaceable BOLT12 offer announcement |

Kind `9736` is the only event counted as a BOLT12 zap.

## BOLT12 Offer Announcement

To receive BOLT12 zaps, a user MUST publish a replaceable event of kind `10058` containing one or more `offer` tags. Each `offer` tag MUST contain a canonical raw BOLT12 offer. The event `content` SHOULD be empty.

```json
{
"kind": 10058,
"content": "",
"tags": [
["offer", "lno1..."]
],
"pubkey": "32e182...e245",
"created_at": 1781699900,
"id": "2b86...b19a",
"sig": "88ce...4d10"
}
```

The complete signed kind `10058` event is the offer announcement. When it contains multiple offers, the payer MAY select any offer it supports. The payer embeds the announcement in the zap intent and zap event so that clients can verify that the recipient authorized the selected offer.

## BOLT12 Zap Targets

Recipient selection is outside this NIP. A client that supports NIP-57 `zap` tags can use those tags to select recipients before creating BOLT12 zaps.

After selecting a recipient, the client fetches that user's latest valid kind `10058` offer announcement.

The canonical raw offer is the lowercase `lno1...` BOLT12 offer string with BOLT12 `+` separators and whitespace removed.

Each recipient payment produces a separate kind `9736` zap event.

## Zap Event

A zap event is a signed event of kind `9736`.

Required tags:

- `description`: serialized zap intent event.
- `p`: recipient pubkey.
- `amount`: amount in millisatoshis.
- `offer_event`: serialized kind `10058` offer announcement.
- `proof`: BOLT12 `lnp` payer proof.

Optional tags:

- `P`: payer pubkey. If present, it MUST equal the zap event `pubkey`.
- `e`: event being zapped.
- `a`: addressable event coordinate being zapped.
- `k`: kind of the event being zapped.

The `description` tag uses the same embedding pattern as NIP-57. Its value MUST be the complete serialized kind `9737` zap intent event as JSON.

`content` MUST equal the zap intent event `content`.

Example:

```json
{
"kind": 9736,
"content": "excellent note",
"tags": [
["description", "{\"kind\":9737,\"content\":\"excellent note\",\"tags\":[[\"p\",\"32e182...e245\"],[\"e\",\"9ae37a...94fb\"],[\"k\",\"1\"],[\"amount\",\"21000\"],[\"offer_event\",\"{\\\"kind\\\":10058,\\\"content\\\":\\\"\\\",\\\"tags\\\":[[\\\"offer\\\",\\\"lno1...\\\"]],\\\"pubkey\\\":\\\"32e182...e245\\\",\\\"created_at\\\":1781699900,\\\"id\\\":\\\"2b86...b19a\\\",\\\"sig\\\":\\\"88ce...4d10\\\"}\"],[\"zap_id\",\"4f1c8f6a0d5e4a4e94f5ef99d1c1e8ad\"]],\"pubkey\":\"97c70a...e322\",\"created_at\":1781699999,\"id\":\"d8f3...9c1a\",\"sig\":\"f4b1...7a02\"}"],
["p", "32e182...e245"],
["P", "97c70a...e322"],
["e", "9ae37a...94fb"],
["k", "1"],
["amount", "21000"],
["offer_event", "{\"kind\":10058,\"content\":\"\",\"tags\":[[\"offer\",\"lno1...\"]],\"pubkey\":\"32e182...e245\",\"created_at\":1781699900,\"id\":\"2b86...b19a\",\"sig\":\"88ce...4d10\"}"],
["proof", "lnp1..."]
],
"pubkey": "97c70a...e322",
"created_at": 1781700012
}
```

The zap event MUST have exactly one `description` tag, exactly one `p` tag, and exactly one `offer_event` tag. It MUST have at most one `e` tag and at most one `a` tag, and MUST NOT contain both `e` and `a`.

Publicly attributed zaps SHOULD include `P`. Anonymous zaps MUST use an ephemeral event pubkey and MUST omit `P`.

The zap event `pubkey`, `content`, `p`, `amount`, `offer_event`, `e`, `a`, and `k` presence and values MUST match the embedded zap intent event.

## Zap Intent

Before paying, the payer creates and signs a zap intent event. The zap intent binds the payer's Nostr key to the recipient, target, amount, and BOLT12 offer before the Lightning payment is attempted. This event is never broadcast and only embedded in the Zap Event.

The payer chooses a `zap_id` with at least 128 bits of entropy, encoded as lowercase hex.

A zap intent event is a signed event of kind `9737`.

Required tags:

- `p`: recipient pubkey.
- `amount`: amount in millisatoshis.
- `offer_event`: serialized kind `10058` offer announcement.
- `zap_id`: random lowercase hex value.

Optional tags:

- `e`: event being zapped.
- `a`: addressable event coordinate being zapped.
- `k`: kind of the event being zapped.

`content` MAY contain a zap comment.

The zap intent event MUST have exactly one `p` tag and exactly one `offer_event` tag. It MUST have at most one `e` tag and at most one `a` tag, and MUST NOT contain both `e` and `a`.

Anonymous zaps use an ephemeral event pubkey.

Example:

```json
{
"kind": 9737,
"content": "excellent note",
"tags": [
["p", "32e182...e245"],
["e", "9ae37a...94fb"],
["k", "1"],
["amount", "21000"],
["offer_event", "{\"kind\":10058,\"content\":\"\",\"tags\":[[\"offer\",\"lno1...\"]],\"pubkey\":\"32e182...e245\",\"created_at\":1781699900,\"id\":\"2b86...b19a\",\"sig\":\"88ce...4d10\"}"],
["zap_id", "4f1c8f6a0d5e4a4e94f5ef99d1c1e8ad"]
],
"pubkey": "97c70a...e322",
"created_at": 1781699999,
"id": "d8f3...9c1a",
"sig": "f4b1...7a02"
}
```

The payer MUST include the zap intent event id as the BOLT12 `invreq_payer_note`:

```text
nostr:nipB1:<zap-intent-event-id>
```

## Payer Proof Disclosure

The `proof` tag MUST contain a bech32-encoded BOLT12 `payer_proof` with human-readable prefix `lnp`.

For NIP-B1 validation, the payer proof MUST disclose at least:

- `invreq_payer_note`;
- `invoice_payment_hash`;
- `invoice_amount`;
- `signature`;
- `proof_preimage`;
- `proof_signature`;

The proof MUST NOT omit any field required by NIP-B1 validation behind `proof_omitted_tlvs`.

`invreq_payer_note` MUST equal:

```text
nostr:nipB1:<zap-intent-event-id>
```

The proof MUST disclose all fields required to run the BOLT12 payer-proof validation algorithm against at least one canonical raw BOLT12 offer in the embedded offer announcement. This NIP does not require verifiers to compare arbitrary offer TLVs one by one. A proof that cannot be validated against any offer in the announcement MUST NOT be counted as a BOLT12 zap.

## Payment Flow

To send a BOLT12 zap:

1. Resolve the target recipient and fetch the recipient's latest valid kind `10058` offer announcement.
2. Choose the Nostr key that will publish the zap event.
3. Choose a random `zap_id`.
4. Create and sign a kind `9737` zap intent event that embeds the offer announcement.
5. Pay the selected offer for the requested amount, including `nostr:nipB1:<zap-intent-event-id>` in BOLT12 payer metadata.
6. Obtain a settled `lnp` payer proof from the payer wallet.
7. Publish a kind `9736` zap event carrying the serialized zap intent event and the proof.

## Async Payments

A kind `9736` zap event MUST prove settled payment.

Pending, queued, held, async-handoff, or invoice-request-only artifacts MUST NOT be published as kind `9736` zaps and MUST NOT be counted as verified zaps.

If an async payment flow does not produce settled payer proof until the recipient or recipient infrastructure comes online, publication of the zap event MUST wait until that proof exists.

This delays zap publication for offline recipients, but prevents clients from counting unverifiable pending payments as valid zaps.

## Validation

A client MUST validate a zap event before counting it.

Validation steps:

1. Verify the zap event signature and kind `9736`.
2. Verify the zap event structure:
- exactly one `description` tag;
- exactly one `p` tag;
- positive `amount`;
- exactly one serialized `offer_event`;
- bech32 `lnp` `proof`;
- at most one `e` tag and at most one `a` tag;
- it does not contain both `e` and `a`;
- `P`, if present, equals the zap event `pubkey`.
3. Parse the `description` tag as a Nostr event and verify that embedded zap intent event has:
- a valid signature;
- kind `9737`;
- the same `pubkey` as the zap event;
- exactly one `p` tag;
- positive `amount`;
- exactly one serialized `offer_event`;
- valid `zap_id`;
- at most one `e` tag and at most one `a` tag;
- it does not contain both `e` and `a`.
4. Verify the zap event and embedded zap intent event match on `content`, `p`, `amount`, `offer_event`, and target tags `e`, `a`, and `k`.
5. Parse the `offer_event` as a Nostr event and verify that it:
- has a valid signature;
- has kind `10058`;
- has a `pubkey` equal to the zap recipient in `p`;
- has a `created_at` no later than the zap intent;
- contains one or more `offer` tags, each with a canonical raw BOLT12 offer.
6. Parse the raw BOLT12 offers from the offer announcement.
7. Decode and validate the `lnp` payer proof according to the BOLT12 payer-proof specification, including proof preimage, invoice signature, and payer proof signature checks.
8. Verify the payer proof binds to this zap:
- `invreq_payer_note` equals `nostr:nipB1:<zap-intent-event-id>`;
- `invoice_amount` equals the event `amount`;
- the proof is valid for at least one canonical raw BOLT12 offer in the offer announcement.

If no settled `lnp` payer proof is available, the event MUST NOT be counted as a BOLT12 zap.

## Counting Zaps

To compute the verified cumulative zap amount for an event, clients query for kind `9736` events with `#e` equal to the event id. For addressable events, clients query by `#a`. For profile zaps, clients query by `#p` and ignore events that also contain `e` or `a`.

Clients MUST validate each zap event before counting it.

Clients MUST deduplicate zap events for the same target by proof identifier. The proof identifier is `invoice_payment_hash` from the validated payer proof.

If two valid zap events for the same target have the same proof identifier, clients MUST count only one. If amounts differ, clients MUST count the lower amount.

The cumulative zap amount is a client-computed view over valid public proof events. It is not consensus.

## Rationale

A separately relayed zap request event is not required when the signed zap intent event is embedded in the final zap event. Verifiers retain the NIP-57 property that the payer signed the Nostr zap context before payment.

The signed offer announcement binds its BOLT12 offers to the recipient's Nostr key. Embedding the announcement allows clients to verify that the paid offer was authorized by the recipient without fetching mutable profile data.

The zap intent event binds the payer's Nostr key to the zap target. The BOLT12 payer note binds the Lightning payment proof to that signed intent. Without this binding, a payer could reuse proof from one payment to claim zaps on unrelated events.

Only settled payer proofs count. Pending async payment artifacts do not prove payment and MUST NOT be counted.
Comment thread
fiatjaf marked this conversation as resolved.

## Compatibility

NIP-B1 does not replace NIP-57. Clients MAY display NIP-57 and NIP-B1 receipts together, but they MUST validate and label them separately.

BOLT12 zap clients can use existing NIP-57 `zap` tags for recipient selection.

## References

- NIP-57: https://github.com/nostr-protocol/nips/blob/master/57.md
- BOLT12: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md
- BOLT12 payer proofs draft: https://github.com/lightning/bolts/pull/1346