opt-in public publishing — fleet metadata + heartbeats (post-launch) #27

Closed
opened 2026-05-26 21:24:07 +00:00 by padreug · 1 comment
Owner

Context

Pre-launch we ripped out the NIP-78 fleet publishing path (commits
131ff92 shipped it, dcd0874 reverted it). Default posture is now
maximum privacy — nothing about an operator's fleet leaks to
public relays. This is the right default: published fleet composition

  • machine locations + fiat codes is a robbery / competitor-intel /
    extortion target surface, and operators never opted in to it.

But there ARE legitimate public-publishing use cases. Operators may
want certain machines to be discoverable (publish location for
walk-in customers, advertise that the ATM is online), and bitSpire
already emits machine-side heartbeats. This issue tracks the design
of a opt-in publishing layer that ships post-launch, after the
core flows are stable and operators can make an informed choice.

What public publishing is for

Two distinct surfaces, very different audience + privacy posture:

A. Machine heartbeat (ATM-side, operator opt-in per machine)

  • Who emits: the ATM itself (bitSpire lamassu-next), not LNbits.
    Already implemented on the ATM side.
  • Audience: customers looking up "is this ATM live?", "what's
    near me?", third-party ATM directories.
  • Payload: liveness signal + sparse public metadata only. Things
    like ATM npub, friendly name, coarse location (city or
    neighbourhood, NOT street address), supported fiat codes,
    active/inactive flag, last-seen timestamp. Explicitly NOT: wallet
    balances, settlement counts, recent transaction amounts, exact
    geo, owner identity.
  • Knob: per-machine is_publicly_discoverable: bool, default
    false. Set in the satmachineadmin UI; surfaced to bitSpire via
    the existing NIP-44 RPC channel (no separate transport).
  • Kind: the bitSpire heartbeat kind (whatever lamassu-next emits
    today — confirm with bitspire session before shipping).
  • Crypto identity: signed by the ATM's own keypair, not the
    operator's. ATMs already hold their own nsec post-S0/S2 bunker
    work, so no operator-key exposure.

