Fleet management: Nostr-native remote control & telemetry for ATM operators #42

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

Migrated from aiolabs/lamassu-next#42 — opened by @padreug on 2026-04-07.\n\n## Problem

A real operator runs anywhere from a handful to ~100 machines. Today the only way to interact with a deployed lamassu-next machine is to SSH in and run atm-tui. That doesn't scale, and it doesn't match the spirit of the rest of the architecture, where every other component (Lightning.Pub, CLINK, customer wallets) talks over Nostr.

This issue proposes a fleet management plane that reuses the primitives we already have in the codebase, plus a seed URL bootstrap that lets an operator enroll a brand-new machine from the dashboard without ever touching the machine's filesystem.

Goals

  • Operator can see the health, inventory, and recent activity of every machine they own from a single dashboard.
  • Operator can issue control commands (read-only and mutating) to any specific machine.
  • Operator can enroll a brand-new machine by generating a seed URL/QR in the dashboard and presenting it to the machine on first boot. No manual .env editing.
  • No inbound network exposure on the machines themselves.
  • No new central database; the relay is the source of truth for in-flight messages and audit history.
  • Reuse the request/response pattern already used for Lightning.Pub RPC (kind 21000) and CLINK (kinds 21001-21003).

Non-goals

  • Customer-facing UX changes.
  • Multi-tenant SaaS dashboard. This is a self-hosted/local-first SPA.
  • Removing SSH/.env as a recovery path. They remain the ultimate escape hatch — they're just not the day-to-day provisioning surface.

Three planes, separated

Plane Direction Mechanism
Discovery — "what machines do I own?" operator → roster NIP-51 parameterized list (kind 30000 or 30003), d tag = lamassu-fleet, signed by the operator pubkey, with one p tag per machine npub
Telemetry — "how is the fleet doing?" machine → public Replaceable events. Kind 30078 availability already exists. Add 30079 (inventory + health snapshot). Optionally a non-replaceable kind for transaction records
Control — "do X on machine Y" operator → machine NIP-44 encrypted request/response addressed to the machine npub, correlated by request_id. Same pattern as the Lightning.Pub RPC client in packages/lightning

Bootstrap: seed URLs

The hardest part of fleet management is the bootstrap problem: a brand-new machine has no idea who its operator is, has no Lightning.Pub account yet, doesn't know which relay to talk to, and has no .env. We solve this with a single connection-string-style URL, modeled directly on NWC connection strings (nostr+walletconnect://...) and NIP-46 bunker URLs (bunker://...).

Format

lamassu+seed://<operator-pubkey>
  ?relay=wss://relay.example.com/
  &relay=wss://backup.example.com/
  &lpub=https://lightning.pub.example.com
  &invite=<one-time-token>
  &nonce=<random-id>
  &exp=<unix-timestamp>
  &fiat=USD
  &model=batm3

A URL scheme (not a new bech32 prefix) is intentional: easy to QR-encode, easy to debug, easy to parse with the standard URL class everywhere, and consistent with NWC/bunker conventions.

Enrollment flow

On the dashboard (operator is logged in with their nsec):

  1. Operator clicks "Add new machine", picks fiat / model / etc.
  2. Dashboard, using the operator's existing Lightning.Pub credentials, mints a one-time invite token scoped to creating one new LP user (UseInviteLink is already in the autogenerated LP RPC client).
  3. Dashboard generates the seed URL above, renders it as a QR code and as copy-pasteable text.
  4. Dashboard subscribes (filtered by nonce) waiting for the new machine to announce itself.

