Choosing peers.
June 2026
"Who should I open a channel to?" is usually answered with centrality — connect
to the most-connected nodes. We think that answers a different question than the
one that matters for a small or mid-sized node, and rank candidates differently.
This note answers how ln-operator plan picks peers and why.
How many channels can I even afford?
The planner reads your on-chain wallet balance from LND and subtracts what is already committed before sizing channels:
wallet balance 3,528,518 sats
− existing anchor −30,000 (already locked by LND)
− treasury (2.5%) −88,212 (TREASURY_MIN_RATIO)
− new anchor −10,000 (per channel)
− open fees −250 (on-chain at 1 sat/vB)
─────────────────────────────────
deployable 3,400,056 → 1 channel at 3.4M sats
Channels are sized at or above PREFERRED_CHANNEL_SIZE_SATS (3M by
default). The command runs only on demand and offers to generate a funding
address with a QR code at the end — it never opens channels itself (opens are
manual via lncli).
How are candidates ranked?
A two-stage, tier-segmented ranking — no weighted blending. Candidates are first bucketed by absolute channel count, then ranked independently inside each bucket:
hub ≥ 100 channels
mid-tier 30 – 99
small 10 – 29
(below 10 dropped as noise) Within each tier the ranking runs in two passes: a cheap centrality prefilter, then a live-graph diversity rerank.
What does stage one (centrality) do?
Centrality — a log-normalised mean of a candidate's channel count and total
capacity — is computed from the local graph and used only as a cheap prefilter:
it picks the top ENRICH_PER_TIER (30) candidates per tier to
enrich. It is derived entirely from the cached network graph
(the nightly refresh_graph digest), so it costs no live LND calls at
all. It does not decide the final order.
What does stage two (diversity) measure, and why 2-hop?
Diversity is the fraction of a candidate's peers that sit outside your 2-hop reachable set — peers that would actually expand your graph horizon, not just add a parallel edge into nodes you can already reach through an existing channel:
diversity = |peers(candidate) not in reachable_2hop|
────────────────────────────────────────
|peers(candidate)|
It is read straight from the cached network graph — a nightly
refresh_graph pull whose digest carries every node's adjacency — then
used to rerank; the top SHOW_PER_TIER (10) per tier are surfaced. So
both stages come from the cache with no live graph pull and no per-candidate
round-trips (the old path made a get_node_info call each — ~90
in total, and the slow part). The 2-hop set is recomputed from your current
peers via the cached adjacency, so it stays correct even if channels changed since
the refresh.
Why 2-hop rather than direct-peer overlap? If "already in your graph" means just your direct peers, the metric collapses when your channel count is low — almost every candidate scores near 100% diversity, because almost nobody shares a direct edge with you, so it effectively re-ranks by size and recommends hubs (the outcome we were trying to avoid). Measuring against the 2-hop horizon keeps it discriminating at any node size: a 50-peer hub with 40 peers already inside your 2-hop set scores 20%, while a curated small node with 18 peers, 15 of them off your map, scores 83% — despite far lower centrality.
Why rank inside tiers instead of one global list?
Because diversity correlates with size in a way that would swamp a single list. A small node's peers are often obscure leaves outside your horizon (high diversity by default), and a hub's peers tend to be other hubs you can already reach (low diversity). A global ranking would just surface backwater nodes. Per-tier ranking asks the right question three times — "the most diversifying hub", "the most diversifying mid-tier", "the most diversifying small" — independently, so you get a usable shortlist at each size and the mix across tiers is left to you.
Why isn't fee level part of the score?
Because local-graph fee data is unreliable — sparse and lagging enough that ranking on it would import noise, the same reason fees are not anchored to neighbours in fee-setting. A candidate's average outbound fee is shown for reference but never scored, and inbound fees are not visible in the local graph at all — so the tool points you at Amboss to sanity-check a candidate before opening.
Why is this on-demand only?
The heavy part — pulling the multi-MB network graph from LND — is done once a
night by refresh_graph into a local cache, not on every run. The
ranking itself reads that cache, so ln-operator plan runs only when
you ask but is now fast (no live pull, no per-candidate round-trips). There is no
Claude/API layer in it: pure local computation over the cached graph. And it scores
reachability, not reliability or liquidity — a high-diversity peer can
still route poorly, so the metric narrows the field and the final choice stays
human.
What if I want the best peer for a specific sink?
plan answers "where should I add capacity in general". For the
targeted question — "which peer should I open toward so refills into this
draining channel get cheaper?" — there's ln-operator suggest_peers
<alias|pubkey>. Same cached graph for stage one (the sink's
neighbours, scored by hub quality), but stage two then prices each
candidate with a live QueryRoutes probe of the real refill shape:
the route Y → … → sink → you
(dest=you, source_pubkey=Y, last_hop=sink) — the path a refill takes
after you open you → Y, with the sink as an intermediate
forwarder that charges its fee, not a free destination. That drops candidates with
no real liquidity and ranks the survivors by true end-to-end refill
cost.
One subtlety makes this honest. LND never charges the source for its own
first hop, so the probe (which starts at Y) counts every hop
after Y but omits Y's own outbound fee — the very
fee you'd pay once Y becomes an intermediate after you open to it. We
add it back from the channel's published policy, shown as (peer-hop N),
so every hop on the path is counted. It matters: probing the sink
as the destination instead (the obvious first cut) makes the final hop free,
so every direct neighbour reads a meaningless 0ppm and the ranking
collapses to a tiebreak — which can put the most expensive peer on top. On a
real Bitfinex sink, that naïve probe ranked WalletOfSatoshi #1 at "0ppm" while its
true refill cost was ~5002 ppm (its own 5000 ppm fee toward
bfx hidden); the honest probe surfaces cheap hubs like Binance (2 ppm) first.
An empty result is the answer: no peer has a cheap live route to
this sink, so the capital move is resize/close, not open. Probe size defaults to a
representative 500k-sat refill chunk and is tunable with --amount
(smaller is easier to route — feasibility; larger amortises base fees — cost
accuracy). The daily-check agent uses this to name concrete peers in its capital
suggestions. This is the one place QueryRoutes' real liquidity beats the
reachability-only graph — and the one thing plan can't do, because it
only knows the graph as it is, not as it would be after a new channel.