DEX Interface (Token Swap) on Website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1215
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1043
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815

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.