Multi-Chain DEX Aggregation: How Lunark Finds the Best Swap Rates
March 27, 2025
A technical walkthrough of Lunark's DEX aggregation system, covering quote fetching across Uniswap, SushiSwap, Curve, PancakeSwap, and TraderJoe, fee tier optimization, and the challenges of building a multi-protocol swap system.
ListenReady
0:00
0:00
4:10
When users ask Lunark to swap tokens, they expect the best rate. But "best rate" is surprisingly complex. Different DEXes have different liquidity, fee structures, and routing algorithms. Uniswap V3 has multiple fee tiers. Some pairs only exist on certain protocols. And all of this varies by network.
Lunark's swap system handles this complexity by aggregating quotes from five DEX protocols across eight networks, then selecting the optimal route automatically.
Each protocol has different strengths. Uniswap V3's concentrated liquidity often provides better rates for popular pairs. Curve excels at stablecoin swaps. SushiSwap might have better liquidity for long-tail tokens.
Network selector supporting 8 EVM chains
Quote Aggregation
When the swap tool is invoked, it fetches quotes from available DEXes sequentially:
TypeScript
asyncfunctiongetSwapQuotes( fromToken:string, toToken:string, amount: bigint, chainId:number):Promise<SwapQuote[]>{const availableDexes =getDexesForChain(chainId);const quotes: SwapQuote[]=[];for(const dex of availableDexes){try{const quote =awaitgetQuoteFromDex(dex, fromToken, toToken, amount, chainId);if(quote && quote.outputAmount >0n){ quotes.push(quote);}}catch(error){// DEX might not support this pair, continue to next}}return quotes.sort((a, b)=>Number(b.outputAmount - a.outputAmount));}
Each DEX is queried one at a time. If one fails, the loop continues to the next DEX.
Uniswap V3 Fee Tier Handling
Uniswap V3 is unique because it has multiple fee tiers for each pair. While the protocol supports four tiers (0.01%, 0.05%, 0.3%, and 1%), the system tries three of them in a specific order: 0.3% (medium) first, then 0.05% (low), then 1% (high). It takes the first successful quote rather than comparing all options:
TypeScript
// Trial order: MEDIUM -> LOW -> HIGH (0.01% tier is not tried)constFEE_TIERS=[3000,500,10000];// 0.3%, 0.05%, 1%asyncfunctiongetUniswapV3Quote( fromToken:string, toToken:string, amount: bigint, chainId:number):Promise<SwapQuote |null>{const quoter =getQuoterContract(chainId);for(const feeTier ofFEE_TIERS){try{const output =await quoter.quoteExactInputSingle({ tokenIn: fromToken, tokenOut: toToken, amountIn: amount, fee: feeTier, sqrtPriceLimitX96:0,});// Return first successful quotereturn{ dex:'uniswap-v3', outputAmount: output.amountOut, feeTier, path:encodePath([fromToken, toToken],[feeTier]),};}catch{// Pool doesn't exist for this fee tier, try nextcontinue;}}returnnull;}
The 0.3% tier is tried first since it's the most common for typical trading pairs. The 0.01% tier is not checked as it's primarily used for stablecoin pairs with very high volume.
V2-Style DEX Quotes
SushiSwap, PancakeSwap, and TraderJoe use the simpler constant product formula. Quotes are straightforward:
Before swapping, the router contract needs approval to spend the user's tokens. The system checks allowance and prepares an approval transaction if needed:
TypeScript
asyncfunctionprepareSwap( fromToken:string, toToken:string, amount:string, context: ToolContext
):Promise<SwapResult>{const quotes =awaitgetSwapQuotes( fromToken, toToken,parseUnits(amount, fromDecimals), context.chainId
);if(quotes.length ===0){return{ error:'No liquidity found for this pair'};}const bestQuote = quotes[0];const routerAddress =getRouterAddress(bestQuote.dex, context.chainId);// Check if approval is neededconst tokenContract =newContract(fromToken,ERC20_ABI, provider);const allowance =await tokenContract.allowance(context.userAddress, routerAddress);let approvalTx =null;if(allowance < bestQuote.inputAmount){ approvalTx ={ to: fromToken, data: tokenContract.interface.encodeFunctionData('approve',[ routerAddress, MaxUint256,// Unlimited approval]),};}const swapTx =buildSwapTransaction( bestQuote, context.userAddress, Math.floor(Date.now()/1000)+20*60,// 20 min deadline50,// 0.5% slippage in basis points);return{ quote: bestQuote, approvalTx, swapTx, alternatives: quotes.slice(1,4),// Show top 3 alternatives};}
Presenting Options to Users
The AI agent doesn't just execute the best quote blindly. It presents the options and explains the tradeoffs:
TypeScript
functionformatQuoteResponse(result: SwapResult):string{const{ quote, alternatives }= result;let response =`Best rate found on ${quote.dex}:
`; response +=`${formatAmount(quote.inputAmount)} → ${formatAmount(quote.outputAmount)}\n`; response +=`Rate: 1 ${fromSymbol} = ${calculateRate(quote)}${toSymbol}\n\n`;if(alternatives.length >0){ response +='Other options:\n';for(const alt of alternatives){const diff =((Number(quote.outputAmount)-Number(alt.outputAmount))/Number(quote.outputAmount)*100).toFixed(2); response +=`• ${alt.dex}: ${formatAmount(alt.outputAmount)} (-${diff}%)\n`;}}if(result.approvalTx){ response +='\nToken approval required before swap.';}return response;}
This gives users visibility into what they're getting and why.
Slippage Protection
Slippage is the difference between expected and actual output. In volatile markets, prices can move between quote and execution. The system uses a default 0.5% slippage tolerance: