feat: operator command channel via Nostr (manual dispense, remote management) #38

Closed
opened 2026-06-13 22:02:56 +00:00 by padreug · 0 comments
Owner

Migrated from aiolabs/lamassu-next#38 — opened by @padreug on 2026-03-23.\n\n## Summary

Add a Nostr-native operator command channel so authorized operators can remotely manage ATMs — starting with manual dispense to remediate failed transactions.

Operator Identity

New env var OPERATOR_PUBKEYS — comma-separated list of hex pubkeys authorized to send commands to this machine.

OPERATOR_PUBKEYS=abcd1234...,ef567890...

The machine subscribes to Kind 21003 (CLINK Manage) events tagged with its own pubkey, filters by sender matching an authorized operator pubkey, and decrypts the command via NIP-44.

This is intentionally separate from LIGHTNING_PUB_PUBKEY — operator identity should not be coupled to the payment backend. An operator can manage multiple machines across different Lightning.Pub instances from their personal Nostr identity.

Commands

Phase 1: Manual Dispense

{
  "method": "dispense",
  "params": {
    "cassette": 1,
    "count": 2,
    "ref_txid": "abc123..."  // optional: link to failed transaction
  }
}

When ref_txid is provided, the original failed transaction gets updated:

  • status → remediated
  • Links to the new manual dispense transaction

This closes the loop: operator sees ERR → triggers manual dispense → transaction shows as remediated in TUI/dashboard.

New transaction type: manual_dispense (distinct from cash_out — no Lightning payment involved).

Safety: Machine must be in idle state. Response includes full cassette result (provisioned/dispensed/rejected).

Phase 2: Remote Management

  • get_status — machine state, cassette inventory, cashbox, uptime
  • empty_cashbox — reset cashbox counters
  • reboot — restart the ATM service
  • get_transactions — recent transaction list with status

Phase 3: Dashboard Integration

  • apps/dashboard/ sends commands as Kind 21003 events signed by operator's key
  • Real-time status via machine's Kind 30078 service beacon
  • Transaction list with ERR/PARTIAL highlighting and "Remediate" button

Protocol Flow

Operator (Nostr client / dashboard / TUI)
  ↓ Kind 21003 encrypted with machine's pubkey
  ↓ Tagged: ['p', machine_pubkey]
Relay
  ↓
Machine (subscribed to Kind 21003 for its own pubkey)
  → Verify sender ∈ OPERATOR_PUBKEYS
  → Decrypt NIP-44
  → Execute command (if idle, if authorized)
  → Respond with Kind 21003 result

Access Points

Operator commands should be sendable from:

  1. Dashboard (apps/dashboard/) — primary UI, web-based
  2. Any Nostr client — send encrypted DM-like command from phone/desktop
  3. TUI (atm-tui) — local management tool, could also gain Nostr command capability

Transaction Status Lifecycle

complete         → successful dispense
dispense_error   → 0 bills dispensed, sats debited
partial          → some bills dispensed, sats debited
remediated       → was dispense_error/partial, operator manually dispensed remaining

Files to Modify

Area Changes
apps/machine/.env.example Add OPERATOR_PUBKEYS
apps/machine/electron/main.ts Read and expose operator pubkeys
apps/machine/src/services/operator.ts New: subscribe to Kind 21003, verify sender, dispatch commands
apps/machine/src/stores/atm.ts Handle dispense commands when idle
apps/machine/electron/state-store.ts Add remediated status, manual_dispense type, link remediation txid
packages/state-machine/src/types.ts (no change if manual dispense bypasses state machine)

What Does NOT Change

  • Customer transaction flow — unaffected
  • CLINK payment protocol — unaffected
  • State machine — manual dispense operates outside the state machine (direct HAL call when idle)
> _Migrated from [aiolabs/lamassu-next#38](https://git.atitlan.io/aiolabs/lamassu-next/issues/38) — opened by @padreug on 2026-03-23._\n\n## Summary Add a Nostr-native operator command channel so authorized operators can remotely manage ATMs — starting with manual dispense to remediate failed transactions. ## Operator Identity New env var `OPERATOR_PUBKEYS` — comma-separated list of hex pubkeys authorized to send commands to this machine. ```env OPERATOR_PUBKEYS=abcd1234...,ef567890... ``` The machine subscribes to Kind 21003 (CLINK Manage) events tagged with its own pubkey, filters by sender matching an authorized operator pubkey, and decrypts the command via NIP-44. This is intentionally separate from `LIGHTNING_PUB_PUBKEY` — operator identity should not be coupled to the payment backend. An operator can manage multiple machines across different Lightning.Pub instances from their personal Nostr identity. ## Commands ### Phase 1: Manual Dispense ```json { "method": "dispense", "params": { "cassette": 1, "count": 2, "ref_txid": "abc123..." // optional: link to failed transaction } } ``` When `ref_txid` is provided, the original failed transaction gets updated: - `status` → `remediated` - Links to the new manual dispense transaction This closes the loop: operator sees `ERR` → triggers manual dispense → transaction shows as `remediated` in TUI/dashboard. New transaction type: `manual_dispense` (distinct from `cash_out` — no Lightning payment involved). **Safety**: Machine must be in `idle` state. Response includes full cassette result (provisioned/dispensed/rejected). ### Phase 2: Remote Management - `get_status` — machine state, cassette inventory, cashbox, uptime - `empty_cashbox` — reset cashbox counters - `reboot` — restart the ATM service - `get_transactions` — recent transaction list with status ### Phase 3: Dashboard Integration - `apps/dashboard/` sends commands as Kind 21003 events signed by operator's key - Real-time status via machine's Kind 30078 service beacon - Transaction list with `ERR`/`PARTIAL` highlighting and "Remediate" button ## Protocol Flow ``` Operator (Nostr client / dashboard / TUI) ↓ Kind 21003 encrypted with machine's pubkey ↓ Tagged: ['p', machine_pubkey] Relay ↓ Machine (subscribed to Kind 21003 for its own pubkey) → Verify sender ∈ OPERATOR_PUBKEYS → Decrypt NIP-44 → Execute command (if idle, if authorized) → Respond with Kind 21003 result ``` ## Access Points Operator commands should be sendable from: 1. **Dashboard** (`apps/dashboard/`) — primary UI, web-based 2. **Any Nostr client** — send encrypted DM-like command from phone/desktop 3. **TUI** (`atm-tui`) — local management tool, could also gain Nostr command capability ## Transaction Status Lifecycle ``` complete → successful dispense dispense_error → 0 bills dispensed, sats debited partial → some bills dispensed, sats debited remediated → was dispense_error/partial, operator manually dispensed remaining ``` ## Files to Modify | Area | Changes | |------|---------| | `apps/machine/.env.example` | Add `OPERATOR_PUBKEYS` | | `apps/machine/electron/main.ts` | Read and expose operator pubkeys | | `apps/machine/src/services/operator.ts` | New: subscribe to Kind 21003, verify sender, dispatch commands | | `apps/machine/src/stores/atm.ts` | Handle dispense commands when idle | | `apps/machine/electron/state-store.ts` | Add `remediated` status, `manual_dispense` type, link remediation txid | | `packages/state-machine/src/types.ts` | (no change if manual dispense bypasses state machine) | ## What Does NOT Change - Customer transaction flow — unaffected - CLINK payment protocol — unaffected - State machine — manual dispense operates outside the state machine (direct HAL call when idle)
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#38
No description provided.