How Liquidity Aggregators Actually Work: A Developer's View from the Inside
How Liquidity Aggregators Actually Work: A Developer's View from the Inside
DeFi summer was winding down when I first had to integrate a liquidity aggregator for real. Not a tutorial. Not a hackathon. Production code that needed to move actual money.
I'd read all the docs. I thought I understood how it worked. You 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 route reverted after the first leg had already executed. The user got their ETH back eventually — we'd built a fallback — but there was a terrifying 30-second window where $4,000 was sitting in limbo inside a router contract that technically had no idea the trade had broken.
After that I started actually reading the source code instead of the docs.
What "Route Splitting" Really Means
When 1inch or Paraswap shows you that they found a better price by splitting your trade across three DEXes, most people imagine something like a clean parallel execution: 40% goes to Uniswap, 35% to Curve, 25% to Balancer, all at the same time.
The reality is messier.
Splits are executed sequentially within a single transaction, not in parallel. The router contract calls DEX A, then uses the output to call DEX B (if the path chains), or calls DEX B independently with a different portion of the input token. All of this happens inside one tx on-chain, which means it either all succeeds or it all reverts.
Except "all reverts" depends on how the router handles individual leg failures.
Some routers will try-catch each leg and continue if one fails, redistributing the expected output from the failed leg. Some will revert the whole transaction. Some will silently succeed with less output than quoted — giving you a worse execution than the quote suggested — with no error thrown. The user just gets less than expected.
When I was at DecenLabs, we built routing logic on top of several aggregators. The thing that took the longest to get right was defining what "success" meant for each integration. For aggregator A, success meant the transaction didn't revert. For aggregator B, success meant the output amount was within our slippage tolerance. These aren't the same check.
Why 1inch and Paraswap Make Different Choices
I spent a lot of time in 2021 reading the router contracts for both. They're solving the same problem with different priority orderings.
1inch's v3 router (the one I was working with) optimized heavily for execution completeness — it would rather execute the trade at a worse price than fail. The router would fallback through routes if primary routes failed. Great for users who want their trade to go through. Terrible if you're a protocol and you need to know exactly what you got.
Paraswap's approach at the time was stricter on output guarantees. The simpleSwap function enforced a minimum return that would cause a hard revert if not met. Cleaner for protocol integrations where you're doing accounting downstream based on the output amount. But it meant more failed transactions for end users in volatile conditions.
Neither is wrong. They're optimizing for different users.
For DeFi protocol integrations — which is what we were building — the Paraswap approach was actually easier to work with because failed transactions are easier to handle than transactions that succeed-but-silently-underperform. You can retry a failed transaction. You can't easily undo an accounting entry that was based on an optimistic output that didn't materialize.
The Slippage Tolerance Lie
This one still bothers me.
When a user sets 0.5% slippage tolerance on a DEX or aggregator, they expect to pay at most 0.5% more than the quoted price. That is not what the parameter does.
Slippage tolerance sets the minimum acceptable output amount relative to the quote. If quoted output is 1000 USDC and slippage is 0.5%, the minimum output encoded in the transaction is 995 USDC.
But the "quote" was computed at block N. By the time the transaction executes at block N+3 (or N+30 if the user has low gas), the pool state has changed. Prices move. Other transactions execute against the same pool. The "quote" is already stale before the transaction even hits the mempool.
In practice what this means: during calm markets, 0.5% slippage is fine. During volatile periods or for tokens with shallow liquidity, the actual execution price can drift much further from the quote than the slippage parameter implies, because the parameter isn't measuring "distance from market price" — it's measuring "distance from a stale quote computed 15 seconds ago."
For the protocol we were building at DecenLabs, this mattered because we were doing yield strategy rebalances where execution price directly affected APY calculations. We eventually moved to a TWAP-based minimum output for any rebalance over a certain size, ignoring the aggregator's quote entirely and computing our own acceptable minimum from a 30-minute oracle.
It was more engineering work. But it was honest about what we actually controlled.
The Bug That Taught Me The Most
Back to the broken trade I mentioned at the start.
We had a routing path that went ETH → USDC (Uniswap v2) → DAI (Curve 3pool). Two hops. The first hop succeeded, converting ETH to USDC. The second hop reverted because the Curve pool was in a transient state from a large trade that had just cleared — the price impact on the second leg was higher than our slippage setting.
The router contract we were using had partial-execution protection — it reverted the entire transaction if any leg failed. So the USDC from the first hop was refunded. Good.
But our backend was watching the original transaction hash and saw... nothing. The transaction reverted, so no events fired. No transfer events. No swap events. From our indexer's perspective, the transaction existed but did nothing.
We'd built our state machine to handle "confirmed" and "failed" transactions. A reverted transaction is technically "failed" — it just burns gas and does nothing. But we hadn't defined what that meant for a pending rebalance job. The job marked itself as failed, which triggered a retry. Which submitted the same transaction again.
The second attempt worked fine. But we now had a rebalance record marked as failed that had been retried successfully, with a separate successful transaction record. Reconciling that state took three hours.
The fix was simple once we understood the failure mode: treat any reverted aggregator transaction as a quote invalidation, re-fetch a fresh quote, and resubmit. Don't retry the same calldata — the conditions that made it fail are still there.
What I'd Do Differently
Four years later, having built a lot more on top of DEX infrastructure:
Use aggregators for UI, not protocol internals. For user-facing swaps where someone clicks "confirm" and wants the best price, aggregators are great. For protocol-internal swaps — rebalances, liquidations, automated strategies — you want direct pool integrations with explicit slippage math you control. The abstraction aggregators provide is exactly what you don't want when you need to reason about execution in your protocol's accounting.
Always test with mainnet forks at realistic block states. Slippage behavior, partial fills, route failures — none of this shows up in unit tests or on testnets. You have to fork mainnet at a specific block and simulate realistic trading conditions. We started doing this about six months in, after two production incidents.
Read the router source, not the API docs. Docs tell you what the happy path looks like. Source code tells you what happens when each step fails. For anything where execution guarantees matter, you have to read the contract.
Looking back, Oct 2021 was a good time to be building this stuff. The infrastructure was messy, the docs were incomplete, and there were real edge cases to discover. That's exactly the environment where you learn the most.