Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NIP-69 Nostr Offer STRings #1460

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
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
234 changes: 234 additions & 0 deletions 69.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
NIP-69
======

Nostr Offer STRings
-------------------

`wip` `optional`

This NIP proposes a format for static payment codes in Nostr as a successor to LNURL-Pay, enabling users to initiate Lightning Network payments by scanning or clicking a string.

## Motivation

Reliance on LNURL has led to centralization via custodial solutions due to the legacy baggage of IP4-NAT/Domain/SSL requirements. Nostr's use-cases are already centered around Lightning, whether as a wallet connector, zap receipts, or its overlapping network effects, rendering Nostr as a de facto "3rd layer" for Lightning and a natural successor to LNURL.

When layered over kind 21000, NWC, or other potential Lightning RPCs, this specification enables a seamless user experience similar to legacy LNURL-Pay, without the domain and SSL requirements. Additionally, the signed nature of Nostr communications minimizes the trust requirements inherent in LNURL when a receiving node must outsource the serving of web requests.

## Specification

### Static Payment Code Format

The static payment code is a bech32 (per [NIP-19](19.md)) encoded string prefixed with `noffer`. The encoded string will include the following TLV (Type-Length-Value) items:

- `0`: The 32 bytes of the receiving service's public key, encoded in hex.
- `1`: The relay URL where the receiving service subscribes to payment requests.
- `2`: The offer identifying string.
- `3`: The price in sats (optional).
- `4`: A flag indicating the pricing type (optional):
- `0`: Fixed price
- `1`: Variable price
shocknet-justin marked this conversation as resolved.
Show resolved Hide resolved
- `2`: Spontaneous payment (payer specifies the amount)

If neither the price nor the pricing type flag is present, the sender may assume it is a spontaneous payment offer.

Example static payment code structure:

```
noffer1...
shocknet-justin marked this conversation as resolved.
Show resolved Hide resolved
0: <receiver_public_key_in_hex>
1: <relay_url>
2: <offer_id_string>
3: <pricing_type_flag> (optional, 0 for fixed, 1 for variable, 2 for spontaneous)
4: <price_in_sats> (optional)
shocknet-justin marked this conversation as resolved.
Show resolved Hide resolved
```

### NIP-05 Integration

To support trust-optimized Lightning Addresses, services can add a `nip69` field in the NIP-05 content to contain an offer for spontaneous payments. This field will provide the offer on name-based lookups.
shocknet-justin marked this conversation as resolved.
Show resolved Hide resolved

Example NIP-05 content with `nip69` field:
```json
{
"pubkey": "hex_pub",
"kind": 0,
"content": "{\"name\": \"bob\", \"nip05\": \"bob@example.com\", \"nip69\": \"noffer1...\"}"
...
}
```

Example NIP-05 Service Response with Offers


```json
{
"names": {
"bob": "hex_pub"
},
"nip69": {
"hex_pub": "noffer1..."
shocknet-justin marked this conversation as resolved.
Show resolved Hide resolved
}
}
```

### Process Flow

1. **Payer Scans or Clicks the Static Payment Code**
2. **Payer's Wallet Decodes the Payment Code**
3. **Payer's Wallet Sends a Nostr Event to the Specified Relay**
- Addressed to the receiver's public key, containing the offer identifying string.
- Event Type: Ephemeral Kind 21001 | NIP-44 Encrypted
- **Conditional**: Include the amount in sats the sender wishes to pay if the payment type is spontaneous.
- Optional: Include additional payer data.
4. **Receiver Responds with Lightning Invoice**
- Generates a Lightning invoice and responds with a Nostr event containing the invoice details.
- Event Type: Ephemeral Kind 21001 | NIP-44 Encrypted
- **Content**: `{"bolt11":"<BOLT11_invoice_string>"}`
- Optional: Include additional purchase data.
5. **Payer Pays the Invoice**
- Completes the payment by settling the Lightning invoice.
6. **Optional: Receiver Emits Payment Receipt**
- This step is out of scope for this NIP, but potential receipt considerations include:
- Nostr Event as Receipt: Emitting a public Nostr event as a "zap" receipt for verifiable record-keeping.
- Lightning Pre-Image: The Lightning pre-image is proof of payment.
- External / WebHook: The receiver may finalize the flow out-of-band.

### Nostr Event

This NIP specifies the use of event kind `21001` with the following structure:

- `content`: NIP-44 encrypted payment details.
shocknet-justin marked this conversation as resolved.
Show resolved Hide resolved
- `tags`:
- `p`: Receiver's public key (hex).
- `e`: Used in response to the requesting event.

Example event:

```json
{
"id": "<event_id>",
"pubkey": "<sender_pubkey>",
"created_at": 1234567890,
"kind": 21001,
"tags": [
["p", "<receiver_pubkey>"],
["e", "<event_id>"] // used only in response by the receiver to identify the original payment request
],
"content": "<NIP-44 encrypted offer identifier and conditional payer data>",
"sig": "<signature>"
}
```

