0xShikhar
Writing

How Liquidity Aggregators Actually Work: A Developer's View from the Inside

· 10 min read· DeFiShare on X ↗

DeFi summer was winding down when I first had to integrate a liquidity aggregator for real. Not a tutorial, not a hackathon. Production code moving actual money.

I'd read all the docs. I thought I understood it. Call the API, get a quote, pass the calldata to a router contract, done.

Then I watched a transaction fail because one leg of a three-way split reverted after the first leg had already executed. The user got their ETH back eventually (we had a fallback) but there was a nasty window where a few thousand dollars sat in limbo inside a router contract that technically had no idea the trade had broken.

After that I started reading source code instead of docs.

What route splitting actually means#

When 1inch or Paraswap says they found a better price by splitting your trade across three DEXes, most people picture clean parallel execution: 40% to Uniswap, 35% to Curve, 25% to Balancer, all at once.

The reality is messier. Splits run sequentially inside one transaction. The router calls DEX A, then uses the output to call DEX B if the path chains, or calls B independently with a different slice of the input. All inside one tx, so it either all succeeds or all reverts.

Except "all reverts" depends on how the router handles individual leg failures.

Some routers try-catch each leg and continue, redistributing expected output from the failed leg. Some revert the whole thing. Some silently succeed with less output than quoted and throw no error at all. The user just gets less than expected and the UI doesn't even apologize.

At DecenLabs we built routing on top of several aggregators, and the longest part was defining what "success" meant per integration. For aggregator A, success meant the tx didn't revert. For aggregator B, it meant output within our slippage tolerance. Not the same check. Not remotely.

Why 1inch and Paraswap choose differently#

I spent a lot of 2021 reading both router contracts. Same problem, different priority ordering.

1inch's v3 router (the one I worked with) optimized hard for execution completeness. It would rather fill your trade at a worse price than fail, and it would fall back through routes if primary ones broke. Great for users who want the trade to go through. Terrible if you're a protocol that needs to know exactly what it received.

Paraswap was stricter on output guarantees. simpleSwap enforced a minimum return that hard-reverted if unmet. Cleaner for protocol integrations doing downstream accounting on the output amount, but it meant more failed transactions for users in volatile conditions.

Neither is wrong. They optimize for different users.

For protocol integrations, the Paraswap approach was easier to build on. Failed transactions are easier to handle than transactions that succeed-but-silently-underperform. You can retry a failed tx. You can't easily undo an accounting entry built on an optimistic output that never materialized.

The slippage tolerance lie#

This one still bothers me.

When a user sets 0.5% slippage on an aggregator, they think they're capping how much worse than market price they can pay. That is not what the parameter does.

Slippage tolerance sets a minimum acceptable output relative to the quote. Quoted output 1000 USDC, slippage 0.5%, the minimum encoded is 995 USDC.

But the quote was computed at block N. By the time your tx executes at block N+3 (or N+30 on low gas), the pool state changed. Prices moved. Other transactions hit the same pool. The quote was stale before your tx even entered the mempool.

In practice: calm markets, 0.5% is fine. Volatile markets or shallow-liquidity tokens, actual execution can drift much further from market price than the parameter implies, because it measures distance from a stale quote, not distance from the market.

This mattered for us because we were doing yield strategy rebalances where execution price fed directly into APY calculations. We ended up ignoring the aggregator's quote for anything over a certain size and computing our own minimum from a 30-minute TWAP oracle instead.

More engineering. But it was honest about what we actually controlled.

The bug that taught me the most#

Back to that broken trade.

We had a route ETH → USDC (Uniswap v2) → DAI (Curve 3pool). Two hops. The first leg succeeded, ETH became USDC. The second reverted because the Curve pool was in a transient state from a large trade that had just cleared, and the price impact on leg two exceeded our slippage.

The router had partial-execution protection, so the whole tx reverted and the USDC refunded. Good.

But our backend was watching the transaction hash and saw nothing. A reverted tx fires no events. No transfers, no swaps. From the indexer's perspective the tx existed and did nothing.

Our state machine handled "confirmed" and "failed". A reverted transaction is technically "failed", it just burns gas and does nothing. But we never defined what that meant for a pending rebalance job. The job marked itself failed, which triggered a retry, which resubmitted the same calldata.

The second attempt worked. Now we had a rebalance record marked failed, retried successfully, plus a separate successful tx record. Reconciling that state took a chunk of an afternoon.

The fix was simple once we saw it: treat any reverted aggregator tx as a quote invalidation. Fetch a fresh quote, resubmit. Don't retry the same calldata, because the conditions that broke it are still there.

What I'd do differently#

A few years later, having built a lot more on DEX infrastructure:

Aggregators are for UI, not protocol internals. User clicks "confirm" and wants the best price, aggregators are great. Protocol-internal swaps (rebalances, liquidations, automated strategies) want direct pool integrations with slippage math you control. The abstraction aggregators provide is exactly what you can't afford when your accounting needs to reason about execution.

Test on mainnet forks at realistic block states. Slippage behavior, partial fills, route failures, none of it shows up in unit tests or on testnets. Fork mainnet at a specific block, simulate real trading conditions. We only started doing this after a couple of production incidents taught us the hard way.

Read the router source, not the API docs. Docs describe the happy path. Source describes what happens when each step fails. For anything where execution guarantees matter, you have to read the contract.

Looking back, late 2021 was a good time to be building this. The infra was messy, the docs were incomplete, the edge cases were real. Which is exactly where you learn the most.

↑ back to top
How Liquidity Aggregators Actually Work: A Developer's View from the Inside — Shikhar Singh — Shikhar Singh (@0xshikhar)