Ingest kind:30078 ATM status beacons into dca_telemetry #24

Closed
opened 2026-05-15 20:54:15 +00:00 by padreug · 2 comments
Owner

Gap surfaced 2026-05-15 during end-to-end testing of S5: the
TelemetrySnapshot model + dca_telemetry table + upsert_beacon_snapshot
CRUD are in place, but no subscriber writes to them. The ATM
publishes a kind:30078 status beacon every boot (and on state changes —
cassette swap, internet outage recovery, etc.) but the events land on
the relay and disappear; dca_telemetry is empty for every machine.

Observed payload

From the boot beacon during the 2026-05-15 Sintra test:

{
  "cash_in": true,
  "cash_out": true,
  "cash_level": "good",
  "fiat": "EUR",
  "model": "sintra"
}

Sparse today — cash_level is qualitative ("good"), no denominations
or counts. The richer payload (denominations_json, fees_json,
limits_json, geo, version) lands when lamassu-next#43 ships
upstream. The model + CRUD already have nullable columns for those
fields, so the subscriber doesn't have to wait for #43 to land — it
just upserts what's present.

What's already in place

  • models.TelemetrySnapshot with all the post-#43 fields nullable
  • crud.upsert_beacon_snapshot(machine_id, ...) — COALESCE-based
    upsert that preserves prior values when the incoming payload omits
    a field
  • crud.get_telemetry(machine_id) — single-row read by machine

What's missing

The actual kind:30078 listener. Two approaches:

Option A (preferred): subscribe via subscribe_payments-style RPC

LNbits' nostr-transport (aiolabs/lnbits PR #4) registers
subscribe_payments for Payment objects. A parallel
subscribe_events(kinds=[30078], authors=[<machine_npub>...]) would
let satmachineadmin's tasks.py open one long-lived sub at startup
for every active machine's npub. The handler upserts via
upsert_beacon_snapshot. This is the layered design from the
security epic (#13, S4-adjacent — same event kind the operator's
fleet roster uses, but d-tag scopes them apart).

Blocker: nostr-transport doesn't expose subscribe_events yet
(only subscribe_payments). Filing the upstream LNbits primitive
work is a prerequisite. See aiolabs/lnbits#14 for the security-
side tracker — the event-subscribe RPC fits there.

Option B (fallback): direct relay subscription

Have satmachineadmin spin up its own websocket subscription to the
configured relay(s), filtered on kinds:[30078] + authors:[active machine npubs]. Verify NIP-01 signature on each event, parse content
JSON, call upsert_beacon_snapshot. Self-contained, no LNbits
primitive required, but duplicates the relay-pool machinery.

Recommendation

Land Option A: file the upstream RPC primitive on aiolabs/lnbits
first, then the satmachineadmin consumer. Until that primitive lands,
the worklist of unattributed knowledge — "what's the cash level on
machine X right now?" — answers from SSH-into-the-machine instead of
the operator dashboard.

Acceptance criteria

  • Subscriber running for every active dca_machines row, keyed
    by machine_npub. Restarts cleanly on container restart.
  • Each received kind:30078 event:
    • Signature verified (NIP-01)
    • pubkey matches a known active machine_npub (else dropped + logged)
    • Optional ["d"] tag scoped to a status namespace if/when bitspire
      adopts one (e.g. bitspire-status:{npub})
    • Content JSON parsed; recognised fields fed to
      upsert_beacon_snapshot
  • beacon_received_at updated on every successful upsert (already
    in the CRUD)
  • Operator dashboard surfaces a "last seen" timestamp per machine
    • the current cash_level chip on the Fleet tab
  • When lamassu-next#43 lands, the richer fields
    (denominations_json, fees_json, limits_json) appear in the
    machine detail view without further code changes

