- TypeScript 56.4%
- Vue 11.2%
- JavaScript 9.4%
- Shell 9%
- Nix 8.1%
- Other 5.9%
Every machine inherited America/Guatemala from the shared base config, which is right for douro and tejo and wrong for sintra in France. It read six hours behind, so its journal timestamps had to be converted by hand against anything on the relay. The clock is not cosmetic here. system.autoUpgrade's `dates = "04:00"` is local time, so the zone decides when the nightly rebuild restarts the app and the Fujitsu dispenser runs its audible init routine. On sintra that was firing at 10:17 in the morning, in the room, rather than at 4am. timeZoneForModel mirrors fiatCodeForModel and lists only the exceptions. The base config keeps America/Guatemala as the fleet default, now under mkDefault so a per-model value wins without mkForce. Verified by eval: sintra resolves to Europe/Paris, douro, tejo and batm3 are unchanged. batm3 is deliberately left alone. It is USD and in the field, and I do not know where. |
||
|---|---|---|
| .claude/skills | ||
| apps/machine | ||
| deploy | ||
| docker | ||
| docs | ||
| nix | ||
| packages | ||
| scripts | ||
| .devenv.flake.nix | ||
| .gitignore | ||
| .prettierrc | ||
| CLAUDE.md | ||
| devenv.lock | ||
| devenv.nix | ||
| devenv.yaml | ||
| flake.lock | ||
| flake.nix | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| tsconfig.json | ||
| turbo.json | ||
bitSpire
A Nostr-native Lightning ATM. KYC-free, open source, auditable. Talks to its Lightning backend over the nostr-native-transport (kind-21000 NIP-44 v2) on a relay — never HTTP — so the kiosk has no admin tokens to leak and no API surface to attack.
Originally
lamassu-next. Renamed during the LNbits-backend transition on thedevbranch (commits leading up to 2026-05-13). Production ATMs (batm3,douro) still run frommainagainst Lightning.Pub until cutover; this README describes thedevbranch state.
What the ATM actually does
| Flow | Customer side | ATM side |
|---|---|---|
| Cash-out (customer pays ATM, gets cash) | scans BOLT11 invoice, pays from any LN wallet | lnbits.createInvoice() over nostr → subscribe_payments({payment_hash}) push fires on settlement → dispense |
| Cash-in (customer hands ATM cash, gets sats) | scans LNURL-withdraw QR, redeems with any LN wallet that supports LNURL-w | lnbits.createWithdrawLink({uses:1}) over nostr → subscribe_payments({tag:"withdraw", link_id}) push fires when LNbits settles → mark complete |
No HTTP to the Lightning backend. No admin tokens on the kiosk. The ATM's nostr private key is its credential — LNbits auto-creates the wallet on first contact via the signature (see aiolabs/lnbits#9).
Prerequisites
- Nix with flakes enabled
- devenv
- Docker + Docker Compose
- A running LNbits instance with the
nostr-native-transportbranch built in. The local dev compose lives at~/dev/local/docker/regtest— that ships an LNbits with the transport pre-enabled and the relevant extensions (withdraw,lnurlp,nostrrelay) installed.
The nostrrelay extension inside LNbits is what the ATM connects to — there is no separate strfry/khatru container in the dev compose. The relay URL is ws://<host>:5001/nostrrelay/test.
Quick Start
# 1. Clone
git clone ssh://forgejo@git.atitlan.io/aiolabs/lamassu-next.git
cd lamassu-next # repo name kept for now — rename to bitSpire is a follow-up
git checkout dev
# 2. Enter the dev environment
devenv shell
# 3. Install JS deps
pnpm install
# 4. Start the regtest stack (bitcoind, LNDs, LNbits with nostr-transport, relay)
cd ~/dev/local/docker/regtest && ./start-regtest
docker logs regtest-lnbits-1 | grep 'Public key (share this)'
# → copy that pubkey, you'll need it as VITE_LNBITS_SERVER_PUBKEY
# 5. Configure the machine app for the dev LNbits
cat > apps/machine/.env <<EOF
VITE_RELAY_URL=ws://localhost:5001/nostrrelay/test
VITE_LNBITS_SERVER_PUBKEY=<paste pubkey from step 4>
VITE_ATM_PRIVATE_KEY=$(openssl rand -hex 32)
EOF
# 6. Run the kiosk in browser dev mode
cd apps/machine && pnpm dev
The kiosk should come up at http://localhost:5173, log [Lightning] LNbits client initialized, and report a wallet id once LNbits auto-creates one for the ATM's pubkey.
Architecture
bitSpire/
├── apps/
│ └── machine/ # Electron + Vue 3 ATM kiosk
├── packages/
│ ├── nostr-client/ # NIP-01 relay client, NIP-44 v2 encryption
│ ├── lnbits/ # LnbitsClient — talks to LNbits over kind-21000 transport
│ ├── clink/ # CLINK protocol (kind-21001/2/3) — still in tree as a
│ │ # reference; unused on dev since the LNbits backend
│ │ # does cash-in via LNURL-withdraw and cash-out via
│ │ # BOLT11, both of which subsume CLINK's role
│ ├── hal/ # Hardware abstraction (JCM iVIZION validator, F56 dispenser)
│ ├── state-machine/ # XState v5 state machine driving cash-out + cash-in
│ ├── cashu/ # Cashu ecash (placeholder)
│ └── ui-shared/ # Shared Vue components (placeholder)
└── deploy/nixos/ # NixOS module + provisioning script for Sintra/tejo/douro/batm3
packages/lightning/ (the Lightning.Pub RPC client) was removed on dev — see git log packages/lightning on main for the historical sources.
Deploying to real hardware
The Sintra/tejo/douro/batm3 disk-image pipeline lives in flake.nix + deploy/nixos/. See deploy/nixos/README.md for the full flow; the abbreviated path:
# Build the disk image for a Sintra
nix build .#disk-image-sintra
# → result/nixos.img
# Flash to a USB stick
sudo dd if=result/nixos.img of=/dev/sdX bs=4M status=progress conv=fsync && sync
# Boot Sintra from the USB, dd onto the eMMC from inside Alpine live (see
# deploy/nixos/README.md), then provision the .env from the dev box:
bash deploy/nixos/provision-atm.sh <sintra-lan-ip>
The auto-upgrade timer (flake.nix:152-160) pulls dev daily at 04:00, so the Sintra stays in sync with whatever's on the dev branch. Production ATMs run from main and are unaffected.
Documentation
| Document | Description |
|---|---|
| docs/machine-installation.md | Step-by-step Sintra install: build, flash, provision |
| deploy/nixos/README.md | NixOS module options, runtime config layout, hardware variants |
| docs/architecture-comparison.md | Nostr-native ATM vs traditional lamassu-server |
| docs/device-configuration.md | Validator/dispenser hardware configuration |
| docs/business-model.md | Deployment economics |
| docs/adr/001-hal-architecture.md | HAL design decision record |
| docs/clink-protocol.md | CLINK protocol reference — historical, no longer wired on dev |
| docs/ndebit-cash-in-flow.md | Pre-LNbits cash-in flow — historical, replaced by LNURL-withdraw + subscribe_payments push |
| CLAUDE.md | Development guidelines and code style (read this if using Claude Code) |
Contributing
See CLAUDE.md for development guidelines, package layout conventions, and code style.
Acknowledgements
bitSpire's hardware drivers (JCM iVIZION / ID003, MEI EBDS, Fujitsu F56, etc.) and the cash-flow state machine derive from prior art first published as open source by Lamassu Industries AG in the lamassu-machine and lamassu-server repositories, up to and including the v8.1.5 release line — the last published under a fully-open license. bitSpire wouldn't exist without that foundation, and we're grateful for the years of operational hardening that went into it.
Lamassu Industries AG transitioned to a proprietary, source-available license on 2024-01-26, with v8.1.6 and subsequent releases gated behind a paid Operator Support Agreement. bitSpire incorporates no code from v8.1.6 or later, is an independent project, and is not affiliated with or endorsed by Lamassu Industries AG.
License
AGPL-3.0 (matches LNbits, which we link against).