Cash-in: Detect ndebit claim success and show confirmation to user #17

Open
opened 2026-06-13 22:02:47 +00:00 by padreug · 1 comment
Owner

Migrated from aiolabs/lamassu-next#17 — opened by @padreug on 2026-01-26.\n\n## Problem

After displaying an ndebit QR code for cash-in, the ATM has no way to detect when the user successfully claims the sats. The user scans, their wallet processes the debit request, Lightning.Pub pays their invoice, but the ATM screen remains static.

Current flow:

User inserts $50 → ATM shows ndebit QR → User scans → ??? → User walks away confused

Desired flow:

User inserts $50 → ATM shows ndebit QR → User scans → ATM shows "Payment sent!" → User sees confirmation

Solution

Subscribe to Lightning.Pub events to detect when the debit was processed successfully.

Similar to how we detect invoice payments for cash-out (#11), we can listen for outgoing payment operations:

// Subscribe to kind 21000 events from Lightning.Pub
const sub = relay.subscribe([{
  kinds: [21000],
  authors: [lightningPubPubkey],
  since: Math.floor(Date.now() / 1000) - 5,
}], {
  onevent: (event) => {
    const response = decrypt(event.content)
    
    if (response.requestId !== 'GetLiveUserOperations') return
    if (!response.operation) return
    
    const op = response.operation
    
    // For ndebit, we're PAYING an invoice (outgoing)
    if (op.type !== 'OUTGOING_INVOICE') return
    
    // Match by pointer or amount+timing
    if (matchesCurrentSession(op)) {
      console.log('Debit claim successful! Amount:', op.amount)
      transitionToSuccess()
    }
  }
})

Option B: Poll GetPaymentState

For ndebit, we're making an outgoing payment, so GetPaymentState should work (unlike cash-out where we create invoices):

async function pollDebitSuccess(sessionPointer: string) {
  const response = await sendRPC('GetPaymentState', {
    // Need to determine what identifier to use here
    // Possibly the pointer or a payment hash
  })
  
  if (response.paid_at_unix > 0) {
    return true
  }
}

Option C: Track via Kind 21002 Response

When Lightning.Pub processes the debit request, it sends a Kind 21002 response. We could listen for this:

// Subscribe to debit responses
const sub = relay.subscribe([{
  kinds: [21002],
  authors: [lightningPubPubkey],
  '#p': [atmPubkey],
}], {
  onevent: (event) => {
    const response = decrypt(event.content)
    if (response.status === 'OK') {
      // Debit was processed
      showSuccess()
    }
  }
})

UI Changes

When claim is detected:

  1. Transition from "Waiting for scan..." to "Payment sent!"
  2. Show amount sent in sats and fiat equivalent
  3. Display for 5 seconds, then return to idle
  4. Optional: Print receipt

State Machine Updates

cashIn: {
  states: {
    // ... existing states ...
    displayingNdebit: {
      invoke: { src: 'watchForDebitClaim' },
      on: {
        CLAIM_DETECTED: 'showingSuccess'
      }
    },
    showingSuccess: {
      after: {
        5000: '#machine.idle'
      }
    }
  }
}

Acceptance Criteria

  • ATM detects when ndebit is successfully claimed
  • Success screen shows amount sent
  • Screen auto-returns to idle after timeout
  • Works with session-scoped pointers (#8)

Priority

P1 - Important UX improvement for cash-in flow

  • #8 (Session-scoped ndebit security)
  • #11 (Cash-out flow - similar detection pattern)
  • packages/lightning/TROUBLESHOOTING.md - Issue #9 documents GetLiveUserOperations pattern
> _Migrated from [aiolabs/lamassu-next#17](https://git.atitlan.io/aiolabs/lamassu-next/issues/17) — opened by @padreug on 2026-01-26._\n\n## Problem After displaying an ndebit QR code for cash-in, the ATM has no way to detect when the user successfully claims the sats. The user scans, their wallet processes the debit request, Lightning.Pub pays their invoice, but the ATM screen remains static. **Current flow:** ``` User inserts $50 → ATM shows ndebit QR → User scans → ??? → User walks away confused ``` **Desired flow:** ``` User inserts $50 → ATM shows ndebit QR → User scans → ATM shows "Payment sent!" → User sees confirmation ``` ## Solution Subscribe to Lightning.Pub events to detect when the debit was processed successfully. ### Option A: Listen for GetLiveUserOperations (Recommended) Similar to how we detect invoice payments for cash-out (#11), we can listen for outgoing payment operations: ```typescript // Subscribe to kind 21000 events from Lightning.Pub const sub = relay.subscribe([{ kinds: [21000], authors: [lightningPubPubkey], since: Math.floor(Date.now() / 1000) - 5, }], { onevent: (event) => { const response = decrypt(event.content) if (response.requestId !== 'GetLiveUserOperations') return if (!response.operation) return const op = response.operation // For ndebit, we're PAYING an invoice (outgoing) if (op.type !== 'OUTGOING_INVOICE') return // Match by pointer or amount+timing if (matchesCurrentSession(op)) { console.log('Debit claim successful! Amount:', op.amount) transitionToSuccess() } } }) ``` ### Option B: Poll GetPaymentState For ndebit, we're making an outgoing payment, so `GetPaymentState` should work (unlike cash-out where we create invoices): ```typescript async function pollDebitSuccess(sessionPointer: string) { const response = await sendRPC('GetPaymentState', { // Need to determine what identifier to use here // Possibly the pointer or a payment hash }) if (response.paid_at_unix > 0) { return true } } ``` ### Option C: Track via Kind 21002 Response When Lightning.Pub processes the debit request, it sends a Kind 21002 response. We could listen for this: ```typescript // Subscribe to debit responses const sub = relay.subscribe([{ kinds: [21002], authors: [lightningPubPubkey], '#p': [atmPubkey], }], { onevent: (event) => { const response = decrypt(event.content) if (response.status === 'OK') { // Debit was processed showSuccess() } } }) ``` ## UI Changes When claim is detected: 1. Transition from "Waiting for scan..." to "Payment sent!" 2. Show amount sent in sats and fiat equivalent 3. Display for 5 seconds, then return to idle 4. Optional: Print receipt ## State Machine Updates ```typescript cashIn: { states: { // ... existing states ... displayingNdebit: { invoke: { src: 'watchForDebitClaim' }, on: { CLAIM_DETECTED: 'showingSuccess' } }, showingSuccess: { after: { 5000: '#machine.idle' } } } } ``` ## Acceptance Criteria - [ ] ATM detects when ndebit is successfully claimed - [ ] Success screen shows amount sent - [ ] Screen auto-returns to idle after timeout - [ ] Works with session-scoped pointers (#8) ## Priority P1 - Important UX improvement for cash-in flow ## Related - #8 (Session-scoped ndebit security) - #11 (Cash-out flow - similar detection pattern) - `packages/lightning/TROUBLESHOOTING.md` - Issue #9 documents GetLiveUserOperations pattern
Author
Owner

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

LNbits gives outcome parity (not protocol parity) for ndebit-success detection

ndebit is Lightning.Pub-specific (CLINK). LNbits has no CLINK support and the nostr-native-transport work (see #22) does not add it. So the LP-side GetLiveDebitRequests / RespondToDebit subscription path stays exactly as written in this issue.

However, the detection this issue asks for — "did the ATM's outgoing payment for the customer succeed?" — is now reachable two equivalent ways post-migration:

  1. CLINK path (unchanged): subscribe to LP's kind 21000 GetLiveUserOperations, filter by request type / payment hash. This is what the existing lamassu-next/packages/lightning/src/client.ts does.

  2. LNbits path (new): if the funds-bearing wallet is on LNbits, subscribe_payments({wallet_id: <atm_wallet>, payment_hash: <hash>}) over the nostr transport. The ATM gets a real-time encrypted push event the moment the outgoing payment settles, including the full Payment object (status, amount, fee, extra).

Recent LNbits commit 085fd501 fixes the gap that previously made outgoing payments invisible to subscribers — now both incoming AND outgoing settlements fan out to invoice_listeners, so the ATM-side subscribe_payments filter matches outgoing settlements correctly. Verified end-to-end with --flow lnurlw in the LNbits driver script.

Optional session-correlation tip: the extra dict on pay_invoice is opaque to LNbits — pass extra={"session_id": "..."} at payment time and the subscription push echoes it back unchanged. Useful for matching the outgoing payment to a specific cash-in session without amount/timing heuristics.

Net: if the ATM still pays from LP, this issue is unchanged. If/when the ATM pays from an LNbits wallet (post-migration), subscribe_payments is the equivalent surface — same UX, different protocol.

> _@padreug commented on 2026-05-13 ([lamassu-next#17](https://git.atitlan.io/aiolabs/lamassu-next/issues/17#issuecomment-563)):_ ## LNbits gives outcome parity (not protocol parity) for ndebit-success detection **ndebit is Lightning.Pub-specific (CLINK).** LNbits has no CLINK support and the `nostr-native-transport` work (see #22) does **not** add it. So the LP-side `GetLiveDebitRequests` / `RespondToDebit` subscription path stays exactly as written in this issue. However, the *detection* this issue asks for — "did the ATM's outgoing payment for the customer succeed?" — is now reachable two equivalent ways post-migration: 1. **CLINK path (unchanged):** subscribe to LP's `kind 21000 GetLiveUserOperations`, filter by request type / payment hash. This is what the existing `lamassu-next/packages/lightning/src/client.ts` does. 2. **LNbits path (new):** if the funds-bearing wallet is on LNbits, `subscribe_payments({wallet_id: <atm_wallet>, payment_hash: <hash>})` over the nostr transport. The ATM gets a real-time encrypted push event the moment the outgoing payment settles, including the full `Payment` object (status, amount, fee, extra). Recent LNbits commit `085fd501` fixes the gap that previously made outgoing payments invisible to subscribers — now both incoming AND outgoing settlements fan out to `invoice_listeners`, so the ATM-side `subscribe_payments` filter matches outgoing settlements correctly. Verified end-to-end with `--flow lnurlw` in the LNbits driver script. **Optional session-correlation tip:** the `extra` dict on `pay_invoice` is opaque to LNbits — pass `extra={"session_id": "..."}` at payment time and the subscription push echoes it back unchanged. Useful for matching the outgoing payment to a specific cash-in session without amount/timing heuristics. **Net:** if the ATM still pays from LP, this issue is unchanged. If/when the ATM pays from an LNbits wallet (post-migration), `subscribe_payments` is the equivalent surface — same UX, different protocol.
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#17
No description provided.