On the new machine (first boot, no .env yet):

  1. The Electron app sees no config and shows a first-boot wizard with a QR-scan / paste-URL screen.
  2. Operator presents the seed URL.
  3. Machine generates its own nsec/npub locally — this never leaves the device. The seed is provisioning data, not identity.
  4. Machine parses the seed, validates exp, then in order:
    • Connects to the relay(s) listed in the seed.
    • Uses the invite token to register itself as a Lightning.Pub user (machine's own npub becomes the LP identifier). Stores the resulting LP credentials.
    • Writes .env (or config.json) with: own nsec, relay URLs, LP URL + token, operator npub allow-list = [<operator-pubkey>], fiat, model.
    • Publishes a signed kind-0 profile and the kind 30078 availability beacon (already implemented in useAvailabilityBroadcast.ts).
    • Publishes a one-shot enrollment-ack event tagged ["e", <nonce>] and ["p", <operator-pubkey>], signed with the new machine key. This is the "I claimed your seed, here is my pubkey" handshake.
  5. Dashboard sees the ack, locks that nonce → that machine pubkey, adds the new npub to the operator's NIP-51 fleet roster, and the machine appears live in the fleet view.

After this, the machine is fully provisioned. Steady-state fleet management uses the same primitives that just bootstrapped it (same relay, same encryption, same signing keys) — there is no separate "provisioning protocol" to maintain.

Seed URL security

  • Mostly non-sensitive. Operator pubkey, relay URLs, LP URL — all public. The only sensitive component is the invite token, which is one-time, short-TTL, and scoped to creating a single LP user.
  • Nonce binding. The dashboard remembers the nonce and the first pubkey to claim it. Any later claim with the same nonce is rejected. This prevents an attacker who intercepts the seed URL from racing the legitimate machine.
  • Short TTL. Seeds expire in (suggested) 1 hour. Re-issue if needed.
  • Invite token revocation. If the operator cancels enrollment, the dashboard tells Lightning.Pub to invalidate the invite token (assuming LP supports this — to be confirmed).
  • Local key generation. The machine's nsec is generated on the device that will hold it. No risk of two machines sharing a key because someone copied a deployment image.
  • No fleet writes from the machine. The machine cannot add itself to the operator's NIP-51 roster — that's a write signed by the operator key, performed by the dashboard after it sees and validates the ack. An attacker who somehow gets a machine onto the relay still cannot insert themselves into the operator's view.

Reliability: how do we know a command was received?

There are two distinct levels of acknowledgement, and we must not conflate them.

  1. Relay-level ack (NIP-01 OK frame). After publishing, the relay returns ["OK", <event_id>, true|false, <reason>]. This proves the relay accepted and stored the event. It does not prove the recipient saw it.
  2. Application-level ack. The recipient signs and publishes a response event tagged with the original request_id. This is the only thing that proves end-to-end delivery.

The dashboard must wait on (2), with (1) used only as an early failure signal. This is exactly what the existing Lightning.Pub RPC client and CLINK client already do — we should factor that pattern into a shared helper rather than reinventing it.

The same two-level acknowledgement applies to the enrollment handshake: the dashboard treats the seed as unclaimed until it sees the signed ack event from the new machine, not just an OK from the relay.

Additional reliability rules:

  • Publish each command to ≥3 relays for redundancy.
  • Timeouts: ~10s for read-only ops, ~30s for ops that touch hardware.
  • The dashboard distinguishes three states: acknowledged, failed, unknown (no response). For cash-affecting actions, "unknown" must never be silently retried — that's what idempotency keys are for.
  • Every mutating command carries a UUID; the machine dedupes within a sliding window so a retry doesn't apply twice.

Authorization model (steady state)

After enrollment, the machine has an operator allow-list seeded with the pubkey from the seed URL. From then on:

  1. Machines reject any control event whose author is not on the list. Auth is via the event signature; NIP-44 encryption is for confidentiality only.
  2. Capability scoping: distinguish read-only commands (GetInventory, GetHealth, GetRecentTransactions) from cash-affecting ones (SetRate, EmptyCassette, Reboot, RotateKeys). A "viewer" key can be granted without "operator" privileges. Capabilities are declared in a manifest published by the machine so the dashboard knows what it can ask for.
  3. Adding a co-operator is itself a control command (AddOperator <npub> --scope ...), signed by an existing operator. No physical access needed.
  4. Revoking an operator is a control command (RevokeOperator <npub>), again signed by an existing operator.
  5. Recovery from total operator-key loss: SSH to the machine and edit the config file (operator allow-list section) directly, then restart the service. This is the only path that requires physical/root access and is intentionally rare.
  6. NIP-42 relay auth on the strfry instance gives an additional layer: only operator keys and machine keys can connect to the private relay at all.

Relevant NIPs

  • NIP-01 — base protocol, OK frames for relay-level ack.
  • NIP-19 — bech32 entities. Not used for the seed URL itself (URL scheme is more practical) but used everywhere else for displaying npubs.
  • NIP-42 — relay auth, already used by the private strfry deployment.
  • NIP-44 — encryption for control commands.
  • NIP-46 (Bunker) — direct inspiration for the seed URL format and the request/response pattern. The fleet control plane is essentially "Bunker for ATMs".
  • NIP-47 (NWC) — additional inspiration for the seed URL format (nostr+walletconnect:// is the closest existing analog).
  • NIP-51 — lists. Used for the operator's machine roster.
  • NIP-65 — relay list metadata, so the dashboard knows where to find each machine.

Proposed components

packages/fleet-protocol (new)

Shared schemas and types between dashboard and machine:

  • Event kind constants and d-tag conventions
  • Zod schemas for every control command and response
  • Capability manifest schema
  • Seed URL parser/encoder + enrollment-ack event schema
  • A FleetClient helper wrapping request/response with request_id correlation, multi-relay publish, timeout, and idempotency. Built on top of @lamassu/nostr-client.

apps/machine additions

  • First-boot wizard: detect missing config, show QR scan / paste UI, parse seed URL, run enrollment flow, write config, restart into normal operation.
  • Subscribe to control events (p-tag = own pubkey, kind = fleet control kind), filter by allow-list, validate against the capability schema.
  • Dispatch into existing services: HAL for hardware ops, Lightning.Pub client for balance/payments, XState machine for transaction state.
  • Publish kind 30079 inventory/health snapshots on a debounce + heartbeat (mirror of the existing useAvailabilityBroadcast composable).
  • Publish capability manifest (replaceable, d = lamassu-capabilities) at boot.

apps/dashboard (currently empty)

  • Vue 3 + Tailwind v4 + shadcn-vue (matches shockwallet-vue stack — operators get a familiar feel).
  • Static SPA. Can be self-hosted or run from file://.
  • "Add new machine" wizard: collects fiat/model, mints LP invite token, generates seed URL, renders QR, listens for enrollment-ack, writes the new machine into the operator's fleet roster.
  • Reads operator's machine roster (NIP-51 list), subscribes to telemetry events, displays a fleet view.
  • Drills into a single machine: inventory, health, recent transactions, controls.
  • Sends control commands via FleetClient. Surfaces the three response states clearly.
  • Local storage for operator nsec (encrypted with passphrase) — or NIP-46 signer integration so the nsec stays in a hardware/remote signer.

Open questions

  1. Invite token revocation. Does Lightning.Pub support invalidating an issued invite token before it's used? If not, the only mitigation is short TTLs.
  2. Seed URL transport. QR code on the dashboard screen scanned by a webcam at the machine is the obvious path. Are there machines without a camera where the operator would have to type/paste? If so, the URL needs to stay short enough to be tractable.
  3. One private relay per operator, or shared relay? Per-operator is simpler for auth (NIP-42 + a simple allow-list at the relay level), but adds operational burden. Shared relay needs per-event filtering.
  4. Transaction record kind. CLAUDE.md mentions kind 30079 for "Transaction Record (replaceable)" but replaceable doesn't fit transactions (each is unique). Reconcile: 30079 = inventory snapshot (replaceable, makes sense), and use a non-replaceable kind for individual transaction records.
  5. NIP-46 reuse. Should the fleet control protocol literally implement NIP-46 framing, or just borrow the pattern? Literal compliance gives us free wallet/signer integration; a custom protocol gives us cleaner schemas.
  6. Re-enrollment. What happens if a machine's .env is wiped after first enrollment? It boots into the wizard again, generates a new keypair, and the operator must issue a fresh seed. The old machine npub becomes a stale entry in the fleet roster — needs a "remove machine" action in the dashboard.

Suggested phasing

  1. Foundations — packages/fleet-protocol with schemas + FleetClient + seed URL parser/encoder. Refactor existing kind-21000 RPC client to use the shared helper.
  2. Telemetry only — machine publishes 30079 snapshots and a capability manifest. Dashboard skeleton can subscribe and render a read-only fleet view for machines that are already configured (manual .env). No control commands, no enrollment yet. Already useful on its own.
  3. Seed URL enrollment — dashboard "Add new machine" wizard + machine first-boot wizard. End-to-end: empty machine → scan QR → live in fleet view. This is the headline feature.
  4. Read-only control — GetInventory, GetHealth, GetRecentTransactions. Exercises the full request/response path with no risk of cash movement.
  5. Mutating control — SetRate, Reboot, EmptyCassette, AddOperator, RevokeOperator, etc. Requires the authorization model and idempotency to be solid first.

References

  • apps/machine/src/composables/useAvailabilityBroadcast.ts — existing kind 30078 telemetry, the template for kind 30079.
  • packages/lightning/ and packages/clink/ — existing request/response patterns to factor into FleetClient.
  • docs/architecture-comparison.md — frames why this is a natural fit for the Nostr-native approach.
  • NIP-46 (Nostr Connect / Bunker) — connection-string format inspiration.
  • NIP-47 (Nostr Wallet Connect) — nostr+walletconnect:// URL pattern.
> _Migrated from [aiolabs/lamassu-next#42](https://git.atitlan.io/aiolabs/lamassu-next/issues/42) — opened by @padreug on 2026-04-07._\n\n## Problem A real operator runs anywhere from a handful to ~100 machines. Today the only way to interact with a deployed `lamassu-next` machine is to SSH in and run `atm-tui`. That doesn't scale, and it doesn't match the spirit of the rest of the architecture, where every other component (Lightning.Pub, CLINK, customer wallets) talks over Nostr. This issue proposes a fleet management plane that reuses the primitives we already have in the codebase, plus a **seed URL bootstrap** that lets an operator enroll a brand-new machine from the dashboard without ever touching the machine's filesystem. ## Goals - Operator can see the health, inventory, and recent activity of every machine they own from a single dashboard. - Operator can issue control commands (read-only and mutating) to any specific machine. - Operator can **enroll a brand-new machine** by generating a seed URL/QR in the dashboard and presenting it to the machine on first boot. No manual `.env` editing. - No inbound network exposure on the machines themselves. - No new central database; the relay is the source of truth for in-flight messages and audit history. - Reuse the request/response pattern already used for Lightning.Pub RPC (kind 21000) and CLINK (kinds 21001-21003). ## Non-goals - Customer-facing UX changes. - Multi-tenant SaaS dashboard. This is a self-hosted/local-first SPA. - Removing SSH/`.env` as a recovery path. They remain the ultimate escape hatch — they're just not the day-to-day provisioning surface. ## Three planes, separated | Plane | Direction | Mechanism | |---|---|---| | **Discovery** — "what machines do I own?" | operator → roster | NIP-51 parameterized list (kind `30000` or `30003`), `d` tag = `lamassu-fleet`, signed by the operator pubkey, with one `p` tag per machine npub | | **Telemetry** — "how is the fleet doing?" | machine → public | Replaceable events. Kind `30078` availability already exists. Add `30079` (inventory + health snapshot). Optionally a non-replaceable kind for transaction records | | **Control** — "do X on machine Y" | operator → machine | NIP-44 encrypted request/response addressed to the machine npub, correlated by `request_id`. Same pattern as the Lightning.Pub RPC client in `packages/lightning` | ## Bootstrap: seed URLs The hardest part of fleet management is the bootstrap problem: a brand-new machine has no idea who its operator is, has no Lightning.Pub account yet, doesn't know which relay to talk to, and has no `.env`. We solve this with a single connection-string-style URL, modeled directly on **NWC connection strings** (`nostr+walletconnect://...`) and **NIP-46 bunker URLs** (`bunker://...`). ### Format ``` lamassu+seed://<operator-pubkey> ?relay=wss://relay.example.com/ &relay=wss://backup.example.com/ &lpub=https://lightning.pub.example.com &invite=<one-time-token> &nonce=<random-id> &exp=<unix-timestamp> &fiat=USD &model=batm3 ``` A URL scheme (not a new bech32 prefix) is intentional: easy to QR-encode, easy to debug, easy to parse with the standard `URL` class everywhere, and consistent with NWC/bunker conventions. ### Enrollment flow **On the dashboard** (operator is logged in with their nsec): 1. Operator clicks "Add new machine", picks fiat / model / etc. 2. Dashboard, using the operator's existing Lightning.Pub credentials, mints a **one-time invite token** scoped to creating one new LP user (`UseInviteLink` is already in the autogenerated LP RPC client). 3. Dashboard generates the seed URL above, renders it as a QR code and as copy-pasteable text. 4. Dashboard subscribes (filtered by `nonce`) waiting for the new machine to announce itself. **On the new machine** (first boot, no `.env` yet): 1. The Electron app sees no config and shows a first-boot wizard with a QR-scan / paste-URL screen. 2. Operator presents the seed URL. 3. Machine **generates its own nsec/npub locally** — this never leaves the device. The seed is provisioning data, not identity. 4. Machine parses the seed, validates `exp`, then in order: - Connects to the relay(s) listed in the seed. - Uses the invite token to register itself as a Lightning.Pub user (machine's own npub becomes the LP `identifier`). Stores the resulting LP credentials. - Writes `.env` (or `config.json`) with: own nsec, relay URLs, LP URL + token, operator npub allow-list = `[<operator-pubkey>]`, fiat, model. - Publishes a signed kind-0 profile and the kind 30078 availability beacon (already implemented in `useAvailabilityBroadcast.ts`). - Publishes a one-shot **enrollment-ack event** tagged `["e", <nonce>]` and `["p", <operator-pubkey>]`, signed with the new machine key. This is the "I claimed your seed, here is my pubkey" handshake. 5. Dashboard sees the ack, locks that nonce → that machine pubkey, adds the new npub to the operator's NIP-51 fleet roster, and the machine appears live in the fleet view. After this, the machine is fully provisioned. Steady-state fleet management uses the same primitives that just bootstrapped it (same relay, same encryption, same signing keys) — there is no separate "provisioning protocol" to maintain. ### Seed URL security - **Mostly non-sensitive.** Operator pubkey, relay URLs, LP URL — all public. The only sensitive component is the invite token, which is one-time, short-TTL, and scoped to creating a single LP user. - **Nonce binding.** The dashboard remembers the nonce and the *first* pubkey to claim it. Any later claim with the same nonce is rejected. This prevents an attacker who intercepts the seed URL from racing the legitimate machine. - **Short TTL.** Seeds expire in (suggested) 1 hour. Re-issue if needed. - **Invite token revocation.** If the operator cancels enrollment, the dashboard tells Lightning.Pub to invalidate the invite token (assuming LP supports this — to be confirmed). - **Local key generation.** The machine's nsec is generated on the device that will hold it. No risk of two machines sharing a key because someone copied a deployment image. - **No fleet writes from the machine.** The machine cannot add itself to the operator's NIP-51 roster — that's a write signed by the operator key, performed by the dashboard *after* it sees and validates the ack. An attacker who somehow gets a machine onto the relay still cannot insert themselves into the operator's view. ## Reliability: how do we know a command was received? There are two distinct levels of acknowledgement, and we must not conflate them. 1. **Relay-level ack** (NIP-01 `OK` frame). After publishing, the relay returns `["OK", <event_id>, true|false, <reason>]`. This proves the relay accepted and stored the event. It does **not** prove the recipient saw it. 2. **Application-level ack**. The recipient signs and publishes a response event tagged with the original `request_id`. This is the only thing that proves end-to-end delivery. The dashboard must wait on (2), with (1) used only as an early failure signal. This is exactly what the existing Lightning.Pub RPC client and CLINK client already do — we should factor that pattern into a shared helper rather than reinventing it. The same two-level acknowledgement applies to the enrollment handshake: the dashboard treats the seed as unclaimed until it sees the signed ack event from the new machine, not just an OK from the relay. Additional reliability rules: - Publish each command to **≥3 relays** for redundancy. - Timeouts: ~10s for read-only ops, ~30s for ops that touch hardware. - The dashboard distinguishes three states: **acknowledged**, **failed**, **unknown** (no response). For cash-affecting actions, "unknown" must never be silently retried — that's what idempotency keys are for. - Every mutating command carries a UUID; the machine dedupes within a sliding window so a retry doesn't apply twice. ## Authorization model (steady state) After enrollment, the machine has an operator allow-list seeded with the pubkey from the seed URL. From then on: 1. Machines reject any control event whose author is not on the list. **Auth is via the event signature**; NIP-44 encryption is for confidentiality only. 2. **Capability scoping**: distinguish read-only commands (`GetInventory`, `GetHealth`, `GetRecentTransactions`) from cash-affecting ones (`SetRate`, `EmptyCassette`, `Reboot`, `RotateKeys`). A "viewer" key can be granted without "operator" privileges. Capabilities are declared in a manifest published by the machine so the dashboard knows what it can ask for. 3. **Adding a co-operator** is itself a control command (`AddOperator <npub> --scope ...`), signed by an existing operator. No physical access needed. 4. **Revoking an operator** is a control command (`RevokeOperator <npub>`), again signed by an existing operator. 5. **Recovery from total operator-key loss**: SSH to the machine and edit the config file (operator allow-list section) directly, then restart the service. This is the only path that requires physical/root access and is intentionally rare. 6. NIP-42 relay auth on the strfry instance gives an additional layer: only operator keys and machine keys can connect to the private relay at all. ## Relevant NIPs - **NIP-01** — base protocol, `OK` frames for relay-level ack. - **NIP-19** — bech32 entities. Not used for the seed URL itself (URL scheme is more practical) but used everywhere else for displaying npubs. - **NIP-42** — relay auth, already used by the private strfry deployment. - **NIP-44** — encryption for control commands. - **NIP-46 (Bunker)** — direct inspiration for the seed URL format and the request/response pattern. The fleet control plane is essentially "Bunker for ATMs". - **NIP-47 (NWC)** — additional inspiration for the seed URL format (`nostr+walletconnect://` is the closest existing analog). - **NIP-51** — lists. Used for the operator's machine roster. - **NIP-65** — relay list metadata, so the dashboard knows where to find each machine. ## Proposed components ### `packages/fleet-protocol` (new) Shared schemas and types between dashboard and machine: - Event kind constants and `d`-tag conventions - Zod schemas for every control command and response - Capability manifest schema - **Seed URL** parser/encoder + enrollment-ack event schema - A `FleetClient` helper wrapping request/response with `request_id` correlation, multi-relay publish, timeout, and idempotency. Built on top of `@lamassu/nostr-client`. ### `apps/machine` additions - **First-boot wizard**: detect missing config, show QR scan / paste UI, parse seed URL, run enrollment flow, write config, restart into normal operation. - Subscribe to control events (`p`-tag = own pubkey, kind = fleet control kind), filter by allow-list, validate against the capability schema. - Dispatch into existing services: HAL for hardware ops, Lightning.Pub client for balance/payments, XState machine for transaction state. - Publish kind `30079` inventory/health snapshots on a debounce + heartbeat (mirror of the existing `useAvailabilityBroadcast` composable). - Publish capability manifest (replaceable, `d` = `lamassu-capabilities`) at boot. ### `apps/dashboard` (currently empty) - Vue 3 + Tailwind v4 + shadcn-vue (matches `shockwallet-vue` stack — operators get a familiar feel). - Static SPA. Can be self-hosted or run from `file://`. - **"Add new machine" wizard**: collects fiat/model, mints LP invite token, generates seed URL, renders QR, listens for enrollment-ack, writes the new machine into the operator's fleet roster. - Reads operator's machine roster (NIP-51 list), subscribes to telemetry events, displays a fleet view. - Drills into a single machine: inventory, health, recent transactions, controls. - Sends control commands via `FleetClient`. Surfaces the three response states clearly. - Local storage for operator nsec (encrypted with passphrase) — or NIP-46 signer integration so the nsec stays in a hardware/remote signer. ## Open questions 1. **Invite token revocation.** Does Lightning.Pub support invalidating an issued invite token before it's used? If not, the only mitigation is short TTLs. 2. **Seed URL transport.** QR code on the dashboard screen scanned by a webcam at the machine is the obvious path. Are there machines without a camera where the operator would have to type/paste? If so, the URL needs to stay short enough to be tractable. 3. **One private relay per operator, or shared relay?** Per-operator is simpler for auth (NIP-42 + a simple allow-list at the relay level), but adds operational burden. Shared relay needs per-event filtering. 4. **Transaction record kind.** `CLAUDE.md` mentions kind `30079` for "Transaction Record (replaceable)" but replaceable doesn't fit transactions (each is unique). Reconcile: `30079` = inventory snapshot (replaceable, makes sense), and use a non-replaceable kind for individual transaction records. 5. **NIP-46 reuse.** Should the fleet control protocol literally implement NIP-46 framing, or just borrow the pattern? Literal compliance gives us free wallet/signer integration; a custom protocol gives us cleaner schemas. 6. **Re-enrollment.** What happens if a machine's `.env` is wiped after first enrollment? It boots into the wizard again, generates a new keypair, and the operator must issue a fresh seed. The old machine npub becomes a stale entry in the fleet roster — needs a "remove machine" action in the dashboard. ## Suggested phasing 1. **Foundations** — `packages/fleet-protocol` with schemas + `FleetClient` + seed URL parser/encoder. Refactor existing kind-21000 RPC client to use the shared helper. 2. **Telemetry only** — machine publishes `30079` snapshots and a capability manifest. Dashboard skeleton can subscribe and render a read-only fleet view for machines that are *already* configured (manual `.env`). No control commands, no enrollment yet. Already useful on its own. 3. **Seed URL enrollment** — dashboard "Add new machine" wizard + machine first-boot wizard. End-to-end: empty machine → scan QR → live in fleet view. This is the headline feature. 4. **Read-only control** — `GetInventory`, `GetHealth`, `GetRecentTransactions`. Exercises the full request/response path with no risk of cash movement. 5. **Mutating control** — `SetRate`, `Reboot`, `EmptyCassette`, `AddOperator`, `RevokeOperator`, etc. Requires the authorization model and idempotency to be solid first. ## References - `apps/machine/src/composables/useAvailabilityBroadcast.ts` — existing kind 30078 telemetry, the template for kind 30079. - `packages/lightning/` and `packages/clink/` — existing request/response patterns to factor into `FleetClient`. - `docs/architecture-comparison.md` — frames why this is a natural fit for the Nostr-native approach. - [NIP-46 (Nostr Connect / Bunker)](https://github.com/nostr-protocol/nips/blob/master/46.md) — connection-string format inspiration. - [NIP-47 (Nostr Wallet Connect)](https://github.com/nostr-protocol/nips/blob/master/47.md) — `nostr+walletconnect://` URL pattern.
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#42
No description provided.