B. Fleet roster / aggregate metadata (operator-side, opt-in per operator)

  • Who emits: satmachineadmin (the LNbits operator's extension)
    via the operator's signer (post-#17 abstraction).
  • Audience: anyone who wants a canonical answer to "is this ATM
    npub a real machine of operator X?" — most notably the future
    LNbits-server-side roster-gating (S6) that decides whether an
    unknown npub should auto-create an account.
  • Payload: list of operator's is_publicly_discoverable machines'
    npubs only. NOT: full fleet (private machines must not leak), not
    fiat codes, not location, not wallet bindings.
  • Knob: operator-level publish_roster_publicly: bool, default
    false. AND per-machine is_publicly_discoverable: bool (the
    same flag (A) uses) — a machine is in the public roster iff BOTH
    flags are true.
  • Kind: kind:30078 (NIP-78 addressable app-specific data, was the
    right fit semantically per the
    feedback_respect_nostr_protocol_semantics rule); d tag
    bitspire-fleet.
  • Crypto identity: signed by the operator's signer via
    lnbits.core.signers.resolve_signer (LocalSigner or RemoteBunkerSigner
    post-lnbits#18).

C. Settlement receipts (out of scope here — tracked in lnbits#22)

Distinct surface, separate opt-in. See aiolabs/lnbits#22 for the
receipt-publishing producer + the privacy-by-default toggle that
gates it operator-side.

Schema sketch

-- Operator-level prefs
CREATE TABLE dca_operator_prefs (
  operator_user_id TEXT PRIMARY KEY,
  publish_roster_publicly BOOLEAN NOT NULL DEFAULT 0,
  publish_receipts_publicly BOOLEAN NOT NULL DEFAULT 0,  -- gates lnbits#22 producer
  updated_at TIMESTAMP NOT NULL
);

-- Per-machine knob (already a column on dca_machines? add if not)
ALTER TABLE dca_machines ADD COLUMN is_publicly_discoverable BOOLEAN
  NOT NULL DEFAULT 0;

dca_machines.is_publicly_discoverable drives (A) heartbeats and
filters (B) roster contents. dca_operator_prefs.publish_roster_publicly
gates the entire (B) publish path.

UI surface

Settings tab (operator-scoped):

  • Toggle: "Publish fleet roster to public Nostr relays" — default OFF.
    Helper text: "Publishes the npubs of machines you've marked
    'publicly discoverable' to your relays. Lets customers and other
    LNbits instances verify which ATMs belong to you. Off by default;
    enable if you want walk-in discoverability."
  • Toggle: "Publish settlement receipts to public Nostr relays" —
    default OFF. (Wires into lnbits#22.)

Per-machine row (in the machines table):

  • Checkbox column: "Publicly discoverable" — default OFF.
    Helper text: "When ON (and your operator publish-roster toggle is
    also ON), this machine's npub + sparse public metadata appears in
    your published fleet roster. Customers can find it; competitors
    can too."

What we should NOT bring back

The original nostr_publish.py published the operator's full active
fleet by default with no toggle. We deleted it for good reason.
Don't restore it wholesale — the right shape is:

  • (A) heartbeats stay on the ATM side, only LNbits' role is to
    propagate the is_publicly_discoverable flag via RPC.
  • (B) roster is opt-in twice (operator-level + per-machine) and
    publishes a strictly narrower payload than the old version.
  • (C) receipts move to lnbits#22 with their own opt-in.

Sequencing

Post-launch. Wait until:

  1. Core cash-out + cash-in flows are stable + audited in production.
  2. Operators have run real settlements and have an informed mental
    model of what data exists vs what they'd want to share.
  3. lnbits#17 (signer abstraction) + #18 (bunker) have landed, so the
    roster publish path can use the proper signer chain without the
    pre-#17 prvkey fallback.

Until those land, the privacy-by-default posture is the ship-it
state
. No external observer can tell which npubs belong to which
operator's fleet from public relays — and that's correct for v2.0.

  • aiolabs/satmachineadmin#18 — original S4 issue that shipped + got
    reverted. Close this issue when (B) ships.
  • aiolabs/lnbits#22 — S3 receipt-publishing producer; uses same
    opt-in shape ((C) above).
  • aiolabs/lnbits#17 — signer abstraction (dependency for (B)).
  • aiolabs/lnbits#18 — NIP-46 bunker (dependency for (B) when
    operator uses bunker).
## Context Pre-launch we ripped out the NIP-78 fleet publishing path (commits `131ff92` shipped it, `dcd0874` reverted it). Default posture is now **maximum privacy** — nothing about an operator's fleet leaks to public relays. This is the right default: published fleet composition + machine locations + fiat codes is a robbery / competitor-intel / extortion target surface, and operators never opted in to it. But there ARE legitimate public-publishing use cases. Operators may *want* certain machines to be discoverable (publish location for walk-in customers, advertise that the ATM is online), and bitSpire already emits machine-side heartbeats. This issue tracks the design of a opt-in publishing layer that ships **post-launch**, after the core flows are stable and operators can make an informed choice. ## What public publishing is for Two distinct surfaces, very different audience + privacy posture: ### A. Machine heartbeat (ATM-side, operator opt-in per machine) - **Who emits:** the ATM itself (bitSpire `lamassu-next`), not LNbits. Already implemented on the ATM side. - **Audience:** customers looking up "is this ATM live?", "what's near me?", third-party ATM directories. - **Payload:** liveness signal + sparse public metadata only. Things like ATM npub, friendly name, coarse location (city or neighbourhood, NOT street address), supported fiat codes, active/inactive flag, last-seen timestamp. Explicitly NOT: wallet balances, settlement counts, recent transaction amounts, exact geo, owner identity. - **Knob:** per-machine `is_publicly_discoverable: bool`, default `false`. Set in the satmachineadmin UI; surfaced to bitSpire via the existing NIP-44 RPC channel (no separate transport). - **Kind:** the bitSpire heartbeat kind (whatever lamassu-next emits today — confirm with bitspire session before shipping). - **Crypto identity:** signed by the **ATM's own keypair**, not the operator's. ATMs already hold their own nsec post-S0/S2 bunker work, so no operator-key exposure. ### B. Fleet roster / aggregate metadata (operator-side, opt-in per operator) - **Who emits:** satmachineadmin (the LNbits operator's extension) via the operator's signer (post-#17 abstraction). - **Audience:** anyone who wants a canonical answer to "is this ATM npub a real machine of operator X?" — most notably the future LNbits-server-side roster-gating (S6) that decides whether an unknown npub should auto-create an account. - **Payload:** list of operator's `is_publicly_discoverable` machines' npubs only. NOT: full fleet (private machines must not leak), not fiat codes, not location, not wallet bindings. - **Knob:** operator-level `publish_roster_publicly: bool`, default `false`. AND per-machine `is_publicly_discoverable: bool` (the same flag (A) uses) — a machine is in the public roster iff BOTH flags are true. - **Kind:** kind:30078 (NIP-78 addressable app-specific data, was the right fit semantically per the `feedback_respect_nostr_protocol_semantics` rule); `d` tag `bitspire-fleet`. - **Crypto identity:** signed by the **operator's signer** via `lnbits.core.signers.resolve_signer` (LocalSigner or RemoteBunkerSigner post-lnbits#18). ### C. Settlement receipts (out of scope here — tracked in lnbits#22) Distinct surface, separate opt-in. See `aiolabs/lnbits#22` for the receipt-publishing producer + the privacy-by-default toggle that gates it operator-side. ## Schema sketch ```sql -- Operator-level prefs CREATE TABLE dca_operator_prefs ( operator_user_id TEXT PRIMARY KEY, publish_roster_publicly BOOLEAN NOT NULL DEFAULT 0, publish_receipts_publicly BOOLEAN NOT NULL DEFAULT 0, -- gates lnbits#22 producer updated_at TIMESTAMP NOT NULL ); -- Per-machine knob (already a column on dca_machines? add if not) ALTER TABLE dca_machines ADD COLUMN is_publicly_discoverable BOOLEAN NOT NULL DEFAULT 0; ``` `dca_machines.is_publicly_discoverable` drives (A) heartbeats and filters (B) roster contents. `dca_operator_prefs.publish_roster_publicly` gates the entire (B) publish path. ## UI surface Settings tab (operator-scoped): - Toggle: "Publish fleet roster to public Nostr relays" — default OFF. Helper text: "Publishes the npubs of machines you've marked 'publicly discoverable' to your relays. Lets customers and other LNbits instances verify which ATMs belong to you. Off by default; enable if you want walk-in discoverability." - Toggle: "Publish settlement receipts to public Nostr relays" — default OFF. (Wires into lnbits#22.) Per-machine row (in the machines table): - Checkbox column: "Publicly discoverable" — default OFF. Helper text: "When ON (and your operator publish-roster toggle is also ON), this machine's npub + sparse public metadata appears in your published fleet roster. Customers can find it; competitors can too." ## What we should NOT bring back The original `nostr_publish.py` published the operator's full active fleet by default with no toggle. We deleted it for good reason. **Don't** restore it wholesale — the right shape is: - (A) heartbeats stay on the ATM side, only LNbits' role is to propagate the `is_publicly_discoverable` flag via RPC. - (B) roster is opt-in twice (operator-level + per-machine) and publishes a strictly narrower payload than the old version. - (C) receipts move to lnbits#22 with their own opt-in. ## Sequencing Post-launch. Wait until: 1. Core cash-out + cash-in flows are stable + audited in production. 2. Operators have run real settlements and have an informed mental model of what data exists vs what they'd want to share. 3. lnbits#17 (signer abstraction) + #18 (bunker) have landed, so the roster publish path can use the proper signer chain without the pre-#17 prvkey fallback. Until those land, **the privacy-by-default posture is the ship-it state**. No external observer can tell which npubs belong to which operator's fleet from public relays — and that's correct for v2.0. ## Related - `aiolabs/satmachineadmin#18` — original S4 issue that shipped + got reverted. Close this issue when (B) ships. - `aiolabs/lnbits#22` — S3 receipt-publishing producer; uses same opt-in shape ((C) above). - `aiolabs/lnbits#17` — signer abstraction (dependency for (B)). - `aiolabs/lnbits#18` — NIP-46 bunker (dependency for (B) when operator uses bunker).
Author
Owner

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

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/15 (aiolabs/spirekeeper#15).** 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#27
No description provided.