DEV Community

Cover image for How to Swap Tokens Programmatically with TypeScript
Moon Soon
Moon Soon

Posted on • Originally published at swapapi.dev

How to Swap Tokens Programmatically with TypeScript

Swapping tokens programmatically with TypeScript requires three things: a swap API that returns executable calldata, a wallet library to sign transactions, and an RPC connection to broadcast them. With DEX aggregators handling over $13.5 billion in daily trading volume and TypeScript dominating web3 frontend and bot development, knowing how to execute on-chain swaps from code is a core skill for any DeFi developer. This guide walks you through swapping tokens on any EVM chain using TypeScript and a free API that requires no key and no SDK — just a single GET request.

What You'll Need

  • Node.js 18+ or Bun (for native fetch)
  • TypeScript (5.x recommended)
  • viem or ethers.js for signing and sending transactions
  • A wallet with a private key (testnet or mainnet)
  • No API key — the swap API used in this guide is free and requires no registration

Install the dependencies:

npm install viem typescript
Enter fullscreen mode Exit fullscreen mode

Step 1: Fetch a Swap Quote

The swap API at api.swapapi.dev takes a GET request with five parameters and returns a complete transaction object. No authentication, no SDK, no POST body.

const res = await fetch("https://api.swapapi.dev/v1/swap/1?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&amount=1000000000000000000&sender=0xYourAddress");
const quote = await res.json();
Enter fullscreen mode Exit fullscreen mode

This fetches a quote to swap 1 ETH for USDC on Ethereum mainnet. The parameters are:

  • chainId (path): 1 for Ethereum, 42161 for Arbitrum, 8453 for Base, 137 for Polygon, 56 for BSC — 46 chains total
  • tokenIn: the input token address. Use 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE for native ETH/BNB/MATIC on any chain
  • tokenOut: the output token address. USDC on Ethereum is 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
  • amount: the input amount in the token's smallest unit (wei). 1 ETH = 1000000000000000000
  • sender: the wallet address that will execute the swap

The response includes everything you need: the transaction object (to, data, value), expected output amounts, slippage protection, token metadata, and even recommended RPC URLs for the target chain.

Step 2: Parse the Response

The API returns a structured envelope with a status field that tells you exactly what happened.

const { success, data } = quote;

if (!success) {
  console.error("API error:", quote.error.message);
  process.exit(1);
}

if (data.status === "NoRoute") {
  console.error("No swap route found for this pair");
  process.exit(1);
}
Enter fullscreen mode Exit fullscreen mode

A successful response contains these key fields:

  • data.status: "Successful" for a full fill, "Partial" if only part of the amount can be swapped, "NoRoute" if no path exists
  • data.expectedAmountOut: the best-case output in raw units
  • data.minAmountOut: the minimum output after slippage (transaction reverts below this)
  • data.priceImpact: price impact as a decimal (e.g., -0.0012 = -0.12%)
  • data.tx: the ready-to-submit transaction object
  • data.rpcUrls: ranked list of recommended public RPCs for the chain

To get the human-readable output amount, divide by the token's decimals:

const amountOut = Number(data.expectedAmountOut) / 10 ** data.tokenTo.decimals;
console.log(`Expected output: ${amountOut} ${data.tokenTo.symbol}`);
Enter fullscreen mode Exit fullscreen mode

Step 3: Check Price Impact

Before executing any swap, check the price impact to avoid losing funds on low-liquidity pairs.

if (data.priceImpact < -0.05) {
  console.error(`Price impact too high: ${(data.priceImpact * 100).toFixed(2)}%`);
  process.exit(1);
}
Enter fullscreen mode Exit fullscreen mode

A price impact of -5% or worse means you're moving the market significantly. For production bots, set a configurable threshold — most strategies use -1% to -3% as a maximum.

Step 4: Handle ERC-20 Approvals

When swapping a native token (ETH, BNB, MATIC), no approval is needed. But when swapping an ERC-20 token, you must approve the router contract to spend your tokens before the swap will succeed.

import { createWalletClient, http, parseAbi } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arbitrum } from "viem/chains";

const account = privateKeyToAccount("0xYourPrivateKey");
const client = createWalletClient({ account, chain: arbitrum, transport: http(data.rpcUrl) });

