Developing a Multi-DEX Order Routing System
When 1inch launched, it showed a simple idea: if you're only looking for the best price on Uniswap, you're leaving money on the table. Over the years, aggregators evolved into complex systems with split routing, multi-hop routes, and specialized optimization algorithms. But the core idea hasn't changed: a router's job is to find the path from token A to token B with minimal losses at a given volume.
Developing your own routing system is necessary when: standard aggregators (1inch, Paraswap, 0x) don't support your chain; you need custom protocol integration; you need control over liquidity sources; or existing APIs are too slow for a trading bot.
Liquidity Graph as Routing Foundation
Routing is a pathfinding problem in a weighted directed graph. Vertices are tokens. Edges are pools (each pool creates two directed edges: A→B and B→A with the price in each direction).
To find the best path at fixed amountIn — the task is pathfinding with maximum product of exchange rates (or equivalently — minimum sum of negative logarithms). This is a modification of Bellman-Ford or Dijkstra's algorithm.
But there's a nuance that makes it harder: price in a pool depends on volume. For amountIn = 100 USDC the best route might be Uniswap V3 0.05% pool. For amountIn = 1,000,000 USDC that same pool gives 3% slippage, while splitting between several pools gives 0.3%.
This turns pathfinding in a graph with fixed weights into an optimization task with volume-dependent weights.
Split Routing Algorithm
For large orders, the optimal solution is not a single route, but volume distribution across multiple paths.
Approach through binary search for optimal split between two routes:
function findOptimalSplit(
routeA: Route,
routeB: Route,
totalAmount: bigint,
steps: number = 20
): { splitA: bigint; splitB: bigint; totalOut: bigint } {
let bestSplit = { splitA: 0n, splitB: totalAmount, totalOut: 0n }
for (let i = 0; i <= steps; i++) {
const fraction = i / steps
const amountA = BigInt(Math.floor(Number(totalAmount) * fraction))
const amountB = totalAmount - amountA
const outA = amountA > 0n ? simulateRoute(routeA, amountA) : 0n
const outB = amountB > 0n ? simulateRoute(routeB, amountB) : 0n
const totalOut = outA + outB
if (totalOut > bestSplit.totalOut) {
bestSplit = { splitA: amountA, splitB: amountB, totalOut }
}
}
return bestSplit
}
For N routes, the problem becomes N-dimensional optimization — gradient descent or Nelder-Mead with constraints (sum of fractions = 1, all fractions ≥ 0).
Pool Simulation: Accuracy vs Speed
Uniswap V2: Exact Formula
function getAmountOutV2(amountIn: bigint, reserveIn: bigint, reserveOut: bigint): bigint {
const amountInWithFee = amountIn * 997n
const numerator = amountInWithFee * reserveOut
const denominator = reserveIn * 1000n + amountInWithFee
return numerator / denominator
}
Uniswap V3: Tick Traversal
V3 requires iterating through tick bitmap to find nearest active ticks. Full simulation is accurate but slow — several milliseconds for large swaps with traversal through many ticks.
For quick estimates (when screening routes) we use approximation through current sqrtPriceX96 and liquidity without tick traversal — accurate for small volumes, with error for large ones. Run full simulation only for final candidates.
Curve StableSwap: Iterative Formula
Curve uses the invariant A * n^n * sum(x_i) + D = A * D * n^n + D^(n+1) / (n^n * prod(x_i)). Calculating amountOut is iterative (Newton's method). For JavaScript/TypeScript — BigInt arithmetic with 18-decimal precision.
Balancer WeightedPool
Balancer with weighted pools (e.g., 80/20 BAL/ETH) uses a different invariant. getAmountOut depends on token weights in the pool — more complex formula than V2.
On-Chain vs Off-Chain Routing
Routing can happen entirely on-chain (smart contract finds route in the transaction) or off-chain (computations off-chain, result passed to contract).
On-chain routing: full transparency, no aggregator manipulation possible. Problem: limited gas, can't check all routes. Used for simple cases (2–3 pools maximum).
Off-chain routing (1inch, Paraswap approach): backend computations, contract gets ready-made route. Contract only executes. More gas-efficient, more complex routes. Risk: backend may return suboptimal route. Protection through slippage protection: minAmountOut in transaction guarantees minimum to user.
Router Contract: Executing Complex Routes
The contract must support heterogeneous routes: partly through Uniswap V2, partly V3, partly Curve.
struct SwapStep {
address pool;
address tokenIn;
address tokenOut;
uint24 fee; // For V3
uint8 dexType; // 0=V2, 1=V3, 2=Curve, 3=Balancer
bytes extraData; // Additional parameters per DEX type
}
function multiSwap(
SwapStep[] calldata steps,
uint256 amountIn,
uint256 minAmountOut,
address recipient
) external returns (uint256 amountOut) {
IERC20(steps[0].tokenIn).transferFrom(msg.sender, address(this), amountIn);
uint256 currentAmount = amountIn;
for (uint256 i = 0; i < steps.length; i++) {
currentAmount = _executeStep(steps[i], currentAmount);
}
require(currentAmount >= minAmountOut, "Slippage exceeded");
IERC20(steps[steps.length-1].tokenOut).transfer(recipient, currentAmount);
return currentAmount;
}
_executeStep dispatches to specific DEX implementation per dexType. Each implementation is a separate library (Solidity library pattern) to save bytecode size.
Pool State Cache
For fast routing without RPC calls for each request, an in-memory cache of current pool state is needed:
WebSocket subscriptions to Sync events (V2 pools) and Swap events (V3 pools) via eth_subscribe("logs"). Each event updates reserves/sqrtPrice in memory.
For 500–1000 active pools this is ~50–100 events/block on Ethereum mainnet. Process via event-driven architecture (Node.js EventEmitter or Rust tokio channel) with ≤1ms update latency.
Cold start: when launching the service, load current state of all pools via multicall. For 1000 pools — 5–10 multicall transactions (up to 200 calls each), takes 1–3 seconds.
Architectural Approach Comparison
| Approach | When It Fits | Complexity | Latency |
|---|---|---|---|
| Simple multi-hop | 3–5 chains, top-5 DEXes | Low | 200–500ms |
| Split routing | Large orders ($50K+) | Medium | 500ms–1s |
| With pool cache | Trading bot, < 50ms | High | 10–50ms |
| On-chain router | Maximum transparency | Medium | 1 block |
Work Process
Analytics (1–2 days). List of target DEXes and chains, latency requirements, expected order volume.
Routing engine development (5–7 days). Pool graph, pathfinding algorithm, pool simulation, split routing.
Router contract (3–5 days). Multi-step execution, Solidity, Foundry fork-tests.
Cache and infrastructure (3–5 days if needed). WebSocket subscriptions, in-memory pool cache.
Timeline Guidelines
A basic off-chain router through 3–5 DEXes with simple multi-hop — 1 week. A complete system with split routing, 500+ pool cache, custom router contract supporting V2/V3/Curve/Balancer — 2–3 weeks.







