Dynamic fees
Most AMMs charge a flat fee on every swap. ClearPortX instead charges a two-component fee: a fixed base fee plus a variable dynamic fee that grows with realized volatility. This page documents the complete mechanism as implemented in production.
Why dynamic fees on a MEV-free chain
Section titled “Why dynamic fees on a MEV-free chain”Canton structurally prevents MEV — there is no public mempool, and the sequencer only sees encrypted payloads. The usual motivation for dynamic fees (penalizing sandwich attacks) does not apply here.
ClearPortX uses dynamic fees for LP income optimization: during volatile periods, liquidity providers bear more impermanent loss. A higher fee during those periods compensates them proportionally, keeping liquidity present when the market needs it most. During calm periods, the variable fee drops to near zero and trading is cheap.
The total fee
Section titled “The total fee”The 10% hard cap prevents runaway fees under any condition.
Base fee
Section titled “Base fee”The base fee is a fixed parameter stored in the pool configuration at creation. It does not change between swaps:
Typical values range from 0.05% (stablecoin pairs) to 0.30% (volatile pairs). The pool creator selects this value based on the expected volatility profile of the pair.
Variable fee
Section titled “Variable fee”The variable fee is quadratic in the volatility accumulator:
where:
- is the volatility accumulator — a value that tracks how much trading activity has occurred recently
- is the bin step (e.g.,
0.01for 1% bin spacing,0.005for 0.5%)
The quadratic relationship is the key design choice: doubling the volatility accumulator quadruples the variable fee. This creates a natural brake on rapid, large trades while keeping small calm trades essentially free of variable fees.
Worked example
Section titled “Worked example”For a CC/cBTC pool with bin_step = 0.01:
| Scenario | (0.30% base) | ||
|---|---|---|---|
| Single swap, cold start | 1.0 | 0.01% | 0.31% |
| Rapid follow-up swap (under 1s) | 1.5 | 0.0225% | 0.32% |
| Burst of 3 rapid swaps | 2.5 | 0.0625% | 0.36% |
| Five rapid swaps | 4.0 | 0.16% | 0.46% |
| Ten rapid swaps | 8.0 | 0.64% | 0.94% |
The volatility accumulator
Section titled “The volatility accumulator”The accumulator has two components:
- Volatility reference () — a persistent value that carries forward between swaps and decays over time
- Instantaneous bins crossed () — the number of bins traversed by the current swap
The accumulator for a given swap is:
where is always at least 1 (even a single-bin swap contributes 1 to the accumulator).
Time-based decay
Section titled “Time-based decay”The reference decays between swaps based on elapsed time, using three regimes:
| Elapsed time () | Condition | Effect |
|---|---|---|
| (filter period) | High-frequency stacking | unchanged. Rapid swaps accumulate volatility without relief. |
| (decay zone) | Gradual decay | where is the decay factor |
| (full reset) | Complete cool-down | . The system returns to baseline. |
Default parameters (configurable per pool):
| Parameter | Default | Purpose |
|---|---|---|
filter_period_ms | 1,000 ms | Swaps faster than this stack without decay |
decay_period_ms | 10,000 ms | After this duration, full reset |
decay_factor | 0.5 | How much volatility survives from one swap to the next |
Decay in action
Section titled “Decay in action”The following sequence illustrates the accumulator behavior:
Swap 1 (cold start): t=0s v_r=0 → v_a=0+1=1 → fee=0.31%Swap 2 (200ms later): t=0.2s v_r=1 → v_a=1+1=2 → fee=0.34% ↑ within filter period: v_r carries forward without decaySwap 3 (300ms later): t=0.5s v_r=2 → v_a=2+1=3 → fee=0.39% ↑ still within filter: stacking continuesSwap 4 (2s later): t=2.5s v_r=1.5 → v_a=1.5+1=2.5 → fee=0.36% ↑ past filter, in decay zone: v_r decayed by 0.5×Swap 5 (12s later): t=14.5s v_r=0 → v_a=0+1=1 → fee=0.31% ↑ past decay period: full reset to baselineState persistence
Section titled “State persistence”The volatility state (v_r, index reference, last swap timestamp) is stored in PostgreSQL and read with a FOR UPDATE lock on each swap. This ensures correctness across ClearPortX’s PM2 cluster mode where multiple Node.js workers share the database but not memory.
-- Read state at swap startSELECT volatility_ref, index_ref, last_swap_tsFROM pools WHERE id = $1 FOR UPDATE;
-- Write state after swapUPDATE pools SET volatility_ref = $1, index_ref = $2, last_swap_ts = $3WHERE id = $4;Implementation reference
Section titled “Implementation reference”The dynamic fee is implemented in server.js:
function computeDynamicFee(pool, binsCrossed, volatilityState) { const baseFee = pool.feeRate; const bw = pool.binWidth;
// Volatility accumulator: persistent ref + bins crossed const va = volatilityState.vr + binsCrossed;
// Variable fee: quadratic in va, scaled by binWidth² const variableFee = va * va * bw * bw; const cappedVariable = Math.min(variableFee, 0.02); // 2% cap
const totalFee = Math.min(baseFee + cappedVariable, 0.1); // 10% hard cap return { baseFee, variableFee: cappedVariable, totalFee, volatilityAccumulator: va };}Every swap response includes the fee breakdown, enabling transparent display in any frontend:
{ "feeBreakdown": { "baseFee": 0.003, "variableFee": 0.0001, "totalFeeRate": 0.0031, "volatilityAccumulator": 1.5 }}Further reading
Section titled “Further reading”- Fees & rewards — the LP claim flow and protocol fee distribution
- DLMM explained — how bins and swaps work
- Liquidity strategies — how strategy choice affects fee capture