Setting fees: a sigmoid with a cost floor
May 2026
Outbound liquidity behaves like inventory: a channel that is nearly full on our side is worth draining cheaply, and the last slice of outbound in a nearly-empty channel is scarce and should be priced accordingly. A single fixed fee rate misprices both. The aim is a fee that tracks how much outbound a channel currently holds, with a hard floor at what that liquidity cost us to acquire.
The base curve
A channel's local balance ratio (local ÷ capacity) is mapped to a base fee through a logistic curve, then scaled by a slow market term:
sig = 1 / (1 + e^(k · (ratio − 0.5))) k = 8
base_ppm = 25 + sig · (250 − 25) band: 25–250 ppm
ppm = base_ppm · (1 + market_mult) When the channel is depleted (low ratio) the curve approaches the top of the band — we want to discourage outbound and defend the remaining liquidity. When it is full it approaches the bottom, encouraging flow. The steepness (k = 8) keeps both tails flat and concentrates the movement in the 0.3–0.7 region, where the decision is genuinely ambiguous; a channel at 80% vs 90% full is priced almost the same. A linear map would over-react at the edges and barely move through the middle.
ratio sig base_ppm
0.20 0.92 ~231
0.35 0.77 ~198
0.50 0.50 ~138
0.65 0.23 ~77
0.80 0.08 ~44 The market term
market_mult is a slow per-channel adjustment (default 0)
recomputed nightly from forwarding activity, bounded to roughly −0.5…+2.0 — so
it can scale the base anywhere from about half up to triple. It is deliberately
slow so day-to-day noise does not move fees; fee flapping disrupts pathfinding
and makes a node look unstable to peers. One guard: in the low-local defense
zone the market term is never allowed to push a fee below the base
curve, which would invite the very drain we are defending against.
The floor that does the real work
The base band tops out at 250 ppm. For a channel that is cheap to keep filled, that is plenty. But if a channel last cost us 350 ppm to refill, selling its outbound anywhere in the 25–250 band is a guaranteed loss. So the final step floors the fee at break-even and caps it at a hard ceiling:
floor = last_refill_ppm · 1.1
target = min( max(ppm, floor), 5000 ) last_refill_ppm is the cost of the most recent successful
rebalance into this specific channel — our measured marginal cost of inbound
liquidity for it. A channel refilled at 350 ppm gets a floor of 385 ppm, which
sits above the entire sigmoid band, so the floor — not the curve — sets its
price. In other words: the sigmoid prices the cheap, healthy channels within a
modest band, and the floor is what lifts the price on channels that are
genuinely expensive to keep liquid. The 1.1 margin is intentionally thin; it
covers base-fee and rounding overhead rather than trying to book a profit on
top. The 5000 ceiling is the last line of defence against bad data, and is set
equal to the maximum rebalance budget so a channel can always charge enough to
cover its own refills.
How the pieces resolve
Four inputs can set a channel's fee — the base curve, the market term, the floor, and the ceiling — and they are resolved in a fixed order rather than blended into a weighted sum:
base = sigmoid(ratio) # 25–250 band
adjusted = base · (1 + market_mult) # but never below base when ratio < 20%
target = max(adjusted, floor) # the cost floor wins if it is higher
target = min(target, 5000) # hard ceiling
Because it is a chain of max/min and not an average,
exactly one input "wins" at any moment, and the engine records which one —
floor, sigmoid+market, or sigmoid — in
the reason it logs for every change. That makes a surprising fee easy to
explain after the fact: you can see whether the curve, the market nudge, or
the refill cost set it, rather than untangling a blend.
Corner cases
- No refill history. A channel that has never been
rebalanced has
floor = 0, so the curve and market term drive it alone. The floor only activates once there is a real cost to defend. - Floor below the curve. If the last refill was cheap, the floor can sit under the current sigmoid output, in which case it does nothing — the curve already prices above cost.
- Floor above the band. Any channel that cost more than ~227 ppm to fill has a floor above the 250-ppm band top, so the floor sets its price. This is the common case for expensive corridors, and it is the point of having a floor at all.
- Negative market term on a depleted channel. A quiet channel earns a downward market nudge, but inside the sub-20% defense zone that nudge is clamped away — we never cheapen a channel we are actively trying to refill.
- Refill cost above the ceiling. If a refill ever cost more than ~4,500 ppm, the floor would exceed the 5000 ceiling and the fee is capped below cost. That is accepted deliberately: 5000 is also the maximum we will ever pay to rebalance, so the two ceilings are matched and the situation cannot persist — we stop refilling that channel before we sell under cost indefinitely.
- A fresh refill moves the floor. A successful rebalance
rewrites
last_refill_ppm, which can jump the floor. If the jump is 30 ppm or more it bypasses the 6-hour cooldown and broadcasts promptly; a smaller change waits for the next meaningful move. - A manual pin below the floor. Pinned channels skip this
calculation entirely, so a pin can legitimately sit below break-even. The
pin is honoured, but
ln-operator overwrite_feewarns at the moment you set it that the rate is under the channel's refill floor.
When we actually broadcast
Computing a target is cheap; broadcasting a fee update is not free — it gossips to the whole network and resets peers' view of you. So a new target only goes out if it is a meaningful change: at least 10 ppm and at least 10% different from the current rate, and not within 6 hours of the last change for that channel. Two things bypass the cooldown — a move of 30 ppm or more, or the balance ratio crossing the 20% / 80% edges — because those are exactly the moments you do not want to sit on a stale fee.
What we deliberately avoid
We do not anchor to neighbours' fees from the local graph. That data is sparse
and lagging enough that "the market rate" is often fiction, and anchoring to a
number we cannot trust just imports someone else's mistake. The floor is
instead anchored to a cost we measured ourselves. Channels with a manual pin
(the fee_overrides table) bypass this calculation entirely — the
pinned rate wins, and the decision stays logged.