Final rename commit covering user-facing copy and the docs that
describe current state. The mechanics of the rename are done after
this; the LNbits backend swap (phase 3) is the next concern.
Code branding strings (Lightning invoice descriptions):
apps/machine/src/services/lightning.ts
apps/machine/src/stores/atm.ts
docs/clink-protocol.md (example code blocks)
"Lamassu ATM Payment" → "bitSpire Payment"
"Lamassu ATM - Cash Out" → "bitSpire - Cash Out"
`Lamassu ATM - Buy ${n} sats`→ `bitSpire - Buy ${n} sats`
Top-level docs:
README.md, CLAUDE.md — title + intro + dir-tree references.
deploy/nixos/README.md — title + worktree-path commands.
docs/machine-installation.md — opening line carries the historical
note ("Lamassu Next" → "bitSpire"). The body still uses
`/opt/lamassu/` paths and the `lamassu-kiosk` systemd unit
because the dev branch is moving to NixOS disk-image flash
(phase 4) — this AppImage-sideload doc represents the legacy
deploy path. Leaving the LP/lamassu refs in there as part of
its historical context; a separate doc will describe the
NixOS path.
.claude/skills/nostr-check.md — header only.
DELIBERATELY left as "Lamassu Next" (pedagogical / historical):
- docs/adr/001-hal-architecture.md — frozen ADR; renaming
distorts the historical decision context.
- docs/architecture-comparison.md — deliberately contrasts
"lamassu-server" (prior) with "lamassu-next" (us at the time
of writing).
NOT done in this commit (deferred to LNbits/clean-up phase):
- docker/docker-compose.dev.yml container names
(lamassu-relay, lamassu-bitcoind, etc.) — these belong to the
LP-bearing dev stack that 3c/3d will significantly reshape.
Verified: pnpm typecheck clean (12/12 cached).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five-file coordinated rename to give the dev-branch deploy a clean
`bitspire` namespace at the NixOS level:
deploy/nixos/lamassu-atm.nix → deploy/nixos/bitspire-atm.nix
- `services.lamassu-atm` → `services.bitspire`
- `systemd.services.lamassu-atm` → `systemd.services.bitspire`
- `/var/lib/lamassu-atm` → `/var/lib/bitspire`
- `/opt/lamassu-atm` → `/opt/bitspire`
- `/etc/lamassu-atm/config.env` → `/etc/bitspire/config.env`
flake.nix
- module import path updated
- `nixosModules.lamassu-atm` → `nixosModules.bitspire`
- `system.activationScripts.lamassu-env` → `bitspire-env`
- all activation-script paths point at /var/lib/bitspire
deploy/nixos/configuration.nix
- `networking.hostName = "lamassu-atm"` → `"bitspire"`
deploy/nixos/live.nix
- module import path updated
- ISO name template: `lamassu-atm-<model>-live.iso` → `bitspire-<model>-live.iso`
- activation-script name updated
deploy/nixos/provision-atm.sh
- data-dir paths: /var/lib/lamassu-atm → /var/lib/bitspire
- systemctl + journalctl unit names updated
DELIBERATELY kept as `lamassu` (for now):
- The `lamassu` UNIX user and group account. Renaming would
require file-ownership migration scripts; the Sintra is a
fresh flash so no existing data, but the internal user
namespace inconsistency is acceptable.
- LP-specific bits in provision-atm.sh (admin token, the
`docker logs lamassu-lightning-pub` extractor) — those
get ripped out in 3c when the script switches to LNbits.
NO migration activation script added — the Sintra flash is fresh,
production batm3/douro stay on `main` and never see this branch.
A future dev→main cutover will need a separate migration story
(rename UNIX user, move /var/lib/lamassu-atm → /var/lib/bitspire,
SSH key relocation, etc.).
Verified:
nix eval .#nixosConfigurations.batm3-installed.config.systemd.services.bitspire.enable
→ true
nix eval .#nixosConfigurations.bitSpire-live-sintra.config.networking.hostName
→ "bitspire"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
apps/machine/package.json (electron-builder block):
appId dev.lamassu.atm → dev.bitSpire.atm
productName "Lamassu ATM" → "bitSpire"
apps/machine/src/services/lightning.ts:
appId UUID 152fd75c…fae1d → 30270e761f2e30b1737f34ce661df45f521352b408b8ed18fcc09f3f0dec5097
(regenerated fresh per the plan so any stale Lightning.Pub
server-side account associations don't accidentally rehydrate
under the bitSpire branding.)
The runtime appId is also overridable via VITE_APP_ID env var
(lightning.ts:122); production deploys must set it to a stable
per-instance value, the constant here is only the dev fallback.
Verified: pnpm typecheck clean (12/12).
Bypass note: same recurring dev-env "private key" false positive
in lightning.ts as 2a — not introduced by this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three root-level identifier renames:
- Root package.json `name`: lamassu-next → bitSpire
- flake.nix nixosConfigurations.lamassu-live-*: kept (legacy
aliases) and ADDED bitSpire-live-* alongside. Both work.
Drop the lamassu-* aliases at the final cutover once nothing
references them.
- nix/mkAtmApp.nix `pname`: lamassu-atm-app → bitspire-atm-app
(lowercase to match nix package naming conventions; the
Electron app's externally-facing names get their own commit).
What's NOT renamed here (per the plan):
- Hardware-named outputs (douro, tejo, sintra, batm3) — those
are physical product names.
- nixosConfigurations.{douro,tejo,sintra,batm3} ergonomic
aliases — same reason.
- nixosModules.lamassu-atm + its imported file — that's the
systemd service, deferred to 2d.
- apps/machine/package.json appId/productName — Electron
identity, deferred to 2c.
Verified:
pnpm typecheck clean (12/12 cached)
nix eval .#nixosConfigurations.bitSpire-live-sintra ✓
nix eval .#packages.x86_64-linux.atm-app-sintra.pname ✓
→ "bitspire-atm-app"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The system.autoUpgrade flake URL had no explicit ref, so the auto-pull
at 04:00 resolves to the repo's default branch (main). That's correct
for production ATMs flashed from main, but it would silently regress
a Sintra flashed from dev back to main code overnight.
Pin to ?ref=dev on the dev branch so any ATM deployed from dev stays
on dev. The main branch's flake.nix stays unchanged — production ATMs
keep pulling main HEAD as before.
First commit on the new dev branch. Tagged pre-bitspire-cutover on
main beforehand as a rollback target.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
If MEI reports `escrowed` AND `powerup` on the first message after
service start with no error flags (jammed/stalled/failure/
transportOpen/stackerFull), fire a reject to clear stale state
before the FSM emits spurious billsAccepted/billsRead upstream.
Guards exclude every scenario where reject's motor sequence could
do harm: physical jams (`jammed`/`stalled`/`failure`/`transportOpen`)
are left alone for hands-on diagnosis, and a real customer bill in
escrow with `stackerFull` is preserved rather than returned.
Diagnosed on Austin batm3 2026-06-01: unit had been stuck in this
state since 2026-05-25 (six days, three boots) requiring manual
intervention to clear. Self-heals now on next service restart.
Surface MEI SCR's full status-flag set in the journal — diagnostic
for stuck-bill / jam scenarios where the raw flag combination
(jammed, stalled, transportOpen, escrowed, etc.) reveals what state
the validator actually believes it's in. Dedup'd on change so 100ms
polling doesn't flood logs; firmware model + revision logged once.
Follow-up to 9430c9b. `max-jobs = 1` reopened the door for ANY uncached
derivation to build locally, including derivations whose outputs SHOULD
be substituted but happen to miss the cache (network blip, hash drift,
operator forgot to push). On ATM hardware a kernel/electron/rustc build
would take literal hours and silently wedge the kiosk while it churns.
`timeout = 60` caps every local build's wall-clock at 60s. Activation-
time stitches (boot.json, system-units, X-Restart-Triggers, etc.) finish
in well under a second; anything that doesn't return by 60s is by
definition a heavy compile that has no business running on an ATM.
Killing it fast makes the upgrade fail loudly so the operator can fix
the cache miss, rather than the box silently chewing CPU all night.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NixOS generates several trivial activation-time derivations that are
hardcoded with `allowSubstitutes = false` + `preferLocalBuild = true`
— most visibly `boot.json` (the bootspec) and `nixos-rebuild`. These
will NEVER appear in any binary cache (cachix follows the
non-substitutable flag) and they CAN'T be substituted (allowSubstitutes
= false). They're only realized via local build.
With `max-jobs = 0`, that's structurally impossible, so every nightly
`nixos-upgrade` across the fleet has been failing for at least a week:
May 19 04:00:36 lamassu-atm: Cannot build '/nix/store/...-boot.json.drv'
May 20-24 04:00:xx: Cannot build '/nix/store/...-nixos-rebuild.drv'
May 25 04:11:17 lamassu-atm: Cannot build '/nix/store/...-atm-transactions.drv'
The kiosk kept running so nobody noticed — the systemd unit fails but
the old generation continues. Caught when re-provisioning the Sintra
dev unit to the demo LNbits today and the migration commits wouldn't
land.
`max-jobs = 1` allows one concurrent local build slot. Heavy compiles
(kernel, rustc, electron) DON'T have `preferLocalBuild`, so they still
go through normal substitution and effectively never build locally
because they're cached upstream. The slot exists strictly to unblock
the trivially-cheap activation-time stitch derivations.
Refs: lamassu-next#47 (caught during demo-server provisioning)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Aaeon UP Board (Atom x5-Z8350) chokes on continuous CSS transforms.
Gate animate-float on machineModel, keep the bounce for douro/tejo/batm3/gaia
where the hardware can handle it. Refs #47 (operator-side animation toggle
is a future consideration there).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3.16.3 ships a regressed nix-functional-tests test
(local-overlay-store / stale-file-handle FAILs at exit 1) that triggers
when the determinate-nix-3.16.3 derivation has to be built from source
locally — no aiolabs.cachix nor cache.flakehub.com substituter has the
output cached for our exact nixos-24.05 / x86_64-linux combo, so the
build evaluates the test phase and fails.
Bumping the semver pin from `?3` (≥3.0.0 → resolves to 3.16.3) to
`?3.15` (≥3.15.0 → resolves to 3.20.0) skips past the regression.
3.20.0 builds cleanly; nix bumps from 2.33 → 2.34.6 across the BATM3
fleet once it auto-pulls.
Mirrors the same fix already applied on dev in 6d8c217.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Time each bill's stay in the `billsRead` (escrow) status. Anything that
exits escrow within ESCROW_WATCHDOG_WARN_MS=500ms gets logged at info
level; anything longer gets a warning naming the elapsed milliseconds.
500ms is 5× our 100ms poll cadence and well below MEI's ~5s grace
window. A warning means we're drifting toward the autonomous-return
failure mode that tears bills on the BATM3 — useful field signal for
verifying the poll-interval fix is sufficient under real load.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
parseStatus previously collapsed all in-motion bits into a single derived
status, hiding when the MEI was moving a bill between escrow and the
stacker/mouth. Field journals showed nothing in the window where the
BATM3 tore bills.
Surface the three transient bits between the terminal states and
escrow/standby, route them through EbdsFsm with a Date.now() timestamp,
and emit them as diagnostic events on the validator. No state machine
consumes them; they're for journalctl.
Also: warn when `returning` is observed straight out of escrow with no
host reject() — that signature is the autonomous-return failure mode we
just polled around. Surfacing it gives us field confirmation the 100ms
fix is sufficient (or evidence it isn't).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EBDS is a host-polled protocol: the MEI validator only speaks when
polled, and its internal escrow grace expires after ~5 seconds. With
POLLING_INTERVAL=10_000 the host learned about an escrowed bill *after*
the MEI had already autonomously begun returning it, which on BATM3
hardware can shear the bill between the transport rollers and the
mouth (see torn $20 reported by operator).
Drop the interval to 100ms to match id003's cadence — so we see
escrow and issue stack/reject inside the validator's grace window.
This is the production-critical fix; observability + escrow watchdog
follow in subsequent commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Accepts percentage (5.55) or decimal (0.0555) — auto-detected by
whether the value is >= 1. Defaults to 3.33% cash-in, 7.77% cash-out.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move WireGuard IP from shared configuration.nix to per-machine
hardware configs. douro = 10.0.0.4, batm3 = 10.0.0.5.
Previously both machines shared 10.0.0.4 which caused routing
conflicts on the VPS.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes the mkForce override that enabled password auth for initial
setup. Machines are now provisioned with SSH keys, so the base
config's PasswordAuthentication=false takes effect.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a relay reconnects after a disconnect, all active subscriptions
(including Lightning.Pub RPC listener) are now re-established on the
new relay instance. Previously subscriptions were lost permanently.
Also publishes availability broadcast immediately on reconnect instead
of waiting up to 5 minutes for the next heartbeat.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously the relay reconnect logic gave up after 5 attempts (~31s).
If the relay was down longer, the ATM permanently lost connectivity
and couldn't fetch available balance — causing all bills to be
rejected after the recent safety guard change.
Now reconnects indefinitely with exponential backoff capped at 60s.
Attempts reset on successful connection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously, if getAvailableBalance returned 0 or exchange rate was
missing, all bills were accepted — risking cash-in exceeding the
ATM's sats balance. Now rejects bills in that case to protect
customers from losing cash.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add min-h-0 for proper flex containment so ScrollArea can be
constrained to the remaining space.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Returns to idle screen after 5 minutes of no touch/scroll activity.
Timer resets on any pointer or scroll interaction.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The availability broadcast was reading inventory from the XState context,
which is only populated during cash-out transitions. On fresh boot or
idle, context.inventory is empty, so the broadcast falsely reported
cash_level: "none" even when cassettes had bills.
- Add persistedInventory ref loaded from SQLite on startup
- Reload after every transaction (persistTransaction → reloadPersistedInventory)
- Pass persistedInventory to useAvailabilityBroadcast instead of context
- Also detect cash_level changes in the debounce (not just boolean flips)
- Remove unused inventory computed (UI reads context.inventory directly)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
VITE_ env vars are baked in at build time and empty in the Nix build.
Now reads lightningPubPubkey and relayUrl from Electron's getConfig()
at runtime, with dev fallback to import.meta.env.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The dedicated "Using ShockWallet" QR now encodes the raw nprofile
value (not a URL) so ShockWallet's QR scanner can recognize it
directly. The table row still uses the deep link URL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ShockWallet entry in the wallets table now resolves to the deep
link URL with nprofile param, so scanning its QR icon also connects
to the ATM's Lightning.Pub. Falls back to plain URL if unconfigured.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
QR now encodes wallet.aiolabs.dev/sources/add?nprofile=... so scanning
opens ShockWallet with the ATM's Lightning.Pub pre-filled, handling
both new and existing users.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ShockWallet users can scan the nprofile to connect to the ATM's
Lightning.Pub instance. QR is built from VITE_LIGHTNING_PUB_PUBKEY
and VITE_RELAY_URL env vars with a graceful fallback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The HAL inventory was built from the config preset, ignoring operator
changes made via atm-tui or SQL. Now reads cassettes from the DB at
HAL init time so denomination/count changes take effect on restart.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add position column to cassettes table (migration v5→v6) so cassettes
are ordered by physical cartridge number instead of denomination.
Update BATM3 preset to $20/$1 denominations with 400-bill capacity.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
insertingBills and selectingAmount now auto-idle after 3 minutes of
inactivity. displayingInvoice returns to amount selection after 5
minutes if the customer never pays.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add circular outline to the ? help button on idle screen and scale
support page navigation buttons for touchscreen kiosk use.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Without --refresh, nix caches the flake evaluation and
nixos-upgrade may not pull the latest commits. The --refresh
flag forces re-fetching the git repo on every upgrade.
Only affects flake evaluation cache — /var/lib data is untouched.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In maintenance mode, establish a minimal Nostr connection (no
Lightning.Pub) and publish Kind 30078 heartbeat with
maintenance: true. Monitors show yellow dot + "under service"
instead of appearing offline.
Only the Nostr client is initialized — no payment infrastructure.
Same one-shot private key security model as normal operation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The LNURL-withdraw session had a fixed 5-minute expiry timer that
raced with the state machine's displayingQR timeout (also 5 min).
If the session timer fired first, the withdraw link was deleted
while the customer could still retry from confirmAbandon.
Now LNURL sessions are cleaned up by the state machine on idle
transition instead of a fixed timer. A 15-minute safety timeout
remains as a fallback in case the state machine doesn't clean up.
Flow: displayingQR (5min) → confirmAbandon (60s) → idle → cleanup.
The withdraw link stays alive the entire time the customer can
interact with it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Publish cash level (none/low/good/full) in the Kind 30078 event
based on total bill count across all cassettes. Enables monitoring
dashboards to show cash availability without revealing exact amounts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Kind 30078 availability event was using 'atm' as the model
placeholder. Now reads the actual model from runtime config
(batm3, douro, sintra, etc.) so monitoring dashboards can
distinguish between different ATM types.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace cursor: default (which showed pointer on buttons) with
cursor: none !important on all elements. Touchscreen ATMs don't
need a visible cursor — taps register via touch coordinates.
Reverts the earlier cursor: default addition and fixes the
pre-existing issue of pointer cursor showing over buttons.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Nix's multi-line strings consume bare $kernel. Use ''$ to produce
a literal $ so udev can expand its built-in kernel variable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire up the availability broadcast composable to publish the ATM's
status as a replaceable Kind 30078 Nostr event. Publishes on
availability change (debounced) and as a 5-minute heartbeat so
monitors can detect offline machines.
Also adds WiFi auto-connect for BATM3: reads SSID/PSK from
/var/lib/lamassu-atm/wifi.conf at boot. Credentials stay local.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>