Last active
September 20, 2025 09:04
-
-
Save rollendxavier/2dc195a906ce39c5313e16d19b6f7bfe to your computer and use it in GitHub Desktop.
How to Build a DEX Aggregator to Find the Best Swap Rates
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Calculate and rank pools by best swap quote | |
function calculateBestQuotes(poolDetails, { amountIn, fromToken, toToken }) { | |
const results = []; | |
for (const pool of poolDetails) { | |
const attr = pool.attributes || {}; | |
const rel = pool.relationships || {}; | |
// Extract token addresses from pool relationships | |
const baseId = rel.base_token?.data?.id || ''; | |
const quoteId = rel.quote_token?.data?.id || ''; | |
const baseAddr = baseId.split('_')[1]?.toLowerCase() || ''; | |
const quoteAddr = quoteId.split('_')[1]?.toLowerCase() || ''; | |
const fromLc = fromToken.toLowerCase(); | |
const toLc = toToken.toLowerCase(); | |
// Determine the correct price factor based on swap direction | |
let priceFactor = null; | |
if (baseAddr === fromLc && quoteAddr === toLc) { | |
// Swapping base token for quote token | |
priceFactor = Number(attr.base_token_price_quote_token); | |
} else if (baseAddr === toLc && quoteAddr === fromLc) { | |
// Swapping quote token for base token | |
priceFactor = Number(attr.quote_token_price_base_token); | |
} | |
// Skip pools that don't have valid price data | |
if (!Number.isFinite(priceFactor) || priceFactor <= 0) continue; | |
// Calculate estimated output with fee adjustment | |
const feePercent = Number(attr.pool_fee_percentage) || 0; | |
const estimatedOutput = Number(amountIn) * priceFactor * (1 - feePercent / 100); | |
results.push({ | |
poolId: pool.id, | |
dexName: rel.dex?.data?.id || 'unknown', | |
address: attr.address, | |
estimatedOutput: estimatedOutput, | |
pricePerToken: priceFactor, | |
liquidityUsd: Number(attr.reserve_in_usd) || 0, | |
feePercent: feePercent, | |
poolName: attr.name || `${baseAddr.slice(0,6)}.../${quoteAddr.slice(0,6)}...` | |
}); | |
} | |
// Sort by estimated output (highest first = best quote) | |
return results.sort((a, b) => b.estimatedOutput - a.estimatedOutput); | |
} | |
// Usage example | |
const sortedPools = calculateBestQuotes(poolDetails, { | |
amountIn: 1000, | |
fromToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC | |
toToken: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' // WETH | |
}); | |
console.log(`Best quote: ${sortedPools[0].estimatedOutput} tokens from ${sortedPools[0].dexName}`); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Get detailed pool information for comparison | |
app.get('/api/pools', async (req, res) => { | |
const { network = 'eth', from, to } = req.query; | |
if (!from || !to) return res.status(400).json({ error: 'Missing query params: from, to' }); | |
try { | |
// 1) Discover pools involving the 'from' token | |
const discovered = await cgFetch(`/networks/${encodeURIComponent(network)}/tokens/${encodeURIComponent(from)}/pools`); | |
const pools = Array.isArray(discovered?.data) ? discovered.data : []; | |
// 2) Filter to pools that pair with the 'to' token | |
const toLc = String(to).toLowerCase(); | |
const matched = pools.filter((p) => { | |
const baseId = p?.relationships?.base_token?.data?.id || ''; | |
const quoteId = p?.relationships?.quote_token?.data?.id || ''; | |
const baseAddr = baseId.split('_')[1] || ''; | |
const quoteAddr = quoteId.split('_')[1] || ''; | |
return baseAddr.toLowerCase() === toLc || quoteAddr.toLowerCase() === toLc; | |
}); | |
// 3) Extract pool addresses from discovery results | |
const poolAddresses = matched | |
.map(pool => pool?.attributes?.address) | |
.filter(Boolean); // Remove any undefined addresses | |
// 4) Fetch detailed info in batches via multi endpoint | |
const chunkSize = 50; | |
const results = []; | |
for (let i = 0; i < poolAddresses.length; i += chunkSize) { | |
const chunk = poolAddresses.slice(i, i + chunkSize); | |
// Format addresses as comma-separated string for multi endpoint | |
const addressesParam = chunk.join(','); | |
// Call multi endpoint: GET /onchain/networks/{network}/pools/multi/{addresses} | |
// Example: https://api.coingecko.com/api/v3/onchain/networks/eth/pools/multi/0xabc...,0xdef...,0x123... | |
const detail = await cgFetch(`/networks/${encodeURIComponent(network)}/pools/multi/${addressesParam}`); | |
if (Array.isArray(detail?.data)) results.push(...detail.data); | |
} | |
res.json({ data: results }); | |
} catch (e) { | |
res.status(500).json({ error: 'Failed to fetch pools', details: e?.response?.data || e.message }); | |
} | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Main dashboard component | |
function DEXAggregatorDashboard() { | |
const [pools, setPools] = useState([]); | |
const [loading, setLoading] = useState(false); | |
const searchPools = async ({ network, fromToken, toToken, amount }) => { | |
setLoading(true); | |
try { | |
// Fetch pool data from our backend API | |
const response = await fetch(`/api/pools?network=${network}&from=${fromToken}&to=${toToken}`); | |
const data = await response.json(); | |
// Calculate best quotes and sort results | |
const sortedPools = calculateBestQuotes(data.data, { | |
amountIn: parseFloat(amount), | |
fromToken, | |
toToken | |
}); | |
setPools(sortedPools); | |
} catch (error) { | |
console.error('Failed to fetch pools:', error); | |
setPools([]); | |
} finally { | |
setLoading(false); | |
} | |
}; | |
return ( | |
<div className="dex-aggregator"> | |
<h1>DEX Aggregator Dashboard</h1> | |
<SwapForm onSearch={searchPools} loading={loading} /> | |
<ResultsTable pools={pools} loading={loading} /> | |
</div> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Function calls: GET /onchain/networks/{network}/tokens/{token_address}/pools | |
// Example: https://api.coingecko.com/api/v3/onchain/networks/eth/tokens/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/pools | |
async function discoverPoolsForToken(network, tokenAddress) { | |
try { | |
const response = await cgFetch(`/networks/${encodeURIComponent(network)}/tokens/${encodeURIComponent(tokenAddress)}/pools`); | |
const pools = Array.isArray(response?.data) ? response.data : []; | |
console.log(`Found ${pools.length} pools containing token ${tokenAddress} on ${network}`); | |
return pools; | |
} catch (error) { | |
console.error('Error discovering pools:', error); | |
throw error; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Input form component for swap parameters | |
function SwapForm({ onSearch, loading }) { | |
const [formData, setFormData] = useState({ | |
network: 'eth', | |
fromToken: '', | |
toToken: '', | |
amount: '1000' | |
}); | |
const handleSubmit = (e) => { | |
e.preventDefault(); | |
if (formData.fromToken && formData.toToken && formData.amount) { | |
onSearch(formData); | |
} | |
}; | |
return ( | |
<form onSubmit={handleSubmit} className="swap-form"> | |
<div className="form-group"> | |
<label>Network</label> | |
<select | |
value={formData.network} | |
onChange={(e) => setFormData({...formData, network: e.target.value})} | |
> | |
<option value="eth">Ethereum</option> | |
<option value="base">Base</option> | |
<option value="arbitrum">Arbitrum</option> | |
<option value="polygon">Polygon</option> | |
</select> | |
</div> | |
<div className="form-group"> | |
<label>From Token (Contract Address)</label> | |
<input | |
type="text" | |
placeholder="0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" | |
value={formData.fromToken} | |
onChange={(e) => setFormData({...formData, fromToken: e.target.value})} | |
/> | |
</div> | |
<div className="form-group"> | |
<label>To Token (Contract Address)</label> | |
<input | |
type="text" | |
placeholder="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" | |
value={formData.toToken} | |
onChange={(e) => setFormData({...formData, toToken: e.target.value})} | |
/> | |
</div> | |
<div className="form-group"> | |
<label>Swap Amount</label> | |
<input | |
type="number" | |
placeholder="1000" | |
value={formData.amount} | |
onChange={(e) => setFormData({...formData, amount: e.target.value})} | |
/> | |
</div> | |
<button type="submit" disabled={loading}> | |
{loading ? 'Finding Best Rates...' : 'Compare Rates'} | |
</button> | |
</form> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Results table component with copy functionality | |
function ResultsTable({ pools, loading }) { | |
const copyToClipboard = (text) => { | |
navigator.clipboard.writeText(text); | |
// Show brief success feedback | |
}; | |
if (loading) return <div className="loading">Fetching pool data...</div>; | |
if (!pools.length) return <div className="no-results">No pools found for this pair</div>; | |
return ( | |
<div className="results-container"> | |
<h3>Best Swap Rates (Sorted by Estimated Output)</h3> | |
<table className="results-table"> | |
<thead> | |
<tr> | |
<th className="estimated-output-header">Estimated Output</th> | |
<th>DEX Name</th> | |
<th>Price per Token</th> | |
<th>Pool Liquidity (USD)</th> | |
<th>Pool Fee (%)</th> | |
<th>Pool Address</th> | |
</tr> | |
</thead> | |
<tbody> | |
{pools.map((pool, index) => ( | |
<tr key={pool.poolId} className={index === 0 ? 'best-rate' : ''}> | |
<td className="estimated-output"> | |
<strong>{pool.estimatedOutput.toFixed(6)}</strong> | |
{index === 0 && <span className="best-badge">Best Rate</span>} | |
</td> | |
<td className="dex-name">{pool.dexName}</td> | |
<td>{pool.pricePerToken.toFixed(8)}</td> | |
<td>${pool.liquidityUsd.toLocaleString()}</td> | |
<td>{pool.feePercent}%</td> | |
<td className="address-cell"> | |
<span className="address">{pool.address.slice(0, 10)}...</span> | |
<button | |
className="copy-btn" | |
onClick={() => copyToClipboard(pool.address)} | |
title="Copy full address"> | |
</button> | |
</td> | |
</tr> | |
))} | |
</tbody> | |
</table> | |
</div> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Complete implementation in server.js | |
app.get('/api/pools', async (req, res) => { | |
const { network = 'eth', from, to } = req.query; | |
if (!from || !to) return res.status(400).json({ error: 'Missing query params: from, to' }); | |
try { | |
// 1) Discover all pools containing the 'from' token | |
const pools = await discoverPoolsForToken(network, from); | |
// 2) Filter to pools that also contain the 'to' token | |
const toLc = String(to).toLowerCase(); | |
const matchedPools = pools.filter((pool) => { | |
const baseTokenId = pool?.relationships?.base_token?.data?.id || ''; | |
const quoteTokenId = pool?.relationships?.quote_token?.data?.id || ''; | |
const baseAddr = baseTokenId.split('_')[1] || ''; | |
const quoteAddr = quoteTokenId.split('_')[1] || ''; | |
return baseAddr.toLowerCase() === toLc || quoteAddr.toLowerCase() === toLc; | |
}); | |
console.log(`Matched ${matchedPools.length} pools for pair ${from}/${to}`); | |
res.json({ data: matchedPools }); | |
} catch (error) { | |
res.status(500).json({ error: 'Failed to fetch pools' }); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment