DEX Aggregation in VoidDex: Building an Optimal Route Finder
January 5, 2026
How VoidDex's backend finds the best swap routes across multiple DEXes, covering the quote service architecture, route optimization strategies, and fee calculation system.
ListenReady
0:00
0:00
4:00
Finding the best swap rate isn't as simple as checking one DEX. VoidDex aggregates quotes from multiple protocols, evaluates split routes, considers multi-hop paths, and calculates the optimal execution strategy. This article covers the backend architecture that makes this work.
VoidDex swap interface
Quote Service Architecture
The NestJS backend organizes quote logic into specialized services:
TypeScript
@Module({ imports:[ConfigModule, ProtocolModule, PoolModule], controllers:[QuoteController], providers:[ QuoteService,// Main entry point DexQuoteService,// Fetches quotes from DEXes LiquidityGraphService,// Builds graph of available liquidity PathfinderService,// Finds optimal paths through the graph RouteQuoteService,// Gets quotes for discovered routes RouteOptimizerService,// Optimizes split routing FeeCalculatorService,// Calculates fees in WETH PriceService,// Token price lookups], exports:[QuoteService],})exportclassQuoteModule{}
The routing architecture uses a dual-flow approach: a primary pathfinder flow for discovering multi-hop routes, with a fallback to direct DEX quotes for simple pairs.
Dual-Flow Quote Architecture
VoidDex implements a sophisticated two-tier routing system:
TypeScript
@Injectable()exportclassQuoteService{constructor(privatereadonly dexQuoteService: DexQuoteService,privatereadonly routeOptimizer: RouteOptimizerService,privatereadonly feeCalculator: FeeCalculatorService,privatereadonly priceService: PriceService,privatereadonly pathfinderService: PathfinderService,privatereadonly routeQuoteService: RouteQuoteService,){}asyncgetQuote(params: QuoteParams):Promise<QuoteResponse>{const{ chainId, fromToken, toToken, amount, slippage =0.5}= params;// Resolve token addresses and get providerconst provider =this.getProvider(chainId);const fromTokenAddress =this.resolveTokenAddress(chainId, fromToken);const toTokenAddress =this.resolveTokenAddress(chainId, toToken);const amountIn =parseUnits(amount, fromDecimals);// === PRIMARY FLOW: Dynamic Route Discovery ===// 1. Discover all possible routes using pathfinderconst discoveredRoutes =awaitthis.pathfinderService.findRoutes( chainId, fromTokenAddress, toTokenAddress,);// 2. If routes found, get quotes and optimizeif(discoveredRoutes.length >0){const routeQuotes =awaitthis.routeQuoteService.getQuotesForRoutes( chainId, provider, discoveredRoutes, amountIn, toDecimals,);if(routeQuotes.length >0){// Try route-level split optimizationconst splitResult =this.optimizeRouteSplit(routeQuotes, amountIn,...);if(splitResult){returnthis.buildSplitRouteResponse(...);}}}// === FALLBACK: Direct DEX Quotes ===const dexQuotes =awaitthis.dexQuoteService.fetchAllQuotes( chainId, provider, fromTokenAddress, toTokenAddress, amountIn,...);// Find optimal route (single or split)const optimalRoute =this.routeOptimizer.findOptimalRoute(dexQuotes,...);// Calculate fees and return responseconst fees =awaitthis.feeCalculator.calculateFees({...});return{...};}}
This dual-flow design ensures:
Multi-hop discovery: The pathfinder can find routes like WBTC -> WETH -> USDC when no direct pool exists
Fallback reliability: Direct DEX quotes work for simple pairs even if the graph is incomplete
Split optimization: Both flows support splitting across multiple routes for better execution
RouteQuoteService: The Route Pricing Engine
The RouteQuoteService is critical for getting accurate quotes on discovered routes. It simulates each hop in sequence:
TypeScript
@Injectable()exportclassRouteQuoteService{/**
* Get quotes for all discovered routes
*/asyncgetQuotesForRoutes( chainId:number, provider: PublicClient, routes: DiscoveredRoute[], amountIn: bigint, toDecimals:number,):Promise<RouteQuote[]>{const quotePromises = routes.map((route)=>this.getQuoteForRoute(chainId, provider, route, amountIn, toDecimals));const results =awaitPromise.all(quotePromises);// Filter out failed quotes and sort by output (best first)const validQuotes = results
.filter((q): q is RouteQuote => q !==null).sort((a, b)=>(b.amountOut > a.amountOut ?1:-1));return validQuotes;}/**
* Get quote for a single route by simulating each hop
*/privateasyncgetQuoteForRoute( chainId:number, provider: PublicClient, route: DiscoveredRoute, amountIn: bigint, toDecimals:number,):Promise<RouteQuote |null>{try{const hopsData: HopQuoteData[]=[];let currentAmount = amountIn;// Simulate each hop in sequencefor(const hop of route.hops){const hopQuote =awaitthis.getHopQuote(chainId, provider, hop, currentAmount);if(!hopQuote){returnnull;// Route fails if any hop fails} hopsData.push(hopQuote); currentAmount = hopQuote.amountOut;// Output becomes next hop's input}return{ route, amountIn, amountOut: currentAmount, amountOutFormatted:formatUnits(currentAmount, toDecimals), priceImpact:this.estimatePriceImpact(route.totalHops, amountIn), estimatedGas: route.estimatedGas, hopsData,};}catch(error){returnnull;}}/**
* Get quote for a single hop (V2 or V3)
*/privateasyncgetHopQuote( chainId:number, provider: PublicClient, hop: RouteHop, amountIn: bigint,):Promise<HopQuoteData |null>{const dexInfo =DEX_INFO[hop.dexId];const dexContracts =DEX_CONTRACTS[chainId]?.[hop.dexId];if(dexInfo.type ==='amm_v3'){returnawaitthis.getV3HopQuote(provider, dexContracts.quoter!, hop, amountIn);}elseif(dexInfo.type ==='amm_v2'){returnawaitthis.getV2HopQuote(provider, dexContracts.router, hop, amountIn);}returnnull;}}
The hop-by-hop simulation ensures accurate output calculations for complex multi-hop routes, where each intermediate swap's output becomes the next swap's input
DEX Quote Fetching
Each DEX requires different query methods. The service abstracts these differences:
Sometimes the best rate requires intermediate tokens. The pathfinder discovers these:
TypeScript
@Injectable()exportclassPathfinderService{// Common bridge tokens for multi-hopprivatereadonly bridgeTokens: Record<number,string[]>={1:['WETH','USDC','USDT','DAI','WBTC'],// Ethereum137:['WMATIC','USDC','USDT','WETH'],// Polygon42161:['WETH','USDC','USDT','ARB'],// Arbitrum};asyncfindPaths( chainId:number, fromToken:string, toToken:string):Promise<TokenPath[]>{const bridges =this.bridgeTokens[chainId]||[];const paths: TokenPath[]=[];for(const bridge of bridges){const bridgeAddress =this.resolveToken(chainId, bridge);// Skip if bridge is one of the swap tokensif( bridgeAddress.toLowerCase()=== fromToken.toLowerCase()|| bridgeAddress.toLowerCase()=== toToken.toLowerCase()){continue;}// Check if path existsconst hasFirstHop =awaitthis.poolExists(chainId, fromToken, bridgeAddress);const hasSecondHop =awaitthis.poolExists(chainId, bridgeAddress, toToken);if(hasFirstHop && hasSecondHop){ paths.push({ tokens:[fromToken, bridgeAddress, toToken], description:`${this.getSymbol(fromToken)} → ${bridge} → ${this.getSymbol(toToken)}`,});}}return paths;}privateasyncpoolExists( chainId:number, tokenA:string, tokenB:string):Promise<boolean>{// Check Uniswap V3 factoryconst factory =this.getUniswapFactory(chainId);for(const fee of[500,3000,10000]){const pool =await factory.getPool(tokenA, tokenB, fee);if(pool !== ethers.constants.AddressZero){returntrue;}}returnfalse;}}
Route Optimization
With all quotes collected, the optimizer finds the best execution strategy:
TypeScript
@Injectable()exportclassRouteOptimizerService{optimize( quotes: DexQuoteResult[], options: OptimizeOptions
): OptimalRoute {// Strategy 1: Best single DEXconst bestSingle =this.findBestSingle(quotes);// Strategy 2: Split across top DEXesconst bestSplit =this.findBestSplit(quotes, options);// Strategy 3: Best multi-hop (already in quotes)const bestMultiHop =this.findBestMultiHop(quotes);// Compare and return bestconst candidates =[bestSingle, bestSplit, bestMultiHop].filter(Boolean);returnthis.selectBest(candidates);}privatefindBestSingle(quotes: DexQuoteResult[]): OptimalRoute |null{const directQuotes = quotes.filter(q =>!q.isMultiHop);if(directQuotes.length ===0)returnnull;const best = directQuotes[0];// Already sorted by outputreturn{ type:'single', steps:[{ dexId: best.dexId, percentage:10000, minAmountOut: best.amountOut, dexData: best.extraData,}], totalOutput:BigInt(best.amountOut), isSplit:false,};}privatefindBestSplit( quotes: DexQuoteResult[], options: OptimizeOptions
): OptimalRoute |null{const directQuotes = quotes.filter(q =>!q.isMultiHop);if(directQuotes.length <2)returnnull;// Try different split ratiosconst splitRatios =this.generateSplitRatios(options.maxSplits);let bestSplit: OptimalRoute |null=null;for(const ratio of splitRatios){if(ratio.length > directQuotes.length)continue;// Calculate output for this splitlet totalOutput =0n;const steps =[];for(let i =0; i < ratio.length; i++){const quote = directQuotes[i];const percentage = ratio[i];// Estimate output for partial amount// This is simplified; real implementation would re-query with partial amountsconst partialOutput =(BigInt(quote.amountOut)*BigInt(percentage))/10000n; totalOutput += partialOutput; steps.push({ dexId: quote.dexId, percentage, minAmountOut: partialOutput.toString(), dexData: quote.extraData,});}if(!bestSplit || totalOutput > bestSplit.totalOutput){ bestSplit ={ type:'split', steps, totalOutput, isSplit:true,};}}return bestSplit;}privategenerateSplitRatios(maxSplits:number):number[][]{// Common split patternsreturn[[5000,5000],// 50-50[6000,4000],// 60-40[7000,3000],// 70-30[5000,3000,2000],// 50-30-20[4000,3000,3000],// 40-30-30].filter(r => r.length <= maxSplits);}privatefindBestMultiHop(quotes: DexQuoteResult[]): OptimalRoute |null{const multiHopQuotes = quotes.filter(q => q.isMultiHop);if(multiHopQuotes.length ===0)returnnull;const best = multiHopQuotes[0];return{ type:'multi-hop', steps: best.hops.map((hop, i)=>({ dexId: hop.dexId, tokenOut: hop.tokenOut, minAmountOut: hop.expectedOutput, dexData: hop.extraData,})), totalOutput:BigInt(best.amountOut), isSplit:false, isSequential:true,};}privateselectBest(candidates: OptimalRoute[]): OptimalRoute {// Simple: pick highest output// Could factor in gas costs, execution complexityreturn candidates.reduce((best, current)=> current.totalOutput > best.totalOutput ? current : best
);}}
Fee Calculation
All fees in VoidDex are denominated in WETH. The fee calculator handles two components: the protocol fee and the broadcaster fee.
TypeScript
@Injectable()exportclassFeeCalculatorService{privatereadonlyVOIDDEX_FEE_BPS=5;// 0.05% of input amountprivatereadonlyBROADCASTER_PROFIT_MARGIN=15;// 15% profit margin on gasconstructor(privatereadonly priceService: PriceService,privatereadonly gasService: GasService,){}asynccalculate(inputAmount:string, chainId:number):Promise<FeeBreakdown>{// Protocol fee: 0.05% of INPUT amount (not output)const inputBigInt =BigInt(inputAmount);const voidDexFee =(inputBigInt *BigInt(this.VOIDDEX_FEE_BPS))/10000n;// Broadcaster fee: estimated gas cost + 15% profit marginconst estimatedGas =awaitthis.gasService.estimateSwapGas(chainId);const gasPrice =awaitthis.gasService.getGasPrice(chainId);const gasCostWei = estimatedGas * gasPrice;const broadcasterFee =(gasCostWei *BigInt(100+this.BROADCASTER_PROFIT_MARGIN))/100n;// Convert protocol fee to WETH if input token is not WETHconst voidDexFeeWeth =awaitthis.priceService.convertToWeth( voidDexFee, inputAmount, chainId
);const totalFeeWeth = voidDexFeeWeth + broadcasterFee;return{ broadcasterFee: broadcasterFee.toString(), voidDexFee: voidDexFeeWeth.toString(), totalFeeWeth: totalFeeWeth.toString(), voidDexFeeBps:this.VOIDDEX_FEE_BPS,};}}
Key points about the fee structure:
VoidDex Protocol Fee: 0.05% (5 basis points) calculated on the INPUT amount, not the output
Broadcaster Fee: Covers gas costs plus a 15% profit margin for the transaction broadcaster
All fees are in WETH: Regardless of the input/output tokens, fees are always denominated in WETH
Price Impact Calculation
Price impact is estimated using heuristics based on trade size in USD. The RouteOptimizerService uses a tiered linear interpolation approach:
TypeScript
/**
* Estimate price impact based on trade size
*/estimatePriceImpact(amountUsd:number):number{if(amountUsd <1000)return0.1;if(amountUsd <10000)return0.2+(amountUsd -1000)*0.00001;if(amountUsd <100000)return0.3+(amountUsd -10000)*0.000005;if(amountUsd <1000000)return0.8+(amountUsd -100000)*0.000002;return3;// Cap at 3%}
$100,000 - $1,000,000: 0.8% - 2.6% (larger trades face more impact)
Over $1,000,000: Capped at 3%
For multi-hop routes, the RouteQuoteService has a separate heuristic that factors in hop count:
TypeScript
/**
* Estimate price impact based on hops and amount
*/privateestimatePriceImpact(hops:number, amountIn: bigint):number{// Base impact per hopconst baseImpact =0.3;// Additional impact for larger amounts (simplified)const amountFactor =Number(amountIn)/1e18;const amountImpact = Math.min(amountFactor *0.001,1);return baseImpact * hops + amountImpact;}
This estimates 0.3% base impact per hop, plus an amount-based factor that scales with trade size (capped at 1% additional).
Response Formatting
Swap details showing quote response
The final quote response provides everything the frontend needs:
TypeScript
interfaceFeeBreakdown{ broadcasterFee:string;// Gas cost + 15% margin (in WETH) voidDexFee:string;// 0.05% of input (in WETH) totalFeeWeth:string;// Sum of all fees (in WETH) voidDexFeeBps:number;// Fee rate in basis points (5)}interfaceQuoteResponse{ fromToken:string; toToken:string; fromAmount:string; toAmount:string; route:{ steps: RouteStep[]; totalSteps:number; isSplit:boolean; isSequential?:boolean;}; fees: FeeBreakdown; meta:{ priceImpact:string; exchangeRate:string; minReceived:string; expiresAt:number;// Unix timestamp};}// Example response{"fromToken":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","toToken":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48","fromAmount":"1000000000000000000","toAmount":"3245670000","route":{"steps":[{"dexId":"uniswap-v3","percentage":10000,"minAmountOut":"3245670000","dexData":{"feeTier":500}}],"totalSteps":1,"isSplit":false},"fees":{"broadcasterFee":"2300000000000000","voidDexFee":"500000000000000","totalFeeWeth":"2800000000000000","voidDexFeeBps":5},"meta":{"priceImpact":"0.05","exchangeRate":"3245.67","minReceived":"3229.45","expiresAt":1705430400000}}
The example response shows a single-DEX route using Uniswap V3. The fees are denominated in WETH (wei units), with the broadcaster fee covering gas costs plus a 15% profit margin.
The frontend uses this to display the route visualization, show the user what they'll receive, and encode the on-chain transaction.