Out of scope

  • The richer payload itself (lamassu-next#43 upstream)
  • The operator's NIP-78 fleet roster (S4 / aiolabs/satmachineadmin#18)
    — same event kind but different concern: operator-signed config
    about which ATMs are in the fleet, vs. ATM-signed status

References

  • ~/dev/shared/extensions/satmachineadmin/models.py:368
    TelemetrySnapshot
  • ~/dev/shared/extensions/satmachineadmin/crud.py:1111
    upsert_beacon_snapshot
  • ~/dev/shared/extensions/satmachineadmin/migrations.py
    dca_telemetry schema
  • Sample boot beacon: regtest-lnbits-1 docker logs, 2026-05-15
    20:41:33.95 (kind 30078, "{...sintra...}")
**Gap surfaced 2026-05-15** during end-to-end testing of S5: the `TelemetrySnapshot` model + `dca_telemetry` table + `upsert_beacon_snapshot` CRUD are in place, but **no subscriber writes to them**. The ATM publishes a kind:30078 status beacon every boot (and on state changes — cassette swap, internet outage recovery, etc.) but the events land on the relay and disappear; `dca_telemetry` is empty for every machine. ## Observed payload From the boot beacon during the 2026-05-15 Sintra test: ```json { "cash_in": true, "cash_out": true, "cash_level": "good", "fiat": "EUR", "model": "sintra" } ``` Sparse today — `cash_level` is qualitative ("good"), no denominations or counts. The richer payload (denominations_json, fees_json, limits_json, geo, version) lands when `lamassu-next#43` ships upstream. The model + CRUD already have nullable columns for those fields, so the subscriber doesn't have to wait for #43 to land — it just upserts what's present. ## What's already in place - `models.TelemetrySnapshot` with all the post-#43 fields nullable - `crud.upsert_beacon_snapshot(machine_id, ...)` — COALESCE-based upsert that preserves prior values when the incoming payload omits a field - `crud.get_telemetry(machine_id)` — single-row read by machine ## What's missing The actual kind:30078 listener. Two approaches: ### Option A (preferred): subscribe via `subscribe_payments`-style RPC LNbits' nostr-transport (`aiolabs/lnbits` PR #4) registers `subscribe_payments` for Payment objects. A parallel `subscribe_events(kinds=[30078], authors=[<machine_npub>...])` would let satmachineadmin's `tasks.py` open one long-lived sub at startup for every active machine's npub. The handler upserts via `upsert_beacon_snapshot`. This is the layered design from the security epic (#13, S4-adjacent — same event kind the operator's fleet roster uses, but `d`-tag scopes them apart). **Blocker:** nostr-transport doesn't expose `subscribe_events` yet (only `subscribe_payments`). Filing the upstream LNbits primitive work is a prerequisite. See `aiolabs/lnbits#14` for the security- side tracker — the event-subscribe RPC fits there. ### Option B (fallback): direct relay subscription Have satmachineadmin spin up its own websocket subscription to the configured relay(s), filtered on `kinds:[30078]` + `authors:[active machine npubs]`. Verify NIP-01 signature on each event, parse content JSON, call `upsert_beacon_snapshot`. Self-contained, no LNbits primitive required, but duplicates the relay-pool machinery. ## Recommendation Land Option A: file the upstream RPC primitive on `aiolabs/lnbits` first, then the satmachineadmin consumer. Until that primitive lands, the worklist of unattributed knowledge — "what's the cash level on machine X right now?" — answers from SSH-into-the-machine instead of the operator dashboard. ## Acceptance criteria - [ ] Subscriber running for every active `dca_machines` row, keyed by `machine_npub`. Restarts cleanly on container restart. - [ ] Each received kind:30078 event: - Signature verified (NIP-01) - `pubkey` matches a known active machine_npub (else dropped + logged) - Optional `["d"]` tag scoped to a status namespace if/when bitspire adopts one (e.g. `bitspire-status:{npub}`) - Content JSON parsed; recognised fields fed to `upsert_beacon_snapshot` - [ ] `beacon_received_at` updated on every successful upsert (already in the CRUD) - [ ] Operator dashboard surfaces a "last seen" timestamp per machine + the current `cash_level` chip on the Fleet tab - [ ] When `lamassu-next#43` lands, the richer fields (`denominations_json`, `fees_json`, `limits_json`) appear in the machine detail view without further code changes ## Out of scope - The richer payload itself (lamassu-next#43 upstream) - The operator's NIP-78 fleet roster (S4 / `aiolabs/satmachineadmin#18`) — same event kind but different concern: operator-signed config about which ATMs are in the fleet, vs. ATM-signed status ## References - `~/dev/shared/extensions/satmachineadmin/models.py:368` — `TelemetrySnapshot` - `~/dev/shared/extensions/satmachineadmin/crud.py:1111` — `upsert_beacon_snapshot` - `~/dev/shared/extensions/satmachineadmin/migrations.py` — `dca_telemetry` schema - Sample boot beacon: `regtest-lnbits-1` docker logs, 2026-05-15 20:41:33.95 (`kind 30078, "{...sintra...}"`)
Author
Owner

2026-05-26 — coordinate with S4 (#18) on kind:30078 usage

S4 (#18 — NIP-78 per-machine config + fleet roster) is starting now on the satmachineadmin side and uses the same event kind as this issue's ingestion target:

Producer Kind d-tag Author Purpose
ATM (useAvailabilityBroadcast in lamassu-next) 30078 atm-availability ATM npub "I'm online, here's my cash level" — the input this issue ingests
Operator (satmachineadmin S4) 30078 bitspire-config:<machine_id> Operator npub "Machine X belongs to me, here's its config" — S4 publishes this
Operator (satmachineadmin S4) 30078 bitspire-fleet Operator npub "Here's my entire fleet" — S4 publishes this

Three replaceable kind:30078 events per (operator, machine) pair, each disambiguated by d-tag + author.

Implications for this issue's ingestion code:

  • Filter by d="atm-availability" in the subscription, OR by authors=[atm_npubs_only], to avoid pulling in operator-published config/roster events.
  • The roster events from S4 will eventually become a separate signal to cross-check incoming beacons against: a beacon from an npub not in the operator's published fleet should be ignored (defense against unknown machines reporting status into our extension).
  • Beacon enrichment (per aiolabs/lamassu-next#43) will add fees/limits/denominations to the beacon payload over time. Update the dca_telemetry schema to absorb those fields when they appear, but don't block ingestion of the minimal current shape.

Sequencing: this issue is independent of S4 implementation-wise but benefits from landing the d-tag discipline now so we don't collide downstream. When S4 publishes the first kind:30078 with d="bitspire-config:...", this ingestor should silently skip rather than try to parse as a beacon.

## 2026-05-26 — coordinate with S4 (#18) on kind:30078 usage S4 (#18 — NIP-78 per-machine config + fleet roster) is starting now on the satmachineadmin side and uses the **same event kind** as this issue's ingestion target: | Producer | Kind | d-tag | Author | Purpose | |---|---|---|---|---| | ATM (`useAvailabilityBroadcast` in lamassu-next) | 30078 | `atm-availability` | ATM npub | "I'm online, here's my cash level" — the input this issue ingests | | Operator (satmachineadmin S4) | 30078 | `bitspire-config:<machine_id>` | Operator npub | "Machine X belongs to me, here's its config" — S4 publishes this | | Operator (satmachineadmin S4) | 30078 | `bitspire-fleet` | Operator npub | "Here's my entire fleet" — S4 publishes this | **Three replaceable kind:30078 events per (operator, machine) pair**, each disambiguated by `d`-tag + author. **Implications for this issue's ingestion code:** - Filter by `d="atm-availability"` in the subscription, OR by `authors=[atm_npubs_only]`, to avoid pulling in operator-published config/roster events. - The roster events from S4 will eventually become a separate signal to cross-check incoming beacons against: a beacon from an npub *not in the operator's published fleet* should be ignored (defense against unknown machines reporting status into our extension). - Beacon enrichment (per `aiolabs/lamassu-next#43`) will add fees/limits/denominations to the beacon payload over time. Update the dca_telemetry schema to absorb those fields when they appear, but don't block ingestion of the minimal current shape. **Sequencing:** this issue is independent of S4 implementation-wise but benefits from landing the d-tag discipline now so we don't collide downstream. When S4 publishes the first kind:30078 with `d="bitspire-config:..."`, this ingestor should silently skip rather than try to parse as a beacon.
Author
Owner

➡️ Migrated to aiolabs/spirekeeper#13 (aiolabs/spirekeeper#13).

The v2-bitspire line of this extension now lives in its own repo, aiolabs/spirekeeper. Tracking for this issue continues there; closing here. (Issue numbers were reassigned in the new repo.)

➡️ **Migrated to https://git.atitlan.io/aiolabs/spirekeeper/issues/13 (aiolabs/spirekeeper#13).** The v2-bitspire line of this extension now lives in its own repo, `aiolabs/spirekeeper`. Tracking for this issue continues there; closing here. (Issue numbers were reassigned in the new repo.)
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/satmachineadmin#24
No description provided.