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.
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-operatorcommand —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_signalsjob refreshes the slow per-channel signals (the market multiplier feedback) that the fast loop reads but does not compute; arefresh_graphjob pulls the multi-MB network graph into a local cache (graph_cache.json) soplan,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.