Closes gap 2 from coord log 2026-06-01T18:30Z. The LNbits withdraw extension's nostr-transport RPC now populates `link.lnurl` from `settings.lnbits_baseurl` (aiolabs/withdraw#1 / commit e9d911e), so the ATM no longer needs a separate HTTP URL on the wire to compose the LNURL-withdraw callback itself. What goes: - `VITE_LNBITS_HTTP_URL` env var (renderer + Electron main) - `lnbitsHttpUrl` field on `LightningConfig`, `RuntimeConfig`, and the Window mirror in `src/types/electron.d.ts` - The manual `${lnbitsHttpUrl}/withdraw/api/v1/lnurl/${unique_hash}` composition in `generateLnurlWithdraw` - The `encodeLnurl` bech32 helper in `lightning.ts` (LNbits returns bech32-encoded; we just `.toUpperCase()` to match BOLT/LNURL convention) - `@scure/base` dep from `apps/machine/package.json` (only used by the removed helper; clink still uses it directly) - The `lnbitsHttpUrl` option + `LNBITS_HTTP_URL=…` env var + boot echo in `deploy/nixos/bitspire-atm.nix` - Doc references in CLAUDE.md, README.md, deploy/nixos/README.md, docs/architecture-comparison.md, and the lightning-check skill What stays: - `link.lnurl` consumption, with an explicit error if LNbits returns null (which signals `LNBITS_BASEURL` is unset on the server side — better to fail clearly than silently) - The receiver-side bech32 uppercasing (LNbits returns lowercase per the standard library) Why this is a net win: - Removes a config-drift surface — if LNbits's external URL moved (DNS, port, reverse-proxy rewrite), every ATM in the field would stop issuing redeemable LNURL-withdraw QRs until reconfigured. Now LNbits derives its own URL from `settings.lnbits_baseurl`, one source of truth. - Removes an extra provisioning step. No more `LNBITS_HTTP_URL=…` before running `provision-atm.sh`; the relay + server pubkey suffice. - Removes the misleading boot echo that triggered the §`18:30Z` smoke triage confusion ("LNbits HTTP: <url>" read like ATM-→-LNbits connectivity, when it was only ever a URL embedded in customer QRs). Also adds a `# pragma: allowlist secret` marker above the `VITE_ATM_PRIVATE_KEY` doc block in `.env.example` so the global secret scanner stops false-positiving on the documentation prose. Workspace typecheck + 24/24 apps/machine tests still green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
7.1 KiB
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).