Routing Engine
How MNMX discovers, evaluates, and executes cross-chain routes.
Overview
The routing engine is the central component of MNMX. It takes a user's intent (source asset, destination asset, amount) and produces an optimal execution plan through five stages: path discovery, state collection, minimax evaluation, route selection, and execution. Each stage is designed to be independently testable and cacheable.
1Engine Pipeline:2
3 User Intent Optimal Route4 │ ▲5 ▼ │6 ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐7 │ Path │→ │ State │→ │ Minimax │→ │ Route │→ │ Route │8 │ Discovery│ │Collection│ │Evaluator │ │ Selector │ │ Executor │9 │ │ │ │ │ │ │ │ │ │10 │ 15-50 │ │ Parallel │ │ Alpha- │ │ Apply │ │ Multi- │11 │ candidate│ │ RPC + API│ │ beta │ │ strategy │ │ step TX │12 │ paths │ │ calls │ │ pruning │ │ weights │ │ signing │13 └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘14 ~100ms 500ms-2s ~50ms ~5ms 30s-20minPath Discovery
Given a source asset on chain A and a destination asset on chain B, the engine builds a tree of all feasible routes:
1findRoutes(1 ETH on Ethereum → SOL on Solana):2
3Direct bridges:4 ├── Wormhole: ETH(Ethereum) → ETH(Solana) → Jupiter → SOL5 ├── deBridge: ETH(Ethereum) → ETH(Solana) → Jupiter → SOL6 └── Allbridge: ETH(Ethereum) → ETH(Solana) → Jupiter → SOL7
8Multi-hop via stablecoins:9 ├── Uniswap(ETH→USDC) → Wormhole(USDC) → Jupiter(USDC→SOL)10 ├── Uniswap(ETH→USDC) → deBridge(USDC) → Jupiter(USDC→SOL)11 └── Uniswap(ETH→USDT) → Allbridge(USDT) → Jupiter(USDT→SOL)12
13Multi-hop via intermediate chains:14 ├── Wormhole(ETH→Arbitrum) → Swap → deBridge(Arbitrum→Solana)15 └── LayerZero(ETH→Base) → Swap → Wormhole(Base→Solana)16
17Total candidate paths: 23Graph Construction
The path discovery module maintains a directed graph where nodes are (chain, token) pairs and edges represent either bridge transfers or DEX swaps. The graph is reconstructed every 30 seconds from bridge adapter health checks.
1class RouteGraph {2 private nodes: Map<string, ChainToken>; // "ethereum:ETH" → node3 private edges: Map<string, RouteEdge[]>; // adjacency list4
5 // Build graph from registered adapters6 buildGraph(bridges: BridgeAdapter[], dexes: DexAdapter[]): void {7 // Add cross-chain edges from bridges8 for (const bridge of bridges) {9 for (const pair of bridge.getSupportedPairs()) {10 this.addEdge({11 from: `${pair.fromChain}:${pair.fromToken}`,12 to: `${pair.toChain}:${pair.toToken}`,13 type: 'bridge',14 provider: bridge.name,15 baseLatency: bridge.getBaseLatency(pair),16 baseFee: bridge.getBaseFee(pair),17 });18 }19 }20
21 // Add intra-chain edges from DEXes22 for (const dex of dexes) {23 for (const pair of dex.getSupportedPairs()) {24 this.addEdge({25 from: `${dex.chain}:${pair.tokenA}`,26 to: `${dex.chain}:${pair.tokenB}`,27 type: 'swap',28 provider: dex.name,29 baseLatency: 15, // ~1 block on most EVM chains30 baseFee: dex.getBaseFee(pair),31 });32 }33 }34 }35
36 // Bounded DFS to find all paths up to maxHops37 findAllPaths(38 from: string,39 to: string,40 maxHops: number41 ): CandidatePath[] {42 const paths: CandidatePath[] = [];43 const visited = new Set<string>();44
45 const dfs = (current: string, path: RouteEdge[], hops: number) => {46 if (current === to) {47 paths.push({ edges: [...path], hopCount: hops });48 return;49 }50 if (hops >= maxHops) return;51
52 visited.add(current);53 for (const edge of this.edges.get(current) || []) {54 if (!visited.has(edge.to)) {55 path.push(edge);56 dfs(edge.to, path, hops + (edge.type === 'bridge' ? 1 : 0));57 path.pop();58 }59 }60 visited.delete(current);61 };62
63 dfs(from, [], 0);64 return paths;65 }66}Token Resolution
When the source and destination tokens differ, the engine must find intermediate swap points. The token resolver identifies all viable swap paths on each chain:
1class TokenResolver {2 // Find all ways to convert tokenA to tokenB on a given chain3 resolveSwapPath(4 chain: string,5 tokenA: string,6 tokenB: string7 ): SwapPath[] {8 const paths: SwapPath[] = [];9
10 // Direct pair (e.g., ETH/USDC on Uniswap)11 if (this.hasPair(chain, tokenA, tokenB)) {12 paths.push({ hops: [{ from: tokenA, to: tokenB }] });13 }14
15 // Via common intermediaries (WETH, USDC, USDT)16 for (const mid of ['WETH', 'USDC', 'USDT']) {17 if (mid === tokenA || mid === tokenB) continue;18 if (this.hasPair(chain, tokenA, mid) &&19 this.hasPair(chain, mid, tokenB)) {20 paths.push({21 hops: [22 { from: tokenA, to: mid },23 { from: mid, to: tokenB },24 ],25 });26 }27 }28
29 return paths;30 }31}Early Pruning
Not all paths are worth evaluating. The engine prunes before the minimax search begins:
- Bridge offline — Skip paths through bridges that are down or congested
- Insufficient liquidity — Skip paths where bridge liquidity is below transfer amount
- Excessive hops — Cap at 4 hops to limit gas accumulation and failure probability
- Dominated paths — Remove paths that are strictly worse than another on all dimensions
- Token blacklist — Exclude paths through tokens flagged as honeypots or low-liquidity traps
- Stale quotes — Exclude paths with bridge quotes older than the configured TTL
1class PathPruner {2 prune(paths: CandidatePath[], state: PathState[]): CandidatePath[] {3 let viable = paths;4
5 // Stage 1: Hard constraints (binary pass/fail)6 viable = viable.filter((path, i) => {7 const s = state[i];8 if (!s.bridgeHealth.every(h => h.online)) return false; // Bridge offline9 if (s.bridgeQuotes.some(q => q.liquidityDepth < path.amount)) // No liquidity10 return false;11 if (path.hopCount > this.config.maxHops) return false; // Too many hops12 if (s.timestamp + this.config.quoteTTL < Date.now()) return false; // Stale data13 return true;14 });15
16 // Stage 2: Dominance pruning17 viable = this.removeDominated(viable, state);18
19 // Stage 3: Sort by estimated quality for alpha-beta efficiency20 viable.sort((a, b) => this.estimateQuality(b) - this.estimateQuality(a));21
22 return viable;23 }24
25 // Path A dominates Path B if A is >= B on ALL dimensions26 private removeDominated(27 paths: CandidatePath[],28 state: PathState[]29 ): CandidatePath[] {30 return paths.filter((pathA, i) => {31 return !paths.some((pathB, j) => {32 if (i === j) return false;33 const sA = state[i], sB = state[j];34 return (35 sB.totalFees <= sA.totalFees &&36 sB.totalSlippage <= sA.totalSlippage &&37 sB.estimatedTime <= sA.estimatedTime &&38 sB.riskScore <= sA.riskScore &&39 sB.mevExposure <= sA.mevExposure &&40 // At least one strict inequality41 (sB.totalFees < sA.totalFees ||42 sB.totalSlippage < sA.totalSlippage ||43 sB.estimatedTime < sA.estimatedTime)44 );45 });46 });47 }48}Typically reduces 20-50 raw paths to 8-15 viable candidates for minimax evaluation. The dominance pruning step alone usually eliminates 30-40% of remaining paths.
State Collection
For each viable path, the StateCollector fetches real-time conditions in parallel:
1// Parallel state collection across all chains and bridges2const states = await Promise.all(3 candidatePaths.map(path => collectPathState(path))4);5
6interface PathState {7 bridgeQuotes: BridgeQuote[]; // Fee, time, liquidity for each bridge hop8 dexQuotes: DexQuote[]; // Swap output, slippage for each DEX hop9 gasEstimates: GasEstimate[]; // Gas cost per chain per transaction10 bridgeHealth: BridgeHealth[]; // Uptime, congestion, recent failures11 timestamp: number; // State freshness12}13
14interface DexQuote {15 dex: string;16 chain: string;17 inputToken: string;18 outputToken: string;19 inputAmount: string;20 outputAmount: string;21 priceImpact: number; // Percentage (0.01 = 1%)22 route: string[]; // Token path through pools23 poolReserves: PoolReserve[]; // Current reserves for slippage calc24}25
26interface GasEstimate {27 chain: string;28 gasPrice: bigint; // Current gas price in wei29 gasLimit: bigint; // Estimated gas units for this tx30 costUSD: number; // Gas cost in USD31 priorityFee: bigint; // EIP-1559 priority fee32 baseFee: bigint; // EIP-1559 base fee33}RPC Connection Management
Each chain has a pool of RPC endpoints with automatic failover. The pool tracks response times and error rates to route requests to the fastest healthy endpoint:
1class RpcConnectionPool {2 private endpoints: Map<string, RpcEndpoint[]>;3 private metrics: Map<string, EndpointMetrics>;4
5 async call(chain: string, method: string, params: unknown[]): Promise<unknown> {6 const endpoints = this.getHealthyEndpoints(chain);7
8 for (const endpoint of endpoints) {9 try {10 const start = performance.now();11 const result = await endpoint.call(method, params);12 const latency = performance.now() - start;13
14 this.metrics.get(endpoint.url)!.recordSuccess(latency);15 return result;16 } catch (error) {17 this.metrics.get(endpoint.url)!.recordFailure(error);18 continue; // Try next endpoint19 }20 }21
22 throw new AllEndpointsFailedError(chain);23 }24
25 private getHealthyEndpoints(chain: string): RpcEndpoint[] {26 return this.endpoints.get(chain)!27 .filter(ep => this.metrics.get(ep.url)!.errorRate < 0.1)28 .sort((a, b) =>29 this.metrics.get(a.url)!.p50Latency -30 this.metrics.get(b.url)!.p50Latency31 );32 }33}Worst-Case Modeling
For each path, the engine models adversarial conditions. These multipliers are calibrated from historical data across millions of bridge transfers:
| Adversarial Factor | Worst-Case Model | Rationale |
|---|---|---|
| Slippage | 2x quoted slippage | Liquidity can drain between quote and execution; large trades can trigger cascading pool imbalances |
| Gas spikes | 1.5x current gas price | Congestion from MEV bots, NFT mints, or airdrop claims can spike gas within seconds |
| Bridge delay | 3x median confirmation time | Guardian/validator set can be slow during network congestion or consensus issues |
| MEV extraction | 0.3% of transfer value on vulnerable hops | Sandwich attacks on DEX swaps; front-running on bridge redemption transactions |
| Price movement | 0.5% adverse price change during bridge transit | Asset prices can move against you during the minutes a bridge transfer takes |
1class WorstCaseModel {2 private config: AdversarialConfig;3
4 evaluateWorstCase(path: CandidatePath, state: PathState): WorstCaseResult {5 let outputUSD = state.quotedOutputUSD;6 const penalties: Penalty[] = [];7
8 // 1. Slippage amplification9 for (const swap of state.dexQuotes) {10 const worstSlippage = swap.priceImpact * this.config.slippageMultiplier;11 const slippageCost = swap.outputAmountUSD * worstSlippage;12 outputUSD -= slippageCost;13 penalties.push({14 type: 'slippage',15 hop: swap.dex,16 amount: slippageCost,17 });18 }19
20 // 2. Gas surge21 for (const gas of state.gasEstimates) {22 const worstGas = gas.costUSD * this.config.gasMultiplier;23 const gasSurge = worstGas - gas.costUSD;24 outputUSD -= gasSurge;25 penalties.push({26 type: 'gas_surge',27 hop: gas.chain,28 amount: gasSurge,29 });30 }31
32 // 3. MEV extraction (only on unprotected swaps)33 for (const swap of state.dexQuotes) {34 if (!swap.mevProtected) {35 const mevCost = swap.outputAmountUSD * this.config.mevExtraction;36 outputUSD -= mevCost;37 penalties.push({38 type: 'mev',39 hop: swap.dex,40 amount: mevCost,41 });42 }43 }44
45 // 4. Adverse price movement during bridge transit46 for (const bridge of state.bridgeQuotes) {47 const transitTime = bridge.estimatedTime * this.config.bridgeDelayMultiplier;48 const priceRisk = bridge.outputAmountUSD * this.config.priceMovement;49 outputUSD -= priceRisk;50 penalties.push({51 type: 'price_movement',52 hop: bridge.bridge,53 amount: priceRisk,54 });55 }56
57 // 5. Bridge-specific risk penalty58 for (const health of state.bridgeHealth) {59 const riskPenalty = this.calculateBridgeRisk(health) * state.quotedOutputUSD;60 outputUSD -= riskPenalty;61 penalties.push({62 type: 'bridge_risk',63 hop: health.bridge,64 amount: riskPenalty,65 });66 }67
68 return {69 worstCaseOutputUSD: outputUSD,70 penalties,71 totalPenalty: state.quotedOutputUSD - outputUSD,72 penaltyPercent: ((state.quotedOutputUSD - outputUSD) / state.quotedOutputUSD) * 100,73 };74 }75
76 private calculateBridgeRisk(health: BridgeHealth): number {77 let risk = 0;78 if (health.recentSuccessRate < 0.99) risk += (1 - health.recentSuccessRate) * 0.1;79 if (health.congestion === 'high') risk += 0.005;80 if (health.congestion === 'medium') risk += 0.002;81 return risk;82 }83}The worst-case output for each path is: quoted output minus all worst-case cost adjustments. The minimax score is this worst-case output — the engine picks the path with the highest floor.
Route Execution
Once the optimal route is selected, the RouteExecutorhandles multi-step execution with monitoring:
1Executing: ETH → Uniswap(ETH→USDC) → Wormhole(USDC) → Jupiter(USDC→SOL)2
3Step 1/3: Swap ETH → USDC on Uniswap4 ├── Build transaction5 ├── Simulate (verify output within tolerance)6 ├── Submit and confirm7 └── Received 3,247.82 USDC8
9Step 2/3: Bridge USDC via Wormhole (Ethereum → Solana)10 ├── Initiate bridge transfer11 ├── Monitor VAA confirmation12 ├── Redeem on Solana13 └── Received 3,247.82 USDC on Solana (2m 14s)14
15Step 3/3: Swap USDC → SOL on Jupiter16 ├── Fetch quote17 ├── Submit and confirm18 └── Received 14.12 SOL19
20Result: 1.0 ETH → 14.12 SOL (guaranteed minimum was 13.8 SOL)Execution State Machine
Each execution follows a strict state machine that ensures funds are tracked at every point:
1State Machine per Hop:2
3 ┌──────────┐ ┌───────────┐ ┌───────────┐4 │ PENDING │ ──→ │ SIMULATED │ ──→ │ SUBMITTED │5 └──────────┘ └───────────┘ └─────┬─────┘6 │7 ┌────────────────┤8 ▼ ▼9 ┌───────────┐ ┌───────────┐10 │ CONFIRMED │ │ FAILED │11 └─────┬─────┘ └─────┬─────┘12 │ │13 ▼ ▼14 ┌───────────┐ ┌───────────┐15 │ COMPLETED │ │ RETRYING │16 └───────────┘ └───────────┘17
18State transitions are logged with:19 - Timestamp (ms precision)20 - Transaction hash (when available)21 - Input/output amounts22 - Gas used23 - Chain + block number1class ExecutionStateMachine {2 private hops: Map<string, HopState>;3 private listeners: ExecutionListener[];4
5 transition(hopId: string, newState: HopStatus, data?: TransitionData): void {6 const hop = this.hops.get(hopId)!;7 const oldState = hop.status;8
9 // Validate transition is allowed10 if (!this.isValidTransition(oldState, newState)) {11 throw new InvalidTransitionError(hopId, oldState, newState);12 }13
14 hop.status = newState;15 hop.lastUpdated = Date.now();16
17 if (data?.txHash) hop.txHash = data.txHash;18 if (data?.output) hop.actualOutput = data.output;19 if (data?.error) hop.error = data.error;20
21 // Notify listeners (progress callbacks, logging, telemetry)22 for (const listener of this.listeners) {23 listener.onTransition(hopId, oldState, newState, data);24 }25 }26
27 // Track exactly where funds are at any point28 getFundsLocation(): FundsLocation {29 for (const [hopId, hop] of this.hops) {30 if (hop.status === 'completed') continue;31 if (hop.status === 'pending') {32 // Funds still at previous hop's destination (or original wallet)33 return this.getPreviousLocation(hopId);34 }35 if (hop.status === 'submitted' || hop.status === 'confirmed') {36 return {37 chain: hop.edge.type === 'bridge' ? 'in_transit' : hop.edge.from.chain,38 token: hop.edge.from.token,39 amount: hop.inputAmount,40 status: 'locked',41 };42 }43 }44 return this.getFinalLocation();45 }46}Pre-Execution Simulation
Before submitting any on-chain transaction, the executor simulates it using eth_call (EVM) or simulateTransaction(Solana) to verify the expected output:
1class TransactionSimulator {2 async simulate(tx: PreparedTransaction): Promise<SimulationResult> {3 if (tx.chain.type === 'evm') {4 // EVM simulation via eth_call5 const result = await this.rpc.call(tx.chain.id, 'eth_call', [{6 from: tx.from,7 to: tx.to,8 data: tx.data,9 value: tx.value,10 gas: tx.gasLimit,11 }, 'latest']);12
13 const decoded = this.decodeOutput(tx.abi, result);14 return {15 success: true,16 expectedOutput: decoded.amountOut,17 gasUsed: decoded.gasUsed,18 };19 }20
21 if (tx.chain.type === 'solana') {22 // Solana simulation23 const sim = await this.connection.simulateTransaction(tx.transaction);24 return {25 success: sim.value.err === null,26 expectedOutput: this.extractSolanaOutput(sim),27 computeUnits: sim.value.unitsConsumed,28 };29 }30
31 throw new UnsupportedChainError(tx.chain);32 }33}Failure Handling
If any step fails or conditions degrade beyond tolerance:
- Pre-bridge failure — Revert source chain transaction. No funds at risk. The executor re-runs path discovery excluding the failed provider and selects the next best route.
- Bridge stall — Monitor until completion or timeout. The executor polls bridge status every 15 seconds. After 3x the expected confirmation time, it alerts the user with the transaction hash and bridge-specific recovery instructions.
- Post-bridge failure — Assets are on destination chain. Re-route the final swap through alternative DEX. If all DEXes fail, the user retains the bridged tokens.
- Partial execution — If execution is interrupted mid-pipeline, the executor records the exact state and provides a recovery plan that can be resumed.
1class FailureHandler {2 async handleFailure(3 execution: Execution,4 failedHop: RouteHop,5 error: Error,6 opts: ExecOpts7 ): Promise<ExecResult> {8 const fundsLocation = execution.getFundsLocation();9
10 // Case 1: Funds still in user's wallet (pre-bridge)11 if (fundsLocation.status === 'wallet') {12 if (opts.autoRetry) {13 // Re-discover routes excluding the failed provider14 const newRoute = await this.router.findRoute({15 ...execution.originalParams,16 options: {17 excludeProviders: [failedHop.provider],18 },19 });20 return this.executor.execute(newRoute, opts);21 }22 return execution.finalize('failed', { error, fundsLocation });23 }24
25 // Case 2: Funds in transit (bridge in progress)26 if (fundsLocation.status === 'in_transit') {27 // Wait for bridge to complete (or timeout)28 const bridgeResult = await this.waitForBridge(29 failedHop,30 execution.config.bridgeTimeout31 );32 if (bridgeResult.completed) {33 // Continue execution from the next hop34 return this.executor.resumeFrom(execution, failedHop);35 }36 return execution.finalize('partial', {37 error,38 fundsLocation,39 recoveryInstructions: this.getRecoveryInstructions(failedHop),40 });41 }42
43 // Case 3: Funds on destination chain (swap failed)44 if (fundsLocation.chain === execution.destinationChain) {45 // Try alternative DEX46 const altDexes = this.dexRegistry.getAlternatives(47 fundsLocation.chain,48 fundsLocation.token,49 execution.destinationToken50 );51 for (const dex of altDexes) {52 try {53 return await this.executor.executeSwap(dex, fundsLocation, opts);54 } catch { continue; }55 }56 // All DEXes failed — user keeps the bridged token57 return execution.finalize('partial', {58 fundsLocation,59 message: `Bridged ${fundsLocation.amount} ${fundsLocation.token} to ${fundsLocation.chain}. Final swap failed — tokens available in wallet.`,60 });61 }62
63 return execution.finalize('failed', { error, fundsLocation });64 }65}getFundsLocation()method always returns the exact chain, token, and amount where user funds currently reside.Gas Optimization
The executor applies several gas optimization techniques to minimize on-chain costs:
| Technique | Savings | Description |
|---|---|---|
| EIP-1559 dynamic fees | 10-30% | Uses maxFeePerGas and maxPriorityFeePerGas instead of legacy gasPrice |
| Gas limit estimation | Prevents overpay | Simulates transaction to get exact gas used, adds 15% buffer |
| Batch approvals | ~21,000 gas per avoided tx | Uses permit2 or infinite approvals where safe to avoid redundant approve calls |
| Calldata optimization | 5-15% | Encodes function calls with minimal calldata where possible |
| Nonce management | Prevents stuck txs | Tracks pending nonces to avoid transaction replacement conflicts |
Monitoring and Telemetry
Every execution emits structured telemetry data that is used to improve routing decisions over time:
1interface ExecutionTelemetry {2 execId: string;3 route: {4 hops: number;5 bridges: string[];6 dexes: string[];7 chains: string[];8 };9 timing: {10 quoteTime: number; // Time to generate quote (ms)11 executionTime: number; // Total execution time (ms)12 perHop: { hop: string; time: number }[];13 };14 accuracy: {15 quotedOutput: string; // What we quoted16 actualOutput: string; // What the user received17 deviation: number; // Percentage deviation18 worstCaseMet: boolean; // Did actual >= guaranteed minimum?19 };20 costs: {21 totalGas: string; // Total gas cost in USD22 totalBridgeFees: string; // Total bridge fees in USD23 totalSwapFees: string; // Total swap fees in USD24 mevExtracted: string; // Detected MEV extraction in USD25 };26}This telemetry feeds back into the worst-case model calibration. If the model consistently over- or under-estimates adversarial conditions for a specific bridge or DEX, the multipliers are adjusted automatically on a weekly basis.
Performance
| Operation | Typical Time | P99 Time | Bottleneck |
|---|---|---|---|
| Path discovery | <100ms | 250ms | Graph traversal depth |
| State collection (cached) | 50-200ms | 500ms | Cache hit rate |
| State collection (cold) | 500ms-2s | 4s | Slowest RPC endpoint |
| Minimax evaluation (15 paths) | <50ms | 120ms | Path count after pruning |
| Total quote time (warm) | 200-500ms | 1s | State collection |
| Total quote time (cold) | 1-3s | 5s | State collection |
| Execution time | Depends on bridge | — | Bridge confirmation time (30s-20min) |
Edge Cases
| Scenario | Behavior |
|---|---|
| No viable route exists | Throws NoRouteFoundError with reason (unsupported chain pair, all bridges offline, etc.) |
| All routes have negative minimax score | Returns the least-negative route with a warning that fees exceed output |
| Transfer amount exceeds all bridge liquidity | Suggests splitting into multiple smaller transfers |
| Bridge goes offline mid-execution | Waits for bridge recovery up to timeout, then provides recovery instructions |
| Gas price spikes above 2x estimate during execution | Pauses execution, re-evaluates cost, continues if within tolerance |
| Same token on same chain (no-op) | Returns identity route with zero fees and zero time |
| Dust amounts below bridge minimum | Throws AmountBelowMinimumError with the bridge-specific minimum |