const erc20Abi = parseAbi(["function approve(address spender, uint256 amount) returns (bool)"]);
const approveTx = await client.writeContract({ address: data.tokenFrom.address, abi: erc20Abi, functionName: "approve", args: [data.tx.to, BigInt(data.amountIn)] });
Enter fullscreen mode Exit fullscreen mode

Wait for the approval transaction to confirm, then fetch a fresh swap quote before proceeding. The calldata is time-sensitive — it includes a deadline, so always re-fetch after approval.

Note: USDT on Ethereum requires setting the allowance to 0 before setting a new non-zero allowance.

Step 5: Simulate with eth_call

Before sending a real transaction and spending gas, simulate the swap using eth_call. This is free and catches most failures before they cost you money.

import { createPublicClient } from "viem";

const publicClient = createPublicClient({ chain: arbitrum, transport: http(data.rpcUrl) });

await publicClient.call({ to: data.tx.to, data: data.tx.data, value: BigInt(data.tx.value), account: account.address });
Enter fullscreen mode Exit fullscreen mode

If eth_call reverts, the swap would fail on-chain. Common reasons: insufficient balance, missing ERC-20 approval, or expired deadline (quote older than 30 seconds).

Step 6: Estimate Gas and Send the Transaction

Estimate gas with a 20% buffer to avoid out-of-gas reverts on complex multi-hop routes, then send the transaction.

const gasEstimate = await publicClient.estimateGas({ to: data.tx.to, data: data.tx.data, value: BigInt(data.tx.value), account: account.address });

const txHash = await client.sendTransaction({ to: data.tx.to, data: data.tx.data, value: BigInt(data.tx.value), gas: (gasEstimate * 120n) / 100n });

console.log(`Transaction sent: ${txHash}`);
Enter fullscreen mode Exit fullscreen mode

The API provides a gasPrice field as a suggestion, but viem and ethers.js automatically handle EIP-1559 gas pricing on supported chains, so you can omit it. Submit within 30 seconds of fetching the quote — the calldata includes a deadline.

Complete Example: Swap ETH for USDC on Arbitrum

Here is a full working script that swaps ETH for USDC on Arbitrum using viem:

import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arbitrum } from "viem/chains";

const SENDER = "0xYourWalletAddress";
const PRIVATE_KEY = "0xYourPrivateKey";

const res = await fetch(`https://api.swapapi.dev/v1/swap/42161?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0xaf88d065e77c8cC2239327C5EDb3A432268e5831&amount=1000000000000000000&sender=${SENDER}`);
const { data } = await res.json();

if (data.status !== "Successful") throw new Error(`Swap failed: ${data.status}`);

const amountOut = Number(data.expectedAmountOut) / 10 ** data.tokenTo.decimals;
console.log(`Swapping 1 ETH for ~${amountOut} USDC on Arbitrum`);

const account = privateKeyToAccount(PRIVATE_KEY);
const publicClient = createPublicClient({ chain: arbitrum, transport: http(data.rpcUrl) });
const walletClient = createWalletClient({ account, chain: arbitrum, transport: http(data.rpcUrl) });

await publicClient.call({ to: data.tx.to, data: data.tx.data, value: BigInt(data.tx.value), account: SENDER });

const gas = await publicClient.estimateGas({ to: data.tx.to, data: data.tx.data, value: BigInt(data.tx.value), account: SENDER });

const txHash = await walletClient.sendTransaction({ to: data.tx.to, data: data.tx.data, value: BigInt(data.tx.value), gas: (gas * 120n) / 100n });

console.log(`TX: ${txHash}`);
Enter fullscreen mode Exit fullscreen mode

This script fetches a quote, validates the response, simulates the swap, estimates gas, and submits the transaction — all in under 25 lines. The API provides the RPC URL, so you don't need to configure a separate node provider.

Swapping on Other Chains

The same code works on any of the 46 supported chains. Just change the chain ID and token addresses:

Polygon (chain 137) — swap MATIC for USDC:

const res = await fetch(`https://api.swapapi.dev/v1/swap/137?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359&amount=1000000000000000000&sender=${SENDER}`);
Enter fullscreen mode Exit fullscreen mode

Base (chain 8453) — swap ETH for USDC:

const res = await fetch(`https://api.swapapi.dev/v1/swap/8453?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&amount=1000000000000000000&sender=${SENDER}`);
Enter fullscreen mode Exit fullscreen mode

