← Tech docs

How the pieces fit together.

June 2026

LN Operator is not one program but a handful of small parts that share a single machine and a single database. This note answers the questions about what those parts are, what runs them, and how data moves between them. Everything below runs on one always-on host you control — typically the same box as your node — and stays on your private network; the only thing that ever leaves the house is a Telegram message to your phone.

🏠 Your always-on home host LND REST API read / write daily review / bug-fix every 2h alerts to your phone CLI commands reads reads local network / web UI reads writes ⚡ LND + ₿ Bitcoin Core your Lightning node 📡 HTLC listener forwarding failures → DB 🤖 Claude AI agent · daily oversight ⏱ cron 2h loop + nightly jobs 🐍 Python engine ln-operator fees · liquidity · health · backups 🗄️ SQLite history & signals 📱 Telegram bot daily alerts & summaries 📊 Dashboard live web dashboard 👤 You CLI · browser · phone

What's actually inside the host boundary?

The dashed box is one physical machine. Inside it: your Bitcoin node (LND + Bitcoin Core - full node, pruned or neutrino), the Python engine, a SQLite database, a small always-on listener daemon, the Telegram bot, and the web dashboard. Nothing 'phones home' and all your data live locally.

What is the source of truth?

Your Lightning node — LND backed by a Bitcoin Core — for channels, balances, fees and forwards. LN Operator never replaces it; it drives it. All access is local, over LND's REST API, authenticated with a least-privilege macaroon and the node's TLS cert (info:read offchain:read/write onchain:read address:write invoices peers:read — no on-chain spend, no channel open/close).

What does the engine do?

The Python engine (ln-operator) is the hub. It reads node and channel state from LND and writes back changes — updated fees and rebalance payments — through the same REST interface. It is where the logic lives: the fee curve, rebalance budgeting and the profitability gate (including a QueryRoutes dry-run that reads the live route price before grinding the budget up over failed runs), peer selection — both the broad plan ranking and the targeted suggest_peers finder, served from a cached copy of the network graph — the optional node-level inbound-fee ladder, health snapshots and off-site backups. Everything else in the diagram either triggers the engine, stores what it produces, or reports on it.

What's in the database?

A single SQLite file holds everything the engine accumulates: forwarding events (forwarding_log), every rebalance and its real cost (rebalance_log), fee-change reasons (fee_updates), dropped forwards (forward_fail_log), the per-channel signals derived from all of it (channel_signals), alerts, backup attempts, manual fee pins (fee_overrides), the daily agent's de-dup memory (daily_findings), and a nightly snapshot of our network position (graph_snapshots). One local file means the whole state of your operation is one thing to back up and inspect — no database server to run. (The bulky cached network graph itself lives beside it as a single graph_cache.json, regenerated nightly — not in the DB.)

What is the forwarding-failure listener?

A small, always-on daemon (ln-operator monitor_htlcs, run as the lnd-htlc-monitor systemd service with Restart=always). It holds open LND's SubscribeHtlcEvents stream (GET /v2/router/htlcevents) and records dropped forwards into forward_fail_log. The events that matter are the link_fail_events, which carry a wire_failure / failure_detail pair — and the INSUFFICIENT_BALANCE detail is the one worth money: a forward you couldn't serve for lack of outbound liquidity. In the diagram it reads from the LND node and writes to SQLite, nothing else.

What it captures shows in the dashboard's lost-revenue watch, and it is the trigger for the fast-drain fee bump (a depleted channel dropping forwards gets its outbound fee raised the same cycle — see idle-channel pricing). This information is also consumed by the agent to suggest next steos, if enabled.

What's the LND API surface it uses?

All of it is LND's REST interface over the local TLS endpoint — no gRPC, no third-party services. The pieces that matter:

