Implementing DEX Interface (Token Swap) on a Website
DEX swap interface is more complex than it seems. Beyond the swap itself: fetching quotes (on-chain or aggregator), calculating price impact, slippage tolerance, transaction deadline, approve + swap two-step flow, handling native ETH vs WETH. This is a full product component built from scratch or integrated via an aggregator.
Two Approaches: Own DEX vs Aggregator
Own router (Uniswap v2/v3 fork): you control the contract, interface works directly with your pool. Quotes are calculated on-chain via getAmountsOut.
Aggregator (1inch, 0x, Paraswap): routes through the best path across all DEXs. Suitable for products where best price matters, not exclusive pool. API returns ready transaction data.
We'll break down both scenarios.
Option 1: Direct Uniswap v2 Router
// lib/swap.ts
import { createPublicClient, http, parseAbi, formatUnits, parseUnits } from 'viem';
const ROUTER_ABI = parseAbi([
'function getAmountsOut(uint256 amountIn, address[] path) view returns (uint256[])',
'function swapExactTokensForTokens(uint256,uint256,address[],address,uint256) returns (uint256[])',
'function swapExactETHForTokens(uint256,address[],address,uint256) payable returns (uint256[])',
'function swapExactTokensForETH(uint256,uint256,address[],address,uint256) returns (uint256[])',
]);
const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' as const;
export async function getQuote(
tokenIn: `0x${string}` | 'ETH',
tokenOut: `0x${string}` | 'ETH',
amountIn: bigint,
decimalsIn: number,
decimalsOut: number,
): Promise<{ amountOut: bigint; path: `0x${string}`[]; priceImpact: number }> {
const client = createPublicClient({ chain: mainnet, transport: http() });
const addressIn = tokenIn === 'ETH' ? WETH : tokenIn;
const addressOut = tokenOut === 'ETH' ? WETH : tokenOut;
// Direct route
const directPath: `0x${string}`[] = [addressIn, addressOut];
// Route through WETH (if tokens don't have direct pair)
const wethPath: `0x${string}`[] = [addressIn, WETH, addressOut];
const [directResult, wethResult] = await client.multicall({
contracts: [
{ address: ROUTER, abi: ROUTER_ABI, functionName: 'getAmountsOut', args: [amountIn, directPath] },
{ address: ROUTER, abi: ROUTER_ABI, functionName: 'getAmountsOut', args: [amountIn, wethPath] },
],
allowFailure: true,
});
const directOut = directResult.status === 'success'
? (directResult.result as bigint[])[directResult.result.length - 1]
: 0n;
const wethOut = wethResult.status === 'success'
? (wethResult.result as bigint[])[(wethResult.result as bigint[]).length - 1]
: 0n;
const bestOut = directOut >= wethOut ? directOut : wethOut;
const bestPath = directOut >= wethOut ? directPath : wethPath;
// Price impact — difference between spot price and actual price
// (simplified via pool reserves)
const priceImpact = 0; // in real project calculated via reserves
return { amountOut: bestOut, path: bestPath, priceImpact };
}
Quote Hook with Debounce
// hooks/useQuote.ts
import { useQuery } from '@tanstack/react-query';
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
export function useQuote(
tokenIn: string,
tokenOut: string,
amountIn: string,
decimalsIn: number,
decimalsOut: number,
) {
// Debounce — don't request on every character
const debouncedAmount = useDebouncedValue(amountIn, 400);
const amountWei = debouncedAmount ? parseUnits(debouncedAmount, decimalsIn) : 0n;
return useQuery({
queryKey: ['quote', tokenIn, tokenOut, amountWei.toString()],
queryFn: () => getQuote(
tokenIn as `0x${string}`,
tokenOut as `0x${string}`,
amountWei,
decimalsIn,
decimalsOut,
),
enabled: amountWei > 0n,
staleTime: 15_000, // quote stales after 15 seconds
refetchInterval: 15_000,
});
}
Option 2: Quotes via 0x API
// lib/0x.ts
export async function get0xQuote(
sellToken: string, // address or "ETH"
buyToken: string,
sellAmount: string, // in wei
takerAddress?: string,
): Promise<{
buyAmount: string;
price: string;
guaranteedPrice: string;
to: string;
data: string;
value: string;
gas: string;
estimatedPriceImpact: string;
}> {
const params = new URLSearchParams({
sellToken,
buyToken,
sellAmount,
...(takerAddress && { takerAddress }),
affiliateAddress: process.env.NEXT_PUBLIC_FEE_RECIPIENT ?? '',
buyTokenPercentageFee: '0.005', // 0.5% protocol fee (optional)
});
const res = await fetch(
`https://api.0x.org/swap/v1/quote?${params}`,
{ headers: { '0x-api-key': process.env.ZRX_API_KEY! } },
);
if (!res.ok) {
const error = await res.json();
throw new Error(error.reason ?? 'Quote failed');
}
return res.json();
}
When using 0x — quote.to and quote.data are passed directly to the transaction, without calling specific router functions. This simplifies integration.
Swap Component
// components/SwapWidget.tsx
export function SwapWidget() {
const { address } = useAccount();
const [tokenIn, setTokenIn] = useState<Token>(ETH_TOKEN);
const [tokenOut, setTokenOut] = useState<Token>(USDC_TOKEN);
const [amountIn, setAmountIn] = useState('');
const [slippage, setSlippage] = useState(0.5); // %
const { data: quote, isLoading: quoteLoading } = useQuote(
tokenIn.address, tokenOut.address, amountIn, tokenIn.decimals, tokenOut.decimals,
);
const amountOut = quote ? formatUnits(quote.amountOut, tokenOut.decimals) : '';
// Minimum considering slippage
const minOut = quote
? quote.amountOut - (quote.amountOut * BigInt(Math.floor(slippage * 100))) / 10000n
: 0n;
// Deadline: 20 minutes from now
const deadline = BigInt(Math.floor(Date.now() / 1000) + 1200);
return (
<div className="w-full max-w-md rounded-2xl border border-white/10 bg-neutral-900 p-4">
<div className="space-y-2">
<TokenInput
label="Sending"
token={tokenIn}
value={amountIn}
onChange={setAmountIn}
onTokenChange={setTokenIn}
balance={walletBalanceIn}
showMax
/>
<SwapDirectionButton onClick={() => {
setTokenIn(tokenOut);
setTokenOut(tokenIn);
setAmountIn(amountOut);
}} />
<TokenInput
label="Receiving"
token={tokenOut}
value={quoteLoading ? '...' : amountOut}
onTokenChange={setTokenOut}
readOnly
/>
</div>
{quote && (
<div className="mt-4 space-y-1.5 rounded-xl bg-neutral-800/50 p-3 text-sm">
<Row label="Rate" value={`1 ${tokenIn.symbol} = ${(parseFloat(amountOut) / parseFloat(amountIn)).toFixed(6)} ${tokenOut.symbol}`} />
<Row label="Price Impact" value={`${quote.priceImpact.toFixed(2)}%`} warn={quote.priceImpact > 2} />
<Row label="Min received" value={`${formatUnits(minOut, tokenOut.decimals)} ${tokenOut.symbol}`} />
<Row label="Slippage" value={`${slippage}%`} />
</div>
)}
<SwapButton
quote={quote}
tokenIn={tokenIn}
amountIn={parseUnits(amountIn || '0', tokenIn.decimals)}
minOut={minOut}
deadline={deadline}
path={quote?.path}
className="mt-4 w-full"
/>
<SlippageSettings value={slippage} onChange={setSlippage} className="mt-3" />
</div>
);
}
Price Impact Handling
function PriceImpactWarning({ impact }: { impact: number }) {
if (impact < 1) return null;
if (impact < 3) return (
<p className="text-sm text-yellow-400">⚠ Price impact {impact.toFixed(2)}% — higher than usual</p>
);
return (
<div className="rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-red-400">
Price impact {impact.toFixed(2)}% — high risk of loss. Reduce swap amount or choose another route.
</div>
);
}
Token Selector with Search
// Token list — from Uniswap token list or custom
const TOKEN_LIST_URL = 'https://gateway.ipfs.io/ipns/tokens.uniswap.org';
export function TokenSelector({ onSelect }: { onSelect: (token: Token) => void }) {
const [search, setSearch] = useState('');
const { data: tokenList } = useQuery({
queryKey: ['tokenList'],
queryFn: () => fetch(TOKEN_LIST_URL).then(r => r.json()).then(d => d.tokens),
staleTime: Infinity,
});
const filtered = tokenList?.filter(t =>
t.chainId === 1 &&
(t.symbol.toLowerCase().includes(search.toLowerCase()) ||
t.name.toLowerCase().includes(search.toLowerCase()) ||
t.address.toLowerCase() === search.toLowerCase()),
) ?? [];
return (
<div>
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Name, symbol or address"
className="w-full rounded-lg border border-white/10 bg-neutral-800 px-3 py-2"
/>
<VirtualList items={filtered} renderItem={token => (
<TokenRow token={token} onClick={() => onSelect(token)} />
)} />
</div>
);
}
Timeline: swap widget over Uniswap v2 router with quotes, slippage, approve and basic error handling — 5–7 days. Full-featured DEX with aggregator (0x/1inch), token selector from Uniswap token list, slippage/deadline settings, transaction history and multi-chain support — 2–3 weeks.







