No description
  • TypeScript 56.4%
  • Vue 11.2%
  • JavaScript 9.4%
  • Shell 9%
  • Nix 8.1%
  • Other 5.9%
Find a file
Padreug 76f3c2ff9d fix(hal): enable Apex escrow, use the real return bit, checksum the whole frame
Three protocol corrections from Pyramid's spec (RS_232 Rev G), all of which
this driver had guessed at because it was written clean-room without it.

**Escrow was never enabled.** BYTE 1 bit 4 is an enable, and the driver left
it clear on every poll. With it clear the acceptor does not stop at escrow,
so the host is never offered the stack-or-return decision and notes are
banked before anything has validated them. The entire FSM here is built
around that decision point, so this was not a missing nicety — the driver's
central flow could not have happened. It is now asserted on every poll.

**Return used the wrong mechanism.** The driver expressed "give the note
back" by zeroing the enable mask mid-escrow, under an in-code assumption
that "Apex has no distinct return opcode". It has one: BYTE 1 bit 6. The old
approach was flagged in a comment as needing hardware verification; the spec
settles it instead. Note the spec also distinguishes Returning (host refused
a valid note) from Rejected (acceptor judged it invalid), which is the
distinction this bit exists to express.

**The checksum range was hardcoded to the host frame.** computeChecksum
always XORed bytes 1..5, which is right for the 8-byte poll and wrong for
the 11-byte reply, where it should span 1..8. So every reply failed
validation. That was masked by the check being non-fatal "pending hardware
verification", which logged a warning and parsed anyway.

The range is now derived from the frame length, and verified against the two
reset frames the spec spells out literally with their checksums — the only
ground truth available without hardware. Those same frames are now a test.

With the range confirmed, a mismatch becomes a hard drop rather than a
warning. A corrupt frame carries a denomination field, and crediting a note
from a frame known to be damaged is the one outcome worth refusing. The raw
bytes are logged so a systematic framing error stays diagnosable.

Also records two operational facts from the spec that were not written down:
the interface is Mars/MEI GL5-compatible (hence the resemblance to the EBDS
driver), and polls must not fall more than 5s apart or the acceptor may dump
an escrowed note and stop accepting until the host resumes. Our 100ms cadence
is comfortably inside that.
2026-09-30 21:59:56 +02:00
.claude/skills refactor(machine): drop VITE_LNBITS_HTTP_URL — lnurl now arrives populated from LNbits (#57 gap 2) 2026-06-01 20:33:28 +02:00
apps/machine fix(machine): make the dispenser optional, as the validator already was 2026-09-29 18:16:41 +02:00
deploy fix(rpi4): enable pcscd — without it the kiosk hangs before drawing 2026-09-25 22:09:36 +02:00
docker feat(docker): streamline regtest dev environment 2026-03-10 01:31:51 -04:00
docs feat(deploy): add aarch64 Raspberry Pi 4 target 2026-09-24 21:44:25 +02:00
nix fix(nix): keep only the serialport prebuild this system can load 2026-09-25 12:30:25 +02:00
packages fix(hal): enable Apex escrow, use the real return bit, checksum the whole frame 2026-09-30 21:59:56 +02:00
scripts test: add LNURL-withdraw Nostr RPC test scripts 2026-03-07 13:02:48 -05:00
.devenv.flake.nix feat(docker): add dev.sh with auto-funding and ATM app setup 2026-02-15 14:19:16 -05:00
.gitignore chore: gitignore nix results, sqlite DBs, compiled JS 2026-03-02 13:49:50 -05:00
.prettierrc feat(docker): add dev.sh with auto-funding and ATM app setup 2026-02-15 14:19:16 -05:00
CLAUDE.md docs: correct the lamassu-machine licensing boundary to commit c0b69d1 2026-07-04 01:00:12 +02:00
devenv.lock feat(docker): add dev.sh with auto-funding and ATM app setup 2026-02-15 14:19:16 -05:00
devenv.nix feat(docker): display Zeus lndconnect as QR code 2026-02-15 14:38:40 -05:00
devenv.yaml feat(docker): add dev.sh with auto-funding and ATM app setup 2026-02-15 14:19:16 -05:00
flake.lock feat(deploy): add aarch64 Raspberry Pi 5 target (sd-image) 2026-08-16 21:30:53 +02:00
flake.nix feat(deploy): add aarch64 Raspberry Pi 4 target 2026-09-24 21:44:25 +02:00
package.json refactor(rename): root + flake output names → bitSpire/bitspire 2026-06-01 19:08:03 +02:00
pnpm-lock.yaml feat(machine): NFC Bolt Card reader driver + IPC (main process) 2026-08-05 04:42:07 +02:00
pnpm-workspace.yaml feat(docker): add dev.sh with auto-funding and ATM app setup 2026-02-15 14:19:16 -05:00
README.md refactor(machine): drop VITE_LNBITS_HTTP_URL — lnurl now arrives populated from LNbits (#57 gap 2) 2026-06-01 20:33:28 +02:00
tsconfig.json refactor: drop Lightning.Pub backend; LNbits-only path (3d) 2026-06-01 19:08:03 +02:00
turbo.json feat(docker): add dev.sh with auto-funding and ATM app setup 2026-02-15 14:19:16 -05:00

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 the dev branch (commits leading up to 2026-05-13). Production ATMs (batm3, douro) still run from main against Lightning.Pub until cutover; this README describes the dev branch 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-transport branch 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).