← Tech docs

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.

Like the thinking? Run it on your own node.