# Fees: source-derived economics and a bounded dynamic policy Pinned checkout: `benedictbrady/amm-challenge@6b6921f8d463314aa7c96234e1be25a25245f704`. I cloned into an empty scratch directory and checked out that commit. **The full static optimum is approximately 76 bps in the experiments below. The centered, isolated-order optimum is approximately 25.4 bps; they answer different questions.** Retail-induced price displacement is the principal reason they differ. A genuine arbitrage execution reveals fair price exactly, but `afterSwap` does not identify arbitrage, expose the competing pool, or run during silent steps. Values are Y-denominated edge, not terminal PnL. Source keys link to pinned files; derivations identify functions and lines. Calculations are my own. This theory item supplies a policy specification, not a claimed globally optimal dynamic contract. ## 1. Retail routing, share, and edge versus the 30 bps pool ### Definitions and executable conventions Let our reserves be `(x,y)`, `k=xy`, spot `s=y/x`, fair price `p`, input multiplier `g=1-f`, and the normalizer's multiplier `g0=0.997`. An ask fee applies when the AMM **sells X**; a bid fee applies when it **buys X**. Initial reserves are `x=100, y=10,000`, with `p=s=100`. Fees are segregated from pricing reserves, so `k` stays constant, apart from floating-point error. These conventions come from [C], `CFMM::{quote_buy_x,quote_sell_x,quote_x_for_y,execute_*}`, L115–289; [I], `TradeInfo`, L6–12; and [D], `BASELINE_SETTINGS`, L25–35. The positive-edge convention actually executed is \[ E_{ask}=Y_{in}-pX_{out},\qquad E_{bid}=pX_{in}-Y_{out}. \] Source: [E], `SimulationEngine::run`, L153–161; arbitrage edge is minus the arbitrageur's profit, L140–147. The displayed edge signs in the repository README and the public HackMD note are reversed relative to this code; I do not use those displayed signs. ### Exact split, including the sell-side distinction A retail buy spends a fixed total `Q` Y. The X output from pool i receiving `q_i` is \[ X_i(q_i)={x_i g_iq_i\over y_i+g_iq_i},\qquad X_i'(q_i)={x_i g_i y_i\over(y_i+g_iq_i)^2}. \] Source: [C], `quote_x_for_y`, L175–195; derivative is algebra. Equalizing these derivatives subject to `q1+q0=Q` gives \[ r=\sqrt{{xg y\over x_0g_0y_0}},\qquad q_{ask}=\left[{r(y_0+g_0Q)-y\over g+rg_0}\right]_0^Q. \] Source: [R], `split_buy_two_amms`, L35–74. Brackets mean clamping to the indicated interval, not an unconstrained split. A retail sell instead inputs **fixed X** `T=Q/p`; it does not request fixed Y output. Its Y output is `y_i g_i t_i/(x_i+g_i t_i)`. The analogous split is \[ t_{bid}=\left[{r(x_0+g_0T)-x\over g+rg_0}\right]_0^T,\qquad T=Q/p. \] Source: [R], `split_sell_two_amms`, L77–116, and `route_to_two_amms`, L219–246; [C], `quote_buy_x`, L119–139. This corrects the fixed-Y sell split appearing in the public HackMD analysis. For equal reserves **and `p=s`**, both directions have the same allocation in Y input-equivalent units: \[ r=\sqrt{g/g_0},\quad q(Q,f)=\left[AQ+C_f\right]_0^Q,\quad A={rg_0\over g+rg_0},\quad C_f={y(r-1)\over g+rg_0}. \] Source: substitute equal reserves in [R], both split functions, L40–116; multiply the X split by `p=y/x`. Equality of the two directional shares requires `p=s`, not merely equal pool reserves. Our share of **this order**, our edge per total unit of order, and our edge per captured unit are respectively \[ h(Q,f)={q\over Q},\qquad {e(Q,f)\over Q}={q\over Q}\left(1-{yg\over y+gq}\right),\qquad {e(Q,f)\over q}={fy+gq\over y+gq}. \] Source: [C], `quote_x_for_y` and `quote_buy_x`, L119–195, combined with [E], edge accounting, L153–161. These include execution price impact; edge is not simply `fee × volume`. The next table uses **one Q=20 Y order**, `p=s=100`, and equal initial reserves. Edge/order is in bps of the whole 20 Y order. The last column is the volume-weighted share over the actual default size distribution, `E[q]/E[Q]`, derived in section 3. It is not `E[q/Q]`. | f (bps) | Our input q (Y) | Share of Q=20 | Edge (Y) | Edge/order (bps) | E[q]/20 | | --- | --- | --- | --- | --- | --- | | 5 | 16.2657 | 81.33% | 0.034521 | 17.260 | 71.10% | | 10 | 15.0151 | 75.08% | 0.037481 | 18.741 | 68.28% | | 15 | 13.7632 | 68.82% | 0.039505 | 19.752 | 64.93% | | 20 | 12.5100 | 62.55% | 0.040588 | 20.294 | 60.90% | | 25 | 11.2557 | 56.28% | 0.040731 | 20.365 | 55.96% | | 30 | 10.0000 | 50.00% | 0.039930 | 19.965 | 50.00% | | 35 | 8.7431 | 43.72% | 0.038185 | 19.092 | 44.03% | | 40 | 7.4849 | 37.42% | 0.035493 | 17.747 | 39.09% | | 50 | 4.9648 | 24.82% | 0.027263 | 13.631 | 31.67% | | 70 | 0.0000 | 0.00% | 0.000000 | 0.000 | 22.47% | Thus 70 bps gets none of a 20 Y order at this centered state, but still gets about 22.5% of aggregate notional under the heavy-tailed size distribution. There is no fee-only universal share: order size, both reserve pairs, both directional fees, and, for sells, fair-price conversion matter. The router also suppresses legs at or below `0.0001` input units: Y for buys, X for sells. Source: [R], `route_to_two_amms`, L188–246. The analytical tables ignore this tiny cutoff; the simulation checks include it. ## 2. Arbitrage, terminal spot, and losses ### Trigger, optimal size, and exact loss Use `z=log(p/s)`, `a=-log(1-askFee)`, `b=-log(1-bidFee)`. The no-arbitrage region is \[ -b\le z\le a,\quad\text{equivalently}\quad s(1-bidFee)\le p\le{s\over1-askFee}. \] Source: positivity of optimal size in [A], `compute_buy_arb`, L54–85, and `compute_sell_arb`, L104–134. Equality gives zero size and no trade. If `z>a`, write `d=z-a>0`, `ga=1-askFee`. The arbitrageur buys X from us: \[ X_{out}=x-\sqrt{k/(g_ap)}=x(1-e^{-d/2}),\qquad Y_{in}={y\over g_a}(e^{d/2}-1), \quad s_{post}=g_ap, \quad L_{ask}={y\over g_a}(e^{d/2}-1)^2. \] Source: [A], `compute_buy_arb`, L54–97, and [C], `quote_sell_x`/`execute_sell_x`, L147–168 and L231–253. `L` is our positive loss, equal to arb profit. The code caps `X_out` at 99% of reserves; the displayed unconstrained formulas apply below that cap, as in the default regime. If `z<-b`, write `d=-z-b>0`, `gb=1-bidFee`. The arbitrageur sells X to us: \[ X_{in}={\sqrt{kg_b/p}-x\over g_b} ={x\over g_b}(e^{d/2}-1),\qquad Y_{out}=y(1-e^{-d/2}), \quad s_{post}=p/g_b, \quad L_{bid}=y(1-e^{-d/2})^2. \] Source: [A], `compute_sell_arb`, L104–146, and [C], `quote_buy_x`/`execute_buy_x`, L119–139 and L200–222. `X_in` is gross input, not the net amount entering the pricing reserves. Consequently, to second order in a small overshoot, \[ L(z)\simeq {y\over4}\{(z-a)_+^2+(-z-b)_+^2\}. \] Source: Taylor expansion of the preceding two [A]/[C] formulas. This is a local approximation, not an assumption that the pool starts every step at fair value. ### One-step expectation: what can and cannot depend only on fee and sigma The fair price evolves as \[ p_{t+1}=p_t\exp(-\sigma^2/2+\sigma Z),\quad Z\sim N(0,1). \] Source: [P], `GBMPriceProcess::{new,step}`, L37–65; `mu=0,dt=1` in [D], L25–35. Sigma is constant within a simulation and sampled uniformly over `[0.000882,0.001008]` by [M], `_build_configs`, L100–138, using the bounds in [D], L38–48. Its midpoint is **0.000945**, whereas the requested representative calculation uses **0.00095**. If current mispricing is `z0` and there are no trades before the next arb pass, let `m=z0-sigma²/2`, `v=sigma²`. Define the truncated Gaussian exponential moments \[ T_c(u)=e^{cm+c^2v/2}\Phi((m+cv-u)/\sqrt v),\qquad U_c(l)=e^{cm+c^2v/2}\Phi((l-m-cv)/\sqrt v). \] Source: Gaussian integration of [P], `step`, L62–65. `T_c=E[e^{cz}1(z>u)]`, and `U_c=E[e^{cz}1(zu)]=e^{j\mu_Q+j^2\tau^2/2} \Phi((\mu_Q+j\tau^2-\log u)/\tau). \] Source: complete the square in the [T], `RetailTrader::new`, L57–60 lognormal density. For `f<30 bps`, `q=Q` below `C_f/(1-A)` and `q=AQ+C_f` above it; for `f>30 bps`, `q=0` below `-C_f/A` and `q=AQ+C_f` above it. These thresholds follow directly from the [R] clamp, not from a linear volume heuristic. | f (bps) | E[q] (Y/order) | Centered retail gain/step (Y) | | --- | --- | --- | | 5 | 14.21997 | 0.04990284 | | 10 | 13.65553 | 0.05289493 | | 15 | 12.98597 | 0.05526803 | | 20 | 12.17918 | 0.05687282 | | 25 | 11.19225 | 0.05751139 | | 30 | 10.00000 | 0.05700361 | | 35 | 8.80665 | 0.05577267 | | 40 | 7.81725 | 0.05445927 | | 50 | 6.33460 | 0.05195062 | | 70 | 4.49470 | 0.04748901 | Direct one-dimensional quadrature puts the maximum of `R_center` at **25.33 bps**. Subtracting the exact *centered* next-step arb loss moves the optimum to **25.36 bps**, with expected net `0.0575111 Y/step`. This is an artificial reset model. It does **not** establish a 575 Y fixed-fee strategy. Reset-model sensitivities: sigma 0.000882→0.001008: 25.35→25.38 bps; lambda 0.6→1.0: 25.37→25.36 bps; M 19→21: 25.05→25.69 bps (one parameter varied at a time). ### The correct whole-simulation objective Let `S` include both reserve pairs and the fair price. For a fixed fee, compose price movement, one arbitrage pass per pool, then the complete Poisson retail batch, in that order. If `P_f` is this transition, and `g_f(S)` is its expected edge, the finite-horizon static objective is \[ J_T(f)=\sum_{t=0}^{T-1}\delta_{S_0}P_f^t g_f,\qquad f^*\in\arg\max_{0\le f\le0.1}J_{10000}(f). \] Source: the executed order in [E], `SimulationEngine::run`, L136–180; fee limits in [B], L17–24 and L80–84. This is the quantity evaluated by the static experiments. A stationary approximation must solve the **joint** state distribution for both pools, rather than insert a centered Gaussian into the loss formula. There is also a useful conservation check. Define active reserve value and its tracking penalty: \[ V(p,s)=xp+y=2\sqrt{kp}+U(p,s),\quad U(p,s)=2\sqrt{kp}\{\cosh(\tfrac12\log(p/s))-1\}\ge0. \] Source: fixed-product reserve accounting in [C], and reserve/fee valuation in [E], L196–204; the identity is algebra. Summing trade edge, accounting for segregated fees, and taking expectations under the zero-drift price martingale gives the exact fixed-sigma identity \[ E[Edge_T]=E[Fees_T\text{ valued at }p_T] +2\sqrt{kp_0}(e^{-T\sigma^2/8}-1)+E[U_T]-U_0. \] Source: [C], segregated fee updates, L206–210, L237–241, L268–272; [E], edge and terminal valuation, L140–161 and L196–204; [P], martingale GBM transition, L48–65. The GBM half-moment supplies the exponential. The expected fair-price revaluation of previously held X is zero. This statement applies to expected edge, not a pathwise equality without the martingale-gain term. At nominal sigma and `T=10,000`, the common half-moment term is about **−22.55 Y**, largely independent of fee. Thus maximizing long-run edge approximately maximizes **all** fee intake, including arbitrage fees, with a small terminal tracking correction. It does not maximize retail fees alone. In the nominal 76 bps calculation, total fee value is about **400.80**, edge **378.29**, and the difference is consistent with this identity. ### Numerical static optimum and sensitivity I used a source-equivalent native calculation for a 2 bps grid from 20 through 200 bps, 2,000 common seeds per condition, then checked the fixed Solidity strategies with the unmodified Rust/revm engine. The calculation reproduces both random streams: `Pcg64(seed)` for prices and `Pcg64(seed+1)` for retail; distribution draws occur in source order. Source: [E], L51–69; [P], L37–49 and L62–65; [T], L50–60 and L75–96. The native calculation skips only constant-fee EVM calls and step-result allocation. Versions: solc 0.8.24; NumPy 2.5.4; rand 0.8.8, rand_distr 0.4.3, rand_pcg 0.3.1; revm 18.0.0. Its arb and retail component totals are diagnostic outputs, not fields fabricated from the standard CLI. | Condition (others nominal) | Best grid f (bps) | n | Mean edge/run | Raw-score SE | Arb edge/run | Retail edge/run | | --- | --- | --- | --- | --- | --- | --- | | sigma=.000950, lambda=.8, M=20 | 76 | 2000 | 378.2851 | 0.4621 | -104.9178 | 483.2028 | | sigma=.000882 | 76 | 2000 | 378.9468 | 0.4582 | -104.5152 | 483.4620 | | sigma=.001008 | 76 | 2000 | 377.6859 | 0.4655 | -105.2963 | 482.9822 | | lambda=.6 | 74 | 2000 | 280.9880 | 0.3857 | -80.8984 | 361.8863 | | lambda=1.0 | 78 | 2000 | 477.8615 | 0.5233 | -129.9898 | 607.8513 | | M=19 | 72 | 2000 | 347.6049 | 0.4182 | -96.4694 | 444.0743 | | M=21 | 82 | 2000 | 410.3445 | 0.5168 | -110.8574 | 521.2019 | | CLI randomized parameters | 76 | 2000 | 379.5576 | 1.4018 | -105.0644 | 484.6220 | These are **grid maxima**, not proofs of a globally unique continuous optimum. Adjacent 2 bps choices have paired differences much smaller than the raw-score SE. The relevant conclusion is a broad nominal/default optimum near **75–80 bps**, little sigma sensitivity, and a size-driven shift to approximately 72–82 bps. The high-fee optimum persists across this range because retail displacement dominates the volatility-only arbitrage cost. An additional 2,000-seed coarse check at 0, 5, 10, 15, 200, 250, 300, 350, 400, 450, 500, 600, 700, 800, 900, and 1000 bps found no higher result (200 bps: 303.35; 1000 bps: 67.48). This is numerical evidence, not an analytic exclusion of every untested fee. ### Required fixed-fee checks and paired measurements The Rust engine and Solidity compiler were built successfully. For each required fixed fee I ran the compiled, validator-accepted constant strategy against the fixed 30 bps normalizer using the pinned `run_batch` API and exactly the CLI's config construction. All seven tested fixed-fee files, including 76 bps, passed the actual `amm-match validate` command. The first 500 observations are therefore the requested **seeds 0..499**, with default parameter randomization. These are actual full-engine results, not the centered model. Observed full-engine amortized wall time was about 0.10–0.19 s/simulation in 8-way batches, excluding setup; the native calculations were about 0.0002–0.0007 s/simulation. These are wall-time ratios, not CPU times. I used API batching rather than launching six separate CLI processes; [M], `_build_configs`/`run_match`, L100–160, and [CLI], `run_match_command`, L85–127 establish equivalence. | Fixed fee (bps) | Full-engine mean, seeds 0..499 | Full-engine n completed | Full-engine mean (completed n) | Max absolute native/engine difference | | --- | --- | --- | --- | --- | | 20 | 285.738090 | 2000 | 281.926519 | 3.8e-07 | | 25 | 326.511258 | 2000 | 322.143681 | 2.6e-07 | | 30 | 347.664643 | 2000 | 342.942182 | 4.5e-07 | | 35 | 355.892692 | 2000 | 350.967749 | 3.3e-07 | | 40 | 363.056841 | 2000 | 357.951804 | 4.3e-07 | | 50 | 374.124792 | 2000 | 368.804421 | 5.8e-07 | The randomized-parameter raw-score SE is about 1.40 Y at n=2,000, far larger than leaderboard tenths. Every comparison below uses **2,000 paired simulations** on seeds 0..1999. Each candidate is compared to the 50 bps starter policy, with the normalizer held at 30 bps in both runs. This is the difference between our two strategy scores on the same paths, not our edge minus the normalizer's edge inside one match. The native grid is exploratory; no dynamic strategy was promoted from it. \[ D_i=Edge_i(f)-Edge_i(50),\quad \bar D={1\over n}\sum_iD_i,\quad SE={\operatorname{sd}(D_i;ddof=1)\over\sqrt n}. \] Source for deterministic seed/config pairing: [M], `_build_configs`, L104–138. The statistics are my calculations. “Worst” means five smallest paired differences, with seed IDs. | f versus starter 50 (bps) | n | Mean paired difference | Paired SE | Five worst seed: difference | | --- | --- | --- | --- | --- | | 20 | 2000 | -86.87790 | 0.40860 | 1849: -154.879; 118: -146.361; 179: -142.388; 1588: -138.079; 76: -136.665 | | 25 | 2000 | -46.66074 | 0.28216 | 1849: -98.504; 118: -90.406; 179: -88.434; 1588: -84.484; 1503: -83.630 | | 30 | 2000 | -25.86224 | 0.19765 | 1849: -63.387; 118: -57.602; 179: -56.159; 1503: -54.003; 1588: -52.966 | | 35 | 2000 | -17.83667 | 0.13775 | 1849: -43.294; 118: -40.291; 179: -38.963; 1503: -38.118; 1660: -36.595 | | 40 | 2000 | -10.85262 | 0.08631 | 1849: -26.456; 118: -25.039; 179: -24.225; 1503: -23.869; 708: -22.653 | A **fresh diagnostic** fixed at 76 bps before inspecting seeds 2000..3999 scored 379.668288 in 2,000 source-equivalent simulations. Versus 50 bps, its paired gain was **+10.867525, SE 0.159558**, comfortably above twice SE. The five worst held-out differences were 3336: -10.510236; 2951: -10.367660; 3716: -7.800562; 2549: -7.152094; 3356: -7.060723. Versus 78 bps the held-out difference was only +0.000112, SE 0.010259; there is no supported promotion from 78 to 76. This fresh set was used once for confirmation, not to choose another fee. Full-engine parity is measured in the table above; the fresh 76/50 comparison itself used the native constant-fee calculation. No prior best dynamic file was supplied, so a comparison to it is unavailable. For reproducibility, the fixed-fee sources are the pinned [Starter] text with only `50 * BPS` replaced by the tested fee and `StarterStrategy` replaced everywhere by `Fixed_FEE`. This naming change does not affect fee decisions. Source-text SHA-256 values, including original newlines, are: | Fee/policy | Source SHA-256 | | --- | --- | | 20 | 8e10fd249e44aee9cfd41b70c733d84c7893b5b828130c713502a093decdce63 | | 25 | efc248916cf06e4a218e0591d31e553a87fdc2d4065a534d40a339872b04de2b | | 30 | a521cde82c3c200f1b1087c9ec039546f943a90630f6d205020be81b7a06c7aa | | 35 | cb888994dd6c26c5ecae53d788b156d17f6d129f3a65c6cd04a604d621795e09 | | 40 | a36a78b4a0b9984af4535f9cc7d4fb21ddafdd7206a044270dfd38e39eadfad5 | | 50 | fcf8e09c0b1bee67cda55d3d8afa2151563c4bea1dd9451ca0d54de6b9bb9af5 | | 76 | e08535cdc3683d267198bca2a1fdf96440ca364c13ab9cc29cf7e1c4ae1a9aeb | | Pinned unmodified Starter | 8f2cea337f6736105f58c6758da6c3ebf002951d34492bcd5b7101bef2d1e7f3 | This analysis has no dynamic-code promotion table: **none were made**. The static 76 bps diagnostic is a better fallback than the starter on the held-out paths, but is far short of the supplied 528.40 leaderboard target. The board figure comes from the assignment, not a refreshed leaderboard measurement. Dynamic pseudocode below has not been scored or gas-validated as a Solidity implementation. ## 4. Dynamic fees after an arb, and conditional relaxation ### What is learned exactly If the AMM just bought X in a genuine, uncapped arb, or just sold X in one, respectively, \[ \hat p=s_{post}(1-bidFee_{used}),\qquad \hat p={s_{post}\over1-askFee_{used}}. \] Source: terminal-spot formulas in section 2, from [A], both compute functions, and [C], reserve updates. The fee **used for this trade** must have been saved before returning new fees. Using the newly calculated fee in this inversion is circular and wrong. Arbitrage is at most the first trade hitting our pool in a timestamp, because the engine completes its arb loop before generating the retail batch. Subsequent own trades in the same timestamp are certainly retail. But the first own trade may also be retail: our pool may not have had an arb, or prior retail may have gone entirely to the normalizer. Source: [E], `run`, L140–161; [R], `route_to_two_amms`, L194–246. No amount/size rule identifies arb perfectly because the lognormal distribution has positive density over every positive size. Let `m=log(s/p)=-z`. Approximate effective margins against fair are \[ d_a\simeq f_a+m,\qquad d_b\simeq f_b-m. \] Source: expand actual marginal ask `s/(1-fa)` and bid `s(1-fb)` from [C], quotes, L119–195. Buying X in arb leaves `m>0`; protecting the bid requires a **higher bid fee**, while the ask can be cheaper. Selling X in arb reverses the roles. A simple fair-centered target with effective half-margin `h` would set \[ f_b=\left[1-{\hat p\over s}(1-h)\right]_0^{0.1},\qquad f_a=\left[1-{s\over\hat p}(1-h)\right]_0^{0.1}. \] Source: set marginal bid to `p_hat(1-h)` and ask to `p_hat/(1-h)` in the same [C] quote formulas, then apply [B]'s allowed clamp. This is a useful parameterization, **not a proof that one common h is optimal**. The competing pool's relative quote matters separately on each side. ### Fees maximizing immediate expected retail edge For a fixed fair price and competitor state, use section 1's exact router separately on each side. If `q(f)` denotes Y input to an ask and `t(f)` denotes X input to a bid, the exact interior first-order conditions for one order are \[ {dE_a\over df}=q'\left(1-{pxgy\over(y+gq)^2}\right) +{pxqy\over(y+gq)^2}=0, \] \[ {dE_b\over df}=t'\left(p-{ygx\over(x+gt)^2}\right) +{ytx\over(x+gt)^2}=0. \] Source: differentiate [C]'s quotes and [E]'s edge formula, with allocation from [R]. For example `q=N/D`, `r'=-r/(2g)`, `N'=r'(y0+g0Q)`, `D'=-1+r'g0`, so `q'=(N'D-ND')/D²` on an unclipped branch; it is zero on a full/zero allocation branch. Integrate these conditions over [T]'s size distribution and compare boundary solutions to obtain the optimum. For a small-order transparent approximation, take equal local depth `B≈y≈px`, and competitor effective margin `c` on the direction considered. Then \[ q\simeq[Q/2+B(c-d)/4]_0^Q,\qquad E\simeq qd+q^2/B. \] Source: first-order expansion of [R]'s marginal-price equality and second-order expansion of [C]/[E]'s payoff. Without clipping and for deterministic Q, the first-order condition gives \[ d^*={c+2Q/B\over3}. \] Source: differentiate the preceding expression: `dE/dd=q/2-Bd/4`, then substitute `q`. For Q=20 and c=30 bps this is 23.33 bps. The exact heavy-tailed integration instead gives 25.33 bps; replacing the size distribution by its mean is not accurate enough. The table solves the **exact immediate-retail problem** by numerical integration, with both pools currently `(100,10000)` and fair price `100 exp(-m/10000)`. Each fee is optimized independently over its full immediate payoff; it assumes the competitor's spot equals our spot. Rates multiply rewards and do not affect this immediate optimizer. | m: spot above fair (log bps) | Immediate optimal bid fee (bps) | Immediate optimal ask fee (bps) | | --- | --- | --- | | 0 | 25.34 | 25.34 | | 10 | 29.86 | 21.93 | | 20 | 42.87 | 18.91 | | 30 | 58.63 | 16.15 | | 40 | 73.59 | 13.58 | | 60 | 102.05 | 8.88 | For example, after a 30 bps bid arb with both pools at the same post-arb spot, the immediate optimum is near **bid 58.7 / ask 16.1 bps**. These are conditional one-order/instantaneous-retail optima, not a claimed optimal future policy. Post-arb knowledge is exact only if the trade classification is correct. ### Future steps: the actual optimization problem Even knowing fair price now does not produce universal optimal fees for all following steps. A future fee choice changes arb triggers, pool state, the retail split, the next observation, and subsequent fee choices. The normalizer's state is hidden. The correct state is a posterior `b` over fair price, normalizer state, and market parameters, plus our observable reserves, current fees, and within-step phase. Let `h` denote that observable information. At an afterSwap decision the exact finite-horizon recursion is \[ V_t(h,b)=\max_{f_b,f_a\in[0,0.1]} E_b\left[\sum_{j:\,\text{until next own callback or end}}Edge_j +V_{t'}(h',\operatorname{Bayes}(b,o';f_b,f_a))\right]. \] Source: [E]'s phase order, [R]'s fee-dependent routing, [A]'s fee-dependent arb, [C], `update_fees`, L292–297, and [I], `afterSwap`, L26–30. A price step without a trade does not call the strategy. This Bellman equation specifies the maximizing fees; the immediate FOCs omit its continuation derivative. I have not solved this full partially observed control problem or proved that a 32-word approximation attains its optimum. A tractable one-step approximation adds predictive adverse-selection cost to the local retail objective. With fair-price posterior log variance `v_p`, set `w²=v_p+sigma²`. For each side, solve \[ \max_{f\in[0,0.1]}\quad J_{local}(f)={\lambda\over2}E_Q[q(Q,f)d(f)+q(Q,f)^2/B] -{B\over4}H(d(f),w). \] Source: section 1's expanded [R]/[C]/[E] retail reward and section 2's [A]/[P] Gaussian overshoot loss. Here `d=fa+m` on asks, `d=fb-m` on bids; a finer version uses exact log barriers and exact exponential losses. This is a specified approximation: it omits retail-induced future reserve changes and future information value, so it must be measured before promotion. For an interior allocation its first-order condition is \[ {\lambda\over2}E_Q[q+q_d(d+2q/B)] +{B\over2}\{w\phi(d/w)-d\Phi(-d/w)\}=0, \qquad q_d=-B/4. \] Source: differentiate the preceding source-derived objective; `H'(d,w)=2[d Phi(-d/w)-w phi(d/w)]` follows by differentiating the normal integral. Use `q_d=0` on clipped branches. The lognormal tail-moment formula in section 3 evaluates the expectations without a trade-history loop. This explicitly shows how uncertainty widens the effective margin, how competition controls volume, and why bid and ask fees should differ after an arb. ### No-arbitrage survival changes the fair-price posterior If there have been no own callbacks, our reserves and displayed fees have not changed. Ignoring for a moment the additional information in absent routed retail, the density of `z=log(p/s)` conditional on no arb at each intervening step obeys a **killed**, not reflected, transition: \[ \rho_{n+1}(z')={1_{[-b,a]}(z')\over Z_n} \int_{-b}^{a}\rho_n(z) {\phi((z'-z+\sigma^2/2)/\sigma)\over\sigma}\,dz. \] Source: [P], `step`, L62–65, and the [A] no-trade inequalities. `Z_n` normalizes survival. Reflection would describe observed repeated arbitrage resets; it is wrong for a path on which no arb happened. Conditioning only the final Gaussian to lie inside the band also misses possible earlier exits. Starting exactly after a bid arb with both old fees 30 bps, `z0=log(0.997)`. I propagated this discrete killed kernel on 1,000 cells, using sigma=0.00095. The following table conditions on no arb through n successive price steps and no reserve changes; m is minus the posterior mean log mispricing. It does not condition on the likelihood of absent routed retail. | Silent price steps n | Mean m (log bps) | Posterior log-price SD (bps) | Survival probability through n | | --- | --- | --- | --- | | 1 | 22.4668 | 5.7259 | 0.499811 | | 2 | 17.8494 | 8.3619 | 0.374768 | | 4 | 11.2495 | 11.8405 | 0.272373 | | 8 | 4.0888 | 14.3824 | 0.182896 | | 16 | 0.5134 | 14.8420 | 0.090277 | | 32 | 0.0186 | 14.8503 | 0.022141 | On the **first** surviving step, the price moves inward by about `sigma sqrt(2/pi)=7.58 bps`; mean m falls from 30.045 to 22.467 bps. Source: truncate the [P] Gaussian increment to the inward half-space in the preceding kernel. Over longer silence the posterior moves toward the band's center, rather than continuing to drift freely from the old arb boundary. For intuition only, the continuous Brownian killed-kernel approximation has band width `W=a+b`, center `cz=(a-b)/2`, quasi-stationary density proportional to `cos(pi(z-cz)/W)`, and \[ Var_{QS}(z)=W^2(1/4-2/\pi^2),\qquad E[z_n]-c_z\propto\exp[-3\pi^2\sigma^2n/(2W^2)]. \] Source: solve the absorbing-boundary Gaussian diffusion approximation to the [P]/[A] killed kernel; the first two eigenvalues are `pi² sigma²/(2W²)` and four times that. These are **my continuous-time approximation**, not exact Rust behavior. The discrete table is preferable when sigma/band-width is not tiny. With asymmetric fees the limiting center is not spot itself: translate `cz` back into fair price before recentering quotes. For a numerical response curve, insert those posterior means/variances into `J_local`, assume equal current pool spots (`m0=m`), and solve each scalar optimum. These are maximizing fees **within the local approximation**, not a solved Bellman policy; amounts are bps. | Surviving steps | Predictive SD w | Bid fee | Ask fee | | --- | --- | --- | --- | | 0 | 9.50 | 59.35 | 16.20 | | 1 | 11.09 | 47.94 | 18.27 | | 2 | 12.66 | 41.61 | 19.61 | | 4 | 15.18 | 33.66 | 21.76 | | 8 | 17.24 | 28.42 | 24.65 | | 16 | 17.62 | 26.72 | 26.25 | | 32 | 17.63 | 26.49 | 26.48 | Absence of own retail is also informative because routing depends on price. The exact silent-step posterior additionally weights by the likelihood that all retail legs routed away, approximately `exp(-lambda P(own leg executes | state))` if the state is frozen. Source: [T], Poisson generation, and [R], routing cutoff. A fully correct filter includes those zero-hit transitions and competitor uncertainty. **Operational limit:** fees cannot relax while no callbacks occur. They remain the last returned numbers. Only when an `afterSwap` arrives can the elapsed timestamp be used to update the posterior and return relaxed fees. Same-timestamp trades entail zero new price variance. Source: [C], `update_fees`, L292–297, called only from execution functions; [E], L136–161. A program that assumes a per-step timer inside Strategy is solving a different problem. ## 5. Is a fee below 30 bps worthwhile on the favorable side? Yes, in a meaningful conditional state, but zero is not generally best. Consider both pools at `(100,10000)`, fair price `99.7`, just after a genuine 30 bps bid arb. On the ask side, our spot is already above fair; the relevant margin is the ask execution price relative to **99.7**, not the nominal fee relative to 30 bps. For a Y allocation q, \[ E_a=q-{99.7\,100(1-f)q\over10000+(1-f)q}. \] Source: [C], `quote_x_for_y`, L175–195, and [E], retail edge, L153–161; allocation remains [R]'s ask split. The first two numeric columns below describe one 20 Y order. The final column is the expected favorable-side edge per step, `0.5 × 0.8 × E_Q[E_a]`, with the default size distribution and this state held fixed. | Ask fee (bps) | q from a 20 Y order | Edge on that order (Y) | Favorable-side expected gain/step | | --- | --- | --- | --- | | 0 | 17.5151 | 0.083077 | 0.04077460 | | 5 | 16.2657 | 0.083214 | 0.04194054 | | 10 | 15.0151 | 0.082414 | 0.04275476 | | 15 | 13.7632 | 0.080676 | 0.04313428 | | 20 | 12.5100 | 0.077997 | 0.04296612 | | 25 | 11.2557 | 0.074375 | 0.04210013 | | 30 | 10.0000 | 0.069810 | 0.04041630 | | 40 | 7.4849 | 0.057841 | 0.03652864 | At this state, 15 bps earns about **0.043134 Y/step** on that side, versus **0.040416** at 30 bps: an improvement of about **0.002718 Y/step**, or **6.7%**, in this conditional immediate calculation. Zero earns only about **0.040775**. Integrating the exact size distribution puts the optimum around **16.1 bps**; optimizing only a 20 Y order instead gives approximately 3.2 bps. The favorable price offset allows a lower fee to capture profitable volume, but heavy-tailed orders still support a positive fee. These differences cannot be multiplied by 10,000 and advertised as a strategy improvement: the state changes after every trade and price step. The answer also depends on the competitor's spot. Equal fees do not imply equal quotes when the spots differ; the general split in section 1 is the test. The favorable-side fee is constrained to be nonnegative, so an ideal fair-centered ask/bid may be unattainable by fees alone. Source: [R], both split functions, and [B], fee clamp. ## 6. Recommended policy, using only afterSwap information The recommendation is **a probabilistic fair-price filter plus separate fee/flow optimizers**, with 76 bps fixed fees as a measured static fallback. The source warrants asymmetric recentering and survival conditioning. It does not warrant treating every first trade as arb, setting the favorable fee to zero unconditionally, or adding an inventory penalty unrelated to this objective. Inventory is already represented through `s=y/x` and local depth; the engine scores edge without an explicit risk-aversion or inventory penalty. Source: [E], L140–161 and L196–204. The following is concrete mathematical pseudocode for a **candidate**, not validated Solidity. It fits within 14 persistent words: log fair-price mean, log variance, sigma², lambda, mean order size, initial k, last timestamp, last reserve X, last reserve Y, last bid fee, last ask fee, within-timestamp trade count, classification probability, initialized flag. Signed fixed-point values can occupy one word each. Constants and scratch arithmetic need no additional persistent slots. Source for the limit/inputs: [B], L30–33; [I], L6–12. If the old band has zero width, skip the eigenmode division: a full silent price step has probability zero in the continuous-price model. Use sigma=0.00095, lambda=0.8, M=20 initially; sigma's true per-run value is hidden. Those priors are identified here as assumptions rather than observations. **Inference details.** On a new timestamp, propagate the previous posterior through `dt-1` silent steps, then one unrestricted GBM step for the current possible-arb trade. For a gas-light prototype, use the last subsection's eigenmode mean/variance approximation for the silent interval, with its stated accuracy limits; do not iterate once per elapsed step. Apply the exact one-step truncated-normal moments for the current no-arb hypothesis: \[ \alpha={-b-\mu_z\over\sqrt v},\ \beta={a-\mu_z\over\sqrt v},\ Z=\Phi(\beta)-\Phi(\alpha), \quad\mu_R=\mu_z+\sqrt v\,{\phi(\alpha)-\phi(\beta)\over Z}, \] \[ v_R=v\left[1+{\alpha\phi(\alpha)-\beta\phi(\beta)\over Z} -\left({\phi(\alpha)-\phi(\beta)\over Z}\right)^2\right]. \] Source: integrate [P]'s normal transition over [A]'s no-arb band; these are derived conditional moments. When v is zero, retain the point mass if inside; when Z is numerically negligible, the retail hypothesis has negligible weight rather than dividing by zero. For the arb hypothesis, invert the old fee band to obtain `pA` as in section 4. The density of an arb's observed input quantity q under a prior log-price density `gP` is proportional to \[ L_A=g_P(\log p_A)\,|d\log p_A/dq|, \] with Jacobian `2 gb/(x_pre+gb q)` for an X-input bid and `2 ga/(y_pre+ga q)` for a Y-input ask. Source: differentiate [A]/[C]'s terminal-price mappings. This formula is only eligible for the first own trade of the timestamp. An implementable approximate retail likelihood is \[ L_R=(\lambda/2)\,Z\,\ell_{LN}(Q_{inferred};M,1.2) |dQ_{inferred}/dq|,\qquad \pi_A={L_A\over L_A+L_R}. \] Source: [T]'s side/size/arrival laws, [A]'s no-arb band, and inversion of [R]'s split. This is a **likelihood approximation**, not a calibrated exact posterior: it replaces the unobserved competitor by a point estimate and the batch by a local event intensity. Invert the interior split as `Q=[q(g+r g0)+u-r u0]/(r g0)`, with u the input-token reserve. On a full-allocation branch use `Q=q`, Jacobian 1. For bids, the inferred total X input is converted to Y notional using the current fair estimate. An exact implementation would integrate over the price and normalizer posterior, and account for skipped retail orders. A deterministic available-data proxy for the normalizer is to use the same initial k and log spot deviation `m0=clip(m,-h0,h0)`, `h0=-log(0.997)`, then infer its reserves from k and the proxy spot. This represents a normalizer near its most recent no-arb band; retail may move it outside that band, so this proxy must be measured rather than treated as an observed reserve. It never reads another contract or outside simulator state. ```text initialize(x, y): k = x*y; logP = log(y/x); variance = 0 sigma2 = 0.00095^2; lambda = 0.8; M = 20 save reserves, initialization flag, and time sentinel save and return bid = ask = 0.0076 afterSwap(isBuy, amountX, amountY, timestamp, reserveX, reserveY): oldBid, oldAsk = saved fees actually used sPre = savedY/savedX; sPost = reserveY/reserveX newStep = first callback ever OR timestamp > lastTimestamp if newStep: dt = elapsed steps (timestamp+1 for the first callback) # No own callback during the dt-1 complete silent steps: move log-price posterior toward the OLD band's center, using the killed-kernel eigenmode mean/variance approximation; keep this approximation bounded within the old band # Current step can contain an arb: logP -= sigma2/2; variance += sigma2 compute current truncated no-arb moments (muR, vR, Z) pA = sPost*(1-oldBid) if isBuy else sPost/(1-oldAsk) compute LA, LR above; piA = LA/(LA+LR) # If both likelihoods underflow, use a log-domain computation; # do not force classification solely from trade size. combine point mass log(pA) with the retail moments: logP = piA*log(pA) + (1-piA)*muR_absolute variance = (1-piA)*vR + piA*(1-piA)*(log(pA)-muR_absolute)^2 withinTimestampCount = 1 else: # Arb pass is already over: this own trade is certainly retail. # Fair price did not move; only our spot/inventory changed. piA = 0; withinTimestampCount += 1 m = log(sPost) - logP B = sqrt(k*exp(logP)) m0 = clip(m, -h0, h0) # explicit competitor approximation w = sqrt(variance + sigma2) for side in the TWO fixed directions, unrolled: d(f) = f+m on ask; f-m on bid c = 0.003+m0 on ask; 0.003-m0 on bid q(Q,f) = clip(Q/2 + B*(c-d(f))/4, 0, Q) maximize J_local(f) from section 4: use the section-3 closed lognormal truncated moments; solve its scalar FOC in a fixed, bounded root search; compare boundary/zero-flow solutions as well clamp chosen fee to [0, 0.1] save timestamp, post reserves, posterior, and returned fee pair return bid, ask ``` The two-direction iteration and a fixed root search are bounded arithmetic, with **no loop over trade history or over elapsed timestamps**. For example, unroll twelve root-refinement evaluations per side and compare endpoints; any non-monotone branch must be checked separately rather than assuming global concavity. Normal CDF/log/exp helpers would need fixed-cost approximations, and the resulting Solidity still needs the actual `amm-match validate` and 250,000-gas trade-call checks before use. The engine limit is in [G], L31–34. This report does not claim those implementation checks for pseudocode. Next implement and ablate the filter, competitor proxy, and continuation approximation, then compare any candidate to the best existing dynamic file on at least 2,000 fresh paired seeds. No dynamic gain is claimed here. ## Assumptions, limitations, and public write-ups 1. All code claims refer to the pinned commit, not moving `main`. I used executable Rust over README prose when they disagree. The Paradigm landing page could not be retrieved by the web tool; the pinned repository supplied the rules used here. 2. The equal-reserve tables additionally require `p=s`; Q=20 is an illustrative order, while integrated tables use the full `tau=1.2` lognormal. The 0.0001 routing cutoffs are omitted only in analytical integrals. No liquidity is assumed to grow from segregated fees. 3. Exact arb formulas assume the 99%-reserve ask cap is inactive. Quadratic overshoot losses and small-order fee optimizers are labeled approximations. Unconditional default loss cannot be inferred from sigma and band width without a state distribution. 4. Centered reset, clipped no-retail stationary, killed no-arb posterior, and full competition are four different models. Their tables must not be added or interchanged indiscriminately. The stationary and killed kernels freeze local liquidity and omit the extra information in absent routed retail. 5. Simulation parameter draws use the Python CLI's NumPy construction, not the Rust `HyperparameterVariance::apply` helper, which uses another RNG. The first-500 engine means, 2,000-path pairing, native component decomposition, exploratory grids, and held-out diagnostics are distinguished above. Grid selection is not a proof of the continuous or dynamic optimum; no server seeds were used. 6. The policy's filter compresses a posterior into mean/variance; true posteriors may be multimodal. First-trade arb classification, normalizer proxy, silent-interval approximation, and local continuation penalty are assumptions, fully disclosed in section 6. The recommended dynamic pseudocode has no measured score or validated gas result. 7. Sigma=0.00095 is the brief's representative value; CLI midpoint sigma is 0.000945. Rate and mean-size sensitivity experiments vary one parameter at a time and keep the other parameters nominal. Reported static effects are local research evidence, not leaderboard claims. 8. Public write-ups found and read: [Algebra's March 27, 2026 account][Algebra] discusses asymmetric protection, estimating hidden market conditions, and using block ordering; it does not publish enough policy parameters or source to reproduce its entry. Its page was readable through web retrieval, while direct HTTP fetch returned 403. [The HackMD analysis][Hack] discusses reflected diffusion and competitive flow; I use its questions as context, while correcting its edge signs, sell routing, and some displayed post-arb orientations against the pinned Rust. All numerical tables here were calculated independently. ## Pinned source index [R]: https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/amm_sim_rs/src/market/router.rs [A]: https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/amm_sim_rs/src/market/arbitrageur.rs [C]: https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/amm_sim_rs/src/amm/cfmm.rs [E]: https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/amm_sim_rs/src/simulation/engine.rs [P]: https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/amm_sim_rs/src/market/price_process.rs [T]: https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/amm_sim_rs/src/market/retail.rs [D]: https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/amm_competition/competition/config.py [M]: https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/amm_competition/competition/match.py [CLI]: https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/amm_competition/cli.py [B]: https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/contracts/src/AMMStrategyBase.sol [I]: https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/contracts/src/IAMMStrategy.sol [Starter]: https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/contracts/src/StarterStrategy.sol [G]: https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/amm_sim_rs/src/evm/strategy.rs [Algebra]: https://medium.com/@crypto_algebra/amm-fee-challenge-notes-from-our-run-on-optimization-arena-4e980dbbd699 [Hack]: https://hackmd.io/6i1LNsRGRJ27YqTHjfO04A