Skip to content

Instantly share code, notes, and snippets.

@rollendxavier
Last active September 20, 2025 09:04
Show Gist options
  • Save rollendxavier/2dc195a906ce39c5313e16d19b6f7bfe to your computer and use it in GitHub Desktop.
Save rollendxavier/2dc195a906ce39c5313e16d19b6f7bfe to your computer and use it in GitHub Desktop.
How to Build a DEX Aggregator to Find the Best Swap Rates
// 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}`);
// 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 });
}
});
// 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>
);
}
// 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;
}
}
// 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>
);
}
// 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>
);
}
// 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