How to Swap Tokens Programmatically
Last updated:
You can swap any ERC-20 token on 46 EVM chains with a single GET request to swapapi. No API key, no SDK, no account. The response includes executable calldata ready for on-chain submission.
Prerequisites
All you need is an HTTP client (curl, fetch, requests) and a wallet to sign the transaction. No registration, no API keys, no SDK installation.
Get a swap quote
Send a GET request with the chain ID in the path, and the token addresses, amount, and sender as query parameters. This example swaps 1 ETH for USDC on Ethereum mainnet.
$ curl "https://api.swapapi.dev/v1/swap/1?\ tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&\ tokenOut=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&\ amount=1000000000000000000&\ sender=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
The native token address 0xEeee...EEeE represents ETH (or the native gas token on any chain). USDC on Ethereum is 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48. The amount is in raw units (wei) — 1 ETH = 1018 wei.
Understand the response
The API returns an envelope with success, data, and timestamp. The data object contains everything you need to execute the swap.
{ "success": true, // always check this first "data": { "status": "Successful", // route found, tx ready "tokenFrom": { "address": "0xEeee...EEeE", "symbol": "ETH", "name": "Ether", "decimals": 18 }, "tokenTo": { "address": "0xA0b8...eB48", "symbol": "USDC", "name": "USD Coin", "decimals": 6 // USDC uses 6 decimals }, "swapPrice": 2500.0, // 1 ETH = 2500 USDC "priceImpact": 0.0012, // 0.12% — ratio, not percentage "amountIn": "1000000000000000000", // 1 ETH in wei "expectedAmountOut": "2500000000", // 2500 USDC (6 decimals) "minAmountOut": "2487500000", // minimum after slippage "tx": { // ready-to-sign transaction "from": "0xd8dA...6045", "to": "0x...router", // router contract address "data": "0x...calldata", // encoded swap calldata "value": "1000000000000000000", // ETH to send (native swaps only) "gas": "250000" // estimated gas limit } }, "timestamp": "2026-03-20T12:00:00.000Z" }
Handle edge cases
The status field has three possible values. Your code should handle all three.
Successful
A full route was found. The tx object is ready to sign and submit. The expectedAmountOut reflects the full amount.
Partial
Only part of the requested amount can be filled due to limited liquidity. The tx object is still present, but it will only swap the available portion. Check amountIn in the response to see how much will actually be swapped.
NoRoute
No swap route exists for this token pair on this chain. There is no tx object. Try a different token pair, a different chain, or a smaller amount.
Execute on-chain
Take the tx object from the response and submit it using your preferred library. Here are examples in TypeScript, Python, and Rust.
TypeScript (viem)
import { createPublicClient, createWalletClient, http } from "viem"; import { mainnet } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; // 1. Get the swap quote const res = await fetch( "https://api.swapapi.dev/v1/swap/1?" + new URLSearchParams({ tokenIn: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", tokenOut: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", amount: "1000000000000000000", maxSlippage: "0.005", sender: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", }) ); const { data } = await res.json(); if (data.status !== "Successful") { throw new Error("No route found"); } // 2. Execute the swap on-chain const account = privateKeyToAccount("0x..."); const client = createWalletClient({ account, chain: mainnet, transport: http(), }); const hash = await client.sendTransaction({ to: data.tx.to, data: data.tx.data, value: BigInt(data.tx.value ?? "0"), }); console.log("Transaction hash:", hash);
Python (web3.py)
import requests from web3 import Web3 # 1. Get the swap quote response = requests.get( "https://api.swapapi.dev/v1/swap/1", params={ "tokenIn": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", "tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "amount": "1000000000000000000", "maxSlippage": "0.005", "sender": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", }, ) data = response.json()["data"] # 2. Execute the swap on-chain w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com")) tx = { "from": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "to": Web3.to_checksum_address(data["tx"]["to"]), "data": data["tx"]["data"], "value": int(data["tx"].get("value", "0")), "gas": int(data["tx"].get("gas", "0")), } signed = w3.eth.account.sign_transaction(tx, private_key="0x...") tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) print(f"Transaction hash: {tx_hash.hex()}")
Rust
use reqwest::Client; use serde::Deserialize; #[derive(Deserialize)] struct ApiResponse { success: bool, data: SwapData, } #[derive(Deserialize)] struct SwapData { status: String, #[serde(rename = "swapPrice")] swap_price: Option<f64>, #[serde(rename = "expectedAmountOut")] expected_amount_out: Option<String>, tx: Option<TxData>, } #[derive(Deserialize)] struct TxData { to: String, data: String, value: Option<String>, } #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let client = Client::new(); let res: ApiResponse = client .get("https://api.swapapi.dev/v1/swap/1?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&amount=1000000000000000000&maxSlippage=0.005&sender=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045") .send() .await? .json() .await?; if res.success && res.data.status == "Successful" { println!("Swap price: {:?}", res.data.swap_price); println!("Expected output: {:?}", res.data.expected_amount_out); } Ok(()) }
ERC-20 approval flow
When swapping native tokens (ETH, BNB, MATIC), no approval is needed — the value is sent directly with the transaction. But for ERC-20 to ERC-20 swaps, you must approve the router contract to spend your tokens first.
Call approve(routerAddress, amount) on the input token contract before submitting the swap transaction. The router address is the tx.to field from the swap response.
The USDT approval gotcha
USDT on Ethereum has a non-standard approve function that requires you to set the allowance to 0 before setting a new value. If the current allowance is non-zero, calling approve with a new value will revert. Always reset to 0 first:
// USDT requires resetting allowance to 0 before approving await usdt.write.approve([routerAddress, 0n]); await usdt.write.approve([routerAddress, amount]);
Common mistakes
Wrong decimals
Token decimals vary by chain. USDC is 6 decimals on Ethereum but 18 decimals on BSC. USDT is 6 on Ethereum but 18 on BSC. Always check the decimals field in the response to interpret expectedAmountOut correctly.
Missing ERC-20 approval
If you skip the approval step for ERC-20 swaps, the transaction will revert on-chain. You will pay gas but get nothing. Always approve before swapping tokens (not needed for native ETH/BNB/MATIC).
Stale calldata
The tx calldata includes a deadline. Submit the transaction within 30 seconds of receiving the quote. If you wait too long, the transaction will revert. Re-fetch the quote if needed.
Not checking priceImpact
The priceImpact field tells you how much your trade moves the market. A value of -0.05 means you are getting 5% less than the market rate. Always check this before executing — high price impact usually means low liquidity. See our price impact and slippage guide for thresholds and best practices.