BSC (chain 56) — swap BNB for USDT:

const res = await fetch(`https://api.swapapi.dev/v1/swap/56?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0x55d398326f99059fF775485246999027B3197955&amount=1000000000000000000&sender=${SENDER}`);
Enter fullscreen mode Exit fullscreen mode

Note that USDT uses 18 decimals on BSC, unlike 6 decimals on Ethereum. The API returns tokenTo.decimals in the response, so always use that value instead of hardcoding decimals.

Using ethers.js Instead of viem

If your project uses ethers.js v6, the same flow applies with different syntax:

import { ethers } from "ethers";

const res = await fetch(`https://api.swapapi.dev/v1/swap/42161?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0xaf88d065e77c8cC2239327C5EDb3A432268e5831&amount=1000000000000000000&sender=${SENDER}`);
const { data } = await res.json();

const provider = new ethers.JsonRpcProvider(data.rpcUrl);
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);

const tx = await wallet.sendTransaction({ to: data.tx.to, data: data.tx.data, value: BigInt(data.tx.value) });
console.log(`TX: ${tx.hash}`);
Enter fullscreen mode Exit fullscreen mode

ethers.js automatically handles gas estimation and EIP-1559 pricing, so you can send the transaction directly. For production use, add the simulation step (provider.call(...)) and gas buffer described earlier.

Handling Partial Fills and Edge Cases

The API returns three status values, all with HTTP 200. Your code should handle each:

if (data.status === "Successful") {
  // Full fill — execute the swap
}

if (data.status === "Partial") {
  const filled = Number(BigInt(data.amountIn)) / Number(BigInt(requestedAmount));
  console.log(`Only ${(filled * 100).toFixed(1)}% of requested amount can be filled`);
}

if (data.status === "NoRoute") {
  console.log("No route — try a different pair or chain");
}
Enter fullscreen mode Exit fullscreen mode

For partial fills, data.amountIn and data.expectedAmountOut reflect the fillable portion, not your original request. The tx object is still executable — it just swaps the partial amount. Do not retry the same amount in a loop; the liquidity cap won't change.

Frequently Asked Questions

How do I swap tokens programmatically with TypeScript?

Fetch a swap quote from a DEX aggregator API, then sign and submit the returned transaction object using viem or ethers.js. With Swap API, a single GET request returns the complete to, data, and value fields — no SDK, no API key, no ABI encoding. The full flow (quote, simulate, send) takes under 25 lines of TypeScript.

Do I need an API key to swap tokens?

No. Swap API is free and requires no API key, no account, and no registration. It supports 46 EVM chains from a single endpoint. Other aggregator APIs like 1inch and 0x require API keys and paid plans for production use.

What TypeScript library should I use for on-chain swaps?

Both viem and ethers.js work. viem is the newer library with better TypeScript types and tree-shaking support. ethers.js v6 is more established with a larger community. Both can sign transactions, estimate gas, and interact with ERC-20 contracts. The swap API is library-agnostic — it returns raw transaction data that any wallet library can submit.

How do I handle token decimals when swapping?

Never hardcode decimals. The API response includes tokenTo.decimals and tokenFrom.decimals for every swap. Use these to convert between raw amounts and human-readable values: humanAmount = rawAmount / 10 ** decimals. For example, USDC uses 6 decimals on Ethereum and Arbitrum, but USDT uses 18 decimals on BSC.

What happens if a swap fails?

Simulate every swap with eth_call before sending a real transaction. If the simulation reverts, the swap would fail on-chain — do not submit. Common failure reasons: insufficient token balance, missing ERC-20 approval, or expired calldata (quotes are valid for 30 seconds). If eth_estimateGas fails, that also indicates the transaction would revert.

Get Started

Swap API is free, requires no API key, and supports 46 EVM chains from a single endpoint. One GET request returns everything you need: executable calldata, expected output amounts, slippage protection, and RPC URLs.

Browse the Swagger UI for interactive testing, or check the OpenAPI specification to auto-generate a typed TypeScript client.

curl "https://api.swapapi.dev/v1/swap/42161?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0xaf88d065e77c8cC2239327C5EDb3A432268e5831&amount=1000000000000000000&sender=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
Enter fullscreen mode Exit fullscreen mode

Top comments (0)