Search projects, hackathons, sponsors and tech

    ← All projects

    Keel

    On-chain Avellaneda-Stoikov pricing A SwapVM opcode on Aqua as a v4 dynamic-fee hook, via a subgraph

    Demo

    About

    Keel is the first on-chain market making position that prices its own inventory risk. Every market maker's real enemy is inventory, not spread. If you quote symmetrically around mid in a trending market, you are ground into holding more and more of the depreciating asset: you earn spread on every fill and still lose money on the position. Professional desks have solved this since 2008 with a reservation price (Avellaneda and Stoikov): as your inventory drifts away from target, you skew your quotes away from mid, so the trade that would hand you even more of what you already hold gets priced worse, and the trade that brings you back gets priced better. No on-chain venue has ever implemented this, and the reason is structural rather than an oversight. On a pool AMM the inventory belongs to the pool, not to any individual maker, so there is no "your position" for a reservation price formula to skew around. The usual workaround is a keeper bot that rebalances after the fact, which reacts late and pays gas to react. Aqua from 1inch is the first venue where this becomes possible. Tokens never leave the maker's own wallet, and aqua.safeBalances(maker, app, strategyHash, tokenIn, tokenOut) exposes a maker's real, live wallet inventory at quote time. That is a number no pool AMM design can offer. Keel is a custom SwapVM instruction that reads it, computes a reservation price skewed in proportion to how far the position leans, and quotes around that instead of raw mid. There is no keeper, no rebalancing transaction and no delay, because the defence is inside the pricing curve itself. Measured against an identical 120 fill adversarial flow, a stock constant product position ends at 0.2110 PnL down while Keel ends at 0.0620 down, an improvement of 0.1490. Both positions take on exactly the same inventory drift (token0 grows 84.39% for both); the entire difference is what each fill cost the taker. It is live on Base Sepolia against real Circle USDC and WETH, not mock tokens, with a shipped position that has taken ten exposed side fills and one covered side fill. The same pricing kernel also runs as a Uniswap v4 dynamic fee hook, so one set of maths serves two venues.

    How it's made

    The kernel. AvellanedaStoikov.sol is pure WAD fixed point maths: reservation price, half spread, soft bound penalty, and the balance re centring that applies all three. The full model's spread term needs a live order book arrival rate, which this build does not have, so that one term is replaced by a maker declared base spread. The inventory skew mechanism, which is the actually novel part, is implemented in full. Dropping the log and exp requirement meant plain WAD arithmetic was enough and no big number library was needed. The 1inch integration is the mechanism, not a wrapper. Keel is opcode 0x92, a reserved but unallocated slot in SwapVM's balance tuning family. KeelRouter inherits the real, unmodified AquaSwapVMRouter and overrides _runOpcode, which upstream declares internal virtual for exactly this purpose: it intercepts 0x92 and delegates every other opcode to super._runOpcode, the stock dispatcher. A test proves that append only property against the real router. The official swap-vm and aqua repositories are cloned verbatim and never patched. Reading the real source changed the design twice. There is no shared price register in SwapVM: Context.SwapRegisters is only {balanceIn, balanceOut, amountIn, amountOut}, and stock instructions "adjust price" by mutating balances in place before a curve instruction consumes them. An early sketch split reservation price, spread and soft bound across three sequential opcodes writing to a shared register; with no such register, the second and third would have read back already mutated balances instead of live inventory. All three are now composed into one atomic transform in a single instruction. Separately, no accessor for live inventory was needed at all, because SwapVM already populates the context from AQUA.safeBalances() before any opcode runs. The bug worth mentioning. The instruction originally keyed its decimal scaling, its inventory sign and its exposed side test to the swap's in and out sides, which is only correct in the A to B direction. Covered side fills reverted at any realistic size. The fix keys all three to tokenA and tokenB instead, which required adding tokenA to the shipped program and therefore a full redeploy, since KeelRouter inlines the instruction as an internal library. The first covered side fill this project ever landed came from that router: 0.0001 WETH in for 3.372388 USDC out. The Graph. A subgraph decodes Aqua's Shipped payload, locates the InventorySkew opcode inside it, and re runs the same Avellaneda Stoikov formula ported to AssemblyScript, so a solver querying it gets the same number a live quote() would return. This answers a real routability problem: SwapVM liquidity is order based and distributed, so there is no single reserve to read. Pushed and Pulled are handled alongside Swapped, because without them the indexed values were deltas since ship rather than balances and could read negative. All router deployments stay in the manifest so historical fills are never orphaned. The official Subgraph MCP endpoint is wired to the same schema, so an agent can ask for a position's live reservation price in natural language. Verified live: deployment QmRFBHuUtSAgDkrLhCe5Ae2cn1Nwp2sqKfeMVvBD5fvZog, no indexing errors, 21 positions and 88 fills indexed. Uniswap v4. KeelSkewHook reuses halfSpreadWad and softBoundPenaltyBps through the dynamic fee override returned from beforeSwap, found by reading LPFeeLibrary directly. It deliberately does not reuse the curve re centring functions: those are specific to SwapVM's balance pair representation, and v4's concentrated liquidity has no equivalent without reimplementing v4's own swap math. That seam is stated rather than hidden. Testing and tooling. 38 Foundry tests and 5 SDK tests pass. The most important is a 2000 run fuzz test asserting that quote() and swap() agree across the whole inventory range, since a maker who quotes one number and settles another is worse than useless. A known limit is tested rather than left as a surprise: re centring can, for a misconfigured strategy, produce a virtual balance exceeding real holdings, and the test asserts it reverts safely instead of mispaying. packages/strategy-sdk builds ship and dock calldata in TypeScript and is byte verified against fixtures generated by the Solidity itself. The Next.js console lets a maker ship a real position from the browser, showing the live quote curve and the literal SwapVM bytecode updating together as parameters change. Special note: The checked in on chain results were decoded from the router's own fill events rather than copied from Foundry's broadcast log. That log prints the simulation pass, whose amounts differ slightly from what actually settled; summing the decoded events reproduces aqua.safeBalances() exactly, which is what makes the file trustworthy during a demo when an RPC may be unreachable.