read    /v1/getinfo, /v1/channels, /v1/graph…   node, channels, graph
read    /v1/fees                                 current fee policy
write   /v1/chanpolicy                           set outbound (+inbound) fee
write   /v2/router/send  (SendPaymentV2)         circular rebalances
read    /v1/switch       (sync forwarding_log)   routed HTLCs / earnings
stream  /v2/router/htlcevents (SubscribeHtlcEvents)  dropped forwards (live-only)
read    /v1/payments     (sync rebalance_log)    confirm + import manual rebals

The fast loop and CLI use the request/response endpoints; only the listener holds a streaming connection. None of the write paths need on-chain or channel-admin permissions, which is what lets the whole thing run on the least-privilege macaroon.

What sets the engine in motion?

  • You, via the CLI. Every action is one ln-operator command — status, pipeline --dry-run, rebalance_channels, overwrite_fee, plan, suggest_peers. The manual control surface.
  • cron, every 2 hours (aka fast loop). The full pipeline runs unattended: adjust fees → rebalance → sync routing → healthcheck, so the node keeps itself tuned.
  • cron, nightly ×2. A recompute_signals job refreshes the slow per-channel signals (the market multiplier feedback) that the fast loop reads but does not compute; a refresh_graph job pulls the multi-MB network graph into a local cache (graph_cache.json) so plan, suggest_peers, and the daily agent read a precomputed digest instead of re-pulling LND.
  • Claude (or any AI agent), once a day (optional). See below.

Why split the fast loop and the nightly job?

To keep the 2h loop cheap and the slow signals stable. The pipeline reads cached signals and makes fast decisions; the nightly recompute_signals does the heavier, slower-moving work — drifting each channel's market multiplier ±0.15, stamping the structural-liquidity flag, logging refill and earned-ppm history. The one thing the 2h loop does touch on the signal side is the up-only fast-drain bump, because a bleeding channel can't wait until 3am. The nightly refresh_graph sits in the same bucket: the network graph changes slowly, so a once-a-day cache is plenty, and pulling it off-peak keeps the multi-MB request off the hot path.

What does Claude do, and is it on by default?

The daily check runs every day either way — by default it runs only the deterministic checks (the reconcile data-integrity arithmetic plus the unit suite) and Telegrams a pass/fail summary with any issues inline. No LLM, no spend. Claude is off by default.

When enabled (one env flag), that same daily run becomes an agent over the whole system instead: it reads recent logs and node state, reasons about what looks wrong, can land actual fixes in the engine (and even commit them, if you let it), and writes a plain-language recap that goes out over Telegram. Think of cron as the reflexes and Claude as a daily review by an experienced operator. It leans on the same deterministic helpers rather than doing everything in its head: the arithmetic runs in Python (reconcile), a dedup store (daily_findings) keeps it from repeating the same finding every morning, and a targeted peer-finder names concrete candidates for capital suggestions instead of hand-waving "add a source". Because it can edit code and move with whatever macaroon it's given, the agent ships as disabled, and is meant to be run with a read-only macaroon — see the project's daily-check docs before turning it on.

What reaches outside the house?

Only the Telegram bot. It reads from the engine and pushes alerts and daily summaries to your phone — health warnings, what the automation did, and Claude's recap. It is push-only notification: you get told what happened, wherever you are.

How do I see everything at once?

The dashboard — a small Flask app on port 4000 — reads from both the LND node (live channel and balance state) and SQLite (history, fees, forwards, the sat-flow map, dropped-forward lost-revenue watch, backup freshness) and renders them on one page. It reads; it never writes. It ships with no authentication, so it binds to loopback / your VPN IP by default — never expose it to the WAN without a reverse proxy in front!

So what's the overall shape?

Deliberately boring: one node, one engine, one database, on one machine. The engine is the only part that changes the node, and it does so through a single API. Everything else is a trigger (you, cron, Claude), a capture or store (the HTLC listener, SQLite), or a read-only view (the dashboard, Telegram). That makes the system easy to reason about — and easy to trust, because you can see exactly where your data lives.

Like the thinking? Run it on your own node.