37 min read
Overview
Liquidity fragmentation is one of the biggest challenges in DeFi. For developers, finding the best price for a token trade often means querying multiple decentralized exchanges (DEXs) individually, which is inefficient and complex to maintain.
By integrating the 0x Swap API through Quicknode, you can solve this by accessing smart order routing that aggregates liquidity from over 150 sources across multiple supported chains. In this guide, you will learn how to integrate this powerful routing logic directly into your trading bots or dApp, enabling you to fetch indicative prices, manage token approvals, and execute trades with the best possible pricing.
We will demonstrate this integration using token swaps on the Base Mainnet, but the same concepts apply to all supported chains.
What You Will Do
- Query the 0x Swap API to get indicative pricing for a token pair
- Analyze API responses to handle token allowances and balance checks
- Generate a signed transaction quote
- Execute the swap using Viem
What You Will Need
- A Quicknode endpoint with the 0x Swap API Add‑on enabled
- Node.js (v20+ recommended) installed
- tsx installed
- A code editor (e.g., VS Code).
- A funded wallet (and its private key) for testing transactions (Ensure you have native gas token for gas and the token you wish to sell).
Why Use the 0x Swap API?
Before writing code, it is helpful to understand why many DeFi teams rely on the 0x Swap API. It provides more than just price discovery. It offers a set of developer-focused features that simplify quoting, simulation, routing, and execution across multiple chains.
- Multi-Chain Support: The API supports major EVM chains including Ethereum, Base, Polygon, BNB Smart Chain, Avalanche, Arbitrum, Optimism, and more. Your integration works the same across chains, which helps reduce fragmentation in your codebase.
- Extensive Liquidity Coverage: The routing engine aggregates liquidity from more than 150 sources. This includes decentralized exchanges such as Uniswap, Curve, Balancer, Aerodrome, PancakeSwap, Trader Joe, as well as professional market makers and RFQ providers.
- Built-in "Pre-Flight" Checks (
issues): Instead of your transaction failing on-chain and wasting gas, the API runs a simulation to warn you about blockers beforehand in theissuesfield of the response. Key checks include:issues.balance: Returns the actual and expected balance for the user's sell token.issues.allowance: Returns the actual allowance for the user's token by the specified spender.
- Smart Order Routing: The API doesn't just give you a price; it gives you the
route. You can see exactly how your trade is being split to minimize slippage. - Monetization Ready: If you are building a wallet or dashboard, you can easily add your own fees. The API supports collecting affiliate fees and trade surplus.
- Gas Optimization: The routing logic considers gas costs when finding the best path. The response provides precise
gasandgasPriceestimates, ensuring your transaction is likely to be executed quickly without overpaying.
Supported Methods
The Quicknode add-on exposes the 0x Swap API under your endpoint, and the two core methods you will use in this guide are:
GET /swap/allowance-holder/price
Use this to fetch an indicative price for a trade. You pass parameters such as chainId, sellToken, buyToken, and sellAmount, and the API returns:
- Expected output amount (
buyAmount) and slippage-aware minimum output (minBuyAmount) - Route details and gas estimates
issuesthat may block execution- Fee breakdowns (protocol, affiliate, gas, etc.)
This endpoint is ideal for building quote previews, updating UIs as users type amounts, or running “what if” checks without committing to a transaction.
GET /swap/allowance-holder/quote
Use this when you are ready to execute a swap. It returns a firm quote along with a fully constructed transaction payload under transaction (including to, data, value, and gas parameters). You can pass that payload directly into your wallet client or signing flow to send the trade onchain.
Together, these two methods let you separate the user experience into a preview step with /price and an execution step with /quote, while keeping your integration simple and consistent across chains.
Project Setup
Step 1: Initialize Project
First, you'll need to set up a simple Node.js environment and install the necessary dependencies. We will use viem, a lightweight and type-safe interface for Ethereum.
- Start a new Node.js project with your preferred package manager
mkdir quicknode-0x-swap-api
cd quicknode-0x-swap-api
npm init -y
- Install the required packages:
npm install viem dotenv
npm install --save-dev @types/node
Step 2: Configure Environment Variables
- Create a
.envfile in your root directory to store your credentials.
touch .env
- Add your Quicknode endpoint URL, specific path extension of the add-on, and private key to the
.envfile.
Note: The 0x Swap API is accessed via a specific path extension (addon/1117) on your Quicknode HTTP Endpoint URL.
# .env
# QuickNode endpoint URL (without addon path)
QUICKNODE_HTTP_URL=https://your-base-endpoint.quiknode.pro/your-key
# 0x Swap API addon path
ADD_ON_PATH=addon/1117
# Your wallet's private key (keep this secret!)
PRIVATE_KEY=0x...your-private-key-here
Using 0x Swap API with Viem
Step 1: Core Imports and Setup
Create a new file named swap.ts in your project root.
touch swap.ts
Then, start your swap.ts file with the necessary imports and configuration.
This section imports the viem library functions for blockchain interaction, sets up environment variables, and creates the wallet and blockchain client connections needed for reading blockchain state and sending transactions.
import {
createWalletClient,
createPublicClient,
http,
parseUnits,
formatUnits,
maxUint256,
erc20Abi,
Address,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
import dotenv from "dotenv";
// Load environment variables
dotenv.config();
// ============================================
// 1. CONFIGURATION & SETUP
// ============================================
// Environment variables validation
const QUICKNODE_HTTP_URL = process.env.QUICKNODE_HTTP_URL;
const ADD_ON_PATH = process.env.ADD_ON_PATH;
const PRIVATE_KEY = process.env.PRIVATE_KEY as Address;
if (!QUICKNODE_HTTP_URL || !ADD_ON_PATH || !PRIVATE_KEY) {
throw new Error(
"Missing required environment variables: QUICKNODE_HTTP_URL, ADD_ON_PATH, and PRIVATE_KEY"
);
}
// Base RPC URL for standard Ethereum RPC calls
const BASE_RPC_URL = QUICKNODE_HTTP_URL;
// Full URL with addon path for 0x Swap API calls
const SWAP_API_URL = `${QUICKNODE_HTTP_URL}/${ADD_ON_PATH}`;
// Token addresses for Base
const TOKENS = {
NATIVE: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", // Native ETH on Base
WETH: "0x4200000000000000000000000000000000000006", // Wrapped ETH on Base
USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
USDe: "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34", // USDe on Base
WBTC: "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c", // WBTC on Base
} as const;
// Chain configuration
const CHAIN_ID = "8453"; // Base chain ID
// Initialize account from private key
const account = privateKeyToAccount(PRIVATE_KEY);
// Initialize Viem clients
const walletClient = createWalletClient({
account,
chain: base,
transport: http(BASE_RPC_URL),
});
const publicClient = createPublicClient({
chain: base,
transport: http(BASE_RPC_URL),
});
Step 2: Type Definitions
Define TypeScript interfaces for API responses to ensure type safety.
These type definitions provide structure for the API responses, helping catch errors at compile time and providing better IDE autocomplete support. They define the exact shape of data we'll receive from the 0x API. Check out the 0x Swap API documentation for more details.
// ============================================
// 2. TYPE DEFINITIONS
// ============================================
interface SwapPriceResponse {
allowanceTarget: Address;
buyAmount: string;
buyToken: Address;
sellAmount: string;
sellToken: Address;
gas: string;
gasPrice: string;
issues: {
allowance?: {
actual: string;
spender: Address;
};
balance?: {
token: Address;
actual: string;
expected: string;
};
simulationIncomplete?: boolean;
};
liquidityAvailable: boolean;
route: any;
fees: any;
minBuyAmount?: string;
}
interface SwapQuoteResponse extends SwapPriceResponse {
transaction: {
to: Address;
data: string;
gas: string;
gasPrice: string;
value: string;
};
}
Step 3: Price Fetching
Implement the function to fetch indicative prices from the 0x API.
This function queries the 0x API for current swap prices without creating an actual order. It's used to show users expected swap outcomes and check for potential issues before committing to a trade.
This step is crucial for validating that the user has enough balance, that the token approvals are sufficient, and that the market has sufficient liquidity.
The API response includes several important fields to help you manage the swap process effectively:
-
issues: Check this object immediately. Ifissues.balance.actualis lower than your sell amount, you can prompt the user to top up before they even try to swap. -
liquidityAvailable: A boolean confirming if there is enough market depth for your trade. -
route: An array showing how the trade is split. You can use this to display a "Smart Route" visualization in your UI.
// ============================================
// 3. PRICE FETCHING
// ============================================
/**
* Fetch indicative price from 0x API
* This is a read-only endpoint to check prices without committing
*/
async function getPrice(
sellToken: Address,
buyToken: Address,
sellAmount: string,
decimals: number = 18,
slippageBps: number = 100
): Promise<SwapPriceResponse> {
const sellAmountWei = parseUnits(sellAmount, decimals).toString();
const params = new URLSearchParams({
chainId: CHAIN_ID,
sellToken,
buyToken,
sellAmount: sellAmountWei,
taker: account.address,
slippageBps: slippageBps.toString(),
});
const headers: HeadersInit = {
"Content-Type": "application/json",
};
const url = `${SWAP_API_URL}/swap/allowance-holder/price?${params.toString()}`;
console.log(` Fetching price from: ${url}`);
const response = await fetch(url, { headers });
if (!response.ok) {
const error = await response.text();
throw new Error(`Price fetch failed: ${error}`);
}
return response.json();
}
Step 4: Balance and Approval Checks
Add these helper functions to your script to standardize how you check balances and allowances, automatically handling the logic differences between native ETH (which requires getBalance) and ERC20 tokens (which require balanceOf and allowance).
While 0x Swap API already includes pre-flight checks in the issues field, we can use these functions for displaying balances and checking approvals.
// ============================================
// 4. BALANCE CHECKING
// ============================================
/**
* Check if a token is the native ETH token
*/
function isNativeToken(tokenAddress: Address): boolean {
return tokenAddress.toLowerCase() === TOKENS.NATIVE.toLowerCase();
}
/**
* Check token balance for a specific address
*/
async function checkBalance(
tokenAddress: Address,
userAddress: Address
): Promise<bigint> {
// For native ETH, use getBalance instead of ERC20 balanceOf
if (isNativeToken(tokenAddress)) {
const balance = await publicClient.getBalance({
address: userAddress,
});
return balance;
}
const balance = await publicClient.readContract({
address: tokenAddress as Address,
abi: erc20Abi,
functionName: "balanceOf",
args: [userAddress as Address],
});
return balance;
}
/**
* Get token decimals
*/
async function getTokenDecimals(tokenAddress: Address): Promise<number> {
// Native ETH always has 18 decimals
if (isNativeToken(tokenAddress)) {
return 18;
}
const decimals = await publicClient.readContract({
address: tokenAddress,
abi: erc20Abi,
functionName: "decimals",
});
return decimals;
}
/**
* Check current allowance for a spender
*/
async function checkAllowance(
tokenAddress: Address,
owner: Address,
spender: Address
): Promise<bigint> {
const allowance = await publicClient.readContract({
address: tokenAddress,
abi: erc20Abi,
functionName: "allowance",
args: [owner, spender],
});
return allowance;
}
Step 5: Issue Detection
Implement logic to detect and handle common swap issues.
This function analyzes the API response for potential problems like insufficient balance or missing token approvals. It provides clear error messages and automatically triggers the approval process when needed, making the swap process smoother. The issues field from the API response is used to determine the type of issue and the appropriate action to take.
// ============================================
// 5. ISSUE DETECTION & HANDLING
// ============================================
/**
* Analyze and handle issues from price response
*/
async function handleIssues(priceData: SwapPriceResponse): Promise<void> {
console.log("🔍 Analyzing potential issues...");
// Check for balance issues
if (priceData.issues.balance) {
const { token, actual, expected } = priceData.issues.balance;
// Skip balance check for native ETH as it's handled differently
if (!isNativeToken(token)) {
const decimals = await getTokenDecimals(token);
const actualFormatted = formatUnits(BigInt(actual), decimals);
const expectedFormatted = formatUnits(BigInt(expected), decimals);
console.log(`\n⚠️ BALANCE ISSUE DETECTED:`);
console.log(` Token: ${token}`);
console.log(` Current balance: ${actualFormatted}`);
console.log(` Required: ${expectedFormatted}`);
console.log(
` Shortfall: ${formatUnits(
BigInt(expected) - BigInt(actual),
decimals
)}`
);
throw new Error(
`Insufficient balance. Need ${expectedFormatted} but have ${actualFormatted}`
);
} else {
// For native ETH, still check but with simpler formatting
const decimals = 18; // Native ETH has 18 decimals
const actualFormatted = formatUnits(BigInt(actual), decimals);
const expectedFormatted = formatUnits(BigInt(expected), decimals);
console.log(`\n⚠️ BALANCE ISSUE DETECTED:`);
console.log(` Token: Native ETH`);
console.log(` Current balance: ${actualFormatted} ETH`);
console.log(` Required: ${expectedFormatted} ETH`);
console.log(
` Shortfall: ${formatUnits(
BigInt(expected) - BigInt(actual),
decimals
)} ETH`
);
throw new Error(
`Insufficient balance. Need ${expectedFormatted} ETH but have ${actualFormatted} ETH`
);
}
}
// Check for allowance issues - skip for native ETH (no approval needed)
if (priceData.issues.allowance && !isNativeToken(priceData.sellToken)) {
const { spender, actual } = priceData.issues.allowance;
const requiredAmount = BigInt(priceData.sellAmount);
const currentAllowance = BigInt(actual);
console.log(`\n⚠️ ALLOWANCE ISSUE DETECTED:`);
console.log(` Current allowance: ${currentAllowance.toString()}`);
console.log(` Required allowance: ${requiredAmount.toString()}`);
console.log(` Spender (AllowanceHolder): ${spender}`);
// Check if current allowance is sufficient for this swap
if (currentAllowance >= requiredAmount) {
console.log(` ✅ Current allowance is sufficient for this swap`);
} else {
console.log(` Action: Setting approval for exact swap amount...`);
await setTokenApprovalForAmount(
priceData.sellToken,
spender,
requiredAmount
);
console.log(`✅ Token approval completed successfully`);
}
} else if (isNativeToken(priceData.sellToken)) {
console.log("✅ Native ETH selected - no approval needed");
} else {
console.log("✅ No issues detected - ready to proceed");
}
// Check for simulation issues
if (priceData.issues.simulationIncomplete) {
console.log("⚠️ Warning: Simulation incomplete - transaction may fail");
}
}
Step 6: Token Approval
Implement secure token approval with exact amounts.
This function handles ERC20 token approvals required before swapping. Instead of using infinite approvals (security risk), it approves only the exact amount needed for each swap, checking existing allowances first (as a second layer of check after the API response) to avoid unnecessary transactions.
// ============================================
// 6. TOKEN APPROVAL
// ============================================
/**
* Set token approval for exact amount needed for swap
* This is safer than infinite approvals
*/
async function setTokenApprovalForAmount(
tokenAddress: Address,
spender: Address,
amount: bigint
): Promise<void> {
try {
// Check current allowance
const currentAllowance = await checkAllowance(
tokenAddress,
account.address,
spender
);
console.log(` Current allowance: ${currentAllowance.toString()}`);
console.log(` Required amount: ${amount.toString()}`);
// Only approve if current allowance is insufficient
if (currentAllowance >= amount) {
console.log(
` ℹ️ Current allowance is already sufficient for this swap`
);
return;
}
// Calculate how much additional approval is needed
// We'll approve for the exact amount needed
const approvalAmount = amount;
console.log(` Approving exact amount: ${approvalAmount.toString()}`);
// Simulate the approval transaction
console.log(` Simulating approval transaction...`);
const { request } = await publicClient.simulateContract({
account,
address: tokenAddress,
abi: erc20Abi,
functionName: "approve",
args: [spender, approvalAmount],
});
// Execute the approval
console.log(` Sending approval transaction...`);
const hash = await walletClient.writeContract(request);
console.log(` Approval tx hash: ${hash}`);
// Wait for confirmation
console.log(` Waiting for confirmation...`);
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") {
throw new Error("Approval transaction failed");
}
console.log(` Approval confirmed in block ${receipt.blockNumber}`);
// Verify the new allowance
const newAllowance = await checkAllowance(
tokenAddress,
account.address,
spender
);
console.log(` New allowance: ${newAllowance.toString()}`);
} catch (error) {
console.error("❌ Approval failed:", error);
throw error;
}
}
Step 7: Quote Fetching
Get firm, executable quotes from the 0x API.
Unlike the price endpoint, the quote endpoint returns a complete, ready-to-execute transaction. This signals intent to trade and often receives better pricing. It includes slippage protection to ensure minimum output amounts.
The response includes a transaction object. This is what you will pass to your wallet or web3 library:
transaction.to: The contract address the transaction will interact with (typically the 0x Exchange Proxy or AllowanceHolder).transaction.data: The encoded hex data containing the swap logic.transaction.value: The amount of native ETH (in wei) to send. This is usually0for ERC-20 swaps, but will be non-zero if you are swapping native ETH.
// ============================================
// 7. QUOTE FETCHING
// ============================================
/**
* Get a firm quote from 0x API
* This returns an executable transaction
*/
async function getQuote(
sellToken: Address,
buyToken: Address,
sellAmount: string,
slippageBps: number,
decimals: number = 18
): Promise<SwapQuoteResponse> {
const sellAmountWei = parseUnits(sellAmount, decimals).toString();
const params = new URLSearchParams({
chainId: CHAIN_ID,
sellToken,
buyToken,
sellAmount: sellAmountWei,
taker: account.address,
slippageBps: slippageBps.toString(),
});
const headers: HeadersInit = {
"Content-Type": "application/json",
};
const url = `${SWAP_API_URL}/swap/allowance-holder/quote?${params.toString()}`;
console.log(` Requesting firm quote...`);
const response = await fetch(url, { headers });
if (!response.ok) {
const error = await response.text();
throw new Error(`Quote fetch failed: ${error}`);
}
return response.json();
}
Step 8: Transaction Execution
Submit the swap transaction to the blockchain.
This function takes the transaction data from the quote and submits it to the blockchain. It handles both token-to-token swaps (value = 0) and native ETH swaps (value > 0), logging important details for transparency.
// ============================================
// 8. TRANSACTION EXECUTION
// ============================================
/**
* Submit the swap transaction to the blockchain
*/
async function submitTransaction(
transaction: SwapQuoteResponse["transaction"]
): Promise<Address> {
console.log("📤 Submitting transaction to the blockchain...");
console.log(` To: ${transaction.to}`);
console.log(` Gas limit: ${transaction.gas}`);
console.log(
` Gas price: ${formatUnits(BigInt(transaction.gasPrice), 9)} Gwei`
);
console.log(` Value: ${transaction.value} wei`);
const hash = await walletClient.sendTransaction({
to: transaction.to,
data: transaction.data as `0x${string}`,
gas: BigInt(transaction.gas),
gasPrice: BigInt(transaction.gasPrice),
value: transaction.value ? BigInt(transaction.value) : 0n,
});
return hash;
}
Step 9: Main Orchestration
Combine all steps into a complete swap flow.
This is the main function that orchestrates the entire swap process. It coordinates all the previous functions in the correct order: fetching prices, handling issues, getting quotes, and executing the swap, with detailed logging at each step.
// ============================================
// 9. MAIN SWAP ORCHESTRATION
// ============================================
/**
* Execute a complete token swap
*/
async function executeSwap(
sellToken: Address,
buyToken: Address,
sellAmount: string,
slippageBps: number = 100
): Promise<any> {
console.log("\n" + "=".repeat(60));
console.log("🔄 STARTING TOKEN SWAP");
console.log("=".repeat(60));
const sellDecimals = await getTokenDecimals(sellToken);
const buyDecimals = await getTokenDecimals(buyToken);
console.log(`\n📊 Swap Parameters:`);
console.log(
` Sell: ${sellAmount} tokens (${sellToken}...)`
);
console.log(` Buy: ${buyToken}...`);
console.log(
` Slippage: ${slippageBps / 100}% (${slippageBps} bps)`
);
console.log(` User: ${account.address}`);
try {
// STEP 1: Get Price
console.log("\n" + "-".repeat(60));
console.log("📈 STEP 1: FETCHING INDICATIVE PRICE");
console.log("-".repeat(60));
const priceData = await getPrice(
sellToken,
buyToken,
sellAmount,
sellDecimals,
slippageBps
);
if (!priceData.liquidityAvailable) {
throw new Error("❌ Insufficient liquidity for this trade");
}
const buyAmountFormatted = formatUnits(
BigInt(priceData.buyAmount),
buyDecimals
);
const minBuyAmountFormatted = priceData.minBuyAmount
? formatUnits(BigInt(priceData.minBuyAmount), buyDecimals)
: "N/A";
console.log(`✅ Price fetched successfully:`);
console.log(` Expected output: ${buyAmountFormatted}`);
console.log(` Minimum output: ${minBuyAmountFormatted}`);
console.log(` Estimated gas: ${priceData.gas} units`);
// STEP 2: Handle Issues
console.log("\n" + "-".repeat(60));
console.log("🔍 STEP 2: CHECKING FOR ISSUES");
console.log("-".repeat(60));
// Handle issues (including approval)
await handleIssues(priceData);
// STEP 3: Get Quote
console.log("\n" + "-".repeat(60));
console.log("📋 STEP 3: FETCHING FIRM QUOTE");
console.log("-".repeat(60));
const quoteData = await getQuote(
sellToken,
buyToken,
sellAmount,
slippageBps,
sellDecimals
);
const quoteBuyAmount = formatUnits(
BigInt(quoteData.buyAmount),
buyDecimals
);
console.log(`✅ Quote received:`);
console.log(` Final output: ${quoteBuyAmount}`);
console.log(` Transaction to: ${quoteData.transaction.to}`);
// STEP 4: Execute Swap
console.log("\n" + "-".repeat(60));
console.log("🚀 STEP 4: EXECUTING SWAP");
console.log("-".repeat(60));
const txHash = await submitTransaction(quoteData.transaction);
console.log(`✅ Transaction submitted!`);
console.log(` Hash: ${txHash}`);
console.log("\n⏳ Waiting for confirmation...");
const receipt = await publicClient.waitForTransactionReceipt({
hash: txHash,
});
console.log("\n" + "=".repeat(60));
if (receipt.status === "success") {
console.log(`🎉 SWAP SUCCESSFUL!`);
console.log(` Block: ${receipt.blockNumber}`);
console.log(` Gas used: ${receipt.gasUsed.toString()}`);
} else {
console.log(`❌ TRANSACTION FAILED`);
}
console.log("=".repeat(60) + "\n");
return receipt;
} catch (error) {
console.error("\n❌ Swap failed:", error);
throw error;
}
}
Step 10: Utility Functions
Add helper functions for displaying balances and running the script.
These utility functions provide convenient ways to check all token balances at once and set up the main execution flow. The displayBalances function helps verify the swap worked correctly by showing before/after balances.
// ============================================
// 10. UTILITY FUNCTIONS
// ============================================
/**
* Display token balances
*/
async function displayBalances(): Promise<void> {
console.log("\n💰 Current Balances:");
for (const [symbol, address] of Object.entries(TOKENS)) {
const balance = await checkBalance(address, account.address);
const decimals = await getTokenDecimals(address);
const formatted = formatUnits(balance, decimals);
// Display native ETH with ETH suffix
if (isNativeToken(address)) {
console.log(` ${symbol}: ${formatted} ETH`);
} else {
console.log(` ${symbol}: ${formatted}`);
}
}
}
Step 11: Main Execution
Finally, we combine everything into a main execution function. This function fetches the price to verify conditions, ensures approvals are in place, gets the firm quote, and submits the transaction to the blockchain.
// ============================================
// 11. MAIN EXECUTION
// ============================================
async function main() {
try {
console.log("🔑 Wallet Configuration:");
console.log(` Address: ${account.address}`);
console.log(` Network: Base (Chain ID: ${CHAIN_ID})`);
// Display current balances
await displayBalances();
// Example swap: 0.000001 ETH -> USDC with 1% slippage (100 bps)
await executeSwap(
TOKENS.NATIVE, // Sell native ETH
TOKENS.USDC, // Buy USDC
"0.000001", // Swap amount
100 // 1% slippage in bps
);
// Display updated balances
await displayBalances();
} catch (error) {
console.error("Error in main:", error);
process.exit(1);
}
}
// Run if called directly
if (require.main === module) {
main();
}
Full Code
Check the below for the complete swap.ts file:
Click to expand
import {
createWalletClient,
createPublicClient,
http,
parseUnits,
formatUnits,
maxUint256,
erc20Abi,
Address,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
import dotenv from "dotenv";
// Load environment variables
dotenv.config();
// ============================================
// 1. CONFIGURATION & SETUP
// ============================================
// Environment variables validation
const QUICKNODE_HTTP_URL = process.env.QUICKNODE_HTTP_URL;
const ADD_ON_PATH = process.env.ADD_ON_PATH;
const PRIVATE_KEY = process.env.PRIVATE_KEY as Address;
if (!QUICKNODE_HTTP_URL || !ADD_ON_PATH || !PRIVATE_KEY) {
throw new Error(
"Missing required environment variables: QUICKNODE_HTTP_URL, ADD_ON_PATH, and PRIVATE_KEY"
);
}
// Base RPC URL for standard Ethereum RPC calls
const BASE_RPC_URL = QUICKNODE_HTTP_URL;
// Full URL with addon path for 0x Swap API calls
const SWAP_API_URL = `${QUICKNODE_HTTP_URL}/${ADD_ON_PATH}`;
// Token addresses for Base
const TOKENS = {
NATIVE: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", // Native ETH on Base
WETH: "0x4200000000000000000000000000000000000006", // Wrapped ETH on Base
USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
USDe: "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34", // USDe on Base
WBTC: "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c", // WBTC on Base
} as const;
// Chain configuration
const CHAIN_ID = "8453"; // Base chain ID
// Initialize account from private key
const account = privateKeyToAccount(PRIVATE_KEY);
// Initialize Viem clients
const walletClient = createWalletClient({
account,
chain: base,
transport: http(BASE_RPC_URL),
});
const publicClient = createPublicClient({
chain: base,
transport: http(BASE_RPC_URL),
});
// ============================================
// 2. TYPE DEFINITIONS
// ============================================
interface SwapPriceResponse {
allowanceTarget: Address;
buyAmount: string;
buyToken: Address;
sellAmount: string;
sellToken: Address;
gas: string;
gasPrice: string;
issues: {
allowance?: {
actual: string;
spender: Address;
};
balance?: {
token: Address;
actual: string;
expected: string;
};
simulationIncomplete?: boolean;
};
liquidityAvailable: boolean;
route: any;
fees: any;
minBuyAmount?: string;
}
interface SwapQuoteResponse extends SwapPriceResponse {
transaction: {
to: Address;
data: string;
gas: string;
gasPrice: string;
value: string;
};
}
// ============================================
// 3. PRICE FETCHING
// ============================================
/**
* Fetch indicative price from 0x API
* This is a read-only endpoint to check prices without committing
*/
async function getPrice(
sellToken: Address,
buyToken: Address,
sellAmount: string,
decimals: number = 18,
slippageBps: number = 100
): Promise<SwapPriceResponse> {
const sellAmountWei = parseUnits(sellAmount, decimals).toString();
const params = new URLSearchParams({
chainId: CHAIN_ID,
sellToken,
buyToken,
sellAmount: sellAmountWei,
taker: account.address,
slippageBps: slippageBps.toString(),
});
const headers: HeadersInit = {
"Content-Type": "application/json",
};
const url = `${SWAP_API_URL}/swap/allowance-holder/price?${params.toString()}`;
console.log(` Fetching price from: ${url}`);
const response = await fetch(url, { headers });
if (!response.ok) {
const error = await response.text();
throw new Error(`Price fetch failed: ${error}`);
}
return response.json();
}
// ============================================
// 4. BALANCE CHECKING
// ============================================
/**
* Check if a token is the native ETH token
*/
function isNativeToken(tokenAddress: Address): boolean {
return tokenAddress.toLowerCase() === TOKENS.NATIVE.toLowerCase();
}
/**
* Check token balance for a specific address
*/
async function checkBalance(
tokenAddress: Address,
userAddress: Address
): Promise<bigint> {
// For native ETH, use getBalance instead of ERC20 balanceOf
if (isNativeToken(tokenAddress)) {
const balance = await publicClient.getBalance({
address: userAddress,
});
return balance;
}
const balance = await publicClient.readContract({
address: tokenAddress as Address,
abi: erc20Abi,
functionName: "balanceOf",
args: [userAddress as Address],
});
return balance;
}
/**
* Get token decimals
*/
async function getTokenDecimals(tokenAddress: Address): Promise<number> {
// Native ETH always has 18 decimals
if (isNativeToken(tokenAddress)) {
return 18;
}
const decimals = await publicClient.readContract({
address: tokenAddress,
abi: erc20Abi,
functionName: "decimals",
});
return decimals;
}
/**
* Check current allowance for a spender
*/
async function checkAllowance(
tokenAddress: Address,
owner: Address,
spender: Address
): Promise<bigint> {
const allowance = await publicClient.readContract({
address: tokenAddress,
abi: erc20Abi,
functionName: "allowance",
args: [owner, spender],
});
return allowance;
}
// ============================================
// 5. ISSUE DETECTION & HANDLING
// ============================================
/**
* Analyze and handle issues from price response
*/
async function handleIssues(priceData: SwapPriceResponse): Promise<void> {
console.log("🔍 Analyzing potential issues...");
// Check for balance issues
if (priceData.issues.balance) {
const { token, actual, expected } = priceData.issues.balance;
// Skip balance check for native ETH as it's handled differently
if (!isNativeToken(token)) {
const decimals = await getTokenDecimals(token);
const actualFormatted = formatUnits(BigInt(actual), decimals);
const expectedFormatted = formatUnits(BigInt(expected), decimals);
console.log(`\n⚠️ BALANCE ISSUE DETECTED:`);
console.log(` Token: ${token}`);
console.log(` Current balance: ${actualFormatted}`);
console.log(` Required: ${expectedFormatted}`);
console.log(
` Shortfall: ${formatUnits(
BigInt(expected) - BigInt(actual),
decimals
)}`
);
throw new Error(
`Insufficient balance. Need ${expectedFormatted} but have ${actualFormatted}`
);
} else {
// For native ETH, still check but with simpler formatting
const decimals = 18; // Native ETH has 18 decimals
const actualFormatted = formatUnits(BigInt(actual), decimals);
const expectedFormatted = formatUnits(BigInt(expected), decimals);
console.log(`\n⚠️ BALANCE ISSUE DETECTED:`);
console.log(` Token: Native ETH`);
console.log(` Current balance: ${actualFormatted} ETH`);
console.log(` Required: ${expectedFormatted} ETH`);
console.log(
` Shortfall: ${formatUnits(
BigInt(expected) - BigInt(actual),
decimals
)} ETH`
);
throw new Error(
`Insufficient balance. Need ${expectedFormatted} ETH but have ${actualFormatted} ETH`
);
}
}
// Check for allowance issues - skip for native ETH (no approval needed)
if (priceData.issues.allowance && !isNativeToken(priceData.sellToken)) {
const { spender, actual } = priceData.issues.allowance;
const requiredAmount = BigInt(priceData.sellAmount);
const currentAllowance = BigInt(actual);
console.log(`\n⚠️ ALLOWANCE ISSUE DETECTED:`);
console.log(` Current allowance: ${currentAllowance.toString()}`);
console.log(` Required allowance: ${requiredAmount.toString()}`);
console.log(` Spender (AllowanceHolder): ${spender}`);
// Check if current allowance is sufficient for this swap
if (currentAllowance >= requiredAmount) {
console.log(` ✅ Current allowance is sufficient for this swap`);
} else {
console.log(` Action: Setting approval for exact swap amount...`);
await setTokenApprovalForAmount(
priceData.sellToken,
spender,
requiredAmount
);
console.log(`✅ Token approval completed successfully`);
}
} else if (isNativeToken(priceData.sellToken)) {
console.log("✅ Native ETH selected - no approval needed");
} else {
console.log("✅ No issues detected - ready to proceed");
}
// Check for simulation issues
if (priceData.issues.simulationIncomplete) {
console.log("⚠️ Warning: Simulation incomplete - transaction may fail");
}
}
// ============================================
// 6. TOKEN APPROVAL
// ============================================
/**
* Set token approval for exact amount needed for swap
* This is safer than infinite approvals
*/
async function setTokenApprovalForAmount(
tokenAddress: Address,
spender: Address,
amount: bigint
): Promise<void> {
try {
// Check current allowance
const currentAllowance = await checkAllowance(
tokenAddress,
account.address,
spender
);
console.log(` Current allowance: ${currentAllowance.toString()}`);
console.log(` Required amount: ${amount.toString()}`);
// Only approve if current allowance is insufficient
if (currentAllowance >= amount) {
console.log(
` ℹ️ Current allowance is already sufficient for this swap`
);
return;
}
// Calculate how much additional approval is needed
// We'll approve for the exact amount needed
const approvalAmount = amount;
console.log(` Approving exact amount: ${approvalAmount.toString()}`);
// Simulate the approval transaction
console.log(` Simulating approval transaction...`);
const { request } = await publicClient.simulateContract({
account,
address: tokenAddress,
abi: erc20Abi,
functionName: "approve",
args: [spender, approvalAmount],
});
// Execute the approval
console.log(` Sending approval transaction...`);
const hash = await walletClient.writeContract(request);
console.log(` Approval tx hash: ${hash}`);
// Wait for confirmation
console.log(` Waiting for confirmation...`);
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") {
throw new Error("Approval transaction failed");
}
console.log(` Approval confirmed in block ${receipt.blockNumber}`);
// Verify the new allowance
const newAllowance = await checkAllowance(
tokenAddress,
account.address,
spender
);
console.log(` New allowance: ${newAllowance.toString()}`);
} catch (error) {
console.error("❌ Approval failed:", error);
throw error;
}
}
// ============================================
// 7. QUOTE FETCHING
// ============================================
/**
* Get a firm quote from 0x API
* This returns an executable transaction
*/
async function getQuote(
sellToken: Address,
buyToken: Address,
sellAmount: string,
slippageBps: number,
decimals: number = 18
): Promise<SwapQuoteResponse> {
const sellAmountWei = parseUnits(sellAmount, decimals).toString();
const params = new URLSearchParams({
chainId: CHAIN_ID,
sellToken,
buyToken,
sellAmount: sellAmountWei,
taker: account.address,
slippageBps: slippageBps.toString(),
});
const headers: HeadersInit = {
"Content-Type": "application/json",
};
const url = `${SWAP_API_URL}/swap/allowance-holder/quote?${params.toString()}`;
console.log(` Requesting firm quote...`);
const response = await fetch(url, { headers });
if (!response.ok) {
const error = await response.text();
throw new Error(`Quote fetch failed: ${error}`);
}
return response.json();
}
// ============================================
// 8. TRANSACTION EXECUTION
// ============================================
/**
* Submit the swap transaction to the blockchain
*/
async function submitTransaction(
transaction: SwapQuoteResponse["transaction"]
): Promise<Address> {
console.log("📤 Submitting transaction to the blockchain...");
console.log(` To: ${transaction.to}`);
console.log(` Gas limit: ${transaction.gas}`);
console.log(
` Gas price: ${formatUnits(BigInt(transaction.gasPrice), 9)} Gwei`
);
console.log(` Value: ${transaction.value} wei`);
const hash = await walletClient.sendTransaction({
to: transaction.to,
data: transaction.data as `0x${string}`,
gas: BigInt(transaction.gas),
gasPrice: BigInt(transaction.gasPrice),
value: transaction.value ? BigInt(transaction.value) : 0n,
});
return hash;
}
// ============================================
// 9. MAIN SWAP ORCHESTRATION
// ============================================
/**
* Execute a complete token swap
*/
async function executeSwap(
sellToken: Address,
buyToken: Address,
sellAmount: string,
slippageBps: number = 100
): Promise<any> {
console.log("\n" + "=".repeat(60));
console.log("🔄 STARTING TOKEN SWAP");
console.log("=".repeat(60));
const sellDecimals = await getTokenDecimals(sellToken);
const buyDecimals = await getTokenDecimals(buyToken);
console.log(`\n📊 Swap Parameters:`);
console.log(
` Sell: ${sellAmount} tokens (${sellToken}...)`
);
console.log(` Buy: ${buyToken}...`);
console.log(
` Slippage: ${slippageBps / 100}% (${slippageBps} bps)`
);
console.log(` User: ${account.address}`);
try {
// STEP 1: Get Price
console.log("\n" + "-".repeat(60));
console.log("📈 STEP 1: FETCHING INDICATIVE PRICE");
console.log("-".repeat(60));
const priceData = await getPrice(
sellToken,
buyToken,
sellAmount,
sellDecimals,
slippageBps
);
if (!priceData.liquidityAvailable) {
throw new Error("❌ Insufficient liquidity for this trade");
}
const buyAmountFormatted = formatUnits(
BigInt(priceData.buyAmount),
buyDecimals
);
const minBuyAmountFormatted = priceData.minBuyAmount
? formatUnits(BigInt(priceData.minBuyAmount), buyDecimals)
: "N/A";
console.log(`✅ Price fetched successfully:`);
console.log(` Expected output: ${buyAmountFormatted}`);
console.log(` Minimum output: ${minBuyAmountFormatted}`);
console.log(` Estimated gas: ${priceData.gas} units`);
// STEP 2: Handle Issues
console.log("\n" + "-".repeat(60));
console.log("🔍 STEP 2: CHECKING FOR ISSUES");
console.log("-".repeat(60));
// Handle issues (including approval)
await handleIssues(priceData);
// STEP 3: Get Quote
console.log("\n" + "-".repeat(60));
console.log("📋 STEP 3: FETCHING FIRM QUOTE");
console.log("-".repeat(60));
const quoteData = await getQuote(
sellToken,
buyToken,
sellAmount,
slippageBps,
sellDecimals
);
const quoteBuyAmount = formatUnits(
BigInt(quoteData.buyAmount),
buyDecimals
);
console.log(`✅ Quote received:`);
console.log(` Final output: ${quoteBuyAmount}`);
console.log(` Transaction to: ${quoteData.transaction.to}`);
// STEP 4: Execute Swap
console.log("\n" + "-".repeat(60));
console.log("🚀 STEP 4: EXECUTING SWAP");
console.log("-".repeat(60));
const txHash = await submitTransaction(quoteData.transaction);
console.log(`✅ Transaction submitted!`);
console.log(` Hash: ${txHash}`);
console.log("\n⏳ Waiting for confirmation...");
const receipt = await publicClient.waitForTransactionReceipt({
hash: txHash,
});
console.log("\n" + "=".repeat(60));
if (receipt.status === "success") {
console.log(`🎉 SWAP SUCCESSFUL!`);
console.log(` Block: ${receipt.blockNumber}`);
console.log(` Gas used: ${receipt.gasUsed.toString()}`);
} else {
console.log(`❌ TRANSACTION FAILED`);
}
console.log("=".repeat(60) + "\n");
return receipt;
} catch (error) {
console.error("\n❌ Swap failed:", error);
throw error;
}
}
// ============================================
// 10. UTILITY FUNCTIONS
// ============================================
/**
* Display token balances
*/
async function displayBalances(): Promise<void> {
console.log("\n💰 Current Balances:");
for (const [symbol, address] of Object.entries(TOKENS)) {
const balance = await checkBalance(address, account.address);
const decimals = await getTokenDecimals(address);
const formatted = formatUnits(balance, decimals);
// Display native ETH with ETH suffix
if (isNativeToken(address)) {
console.log(` ${symbol}: ${formatted} ETH`);
} else {
console.log(` ${symbol}: ${formatted}`);
}
}
}
// ============================================
// 11. MAIN EXECUTION
// ============================================
async function main() {
try {
console.log("🔑 Wallet Configuration:");
console.log(` Address: ${account.address}`);
console.log(` Network: Base (Chain ID: ${CHAIN_ID})`);
// Display current balances
await displayBalances();
// Example swap: 0.000001 ETH -> USDC with 1% slippage (100 bps)
await executeSwap(
TOKENS.NATIVE,
TOKENS.USDC,
"0.000001", // Swap amount
100 // 1% slippage in bps
);
// Display updated balances
await displayBalances();
} catch (error) {
console.error("Error in main:", error);
process.exit(1);
}
}
// Run if called directly
if (require.main === module) {
main();
}
Running the Script
To run your bot, adjust the swap parameters in the main function and run the script:
tsx swap.ts
The output should look like this:
🔑 Wallet Configuration:
Address: 0x0a417DDB75Dc491C90F044Ea725E8329A1592d00
Network: Base (Chain ID: 8453)
💰 Current Balances:
NATIVE: 0.007370389645673835 ETH
WETH: 0
USDC: 0
USDe: 0
WBTC: 0
============================================================
🔄 STARTING TOKEN SWAP
============================================================
📊 Swap Parameters:
Sell: 0.000001 tokens (0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE...)
Buy: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913...
Slippage: 1% (100 bps)
User: 0x0a417DDB75Dc491C90F044Ea725E8329A1592d00
# ... rest of the output showing each step ...
============================================================
🎉 SWAP SUCCESSFUL!
Block: 38479262
Gas used: 483760
============================================================
💰 Current Balances:
NATIVE: 0.007364615567936407 ETH
WETH: 0
USDC: 0.00278
USDe: 0
WBTC: 0
Conclusion
You have successfully integrated the 0x Swap API to perform programmatic token swaps. By using this flow, your application can now source the most efficient pricing from over 150 liquidity venues without managing complex routing logic yourself.
Next Steps
-
Explore Advanced Routing: Look into the
includedSourcesparameter to restrict swaps to specific DEXs (e.g., only Uniswap or Curve). -
Use Slippage Protection: Modify the
slippageBpsparameter (Basis Points) in your/quoterequest to protect your trade against market volatility during execution (e.g., set to 200 for 2%). -
Build a Frontend: Connect this logic to a React frontend using
wagmito allow users to swap tokens directly from your UI.
For more details on available parameters, check the official 0x API documentation.
Subscribe to our newsletter for more articles and guides on Web3 and blockchain. If you have any questions or need further assistance, feel free to join our Discord server or provide feedback using the form below. Stay informed and connected by following us on X (@Quicknode) and our Telegram announcement channel.
We ❤️ Feedback!
Let us know if you have any feedback or requests for new topics. We'd love to hear from you.