# Evolving Lightning's next routing algorithm — lnd × GEPA
An LLM-driven evolutionary search over Lightning routing algorithms, run against lnd's real pathfinding stack in an in-process payment simulator. What it found, and how.

Skip to content
Research log/Lightning Network pathfinding/July 2026

# Evolving Lightning's next routing algorithm

We pointed an LLM-driven evolutionary search at lnd's real pathfinding stack, inside a simulator that hides channel liquidity the way the network does. Tuning its parameters found nothing. Replacing the algorithm found a lot.

attempts per payment
mainnet, vs lnd's 19.8 2.3
objective, mainnet
vs lnd's 0.694 0.791
evolved lines of Go
from a 300-line seed 1,525
tiers the integrated branch leads
stock lnd, zero losses 14 / 14
best evolved routers (mx_c3/hb1) against lnd's production stack; the last row is the interval-router branch measured inside lnd's own payment lifecycle (exp-027)
method GEPA reflective evolution reflection LM gpt-5.6-sol evaluator cmd/routesim experiments exp-001 … exp-031 champions hb1 + mx_c3, unchanged since exp-007 · specialists atomic1 + econ2 shipping interval-lnd, 14/14 over stock lnd · release candidate eb4fc3e62, rebased b489649f6 on the fork tracks evolution at its measured boundary (exp-031) · the ship track owns the frontier · replay on real payment history next week
01

## What the search found

Every number below is held out. The closing check ran on a real mainnet graph snapshot — lnd's home turf, and a topology the evolved routers had never seen.

8.6×
fewer HTLC attempts per payment than lnd on the real mainnet graph, at the same success rate, on a perfect failure channel
2.3 vs 19.8 attempts · exp-009 · ratio retired by exp-019: under realistic attribution degradation the edge converts to success
6of 6
held-out tiers the evolved routers lead: sealed synthetic test, out-of-distribution topologies, mainnet snapshot, drifting liquidity, splitting pressure, and the atomic arena
nine challengers have since failed to displace them — though the frontier is now three regimes deep: hb1/mx_c3 where information wins, atomic1 where holds contend, econ2 where money is scarce
0
parameter settings, out of 400 evaluations of estimator and cost knobs, that beat the lnd defaults
exp-002 · the negative result that redirected the project
Attempts per payment on a mainnet snapshot Fig. 1 · lower is better
12,161 nodes, 39,659 channels, 100 payments from the network's highest-degree node. Success rates are close — lnd 0.790, seed 0.820, evolved 0.810 — so the whole difference lands in how much probing it takes to get there. This is the tier lnd should win: its defaults were tuned for this graph, while the evolved routers were bred entirely on synthetic topologies. The attempt ratio is a perfect-channel figure; exp-019 retired it, and under a degraded failure channel the same edge shows up as success instead (the ladder).
Composite objective, by router and held-out tier Fig. 2 · higher is better
evolved by GEPA baseline (lnd, or hand-written)
Objective is success − 0.01·min(extra attempts, 15) − 0.00002·min(fee ppm, 5000). The two evolved routers lead on all three tiers. On synthetic corpora the gap is mostly success rate; on the real graph it is almost entirely efficiency. Reruns are bit-identical (stdev 0.00000 over five repeats), and both champions were audited clean of simulator exploits.
Table view

Composite objective. Combined is the mean of the two synthetic held-out sets.
router | mainnet | hard sealed test | out-of-distribution | combined
--- | --- | --- | --- | ---
lnd production stackDijkstra + mission control | 0.694 | 0.309 | 0.357 | 0.333
hand-written seed~300 lines, cheapest path + blacklist | 0.762 | 0.530 | 0.487 | 0.509
hb1evolved, 872 lines | 0.790 | 0.586 | 0.545 | 0.565
mx_c3evolved, 1,525 lines | 0.791 | 0.583 | 0.581 | 0.582

The headline is not the size of the win, it is where the win came from. A 400-evaluation search over lnd's own pathfinding knobs — which probability estimator, how much a failed attempt should virtually cost, the floor on acceptable route probability — produced no setting that beat the shipped defaults on validation. The defaults are locally robust; the headroom is not in the knobs.

Meanwhile a deliberately naive 300-line router with a different paradigm beat lnd's full production stack by 39% on the objective, winning or tying every example. Handing that seed to the optimizer, and letting it rewrite the whole algorithm rather than adjust its settings, produced routers that beat both. What they invented — liquidity intervals instead of decaying penalties — is the interesting part.

Read the full findings →

02

## How the search works

