Feature: Optional npub identification for cash-in via NIP-17 #4

Open
opened 2026-06-13 21:58:16 +00:00 by padreug · 1 comment
Owner

Migrated from aiolabs/lamassu-next#4 — opened by @padreug on 2026-01-24.\n\n## Overview

Allow users to optionally identify themselves during cash-in by sending a NIP-17 encrypted message to the ATM. The signature proves ownership of the npub, enabling direct CLINK offer delivery and receipts.

Complexity: Medium
Dependencies: NIP-17 (Gift Wrap), NIP-44 (already implemented), CLINK

Motivation

Currently, cash-in requires the user to scan a QR code to receive the CLINK offer. With npub identification:

  1. ATM can send offers directly to user's Nostr wallet
  2. User receives encrypted receipts for their records
  3. Enables customer support channel if issues arise
  4. Foundation for loyalty programs (repeat customer recognition)
  5. All while remaining KYC-free (npub is pseudonymous)

Cryptographic Proof

The elegance is that the signature is the proof of ownership:

User claims: "I am npub1xyz"
User signs:  Event with private key for npub1xyz
ATM verifies: Signature valid → User controls npub1xyz

No third party, no OAuth, no identity provider. Pure Nostr-native authentication.

Proposed Flow

┌─────────────────────────────────────────────────────────────────┐
│  1. User inserts cash                                           │
│                                                                 │
│  2. ATM displays its npub + transaction ID                      │
│     └─> QR code: nostr:npub1atm...?tx=abc123                    │
│                                                                 │
│  3. User scans and wallet sends NIP-17 Gift Wrap:               │
│     └─> Kind 1059 wrapping Kind 14                              │
│     └─> Content: {"tx": "abc123"}                               │
│     └─> Recipient: ATM's pubkey                                 │
│     └─> Signed by user's key (proves identity)                  │
│                                                                 │
│  4. ATM receives and verifies:                                  │
│     └─> Unwrap gift wrap                                        │
│     └─> Valid signature? ✓                                      │
│     └─> tx matches active transaction? ✓                        │
│     └─> Extract sender pubkey from inner event                  │
│                                                                 │
│  5. ATM sends CLINK offer directly to user's pubkey             │
│     └─> Kind 21001 with p-tag to user                           │
│     └─> User receives in Nostr wallet                           │
│                                                                 │
│  6. User provides invoice, ATM pays                             │
│                                                                 │
│  7. ATM sends encrypted receipt to user                         │
│     └─> NIP-17 with transaction details                         │
└─────────────────────────────────────────────────────────────────┘

NIP-17 Structure

User → ATM (Identification)

// Inner event (Kind 14 - Chat Message)
{
  kind: 14,
  content: nip44Encrypt('{"tx": "abc123"}'),
  tags: [["p", ATM_PUBKEY]],
  created_at: randomizedTimestamp(),
  pubkey: USER_PUBKEY,
  // ... signature
}

// Outer event (Kind 1059 - Gift Wrap)
{
  kind: 1059,
  content: nip44Encrypt(innerEvent),
  tags: [["p", ATM_PUBKEY]],
  created_at: randomizedTimestamp(),
  pubkey: EPHEMERAL_KEY,  // One-time key for privacy
  // ... signature with ephemeral key
}

ATM → User (Receipt)

// Inner event content
{
  "type": "receipt",
  "tx": "abc123",
  "fiatAmount": 100.00,
  "currency": "USD",
  "satsReceived": 250000,
  "rate": 40000,
  "timestamp": 1706140800,
  "machineId": "npub1atm..."
}

Privacy Properties

Property Status
Relay can't see sender ✓ (ephemeral key in outer wrap)
Relay can't see content ✓ (NIP-44 encryption)
Relay can't see recipient Partial (p-tag visible, but could use relay-specific delivery)
User identity is pseudonymous ✓ (npub only)
Feature is optional ✓ (QR flow still works)
User can use fresh keypair ✓ (maximum privacy option)

Implementation Tasks

  • Add NIP-17 gift wrap/unwrap to @lamassu/nostr-client
  • Define transaction identification message schema
  • Add receipt message schema
  • Update state machine to handle npub identification path
  • Update ATM UI to show npub identification option
  • Test with existing Nostr wallets that support NIP-17

Wallet Compatibility

NIP-17 support in wallets (as of 2024):

  • Amethyst ✓
  • Damus ✓
  • Primal ✓
  • Coracle ✓
  • Snort ✓

Questions to Resolve

  1. Should tx ID be in QR or should ATM subscribe to all DMs and match by timing?
  2. Timeout duration before falling back to QR-only flow?
  3. Should receipts include a preimage or other proof of payment?
  4. NIP proposal for ATM identification pattern?

References

> _Migrated from [aiolabs/lamassu-next#4](https://git.atitlan.io/aiolabs/lamassu-next/issues/4) — opened by @padreug on 2026-01-24._\n\n## Overview Allow users to optionally identify themselves during cash-in by sending a NIP-17 encrypted message to the ATM. The signature proves ownership of the npub, enabling direct CLINK offer delivery and receipts. **Complexity**: Medium **Dependencies**: NIP-17 (Gift Wrap), NIP-44 (already implemented), CLINK ## Motivation Currently, cash-in requires the user to scan a QR code to receive the CLINK offer. With npub identification: 1. ATM can send offers **directly to user's Nostr wallet** 2. User receives **encrypted receipts** for their records 3. Enables **customer support** channel if issues arise 4. Foundation for **loyalty programs** (repeat customer recognition) 5. All while remaining **KYC-free** (npub is pseudonymous) ## Cryptographic Proof The elegance is that the signature *is* the proof of ownership: ``` User claims: "I am npub1xyz" User signs: Event with private key for npub1xyz ATM verifies: Signature valid → User controls npub1xyz ``` No third party, no OAuth, no identity provider. Pure Nostr-native authentication. ## Proposed Flow ``` ┌─────────────────────────────────────────────────────────────────┐ │ 1. User inserts cash │ │ │ │ 2. ATM displays its npub + transaction ID │ │ └─> QR code: nostr:npub1atm...?tx=abc123 │ │ │ │ 3. User scans and wallet sends NIP-17 Gift Wrap: │ │ └─> Kind 1059 wrapping Kind 14 │ │ └─> Content: {"tx": "abc123"} │ │ └─> Recipient: ATM's pubkey │ │ └─> Signed by user's key (proves identity) │ │ │ │ 4. ATM receives and verifies: │ │ └─> Unwrap gift wrap │ │ └─> Valid signature? ✓ │ │ └─> tx matches active transaction? ✓ │ │ └─> Extract sender pubkey from inner event │ │ │ │ 5. ATM sends CLINK offer directly to user's pubkey │ │ └─> Kind 21001 with p-tag to user │ │ └─> User receives in Nostr wallet │ │ │ │ 6. User provides invoice, ATM pays │ │ │ │ 7. ATM sends encrypted receipt to user │ │ └─> NIP-17 with transaction details │ └─────────────────────────────────────────────────────────────────┘ ``` ## NIP-17 Structure ### User → ATM (Identification) ```typescript // Inner event (Kind 14 - Chat Message) { kind: 14, content: nip44Encrypt('{"tx": "abc123"}'), tags: [["p", ATM_PUBKEY]], created_at: randomizedTimestamp(), pubkey: USER_PUBKEY, // ... signature } // Outer event (Kind 1059 - Gift Wrap) { kind: 1059, content: nip44Encrypt(innerEvent), tags: [["p", ATM_PUBKEY]], created_at: randomizedTimestamp(), pubkey: EPHEMERAL_KEY, // One-time key for privacy // ... signature with ephemeral key } ``` ### ATM → User (Receipt) ```typescript // Inner event content { "type": "receipt", "tx": "abc123", "fiatAmount": 100.00, "currency": "USD", "satsReceived": 250000, "rate": 40000, "timestamp": 1706140800, "machineId": "npub1atm..." } ``` ## Privacy Properties | Property | Status | |----------|--------| | Relay can't see sender | ✓ (ephemeral key in outer wrap) | | Relay can't see content | ✓ (NIP-44 encryption) | | Relay can't see recipient | Partial (p-tag visible, but could use relay-specific delivery) | | User identity is pseudonymous | ✓ (npub only) | | Feature is optional | ✓ (QR flow still works) | | User can use fresh keypair | ✓ (maximum privacy option) | ## Implementation Tasks - [ ] Add NIP-17 gift wrap/unwrap to `@lamassu/nostr-client` - [ ] Define transaction identification message schema - [ ] Add receipt message schema - [ ] Update state machine to handle npub identification path - [ ] Update ATM UI to show npub identification option - [ ] Test with existing Nostr wallets that support NIP-17 ## Wallet Compatibility NIP-17 support in wallets (as of 2024): - Amethyst ✓ - Damus ✓ - Primal ✓ - Coracle ✓ - Snort ✓ ## Questions to Resolve 1. Should tx ID be in QR or should ATM subscribe to all DMs and match by timing? 2. Timeout duration before falling back to QR-only flow? 3. Should receipts include a preimage or other proof of payment? 4. NIP proposal for ATM identification pattern? ## References - [NIP-17: Private Direct Messages](https://github.com/nostr-protocol/nips/blob/master/17.md) - [NIP-44: Encrypted Payloads](https://github.com/nostr-protocol/nips/blob/master/44.md) - [NIP-59: Gift Wrap](https://github.com/nostr-protocol/nips/blob/master/59.md)
padreug changed title from [reserved] migration number alignment to Feature: Optional npub identification for cash-in via NIP-17 2026-06-14 06:54:36 +00:00
padreug reopened this issue 2026-06-14 06:54:37 +00:00
Author
Owner

@padreug commented on 2026-05-13 (lamassu-next#4):

This issue's "optional npub identification for cash-in" mechanism (NIP-17 gift-wrapped DMs paired with the CLINK ndebit flow) is Lightning.Pub-side / CLINK-side territory. LNbits has no knowledge of CLINK and the nostr-native-transport work (see #22) does not add CLINK support. So:

  • The NIP-17 identity-attach happens between the customer's wallet and the ATM via CLINK-aware nostr events (kind 1059 gift wraps, etc.). LNbits sees none of it.
  • The downstream payment settlement on the LNbits wallet (if/when LNbits is the funds-bearing side post-migration) is detectable via subscribe_payments — but the identity association doesn't live in the LNbits payment object. If the ATM needs to record "this cash-in was from npub X", it owns that mapping locally (e.g. session_id → npub) and threads extra={"session_id": "..."} through the LNbits invoice; the npub itself never reaches LNbits.

This issue stays scoped to the ATM + CLINK + ShockWallet side. No re-architecture needed for the LNbits migration. If LNbits ever adopts CLINK (deferred — see #22), the NIP-17 association would carry over naturally.

> _@padreug commented on 2026-05-13 ([lamassu-next#4](https://git.atitlan.io/aiolabs/lamassu-next/issues/4#issuecomment-578)):_ ## Scope note: NIP-17 + CLINK are LP-side; LNbits ignores both This issue's "optional npub identification for cash-in" mechanism (NIP-17 gift-wrapped DMs paired with the CLINK ndebit flow) is **Lightning.Pub-side / CLINK-side** territory. LNbits has no knowledge of CLINK and the `nostr-native-transport` work (see #22) does not add CLINK support. So: - The NIP-17 identity-attach happens between the customer's wallet and the ATM via CLINK-aware nostr events (kind 1059 gift wraps, etc.). LNbits sees none of it. - The downstream payment settlement on the LNbits wallet (if/when LNbits is the funds-bearing side post-migration) is detectable via `subscribe_payments` — but the *identity association* doesn't live in the LNbits payment object. If the ATM needs to record "this cash-in was from npub X", it owns that mapping locally (e.g. `session_id → npub`) and threads `extra={"session_id": "..."}` through the LNbits invoice; the npub itself never reaches LNbits. This issue stays scoped to the ATM + CLINK + ShockWallet side. No re-architecture needed for the LNbits migration. If LNbits ever adopts CLINK (deferred — see #22), the NIP-17 association would carry over naturally.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/bitspire#4
No description provided.