Inventory risk, priced into the curve.
The long version: why symmetric quoting loses money in a trend, what a reservation price does about it, and how it fits into a SwapVM instruction without touching a single official contract.
Quote symmetrically, and a trend grinds you down.
Every market maker's real enemy is inventory risk, not spread. Quote symmetrically around mid and a trending market fills you on one side over and over — you earn the spread on every fill and lose money on the position, because you're accumulating the asset that's falling.
Every professional desk fixes this with a reservation price: skew your quotes away from mid as inventory drifts from target. Avellaneda & Stoikov formalised it in 2008.
No on-chain venue had ever implemented it — because on a pool AMM the inventory belongs to the pool, not to any one maker. There's nothing for the formula to skew around.
Aqua exposes the one number the formula needs.
In Aqua, tokens never leave the maker's wallet. Multiple positions share the same approved balance, and aqua.safeBalances() reads a maker's real, live, individual inventory. By the time any opcode runs, that number is already sitting in the VM's balance registers — which is exactly the the reservation price needs.
is the maker's live Aqua balance minus their declared target. Everything else is a constant shipped with the strategy.
Same size. Same block. Different price.
Drag the drift below. As the position leans, the trade that would push it further gets worse, and the trade that would bring it back gets better — automatically, with no transaction in between.
And here it is, actually happening on Base Sepolia.
The lab above is a computed toy — it evaluates the formula in your browser. This is the same strategy shipped to a live chain and hit with 10 identical fills, each one leaning the position further out. The rate a taker got fell on every single one, exactly as the mechanism says it should.
| Tick | Drift q | Realised rate | Transaction |
|---|---|---|---|
| 00 | +0 | 0.0002310000 | 0x81bffe28… ↗ |
| 01 | +1 | 0.0001900000 | 0x2bffc6e0… ↗ |
| 02 | +2 | 0.0001570000 | 0x5bdc9bac… ↗ |
| 03 | +3 | 0.0001300000 | 0x5b6925f6… ↗ |
| 04 | +4 | 0.0001070000 | 0x7e494f44… ↗ |
| 05 | +5 | 0.0000880000 | 0x4de92c99… ↗ |
| 06 | +6 | 0.0000720000 | 0x41c470be… ↗ |
| 07 | +7 | 0.0000580000 | 0xe75ef088… ↗ |
| 08 | +8 | 0.0000460000 | 0x17bf1480… ↗ |
| 09 | +9 | 0.0000350000 | 0xb7982812… ↗ |
Honest caveat: the 10 fills plotted above are all exposed-side, so the curve measures only the side that gets worse. One covered-side fill was landed in the same run — the first this project has managed on chain, since that direction was mispriced and reverted on the previous router — but a single point is not a curve, so the covered line in the lab above is still what the formula computes rather than something these transactions demonstrate.
One instruction, in a slot that was already free.
SwapVM's opcode space is a fixed 256-slot table, banked by instruction family, dispatched by raw byte. 0x92 is a reserved-but-unallocated slot in the balances-tuning bank — the documented extension point, and exactly the right family: like the stock Dutch-auction and piecewise-scale instructions, it mutates the balance registers before a swap-curve opcode prices against them.
Nothing in the official contracts was modified. KeelRouter inherits the real AquaSwapVMRouter and intercepts one byte, delegating everything else to the stock dispatcher — so every program the Aqua SDK emits still runs byte-identically.
Quote and swap never disagree.
Reading live inventory at quote time makes divergence more likely, not less — a solver quotes one number, execution returns another, and the taker's slippage check reverts. So parity isn't asserted in a comment; it's fuzzed.
$ forge test [PASS] testFuzz_QuoteEqualsSwap_AcrossInventoryRange (runs: 2000) [PASS] test_QuoteEqualsSwap_AtSoftBoundExactly [PASS] test_QuoteEqualsSwap_PastSoftBound [PASS] test_InsolventSkew_SwapRevertsSafely Suite result: ok. 24 passed; 0 failed; 0 skipped
One pricing kernel. Two venues.
Re-centres the constant-product curve
reservationPriceWad + recenterBalances rewrite the balance registers before XYCSwap prices against them.
Overrides the LP fee per swap
halfSpreadWad + softBoundPenaltyBps feed beforeSwap's dynamic-fee override.
One AvellanedaStoikov.sol, imported unmodified by both.
The v4 hook reuses the kernel's spread and bound functions, not its curve-reshaping ones — a concentrated-liquidity pool has no balance pair to re-centre without reimplementing v4's own swap math. Each venue gets the subset that maps onto how it actually prices a fill.
A solver can't read a reserve that doesn't exist.
SwapVM liquidity is order-based and distributed — there's no single pool contract holding reserves. The subgraph reconstructs live reservation price and inventory from on-chain events: it decodes Aqua's Shipped payload, finds the InventorySkew opcode inside it, and re-runs the same formula the contract runs, ported to AssemblyScript. Query it and you get the number a live quote() would return.
{ keelPosition(id: $strategyHash) { currentBalanceAWad currentReservationPriceWad currentHalfSpreadWad boundWad } }

