How the fee is set
Four steps, all inside the swap transaction: remember, read, decide, remember again. No data comes from outside the pool.
The loop
- Before the swap (
beforeSwap): the hook turns the pool’s memory and the block’s timestamp into eight integers. - The model reads the eight integers and returns one number: how far the price is likely to move in the next minute.
- The policy turns that number into a fee between a floor and a cap, and the hook sets it for this swap, setting aside the Treasury’s share.
- After the swap (
afterSwap): if the price really moved, the hook folds the move (the new price, the time that passed) into its memory. Either way it emits an event with everything it used, and mints the Treasury’s share.
1. What the pool remembers
The memory of a pool is one storage slot of 208 bits. From it, and from the current block’s timestamp, the hook computes eight integers. Nothing else is used: no price feed, no off-chain data and not the size of your swap.
| Input | Name | What it measures |
|---|---|---|
a_last | Last move | |Δtick| of the last observation, capped at 4096. 0 until there is one. |
ewFast | Fast average | Moving average of |Δtick| over observations, weight 1/4 each. Reacts within a few observations. |
ewMid | Medium average | The same average with weight 1/16. The rule-based baseline reads only this one. |
ewSlow | Slow average | The same average with weight 1/64: the long-run level of movement. |
ewSigned | Drift | Moving average of the signed Δtick (weight 1/4): which way the price is leaning. |
streak | Streak | Consecutive observations that moved the price the same way, from 0 to 16. |
dtNow | Quiet time | Bits in the seconds since the last observation, at the current block: 0 for none, 1 for one second, 2 for 2–3 s, up to 12 for 34 minutes or more. |
dtEwma | Move tempo | Moving average of the seconds between observations (weight 1/8). |
The averages are exponential moving averages with whole-number arithmetic, in fixed point with eight fractional bits (so a stored 256 means 1.0 tick). Each observation updates them with one shift, ew ← ew + ((x·256 − ew) >> k), which is why the state machine fits in a slot and costs little gas.
When the memory changes
Not on every swap. After a swap the hook compares the pool’s tick with the tick of the last observation. If they are less than 3 ticks apart, or the block is not later than the last observation’s, nothing is written, not even a timestamp: the swap is not an observation. Otherwise the move since the last observation (which may span several swaps) and the time since it are folded into the averages, and this swap becomes the last observation.
So a swap of one wei, a flicker between two adjacent ticks and a burst of swaps inside one second leave the memory as it was, and small moves add up until they reach 3 ticks. At most one observation happens per second, and each needs a real move of the price of more than 2 ticks. The two clocks (dtNow and dtEwma) run from the last observation, not from the last swap.
Why it ignores small moves
The first version of the memory counted every swap. A swap of one wei moves neither the price nor the tick, yet it aged the memory by a whole step: 64 of them in one second took a fee from 0.1011% to the 0.4127% cap, for the price of a wei and some gas. Now the cheapest way found to push the fee to its cap takes a median of 68 ticks of real price movement over about 21 seconds (see Risks). The price of this: in a deep pool most swaps are not observations, so the model sees the pool through fewer events.
2. What the model says
The model is a neural network: 8 → 16 → 8 → 1, 289 parameters, whole numbers only. Its output pred is ln(1 + movement) × 256, where movement is the total tick movement it expects over the next 60 seconds. So pred = 521 means about e^(521/256) − 1 ≈ 6.7 ticks, and pred = 0 means no movement. More about it on The model.
3. What the policy does
The policy is a floor, a slope above a threshold, and a cap. In whole numbers, with fees in hundredths of a basis point (so 3,000 is 0.30%):
fee = clamp( feeMin + ((slope × max(0, pred − threshold)) >> slopeShift), feeMin, feeMax )These are the numbers this deployment uses. They are immutable values of the hook; nobody can change them.
| Parameter | Value | Meaning |
|---|---|---|
feeMin | 516 (0.0516%) | The floor: what a calm pool charges. |
feeMax | 4127 (0.4127%) | The cap: no fee is ever higher. |
threshold | 274 | Below this model output the fee stays at the floor. It is about 1.9 ticks of expected movement. |
slope | 63 ÷ 2^4 = 3.94 | How fast the fee rises above the threshold: units of fee per unit of model output. |
feeFallback | 1032 (0.1032%) | Used if the model call ever fails, so a swap never reverts because of the model. |
protocolShare | 100000 of 1,000,000 (10%) | The part of every fee that goes to the Treasury. |
A worked example: pred = 521. Above the threshold by 521 − 274 = 247. Then 63 × 247 = 15561, shifted right by 4 is 972. Add the floor 516: a fee of 1488, or 0.1488%.
4. The split
The fee the policy chooses is the total the trader pays. The hook takes floor(fee × 100000 ÷ 1,000,000) of it for the Treasury (so 10%) and applies the rest as the pool’s fee, which goes to liquidity providers. The two add up to the policy’s fee, up to rounding.
5. After the swap
The hook reads the pool’s new tick and decides whether the swap is an observation (above). If it is, it updates the averages and the run length and stores the new memory; if not, it stores nothing. Either way it emits one event, Reflex, with the eight inputs it used, the model output, the fee and the new tick. That event is what the Live page draws, and what Verify replays to check every swap.
A real swap, taken apart
What it costs
One prediction costs 21,890 gas. The whole hook (reading the memory, predicting, applying the policy, the event, updating the memory and minting the Treasury’s share) adds about 60,000 gas to a swap that leaves the memory alone and about 76,600 to one that writes it, measured in the contract tests. At the gas prices of Robinhood Chain that is a fraction of a cent.
Why a model, and not a formula?
A formula on one input (say, a moving average of recent price moves) is the obvious alternative, and we tested it: the simple rule is the baseline in our backtest. The network sees eight inputs at once, including the direction of recent moves and how quickly they arrive, and predicts the next minute clearly better (a test error of 0.79 against 1.03). Whether that is worth the extra complexity for liquidity providers is what the backtest results discuss.