Feature: Remote Initiation, Local Redemption #2

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

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

Users can initiate a cash-out transaction remotely (from their phone/wallet) and redeem it physically at the ATM later.

Status: Fully Achievable
Complexity: Medium
Dependencies: CLINK protocol (kinds 21001-21003), signed claims

User Flow

┌─────────────────────────────────────────────────────────────────┐
│                     REMOTE INITIATION                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. User opens wallet app                                       │
│     └─> Selects "Cash Out at ATM"                               │
│                                                                 │
│  2. Wallet shows available ATMs (from beacons)                  │
│     └─> User selects ATM and amount ($100)                      │
│                                                                 │
│  3. Wallet sends CLINK request to ATM via relay                 │
│     └─> kind 21002 (debit request)                              │
│                                                                 │
│  4. ATM responds with signed claim                              │
│     └─> "Bearer of this claim can redeem $100"                  │
│     └─> Includes: amount, expiry, ATM signature, claim_id       │
│                                                                 │
│  5. User pays Lightning invoice (from wallet balance)           │
│     └─> Payment settles instantly                               │
│                                                                 │
│  6. Wallet stores claim locally                                 │
│     └─> Claim is valid for 24 hours                             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                     LOCAL REDEMPTION                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  7. User arrives at ATM (minutes to hours later)                │
│     └─> Taps "Redeem" on ATM screen                             │
│                                                                 │
│  8. ATM displays QR code for claim submission                   │
│     └─> Wallet scans and sends signed claim                     │
│                                                                 │
│  9. ATM verifies claim                                          │
│     └─> Checks signature (did I sign this?)                     │
│     └─> Checks expiry (still valid?)                            │
│     └─> Checks claim_id (not already redeemed?)                 │
│                                                                 │
│  10. ATM dispenses cash                                         │
│      └─> Marks claim as redeemed (local DB + Nostr)             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Claim Structure

interface RedemptionClaim {
  version: 1
  claim_id: string // Unique identifier
  machine_pubkey: string // ATM that will honor this
  amount_cents: number // Fiat amount to dispense
  currency: string // "USD"
  created_at: number // Unix timestamp
  expires_at: number // Unix timestamp (24h default)
  payment_preimage: string // Proof of payment
  user_pubkey?: string // Optional: restrict to specific user
  signature: string // ATM's signature over above fields
}

Security Model

  1. Claim is bearer token: Whoever presents valid claim gets cash
  2. Single-use: claim_id tracked to prevent double-redemption
  3. Time-limited: Expires after configurable window
  4. User-binding (optional): Can restrict to specific npub
  5. Offline-capable: ATM can verify signature without network

State Sync

  • Redeemed claims published to relay (for dashboard visibility)
  • Claims stored locally with SQLite for offline redemption
  • Periodic sync ensures consistency

