Keep your wallet. Put it to work anyway.
A budget layer for 1inch Aqua. Aqua lets one wallet back several trading strategies at once by shipping promises instead of deposits, and a promise can exceed what you actually hold, by design. Doca adds the missing check: every strategy carries a budget that cannot exceed your real balance, and the Harbormaster docks and re-ships when the balance moves.
Built at ETHGlobal Lisbon 2026.
Landing · Demo video, 3:30 · App · Deck · Design dossier · Agent surface · Brand
- The gap
- The design
- Ours and 1inch's
- Live market reference: Uniswap API
- Measurements
- Run it
- Tests
- Prior art
- Repo map
Aqua is a shared-liquidity settlement layer. A maker deposits nothing: they approve Aqua once, then
ship a promise: a record saying "this strategy may sell up to this much of my wallet, under these
price rules". Tokens move only during a fill, straight out of the maker's wallet. dock cancels the
promise. Nothing is ever custodied.
That is where the capital efficiency comes from, and it opens a failure mode nobody was handling.
Aqua.ship writes the promise without checking balance or allowance
(Aqua.sol:40-52), so promises can exceed what the maker holds.
When flow turns directional the wallet empties while every strategy's virtual book still advertises inventory. Takers get quotes that revert, and the maker's next refill is arbitraged at a stale price. The Aqua whitepaper names this in §3 and prescribes a manual fix:
"While Aqua doesn't automatically pause illiquid positions, Makers are strongly recommended to manually dock strategies that become chronically underfunded to prevent accumulating unfavorable price exposure."
One week before this hackathon, a 1inch-commissioned Dune study put numbers on the adjacent gap: 85% of tracked concentrated liquidity ($1.6B of $1.84B analyzed) was underutilized in H1 2026, and individually managed positions accounted for most of the attributed idle capital on Uniswap v3 (CoinDesk, 2026-07-18). That study measures manual LP management on a different venue, not Doca's failure mode directly. We cite it as evidence the underlying discipline (sizing a position to what you actually hold) is a gap the market already recognizes.
One invariant, two pieces.
The invariant. A promise may exceed your wallet. That is the point of Aqua. A budget may not. Every strategy carries a budget of what it is allowed to consume, sized against what the wallet really holds. Doca prices depletion on-chain as a budget drains and repairs allocations off-chain before underfunding turns into persistent failed quotes.
On-chain: InventorySkewProvider. An IProtocolFeeProvider plugged into SwapVM's stock
AquaDynamicProtocolFeeAmountIn instruction (opcode 30 at the pinned swap-vm#b44977a1; upstream
main has since re-banked the opcode space). The fee is flat while a budget is healthy
and rises quadratically as it drains. Three properties:
| Property | What it means |
|---|---|
| Directional | The fee reads the outgoing token only, so draining the scarce side costs progressively more while the direction that refills you stays at the base rate. Takers are paid, in price, to rebalance you. |
| De-leveraging | SwapVM pulls the surcharge from the maker's Aqua balance and forwards it to a recipient of the maker's choosing, so value leaves the shared pool exactly when oversubscription risk peaks. |
| No new inputs | It reads only AQUA.rawBalances, never the wallet balance or allowances, which is what the whitepaper says an app should price against. |
Off-chain: the Harbormaster. Watches every strategy a maker has shipped; when one goes under its waterline it docks it and re-promises against the balances the wallet holds now: the whitepaper's manual recommendation, automated.
Trust model. Demo: a local demo signer lets the Harbormaster act autonomously on camera. Real
connected wallet: the same actions currently require your signature. Production: a scoped session
key or smart-account module would authorize only dock, ship and waterline updates, with token
limits, budget limits and expiry: session keys under ERC-4337 accounts with ERC-7715/ERC-7710
style scoped, expiring, revocable grants. MCP: read-only observability (see mcp/), not
the transaction executor. It is a deterministic risk keeper, not an autonomous AI agent.
InventorySkewProvider prices cumulative depletion measured before a fill runs; it has no notion of
what the fill about to execute does to the budget it consumes. The single-large-fill test in
contracts/test/adversarial.test.ts documents the
consequence directly: one trade sized past the waterline settles at the pre-fill base rate, not the
max rate its own resulting state would justify. Closing that gap is a separate SwapVM instruction,
not a change to the pricing curve described above, and it does not exist yet.
// Not built. Sketch of a SwapVM instruction that checks a fill against the budget it is about to
// consume, instead of only pricing depletion measured before the fill runs.
interface IBudgetGuard {
enum OnBreach { Revert, CapOutput, PricePostTradeState }
// consumedAfterFill = consumed-before-fill + amountOut
// if consumedAfterFill > orderBudget, apply onBreach: revert, cap amountOut down to the
// budget, or reprice using the post-trade remaining fraction instead of the pre-fill one.
function checkFill(
bytes32 orderHash,
address maker,
address tokenOut,
uint256 amountOut,
OnBreach onBreach
) external returns (uint256 allowedAmountOut, uint32 postTradeFeeBps);
}A revert is the simplest option and the cheapest one gas-wise. A capped output changes the amount the taker actually receives mid-instruction, which SwapVM's current pipeline is not set up to do after a fee opcode has already run. Pricing the post-trade state is the closest match to what the single-large-fill test shows missing, but it needs the fee to be computed after the curve runs instead of before it, which is a reordering of the instruction pipeline, not a parameter change.
Everything that settles value is 1inch's own code; we add two contracts. All of it is deployed and live on Base mainnet:
| Component | Address on Base | Whose |
|---|---|---|
| Aqua registry | 0x4999…6d31 |
1inch, canonical, live |
AquaSwapVMRouter |
0xc717…B989 |
1inch code, unmodified, our deployment |
InventorySkewProvider |
0x768F…54D9 |
ours, 178 lines |
DocaApp |
0x8A15…9694 |
ours, 93 lines, builds the SwapVM program |
AquaAMM |
0x400a…8234 |
template code, our deployment |
Both of our contracts are source-verified with an exact bytecode match on Sourcify:
InventorySkewProvider ·
DocaApp, so the
source above is provably the code at those addresses.
Full addresses in web/src/deployment.base.json. The demo runs on
a fork of the same chain, so what you see in the video executes against these exact contracts.
Why our own router deployment. The live routers (12 chains, June, eip712Domain() reports
1.0.0) predate the order-data layout the hackathon template targets. On July 24 1inch updated
swap-vm-template to pin swap-vm#b44977a1 (release-1.2), which prefixes order data with the
40-byte token pair and adds taker flags, so orders built with the current template revert against
the June router by design. The template's own deploy flow ships a fresh router; we do the same and
wire it to the canonical live Aqua registry, which the bounty permits. Our extension point is
untouched by the skew: opcode 30 is _aquaDynamicProtocolFeeAmountInXD in both revisions.
The practice fork is frozen at a pinned block; the real market is not. Doca uses the Uniswap Trading API as its live Base mainnet reference so nothing the user sees is priced in a vacuum:
web/src/lib/uniswap-price.ts: the client:POST /v1/quote(EXACT_INPUT WETH→USDC, chain 8453, V3+V4). Handles both CLASSIC and X response shapes.web/src/App.tsx: the Harbormaster annotates every dock/re-ship decision with the live price (markRef), and the header pill shows the current Uniswap quote next to the fork state.web/src/LpDesk.tsx+web/src/lib/pnl.ts: positions are valued mark-to-market against the quote; the PnL chart and history ticks are denominated in it.web/plugins/lp-desk-dev.ts: dev proxy that injectsx-api-keyserver-side (the key never reaches the browser bundle) and polls spot for position history.
Developer feedback for the Uniswap team lives in FEEDBACK.md.
Paired control: two strategies shipped from the same wallet, same curve, same liquidity, same taker flow, one instruction of difference. Every number below reproduces from a script in this repo.
contracts/scripts/waterline-scenario.ts: 30 fills into a 100/100 strategy, curve capped at 50%:
| plain | with the skew | |
|---|---|---|
| Inventory left | 6.25 (6.3%) | 8.08 (8.1%) |
| Realized price for the LP | 16.0000 | 16.3187 |
| Pulled out of the shared pool | 0 | 362.54 |
29% more inventory standing, 1.99% better realized price on the drained leg. The 362 is de-leveraging, not profit: it moves from committed liquidity to free balance, and the maker owns both sides.
contracts/scripts/amplification-experiment.ts: trending market, price-sensitive ordinary
flow, and an arbitrageur that only trades when a quote is stale. Same market at 1x, 2x and 4x
amplification, with and without management:
| N | arm | flow fills | unhonored fills | LP end value | vs holding |
|---|---|---|---|---|---|
| 1 | unmanaged | 12 | 0 | 283.11 | -5.63% |
| 1 | managed | 12 | 0 | 283.11 | -5.63% |
| 2 | unmanaged | 24 | 0 | 266.22 | -11.26% |
| 2 | managed | 24 | 0 | 266.22 | -11.26% |
| 4 | unmanaged | 43 | 43 | 234.62 | -21.79% |
| 4 | managed | 40 | 0 | 241.05 | -19.65% |
- Unmanaged amplification publishes quotes it cannot honor. At 4x, 43 of 86 attempted fills reverted: the wallet was empty while every strategy still advertised inventory. With budgets: zero. That is binary, and it is the aggregator's problem as much as the maker's.
- Amplification multiplies impermanent loss close to linearly: -5.6%, -11.3%, -21.8% versus holding at 1x, 2x, 4x. Each strategy sells the same real inventory into the same move.
- Management recovers 2.74% of maker value at 4x, costing 3 of 43 fills of volume, because a taker who wanted to buy cheap walks away.
At 1x and 2x the mechanism does nothing, which is correct: budgets are never exhausted, so it costs nothing when it is not needed.
Everything runs against a fork of Base, where the canonical Aqua is live and WETH and USDC are the real token contracts. No funds are spent.
# 1. a node forking Base at a pinned block
anvil --fork-url https://mainnet.base.org --fork-block-number 49093600 --chain-id 8453
# 2. contracts: install, compile, test
cd contracts && yarn && npx hardhat test
# 3. deploy our side and seed the demo wallet
npx hardhat run scripts/deploy-for-web.ts --network localhost
# 4. the app
cd ../web && bun install && bun run dev # http://127.0.0.1:5273
# the same flow, headless
bun run scripts/smoke.tsThe hosted app at /app/ reads the same fork, so it needs a
node in reach; without one it says so instead of rendering empty state.
With your own wallet. If an injected wallet is present, a Connect wallet button appears and
your account becomes the maker: every ship, dock and waterline change is signed by it. On the
fork, one-click seeding (WETH + USDC + approvals) lets any empty account run the whole journey. The
demo signer stays the default when no wallet is installed, so the recorded flow is reproducible.
Other scripts: demo-flow-base.ts (the whole loop against the canonical registry),
waterline-scenario.ts and amplification-experiment.ts (the measurements above).
npx hardhat test runs 11, four of them the official template's own:
- a healthy budget quotes the base fee, so ordinary flow is untouched
- the surcharge ramps quadratically past the kink and caps at the waterline, asserted against the closed form rather than a magic number
- the fee is directional: taking the scarce leg is surcharged, refilling it is not
- identical taker flow through a skewed and a plain program, shipped from the same wallet, leaves strictly more inventory standing in the skewed one, and the exact surcharge lands outside the pool, asserted with real ERC-20 balance changes
Three more, in contracts/test/adversarial.test.ts, document
what InventorySkewProvider does and does not catch:
- a single fill that crosses the waterline in one shot settles at the pre-fill base rate, not the post-trade max rate its own resulting state would justify
- an external wallet transfer, made outside Aqua entirely, leaves the fee curve quoting the base
rate against a wallet it can no longer see accurately; the fill only fails once the ERC20
transfer underneath
Aqua.pull()runs out of real balance to move - two strategies shipped against the same wallet price independently, so one strategy's own budget can look nearly untouched while the wallet backing it has already been drained by the other
RiverSwap (1st, ETHGlobal New York 2026) uses the same extension point, an
IProtocolFeeProvider on opcode 30, with an auction deciding the fee: whoever wins an epoch pays
rent and sets feeBps. Same slot, opposite input. Theirs is an auction winner, ours is how much
budget is left. Theirs is market design, ours is balance-sheet risk.
progressiveFeeIn (opcode 37, FeeExperimental.sol) already makes draining a reserve cost
more, via dx_eff = dx / (1 + λ·dx/x) on balanceIn. It prices trade size against the inbound
reserve, so a thousand small fills drain you without it noticing, and it is not wired into
AquaOpcodes, so it is unreachable from the Aqua-backed router. We price cumulative consumption
against a budget. The two compose rather than compete.
| Path | What |
|---|---|
contracts/ |
InventorySkewProvider.sol, DocaApp.sol, tests, measurement scripts |
web/ |
the app: src/lib/doca.ts is every chain call the UI makes |
src/ |
scaffold for a future headless keeper, not wired into anything; the live Harbormaster keeper runs in web/src/App.tsx |
mcp/ |
read-only MCP server (bun mcp/server.ts): wallet, positions, health |
landing/, deck/, docs/ |
landing page, pitch deck, design dossier and agent docs |
site/ |
static site assembly and deploy (bun site/build.mjs, site/deploy.sh) |
Deeper documentation: DEV.md (working on it), DESIGN.md (the visual
system), BRAND.md (tokens, voice, marks).