The `content` field, after NIP-44 decryption, should contain an object with the following structure:

```json
{
"offer": "<offer_string>",
"amount": "<conditional_amount_in_sats>",
"payer_data": "<optional_payer_data>"
shocknet-justin marked this conversation as resolved.
Show resolved Hide resolved
}
```

Example response event with `bolt11` invoice:
shocknet-justin marked this conversation as resolved.
Show resolved Hide resolved

```json
{
"id": "<response_event_id>",
"pubkey": "<receiver_pubkey>",
"created_at": 1234567891,
"kind": 21001,
"tags": [
["p", "<sender_pubkey>"],
["e", "<event_id>"] // original payment request event ID
],
"content": "<NIP-44 encrypted {\"bolt11\":\"<BOLT11_invoice_string>\"}>",
"sig": "<signature>"
}
```

## Client Behavior

### Mandatory

Clients implementing this NIP must:

1. Decode and validate the structure of the `noffer` bech32-encoded static payment code.
2. Use NIP-44 encryption for all communication between payer and receiver.
3. Generate and send kind 21001 events as specified in this NIP.
4. Parse the `content` field of received events by decrypting and JSON-parsing the stringified content.
5. Handle potential errors gracefully, providing clear feedback to users.
6. Respect the relay URL specified in the static payment code for sending payment request events.

### Optional

Clients implementing this NIP may:

1. Support both this protocol and LNURL for backward compatibility during the transition period.
2. Implement additional features such as multi-recipient payments or integration with other Lightning-related NIPs.
3. **Use Addressable Events**: Implement addressable events to allow for updates to the recipient key or relay URL associated with the offer.
4. **Attempt Payments by Name**: Use the combination of NIP-05 and NIP-69 to attempt payments by name-based lookups.

### Prohibited

Clients implementing this NIP MUST NOT:

1. Send unencrypted sensitive information in event content or tags.
2. Ignore or modify any fields from the encoded static payment code.

## Service Behavior

### Optional

Services implementing this NIP may:

1. Use an account identifier as a default offer string for spontaneous payments without explicit user setup.
2. **NIP-05 Integration**: Add a `nip69` field in the NIP-05 content to provide the offer on name-based lookups for trust-optimized Lightning Addresses.

## Error Handling

To ensure consistent error handling across implementations, this NIP defines the following error responses, similar to other NIPs:

1. **Invalid Offer**: When the offer ID is invalid or no longer available.
2. **Temporary Failure**: When the receiver is temporarily unable to process the request.
3. **Expired Offer**: When the offer has expired and is no longer valid.
4. **Unsupported Feature**: When the receiver doesn't support a feature requested by the payer.
5. **Invalid Amount**: When the amount specified is too big or too small, providing the acceptable range.

Error responses should be sent as kind 21001 events with the following structure in the encrypted content:

```json
{
"error": "<error_message>",
"code": "<error_code>",
"range": {
"min": "<minimum_amount_in_sats>",
"max": "<maximum_amount_in_sats>"
}
}
```

Error codes:

- 1: Invalid Offer
- 2: Temporary Failure
- 3: Expired Offer
- 4: Unsupported Feature
- 5: Invalid Amount

Clients MUST handle these error responses gracefully and display appropriate messages to users.

## Transition from LNURL

LNURL services wishing to migrate to Nostr Offer STRings should consider adding a `noffer` tag to their responses.

## Future Considerations

Future versions of this NIP may consider additional features such as:

1. Multi-recipient payments.
2. Integration with other Lightning-related NIPs.
3. Extended metadata for more complex payment scenarios.
4. **Addressable Events**: In Nostr apps where the offer is used, consider using addressable events to allow for updates to the recipient key or relay URL associated with the offer.

## Reference Implementation

Lightning.Pub / ShockWallet offers branches
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ They exist to document what may be implemented by [Nostr](https://github.com/nos
- [NIP-59: Gift Wrap](59.md)
- [NIP-64: Chess (PGN)](64.md)
- [NIP-65: Relay List Metadata](65.md)
- [NIP-69: Nostr Static Payment Codes](69.md)
- [NIP-70: Protected Events](70.md)
- [NIP-71: Video Events](71.md)
- [NIP-72: Moderated Communities](72.md)
Expand Down Expand Up @@ -158,6 +159,7 @@ They exist to document what may be implemented by [Nostr](https://github.com/nos
| `10096` | File storage server list | [96](96.md) |
| `13194` | Wallet Info | [47](47.md) |
| `21000` | Lightning Pub RPC | [Lightning.Pub][lnpub] |
| `21001` | Static Payment Code | [69](69.md) |
| `22242` | Client Authentication | [42](42.md) |
| `23194` | Wallet Request | [47](47.md) |
| `23195` | Wallet Response | [47](47.md) |
Expand Down