Implementation Notes

  • Claim fits in QR code (< 2KB when base64 encoded)
  • Works offline after initial payment (ATM doesn't need network to verify)
  • Natural fit with Cashu tokens (future enhancement)

Priority

P2 - Medium effort, high value

> _Migrated from [aiolabs/lamassu-next#2](https://git.atitlan.io/aiolabs/lamassu-next/issues/2) — opened by @padreug on 2026-01-24._\n\n## Overview Users can initiate a cash-out transaction remotely (from their phone/wallet) and redeem it physically at the ATM later. **Status**: Fully Achievable **Complexity**: Medium **Dependencies**: CLINK protocol (kinds 21001-21003), signed claims ## User Flow ``` ┌─────────────────────────────────────────────────────────────────┐ │ REMOTE INITIATION │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 1. User opens wallet app │ │ └─> Selects "Cash Out at ATM" │ │ │ │ 2. Wallet shows available ATMs (from beacons) │ │ └─> User selects ATM and amount ($100) │ │ │ │ 3. Wallet sends CLINK request to ATM via relay │ │ └─> kind 21002 (debit request) │ │ │ │ 4. ATM responds with signed claim │ │ └─> "Bearer of this claim can redeem $100" │ │ └─> Includes: amount, expiry, ATM signature, claim_id │ │ │ │ 5. User pays Lightning invoice (from wallet balance) │ │ └─> Payment settles instantly │ │ │ │ 6. Wallet stores claim locally │ │ └─> Claim is valid for 24 hours │ │ │ └─────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────┐ │ LOCAL REDEMPTION │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 7. User arrives at ATM (minutes to hours later) │ │ └─> Taps "Redeem" on ATM screen │ │ │ │ 8. ATM displays QR code for claim submission │ │ └─> Wallet scans and sends signed claim │ │ │ │ 9. ATM verifies claim │ │ └─> Checks signature (did I sign this?) │ │ └─> Checks expiry (still valid?) │ │ └─> Checks claim_id (not already redeemed?) │ │ │ │ 10. ATM dispenses cash │ │ └─> Marks claim as redeemed (local DB + Nostr) │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ## Claim Structure ```typescript interface RedemptionClaim { version: 1 claim_id: string // Unique identifier machine_pubkey: string // ATM that will honor this amount_cents: number // Fiat amount to dispense currency: string // "USD" created_at: number // Unix timestamp expires_at: number // Unix timestamp (24h default) payment_preimage: string // Proof of payment user_pubkey?: string // Optional: restrict to specific user signature: string // ATM's signature over above fields } ``` ## Security Model 1. **Claim is bearer token**: Whoever presents valid claim gets cash 2. **Single-use**: claim_id tracked to prevent double-redemption 3. **Time-limited**: Expires after configurable window 4. **User-binding** (optional): Can restrict to specific npub 5. **Offline-capable**: ATM can verify signature without network ## State Sync - Redeemed claims published to relay (for dashboard visibility) - Claims stored locally with SQLite for offline redemption - Periodic sync ensures consistency ## Implementation Notes - Claim fits in QR code (< 2KB when base64 encoded) - Works offline after initial payment (ATM doesn't need network to verify) - Natural fit with Cashu tokens (future enhancement) ## Priority P2 - Medium effort, high value
padreug changed title from [reserved] migration number alignment to Feature: Remote Initiation, Local Redemption 2026-06-14 06:54:33 +00:00
padreug reopened this issue 2026-06-14 06:54:34 +00:00
Author
Owner

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

This issue's "signed bearer claim" mechanism rests on CLINK kinds 21001-21003 (offer / debit / response), which are Lightning.Pub-specific protocol events from https://github.com/shocknet/CLINK. LNbits has no knowledge of CLINK and the nostr-native-transport work (see #22) does not add it.

The remote-initiation / local-redemption flow as written stays scoped to Lightning.Pub. No re-architecture is needed for the LNbits migration.

One mechanical bridge worth noting: the final "wallet pays an invoice" step (whether the invoice originates from the bearer-claim flow or any other source) can target an LNbits wallet via the transport's pay_invoice RPC if the funds-bearing wallet is on LNbits. The signed-claim verification and CLINK-event signing both remain LP-side.

Future LNbits CLINK adoption: possible but deferred (see the LP-vs-LNbits separation memo and the future-features discussion in #22). If/when LNbits implements CLINK, this issue's mechanism would carry over. Until then, treat as LP-only.

> _@padreug commented on 2026-05-13 ([lamassu-next#2](https://git.atitlan.io/aiolabs/lamassu-next/issues/2#issuecomment-569)):_ ## Pure CLINK / Lightning.Pub scope — unaffected by the LNbits migration This issue's "signed bearer claim" mechanism rests on **CLINK kinds 21001-21003** (offer / debit / response), which are Lightning.Pub-specific protocol events from https://github.com/shocknet/CLINK. **LNbits has no knowledge of CLINK** and the `nostr-native-transport` work (see #22) does not add it. The remote-initiation / local-redemption flow as written stays scoped to Lightning.Pub. No re-architecture is needed for the LNbits migration. One mechanical bridge worth noting: the final "wallet pays an invoice" step (whether the invoice originates from the bearer-claim flow or any other source) can target an LNbits wallet via the transport's `pay_invoice` RPC if the funds-bearing wallet is on LNbits. The signed-claim verification and CLINK-event signing both remain LP-side. **Future LNbits CLINK adoption:** possible but deferred (see the LP-vs-LNbits separation memo and the future-features discussion in #22). If/when LNbits implements CLINK, this issue's mechanism would carry over. Until then, treat as LP-only.
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#2
No description provided.