The fleet table called sintra a working dev unit; its nixos-upgrade had failed every night 2026-10-06 → 10-09 on the same 60 s atm-app timeout as batm3, so nothing merged that week reached it. Recorded with the interim rule — push-cache after every dev push, because mkAtmApp's src = self makes any commit invalidate the cached toplevel — and the two traps found while applying it: a lockfile change silently reuses a stale pnpmDeps store unless the hash is re-derived, and activation scripts don't have grep/sed on PATH.
323 lines
20 KiB
Markdown
323 lines
20 KiB
Markdown
# CLAUDE.md
|
|
|
|
Guidance for Claude Code when working in this repo. Read this before touching code.
|
|
|
|
## Project Overview
|
|
|
|
**bitSpire** is a Nostr-native Lightning ATM running **LNbits over the nostr-native-transport**. The `dev` branch, which this file describes, is what the machines run.
|
|
|
|
Core principles:
|
|
|
|
- **KYC-free** — no identity collection, no compliance theater
|
|
- **No HTTP to the Lightning backend** — every ATM↔LNbits RPC goes over kind-21000 NIP-44 v2 events on a relay
|
|
- **The ATM's nostr private key IS the credential** — LNbits auto-creates the wallet on first contact (issue aiolabs/lnbits#9 alignment, `prvkey=NULL`)
|
|
- **Open source first** — every component auditable and forkable
|
|
|
|
## Provenance + legal status
|
|
|
|
The HAL drivers (validators / dispensers / printers) and the cash-flow state machine derive from Lamassu Industries AG's `lamassu-machine` repository, **only up to commit `c0b69d1ed196d396c5f057478c2ea290babd58ab`** ("chore: v8.6.0-beta.9", 2023-09-19) — the last commit published into the public domain (`UNLICENSE` in tree). The very next commit, `a9234d124d` ("chore: add LICENSE (#1019)", 2023-09-19), removed `UNLICENSE` and added Lamassu's proprietary "Appendix A SLA". **The `v8.1.5` tag (2023-09-21) already ships the Appendix A license** — the previously documented "8.1.5 is the open boundary" was wrong (verified against GitHub history 2026-07-04). Note the public-domain boundary sits on the 8.6-beta line, which is *further along* than 8.1.5 feature-wise.
|
|
|
|
**Reference permission (2026-10-09).** The maintainer taking over the Lamassu
|
|
codebase has given us permission to use lamassu-machine and lamassu-server — including
|
|
post-boundary code — as prior art in any way that improves this codebase (relayed by
|
|
padreug, 2026-10-09; the earlier hard rule — reference only `c0b69d1` or earlier — is
|
|
superseded). Prefer to *reference* over
|
|
*port*: read it, take the behaviour model, reimplement in our idiom. When a block is
|
|
ported verbatim, say so in the commit, with the source commit, so the provenance is in
|
|
`git log`. The pre-boundary tree needs no permission at all and is the first place to look.
|
|
|
|
**Boundaries, for the record.** lamassu-machine's last public-domain commit is `c0b69d1`
|
|
(2023-09-19, v8.6.0-beta.9); `a9234d124d` added Appendix A the same day, so every 8.1.5+
|
|
tag is proprietary. lamassu-server's last public-domain commit is `adbc9709` (2023-09-19,
|
|
v8.6.0-beta.9), licence added in `d06a8f54` the same day. Both GitHub repos are gone
|
|
(404); full history survives in `~/dev/repos/` and at Software Heritage (crawls 2026-03-02
|
|
and 2026-07-19, tips identical to the local mirrors). **`~/lamassu/lamassu-server` is a
|
|
squashed v12 repo** whose first commit (`e2c49ea`, 2025-12-31) already carries Appendix A —
|
|
it has no open era to reference; use `~/dev/repos/` for that. `~/lamassu/` also holds the
|
|
33 surviving github.com/lamassu dependency repos (cloned 2026-09-27) and
|
|
`bnr-xfs-salvage/` (the MIT bnr-xfs / bnr crates, checksums verified). The curated
|
|
*Lamassu Port Backlog* (claude.ai artifact `61af38f6`, 2026-09-27) ranks what is worth
|
|
taking and tags each item's provenance.
|
|
|
|
bitSpire is an independent project under AGPL-3.0 and is not affiliated with Lamassu Industries AG.
|
|
|
|
## Branch model
|
|
|
|
- `dev` — **what every live machine runs.** Not a staging branch any more. Verified
|
|
2026-09-24 on batm3, whose `nixos-upgrade` unit pulls
|
|
`git+ssh://…/bitspire.git?ref=dev#batm3-installed` daily at 04:00. "Push freely
|
|
to dev" is no longer safe advice: a bad commit reaches production hardware the
|
|
next morning, unattended.
|
|
- `main` — Lightning.Pub era, historical. Tag `pre-bitspire-cutover` is the
|
|
rollback target if the migration ever has to be reverted.
|
|
|
|
> This section previously said the production ATMs ran `main` against
|
|
> Lightning.Pub and that only Sintra was on `dev`. That was stale and it was
|
|
> repeatedly taken at face value. Check the machine, not this file, before
|
|
> relying on which stack a given box runs: `systemctl cat nixos-upgrade` gives
|
|
> the branch, `/var/lib/bitspire` vs `/var/lib/lamassu-atm` gives the era.
|
|
|
|
### Fleet state (surveyed 2026-09-24)
|
|
|
|
| Machine | Reachable | Stack | GPU | Notes |
|
|
|---|---|---|---|---|
|
|
| `sintra` | LAN `192.168.0.252` | dev / LNbits | Braswell `8086:22b0` → crocus | dev unit; ethernet `r8169`. **Its nightly upgrade failed every night 2026-10-06 → 10-09** on the same 60 s timeout as batm3 (below); nothing merged that week reached it until a cachix push on 10-10 |
|
|
| `batm3` | wg `10.0.0.5` | dev / LNbits | Haswell GT2 `8086:0412` → crocus | **networks over WiFi**, `iwlwifi` 7260; ethernet down |
|
|
| `douro` | **down** | — | Bay Trail (Gen7) | needs reflashing with the current image and reconnecting to WireGuard |
|
|
| `tejo` | wg `10.0.0.3` | **Debian** (`ubilinux4`, kernel 4.9) | Braswell `8086:22b0` | never had bitspire installed; a flake target, not a deployment |
|
|
|
|
Two consequences worth holding onto. Every GPU in the fleet binds **crocus**, not
|
|
iris — sintra's Braswell does so despite being Gen8. And batm3's only working
|
|
network path is Intel WiFi, so `intel/iwlwifi` firmware is load-bearing there;
|
|
trimming it would strand the machine with no way back in.
|
|
|
|
### The nightly upgrade fails on any machine that has to build the app (batm3, and sintra too)
|
|
|
|
Confirmed on batm3 2026-09-24 and on sintra 2026-10-10 (failing since 10-06). The run dies at:
|
|
|
|
```
|
|
04:03:26 building '…-bitspire-atm-app-0.1.0.drv'...
|
|
04:04:28 error: timed out after 60 seconds
|
|
```
|
|
|
|
The ATM app is built in-house and is **not in `aiolabs.cachix.org` or
|
|
`cache.nixos.org`**, so batm3 has to build it locally, and `nix.settings.timeout
|
|
= 60` in `flake.nix` kills it. The comment there assumes heavy derivations are
|
|
"effectively cache-only … upstream-cached", which is true of nixpkgs and false of
|
|
our own app.
|
|
|
|
So the machine is pinned to whatever generation last succeeded, and nothing
|
|
merged to `dev` reaches it. This is the same class of silent-updater failure as
|
|
#98, in a new form. The fix is pushing `atm-app-*` to the aiolabs cachix as part
|
|
of releasing, not raising the timeout — a 60s ceiling on ATM hardware is correct.
|
|
|
|
**Interim rule (2026-10-10): every push to `dev` is followed by
|
|
`./deploy/push-cache.sh sintra && ./deploy/push-cache.sh batm3` from bohm**
|
|
(cachix is authenticated there). `mkAtmApp` takes `src = self` — the whole flake
|
|
tree — so *any* commit, docs included, changes the app derivation and a cached
|
|
toplevel no longer matches what `?ref=dev` resolves to. Three more things that
|
|
bit on 10-10:
|
|
|
|
- **`pnpm-lock.yaml` changes require re-deriving `pnpmDeps.hash` in
|
|
`nix/mkAtmApp.nix`** (blank it, build, paste the `got:` value). Nix reuses the
|
|
stale fixed-output store otherwise and the sandboxed `pnpm install --offline`
|
|
fails with `ERR_PNPM_NO_OFFLINE_TARBALL`. A passing local `pnpm build` says
|
|
nothing about the nix build.
|
|
- **Activation scripts run with a minimal PATH** — coreutils yes, `grep`/`sed`
|
|
no. Reference `${pkgs.gnugrep}/bin/grep` / `${pkgs.gnused}/bin/sed` by store
|
|
path; the `.env` migration printed success and then died 127.
|
|
- `nixos-rebuild switch --flake .#<model>-installed --target-host <model> --sudo
|
|
--use-substitutes` from bohm is the fast manual path once the cache has the
|
|
toplevel: store hit here, closure copied, nothing built on the UP board.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
bitSpire/
|
|
├── apps/
|
|
│ └── machine/ # Electron + Vue 3 ATM kiosk
|
|
├── packages/
|
|
│ ├── nostr-client/ # NIP-01 client, NIP-44 v2 (encryptContentV2/decryptContentV2)
|
|
│ ├── lnbits/ # LnbitsClient — kind-21000 RPC over nostr-transport
|
|
│ ├── clink/ # CLINK protocol (kinds 21001-21003) — IN TREE BUT UNUSED ON DEV
|
|
│ ├── state-machine/ # XState v5 ATM state machine
|
|
│ ├── hal/ # Hardware abstraction (JCM iVIZION, Fujitsu F56, etc.)
|
|
│ ├── cashu/ # placeholder
|
|
│ └── ui-shared/ # placeholder
|
|
└── deploy/nixos/ # NixOS module + provisioning for Sintra/tejo/douro/batm3
|
|
```
|
|
|
|
`packages/lightning/` (the Lightning.Pub RPC client) was deleted on `dev` in commit `a51d7af`. If a piece of code on `dev` needs LP semantics, look at the equivalent in `packages/lnbits/` or in `apps/machine/src/services/lightning.ts` (which has a `LightningBackend` interface implemented by an LNbits-backed adapter).
|
|
|
|
## Package status
|
|
|
|
| Package | Scope | Notes |
|
|
|---|---|---|
|
|
| `@bitSpire/nostr-client` | active | NIP-01 + NIP-44 v2 helpers. Used by every other package. |
|
|
| `@bitSpire/lnbits` | active | Wraps the nostr-native-transport API (10 core RPCs + lnurlw/lnurlp + subscribe_payments). Mirror peer of the old `@bitSpire/lightning`. |
|
|
| `@bitSpire/state-machine` | active | XState v5. Tests pass. `generateNdebit` / `generateClinkOffer` / `generateNoffer` services are stubs returning placeholder strings (state machine contract preserved; outputs unused by views). |
|
|
| `@bitSpire/hal` | active | TypeScript HAL. JCM iVIZION + Fujitsu F56 drivers. |
|
|
| `@bitSpire/clink` | dormant | Code kept for future re-wire if direct nostr-to-nostr cash-in returns. **Don't import on dev — `services/lightning.ts` no longer wires it.** |
|
|
| `@bitSpire/cashu` | placeholder | empty |
|
|
| `@bitSpire/ui-shared` | placeholder | empty |
|
|
|
|
## Backend wiring
|
|
|
|
`apps/machine/src/services/lightning.ts` is the central file. After the 3d cutover it:
|
|
|
|
1. Loads config from Electron IPC (`get-config` + one-shot `get-atm-secrets`) or from `import.meta.env` (browser dev)
|
|
2. Creates an `LnbitsClient`, calls `list_wallets`, picks the first wallet — fails-fast if either step doesn't return data
|
|
3. Exposes a tiny `LightningBackend` adapter on `services.lightningPub` that satisfies `apps/machine/src/stores/atm.ts`'s expectations:
|
|
- `getBalance()` → wraps `lnbits.getBalance(walletId)`
|
|
- `watchBalance(cb)` → wraps `lnbits.watchWallet` and refetches balance per push
|
|
- `createInvoice({amountSats, description})` → wraps `lnbits.createInvoice(walletId, {amount, memo, unit:'sat'})`
|
|
- `payInvoice(bolt11, amountSats)` → wraps `lnbits.payInvoice(walletId, {bolt11, max_sat})`
|
|
4. Implements the four flow-critical `ATMServices` methods on top of LNbits:
|
|
- `generateInvoice(msat)` → cash-out BOLT11
|
|
- `watchInvoice(bolt11, cb)` → `subscribe_payments({payment_hash, max_seconds:600})` push
|
|
- `generateLnurlWithdraw(ctx)` → `lnurlw_create_link({uses:1, ...})`, use `link.lnurl` directly (LNbits populates it from `settings.lnbits_baseurl` per aiolabs/withdraw#1 / `e9d911e`), subscribe for `tag:"withdraw", link_id` settlement push
|
|
- `getAvailableBalance()` → wraps `lnbits.getBalance(walletId).balanceSats`
|
|
|
|
`CashInView.vue` calls `atmStore.generateLnurlWithdraw()` directly when entering `displayingQR`; the state machine still invokes `generateNdebit` as an actor but its output is discarded (kept only to avoid an invasive state-machine rewrite).
|
|
|
|
## Environment variables
|
|
|
|
Renderer reads (Electron IPC or Vite `import.meta.env`):
|
|
|
|
| Var | Required | Notes |
|
|
|---|---|---|
|
|
| `VITE_RELAY_URL` | no (seed-provided) | Relay both ATM and LNbits subscribe to. **Comes from the pairing seed** (aiolabs/bitspire#70); set this only as an override — it WINS over the seed via env-first precedence. Dev override: `ws://localhost:5001/nostrrelay/test` (LNbits's bundled `nostrrelay` extension — no separate strfry container) |
|
|
| `VITE_LNBITS_SERVER_PUBKEY` | no (seed-provided) | 64-char hex transport pubkey. **Comes from the seed's `lnbits_npub`** (#70); env override only. LNbits prints it on startup (`docker logs lnbits \| grep 'Public key (share this)'`) |
|
|
| `VITE_SPIRE_SEED` | yes (prod) | Spire pairing seed (`spire-seed:v1:<base64url>`) from spirekeeper. Carries the relay(s), the LNbits transport pubkey (`lnbits_npub`), the spire signing pubkey (`spire_npub`), and a one-shot NIP-46 connect token (#70 slimmed the shape). First boot redeems it and persists the binding to `state.db`; later boots resume by fingerprint. A changed seed re-pairs. Provisioning it up front is optional — an unpaired machine renders an on-screen QR-pairing wizard that scans the seed off the camera (see below). See aiolabs/bitspire#52. |
|
|
| `VITE_ATM_PRIVATE_KEY` | dev only | 64-char hex raw nsec fallback for running without a bunker. Ignored when `VITE_SPIRE_SEED` or a stored binding exists. |
|
|
| `VITE_OPERATOR_PUBKEYS` | optional | Comma-separated hex pubkeys allowed to send kind-21003 management commands |
|
|
|
|
The LP-era vars (`VITE_LIGHTNING_PUB_PUBKEY`, `VITE_LIGHTNING_PUB_API_URL`, `VITE_EXTENSION_API_URL`, `VITE_ADMIN_TOKEN`) are gone from the dev branch's `.env.example` and `LightningConfig` interface.
|
|
|
|
## Pairing (on-machine QR wizard)
|
|
|
|
A machine with no seed **and** no stored binding boots `unpaired` and, under
|
|
Electron, renders an interactive wizard (`src/components/PairingWizard.vue`)
|
|
instead of a dead-end fault screen. The operator displays the `spire-seed`
|
|
QR (minted by spirekeeper's `/pair`) to the machine's camera; the wizard:
|
|
|
|
1. captures + decodes via a `PairingSource` (`src/services/pairing/`) — camera
|
|
today (decode through `qr`, paulmillr's zero-dep lib), NFC scaffolded;
|
|
2. validates the scan parses as a spire-seed (`ingestScannedSeed`), rejecting
|
|
a stray QR;
|
|
3. persists it as `VITE_SPIRE_SEED` via the `state:save-spire-seed` IPC and
|
|
relaunches (`app:relaunch`).
|
|
|
|
Pairing itself is **not** done in the wizard — relaunch lets the normal boot
|
|
path (`signer-resolver` → `connectNewSeed`) redeem the one-shot token, so
|
|
there's one tested pairing path. A revoked/expired binding lands on the same
|
|
wizard (re-pair = scan a fresh seed). Provisioning `VITE_SPIRE_SEED` up front
|
|
still works and skips the wizard.
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
# Inside devenv shell
|
|
pnpm install # install all workspace deps
|
|
pnpm dev # start the machine app (Vite + Electron with hot reload)
|
|
pnpm typecheck # vue-tsc --noEmit
|
|
pnpm test # vitest across all packages
|
|
|
|
# In apps/machine specifically
|
|
cd apps/machine && pnpm build # full prod build: vue-tsc + vite build + electron tsc + fund-atm esbuild bundle
|
|
```
|
|
|
|
Disk image build (for Sintra/douro/etc.):
|
|
|
|
```bash
|
|
nix build .#disk-image-sintra # → result/nixos.img
|
|
```
|
|
|
|
## Nostr event kinds
|
|
|
|
| Kind | Role | Status on dev |
|
|
|---|---|---|
|
|
| 21000 | LNbits nostr-native-transport (request + ack + push) | **active** — all backend traffic |
|
|
| 21001 | CLINK Offer (invoice request/response) | dormant |
|
|
| 21002 | CLINK Debit (payment authorization) | dormant |
|
|
| 21003 | CLINK Manage (operator commands) | partially wired — `clink.onManagement` still listens for operator dispense, decoupled from LP |
|
|
| 30078 | Service Beacon (replaceable, ATM availability) | active — `startAvailabilityBroadcast()` heartbeats every 5 min |
|
|
| 30079 | Transaction Record (replaceable) | planned |
|
|
|
|
## Wire envelope (kind 21000 RPC)
|
|
|
|
Plaintext JSON, encrypted as NIP-44 v2 inside the event `content`:
|
|
|
|
```jsonc
|
|
// Request (client → server)
|
|
{
|
|
"rpc_name": "create_invoice",
|
|
"request_id": "create_invoice-7-ab12cd",
|
|
"wallet_id": "<uuid>",
|
|
"body": { "amount": 1000, "memo": "...", "unit": "sat" }
|
|
}
|
|
|
|
// Reply (server → client, one-shot)
|
|
{
|
|
"status": "OK",
|
|
"request_id": "create_invoice-7-ab12cd",
|
|
"data": { /* Payment object */ }
|
|
}
|
|
|
|
// Subscription push (server → client, repeated)
|
|
{
|
|
"status": "OK",
|
|
"request_id": "sub-9-ef34gh",
|
|
"subscription_id": "<server-issued>",
|
|
"data": { "payment": { /* Payment */ } }
|
|
}
|
|
|
|
// Subscription close (server → client, terminal)
|
|
{
|
|
"status": "OK",
|
|
"request_id": "sub-9-ef34gh",
|
|
"subscription_id": "<server-issued>",
|
|
"data": { "closed": true, "reason": "ttl" | "unsubscribed" }
|
|
}
|
|
```
|
|
|
|
See `packages/lnbits/src/types.ts` for the TypeScript surface and `~/dev/lnbits/nostr-transport/docs/devs/nostr-transport.md` for the authoritative wire spec.
|
|
|
|
## Hardware drivers
|
|
|
|
TypeScript drivers in `packages/hal/`. Coverage by device class:
|
|
|
|
| Category | Drivers |
|
|
|---|---|
|
|
| Validators | id003, ebds |
|
|
| Dispensers | f56, puloon |
|
|
| Recyclers | none — MEI SCR hardware in BATM3; a clean-room BNR Advance route exists via the MIT `bnr-xfs` crate (see the port backlog) |
|
|
| Printers | none — `packages/hal/src/printers/` does not exist |
|
|
|
|
This table previously listed ccnet, cashflow_sc, bnr_advance, genmega, hcm2, gsr50 and
|
|
three printers that are not in the tree (corrected 2026-10-09). `packages/hal/src` also
|
|
carries orphaned `*.rs` files (`lib.rs`, `error.rs`, `mod.rs`, `traits.rs`, `mock.rs`) from
|
|
an abandoned Rust HAL; they are not built.
|
|
|
|
### Sintra hardware specifics (Aaeon UP Board)
|
|
|
|
- Validator (JCM iVIZION, ID003): FTDI USB-serial bridge → `/dev/ttyUSB1` → udev symlink `/dev/ttyJ5`
|
|
- Dispenser (Fujitsu F56): SoC MMIO UART (the only on-carrier RS-232) → `/dev/ttyS4` → udev symlink `/dev/ttyJ7`. **ttyS4 must NOT be the kernel console** — `upboard.nix` keeps `console=tty0` only
|
|
- The kernel-enumerated `ttyS1`/`ttyS2`/`ttyS3` are placeholder nodes that error on any I/O — the legacy 16550A is `ttyS0`, the SoC MMIO is `ttyS4`. Confirm with `dmesg \| grep ttyS`
|
|
|
|
### Initrd modules for eMMC boot
|
|
|
|
UP Board enumerates its eMMC controller via ACPI, not PCI. `upboard.nix` force-loads `sdhci-acpi` and `mmc_block` in `initrd.kernelModules` so root-by-label resolves in stage 1. If you ever rebuild the disk image for a different SoC, double-check this list.
|
|
|
|
## Code style
|
|
|
|
- TypeScript: ESM only, strict mode + `strictNullChecks` + `noUncheckedIndexedAccess`. No `any`. Zod for runtime validation at boundaries.
|
|
- Vue 3: Composition API, `<script setup lang="ts">`. Pinia for state.
|
|
- Formatting: Prettier (2 spaces, no semicolons, single quotes). Pre-commit hooks enforce.
|
|
- Tests: Vitest. Cash-out is the critical-path flow — keep `packages/state-machine` coverage tight.
|
|
|
|
## Security priorities
|
|
|
|
1. **Private keys** — Never log nsec. In production the ATM holds no signing nsec: `VITE_SPIRE_SEED` (in `/var/lib/bitspire/.env`, mode 0600) carries a one-shot connect token, and the ATM's own NIP-46 *transport* key (`client_secret_hex`) lives in `state.db` (`bunker_binding`). The operator's signing key stays in the bunker. The legacy `VITE_ATM_PRIVATE_KEY` is a dev-only fallback.
|
|
2. **Payments** — Validate the bolt11 amount on cash-out before exposing the QR. Decode `payment_hash` from the bolt11 (cheap, avoids a roundtrip) and use it as the `subscribe_payments` filter.
|
|
3. **Replay** — LNURL-withdraw links use `uses:1` and are deleted on session abort.
|
|
4. **Encryption** — All RPC content is NIP-44 v2. NIP-04 is forbidden.
|
|
|
|
## Useful invariants when debugging
|
|
|
|
- The renderer logs prefix every line with a tag: `[Lightning]`, `[ATM]`, `[ATM Service]`, `[LNURL Session]`, `[CLINK]`, `[StateStore]`. `journalctl -u bitspire | grep '\['` is your friend.
|
|
- **Never pass an object as a console argument in the renderer.** Electron's console bridge stringifies each argument, so `console.log('msg:', { a, b })` reaches the journal as `msg: [object Object]` and every field is lost. Interpolate instead. Cost a debugging session on 2026-09-23, when a cassette publish that had worked looked like it had done nothing.
|
|
- `bitspire.service` runs as the `bitspire` user (verified on sintra 2026-09-24; this line used to say `lamassu`, left over from the rename in `46e52f6`); `/var/lib/bitspire` is its `dataDir` (ReadWritePaths). DB lives at `/var/lib/bitspire/state.db` (we previously had `/var/lib/lamassu-atm` — that path is gone on dev, see commit `9c455d6`).
|
|
- The `lightning.lightningPub` field on `LightningServices` is a `LightningBackend` *adapter*, not a `LightningPubClient`. Don't try to call LP-only methods on it.
|
|
|
|
## Related documentation
|
|
|
|
- `docs/machine-installation.md` — Sintra deployment walkthrough (build, flash, provision)
|
|
- `deploy/nixos/README.md` — NixOS module options, runtime config layout
|
|
- `docs/architecture-comparison.md` — Nostr-native vs traditional lamassu-server
|
|
- `.claude/skills/*.md` — Custom skills (`/security`, `/nostr-check`, `/test`, etc.)
|
|
|
|
External:
|
|
- [LNbits nostr-transport docs](~/dev/lnbits/nostr-transport/docs/devs/nostr-transport.md) — wire spec
|
|
- [NIP-44 encryption](https://github.com/nostr-protocol/nips/blob/master/44.md)
|
|
- [CLINK Protocol Spec](https://github.com/shocknet/clink) (historical reference)
|