A candidate is a whole routing algorithm in Go. Every proposal is compiled into a simulator and made to actually pay.

One turn of the evolutionary loop Fig. 3
scroll the diagram sideways →
The optimizer never sees hidden balances. A candidate gets the public gossip graph, its own channel balances, and the errors its attempts provoke — the same information a real Lightning sender has.

### The simulator runs lnd's real code

`routing/sim_graph.go` is an in-memory Lightning network with hidden per-channel balances and per-direction policies: base and proportional fees, CLTV deltas, min and max HTLC. It implements lnd's own `Graph` interface, so lnd's production pathfinding and mission control run against it unmodified — which is what makes the baseline honest.

Forwarding enforces real BOLT semantics. An attempt that asks too little fee gets `FeeInsufficient`; one that asks for more than a hop holds gets `TemporaryChannelFailure`, the liquidity miss that all of routing is really about. Balances move on success and unwind on failure. Topologies are synthetic (line, grid, hub-and-spoke, small-world, scale-free) or loaded from a mainnet `describegraph` snapshot, and hidden balances follow a half, uniform, or bimodal model — the last one hugging one end of each channel, which is both realistic and the hard case.

It is fast enough to be an inner loop: ten payments on a 200-node network in about 0.3 seconds including process startup, so thousands of evaluations are cheap.

### A candidate is an algorithm, not a config

The evolvable unit is a Go file behind a paradigm-free interface: gossip view, local balances, and per-attempt feedback in; a route out. GEPA rewrites that file, `go build -overlay` compiles the rewrite into the simulator, and the evaluator scores it on a corpus split into train, validation and sealed test. Nothing in the contract mentions Dijkstra, mission control, or probability estimators. Those become background knowledge the reflection prompt may draw on, keep, reshape, or discard.

#### the sandbox held

An independent audit found that the graph session handed candidates the concrete simulator graph, type-assertable back into a read of hidden balances or an outright rewrite of ground-truth liquidity — a perfect-score reward hack using no banned identifiers. It was sealed the same day, and every in-flight candidate was checked against it: zero hits. No result on this site was produced through that hole.

03

## The scenarios it is scored against

— scenario files, — payments, split —. Validation picks the incumbent; the test split stays sealed until a champion is declared.

Topology files
Hidden liquidity model files
Bimodal is the hard regime: funds parked at one end of each channel, so capacity tells you almost nothing about what will actually pass.
Payment amount scenarios, satoshis
Shard budget scenarios, max parts
04

## Run telemetry (latest completed optimizer run)

Telemetry from the optimizer, exported straight out of the GEPA run directory. code_drift1 has since finished all 400 evaluations, and so have exp-010's three splitting arms — all of them beaten by the champions (the verdict). exp-010b's two atomic-arena arms have since closed the same way — the champion's fifth hold, against an arena built to charge its probe ladder honestly (the atomic verdict) — and so has exp-012, which found no hot-cache regime anywhere but did find the one change this work argues for upstream (cold cache, hot load). Nothing is live now. The telemetry below is still exp-018's gepa arm, the engine adjudication, which gave three optimizers the same seed, corpus and eval budget and found that only gepa produced a router at all (the adjudication). Two optimizer runs have finished since, both overnight and both closed: exp-022 bred a router against a channel that lies about which hop failed (the lying channel), and exp-024 gave the runner-up engine ten times the evaluations to see whether the band was ever budget starvation (the ceiling arm). In between, exp-021 turned the findings into a flag-gated diff against lnd's own stack, where one mechanism is now PR-ready and the other is a measured null (the distillation patch). The most recent run of all is exp-025, the first bred in a world where money is real, and its winner is the first router here ever to read the fee budget it is handed (evolution learns to count). Two runs have closed since: the compose world, every economic knob plus the lying channel at once, which returned its seed unchanged, and exp-028, its first pre-registered escape, which seeded the same world from the fee-budget specialist and reproduced the give-up attractor from a second lineage (the rule). The 800-evaluation arm, code_full3, is live and carries what is left of the compose question.

#### where the program stands

No optimizer run is live. Champions are unchanged at hb1 and mx_c3, nine challengers deep, and the arms still open are about changing the environment rather than changing the search (§15 is why).

(Update, 30 July: the frontier is unchanged and the ledger still reads nine challengers. Champions hb1 and mx_c3, specialists atomic1 where holds contend and econ2 where money is scarce. What has changed is where the work points: the integration branch now measures at 14 of 14 tiers interval-solid over stock lnd with zero losses, margins intact under the production default fee limit (§19), and the stated target is shipping it in the next lnd major release. One optimizer run is live, the 800-evaluation compose arm code_full3.)

(Update, 31 July: code_full3 has closed, and with it the evolution track's search for new machinery here. 800 evaluations returned the hand seed to seven digits, which fails the second of the two pre-registered escapes and makes the compose world closed to this recipe at any seed and any practical budget (§23). Nothing is live; no run holds the router. The frontier now belongs to the ship track. The release candidate is interval-router at eb4fc3e62, rebased as b489649f6 on the fork, and it has been validated twice more since: champion-grade to the third decimal on a liquidity family nobody here authored (§21), and carrying a fix for the one interaction §19 could not explain, where shifted blame fabricates an alibi for the guilty channel (§22). Champions unchanged, ledger still nine challengers deep. Next is offline replay on a real node's payment history, which is the last escape from "simulator-shaped" this program has left.)

exp-023 and exp-025, economic realism, both closed. All five stages are merged and flag-gated — min and max HTLC pressure, inbound fees, fees as a first-class cost, concurrent payments, latency — each proven byte-identical with its flag off. The 1,920-run sweep says the champions' edge is informational rather than a pricing edge (the priced world), and the evolution run that followed produced econ2, the fee-budget specialist and challenger number nine (evolution learns to count). The compose-world run (all five economic knobs plus the lying channel) is live as code_full1; its five knobs are live at once. (It has since closed, returning its seed unchanged, and so has the first of its two escapes (§20); the second is live now.)

The interval-router branch. Separately from the simulator, the champions' paradigm is being ported into lnd proper: fourteen commits on interval-router, unchanged through both economic cycles, that put a liquidity interval per directed channel behind routerrpc.router=interval, off by default, with the stock stack untouched when the flag is unset. It still has not been benchmarked in this simulator. Nothing on this site is a measurement of that branch, and no number here should be read as one — though the hybrid it builds, evolved beliefs on top of lnd's own fee-aware pricing, is exactly what §17 says to build. (That benchmark has since run, and this paragraph is the one thing on the page it retires: exp-027 put the branch inside lnd's real payment lifecycle and measured it over fourteen tiers, where it scores 0.788 on mainnet against stock lnd's 0.694 and leads stock lnd on all fourteen with zero losses (§19). The hybrid §17 asked for is built, measured, and the thing we intend to ship.)

run — reflection — mode — budget 150 evals

`code_drift1` was the first run in a moving world. Its predecessor, `code_gen2`, showed that more evaluations of a static environment buy nothing — three independently bred routers landed inside a band 0.014 wide (the paradigm ceiling). So this run changed the environment instead of the budget: a virtual clock and background senders moving hidden liquidity between our payments, and the question of whether time-awareness re-evolves once knowledge genuinely goes stale. It did — and it still lost. The winner stamps every belief and halves its confidence every 35 virtual minutes, and the time-less champions beat it on all four held-out tiers, drift included. The mechanism, the numbers and the caveats are on the drift page.

Score over iterations Fig. 4 · minibatch objective
scroll the chart sideways →
best kept proposed seed baseline
These are per-minibatch scores on the training split, not held-out numbers — they are how GEPA decides what to keep, and they run optimistically high. Every comparison in section 01 comes from separate held-out runs instead.
Candidate lineage Fig. 5 · select a candidate
Each node is one compiled, scored router. Accepted candidates stay on the frontier and can be mutated further; rejects are dead ends. The scores are per-minibatch and each is measured on a different subset, so a rejected candidate can carry a higher number than the incumbent without being better.
Candidate detail —
What the mutation changed line diff
base
→ candidate

read next
Findings: what the evolved routers kept, dropped, and invented

The definitive write-up — the paradigm result, the champion scores, an anatomy of the evolved algorithms against lnd's, the ceiling three separate lineages ran into, what happens when a router is handed knowledge it did not pay for, the one patch that came out of all this and the theory that died beside it (the distillation patch), what a second optimizer does with ten times the budget (the ceiling arm), what a router bred against a channel that lies buys its robustness with (the lying channel), what happens to all of it once the world has prices (the priced world and evolution learns to count), what happens when the whole evolved belief system is put inside lnd's own payment lifecycle and measured there (the integration benchmark), what happens to the whole ordering on a balance sheet nobody here drew (the foreign balance sheet), the one unexplained interaction on the release candidate's record and the trust boundary that closed it (misattribution manufactures innocence), where the evolution track finally ran out of room (the compose world is closed), the corrections we have had to make to our own record, and the process lessons from thirty-one experiments.