← Tech docs

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_fee warns 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.

Like the thinking? Run it on your own node.