DEX Aggregation in VoidDex: Building an Optimal Route Finder

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](/images/menu/void-dex/swap.jpg)

## Quote Service Architecture

The NestJS backend organizes quote logic into specialized services:

```typescript:src/modules/quote/quote.module.ts
@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],
})
export class QuoteModule {}
```

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:src/modules/quote/quote.service.ts
@Injectable()
export class QuoteService {
constructor(
private readonly dexQuoteService: DexQuoteService,
private readonly routeOptimizer: RouteOptimizerService,
private readonly feeCalculator: FeeCalculatorService,
private readonly priceService: PriceService,
private readonly pathfinderService: PathfinderService,
private readonly routeQuoteService: RouteQuoteService,
) {}

async getQuote(params: QuoteParams): Promise<QuoteResponse> {
const { chainId, fromToken, toToken, amount, slippage = 0.5 } = params;

// Resolve token addresses and get provider
const 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 pathfinder
const discoveredRoutes = await this.pathfinderService.findRoutes(
chainId,
fromTokenAddress,
toTokenAddress,
);

// 2. If routes found, get quotes and optimize
if (discoveredRoutes.length > 0) {
const routeQuotes = await this.routeQuoteService.getQuotesForRoutes(
chainId,
provider,
discoveredRoutes,
amountIn,
toDecimals,
);

if (routeQuotes.length > 0) {
// Try route-level split optimization
const splitResult = this.optimizeRouteSplit(routeQuotes, amountIn, ...);
if (splitResult) {
return this.buildSplitRouteResponse(...);
}
}
}

// === FALLBACK: Direct DEX Quotes ===
const dexQuotes = await this.dexQuoteService.fetchAllQuotes(
chainId, provider, fromTokenAddress, toTokenAddress, amountIn, ...
);

// Find optimal route (single or split)
const optimalRoute = this.routeOptimizer.findOptimalRoute(dexQuotes, ...);

// Calculate fees and return response
const fees = await this.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:src/modules/quote/services/route-quote.service.ts
@Injectable()
export class RouteQuoteService {
/**
* Get quotes for all discovered routes
*/
async getQuotesForRoutes(
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 = await Promise.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
*/
private async getQuoteForRoute(
chainId: number,
provider: PublicClient,
route: DiscoveredRoute,
amountIn: bigint,
toDecimals: number,
): Promise<RouteQuote | null> {
try {
const hopsData: HopQuoteData[] = [];
let currentAmount = amountIn;

// Simulate each hop in sequence
for (const hop of route.hops) {
const hopQuote = await this.getHopQuote(chainId, provider, hop, currentAmount);

if (!hopQuote) {
return null; // 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) {
return null;
}
}

/**
* Get quote for a single hop (V2 or V3)
*/
private async getHopQuote(
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') {
return await this.getV3HopQuote(provider, dexContracts.quoter!, hop, amountIn);
} else if (dexInfo.type === 'amm_v2') {
return await this.getV2HopQuote(provider, dexContracts.router, hop, amountIn);
}
return null;
}
}
```

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:

```typescript:src/quote/dex-quote.service.ts
@Injectable()
export class DexQuoteService {
private readonly providers = new Map<number, PublicClient>();

async fetchAllQuotes(
chainId: number,
fromToken: string,
toToken: string,
amount: string,
path?: OptimalPath
): Promise<QuoteResults> {
const dexes = this.getAvailableDexes(chainId);

const quotePromises = dexes.map(async dex => {
try {
return await this.getQuoteFromDex(dex, chainId, fromToken, toToken, amount);
} catch (error) {
// DEX might not support this pair
this.logger.debug(`${dex.name} quote failed: ${error.message}`);
return null;
}
});

const results = await Promise.allSettled(quotePromises);

const quotes = results
.filter((r): r is PromiseFulfilledResult<DexQuoteResult> =>
r.status === 'fulfilled' && r.value !== null
)
.map(r => r.value)
.sort((a, b) => BigInt(b.amountOut) - BigInt(a.amountOut) > 0n ? 1 : -1);

return {
quotes,
bestOutput: quotes[0] ? BigInt(quotes[0].amountOut) : 0n,
};
}

private async getQuoteFromDex(
dex: DexConfig,
chainId: number,
fromToken: string,
toToken: string,
amount: string
): Promise<DexQuoteResult> {
switch (dex.type) {
case 'uniswap-v3':
return this.getUniswapV3Quote(chainId, fromToken, toToken, amount, dex);
case 'uniswap-v2':
return this.getUniswapV2Quote(chainId, fromToken, toToken, amount, dex);
default:
throw new Error(`Unsupported DEX type: ${dex.type}`);
}
}
}
```

### Uniswap V3 Quotes

Uniswap V3 has multiple fee tiers. We query all and pick the best:

```typescript:src/quote/dex-quote.service.ts
async getUniswapV3Quote(
chainId: number,
fromToken: string,
toToken: string,
amount: string,
config: DexConfig
): Promise<DexQuoteResult> {
const provider = this.providers.get(chainId);
const quoter = new Contract(config.quoterAddress, QUOTER_V2_ABI, provider);

const feeTiers = [100, 500, 3000, 10000]; // 0.01%, 0.05%, 0.3%, 1%
let bestQuote: { amountOut: bigint; feeTier: number } | null = null;

for (const feeTier of feeTiers) {
try {
const result = await quoter.callStatic.quoteExactInputSingle({
tokenIn: fromToken,
tokenOut: toToken,
fee: feeTier,
amountIn: parseUnits(amount, await this.getDecimals(fromToken)),
sqrtPriceLimitX96: 0,
});

if (!bestQuote || result.amountOut > bestQuote.amountOut) {
bestQuote = { amountOut: result.amountOut, feeTier };
}
} catch {
// Pool doesn't exist for this fee tier
continue;
}
}

if (!bestQuote) {
throw new Error('No Uniswap V3 pool found');
}

return {
dexId: 'uniswap-v3',
dexName: 'Uniswap V3',
amountOut: bestQuote.amountOut.toString(),
path: this.encodeUniswapV3Path(fromToken, toToken, bestQuote.feeTier),
extraData: { feeTier: bestQuote.feeTier },
};
}

private encodeUniswapV3Path(
tokenIn: string,
tokenOut: string,
feeTier: number
): string {
// Uniswap V3 path encoding: token + fee + token + fee + ...
return encodePacked(
['address', 'uint24', 'address'],
[tokenIn, feeTier, tokenOut]
);
}
```

## Multi-Hop Path Finding

![Transaction route visualization](/images/menu/void-dex/transaction-route.jpg)

Sometimes the best rate requires intermediate tokens. The pathfinder discovers these:

```typescript:src/quote/pathfinder.service.ts
@Injectable()
export class PathfinderService {
// Common bridge tokens for multi-hop
private readonly bridgeTokens: Record<number, string[]> = {
1: ['WETH', 'USDC', 'USDT', 'DAI', 'WBTC'], // Ethereum
137: ['WMATIC', 'USDC', 'USDT', 'WETH'], // Polygon
42161: ['WETH', 'USDC', 'USDT', 'ARB'], // Arbitrum
};

async findPaths(
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 tokens
if (
bridgeAddress.toLowerCase() === fromToken.toLowerCase() ||
bridgeAddress.toLowerCase() === toToken.toLowerCase()
) {
continue;
}

// Check if path exists
const hasFirstHop = await this.poolExists(chainId, fromToken, bridgeAddress);
const hasSecondHop = await this.poolExists(chainId, bridgeAddress, toToken);

if (hasFirstHop && hasSecondHop) {
paths.push({
tokens: [fromToken, bridgeAddress, toToken],
description: `${this.getSymbol(fromToken)} → ${bridge} → ${this.getSymbol(toToken)}`,
});
}
}

return paths;
}

private async poolExists(
chainId: number,
tokenA: string,
tokenB: string
): Promise<boolean> {
// Check Uniswap V3 factory
const factory = this.getUniswapFactory(chainId);
for (const fee of [500, 3000, 10000]) {
const pool = await factory.getPool(tokenA, tokenB, fee);
if (pool !== ethers.constants.AddressZero) {
return true;
}
}
return false;
}
}
```

## Route Optimization

With all quotes collected, the optimizer finds the best execution strategy:

```typescript:src/quote/route-optimizer.service.ts
@Injectable()
export class RouteOptimizerService {
optimize(
quotes: DexQuoteResult[],
options: OptimizeOptions
): OptimalRoute {
// Strategy 1: Best single DEX
const bestSingle = this.findBestSingle(quotes);

// Strategy 2: Split across top DEXes
const bestSplit = this.findBestSplit(quotes, options);

// Strategy 3: Best multi-hop (already in quotes)
const bestMultiHop = this.findBestMultiHop(quotes);

// Compare and return best
const candidates = [bestSingle, bestSplit, bestMultiHop].filter(Boolean);
return this.selectBest(candidates);
}

private findBestSingle(quotes: DexQuoteResult[]): OptimalRoute | null {
const directQuotes = quotes.filter(q => !q.isMultiHop);
if (directQuotes.length === 0) return null;

const best = directQuotes[0]; // Already sorted by output
return {
type: 'single',
steps: [{
dexId: best.dexId,
percentage: 10000,
minAmountOut: best.amountOut,
dexData: best.extraData,
}],
totalOutput: BigInt(best.amountOut),
isSplit: false,
};
}

private findBestSplit(
quotes: DexQuoteResult[],
options: OptimizeOptions
): OptimalRoute | null {
const directQuotes = quotes.filter(q => !q.isMultiHop);
if (directQuotes.length < 2) return null;

// Try different split ratios
const 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 split
let 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 amounts
const 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;
}

private generateSplitRatios(maxSplits: number): number[][] {
// Common split patterns
return [
[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);
}

private findBestMultiHop(quotes: DexQuoteResult[]): OptimalRoute | null {
const multiHopQuotes = quotes.filter(q => q.isMultiHop);
if (multiHopQuotes.length === 0) return null;

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,
};
}

private selectBest(candidates: OptimalRoute[]): OptimalRoute {
// Simple: pick highest output
// Could factor in gas costs, execution complexity
return 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:src/quote/fee-calculator.service.ts
@Injectable()
export class FeeCalculatorService {
private readonly VOIDDEX_FEE_BPS = 5; // 0.05% of input amount
private readonly BROADCASTER_PROFIT_MARGIN = 15; // 15% profit margin on gas

constructor(
private readonly priceService: PriceService,
private readonly gasService: GasService,
) {}

async calculate(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 margin
const estimatedGas = await this.gasService.estimateSwapGas(chainId);
const gasPrice = await this.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 WETH
const voidDexFeeWeth = await this.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:src/modules/quote/services/route-optimizer.service.ts
/**
* Estimate price impact based on trade size
*/
estimatePriceImpact(amountUsd: number): number {
if (amountUsd < 1000) return 0.1;
if (amountUsd < 10000) return 0.2 + (amountUsd - 1000) * 0.00001;
if (amountUsd < 100000) return 0.3 + (amountUsd - 10000) * 0.000005;
if (amountUsd < 1000000) return 0.8 + (amountUsd - 100000) * 0.000002;
return 3; // Cap at 3%
}
```

This provides graduated estimates:
- **Under $1,000**: 0.1% (minimal impact)
- **$1,000 - $10,000**: 0.2% - 0.29% (linear interpolation)
- **$10,000 - $100,000**: 0.3% - 0.75% (slower growth)
- **$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:src/modules/quote/services/route-quote.service.ts
/**
* Estimate price impact based on hops and amount
*/
private estimatePriceImpact(hops: number, amountIn: bigint): number {
// Base impact per hop
const 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](/images/menu/void-dex/swap-details.jpg)

The final quote response provides everything the frontend needs:

```typescript:src/quote/types.ts
interface FeeBreakdown {
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)
}

interface QuoteResponse {
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.