Build Swap Applications on EVM#
There are two approaches to building swap applications with OKX DEX on EVM networks:
- The API-first approach - directly interacting with OKX DEX API endpoints
- The SDK approach - using the @okx-dex/okx-dex-sdk package for a simplified developer experience
This guide covers both methods to help you choose the approach that best fits your needs.
Method 1: API-First Approach#
This approach demonstrates a token swap using the OKX DEX API endpoints directly. You will swap USDC to ETH on the Ethereum network.
1. Set Up Your Environment#
// --------------------- npm package ---------------------
import { Web3 } from 'web3';
import axios from 'axios';
import * as dotenv from 'dotenv';
import CryptoJS from 'crypto-js';
// The URL for the Ethereum node you want to connect to
const web3 = new Web3('https://......com');
// --------------------- environment variable ---------------------
// Load hidden environment variables
dotenv.config();
// Your wallet information - REPLACE WITH YOUR OWN VALUES
const WALLET_ADDRESS: string = process.env.EVM_WALLET_ADDRESS || '0xYourWalletAddress';
const PRIVATE_KEY: string = process.env.EVM_PRIVATE_KEY || 'YourPrivateKey';
// Token addresses for swap on Ethereum Mainnet
const ETH_ADDRESS: string = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; // Native ETH
const USDC_ADDRESS: string = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; // USDC
// Chain ID for Ethereum Mainnet
const chainId: string = '1';
// API URL
const baseUrl: string = 'https://web3.okx.com/api/v5/';
// Amount to swap in smallest unit (approx $1 of ETH)
// 1 ETH = 10^18 wei, so 0.0005 ETH
const SWAP_AMOUNT: string = '500000000000000'; // 0.0005 ETH (approx $1)
const SLIPPAGE: string = '0.5'; // 0.5% slippage tolerance
// --------------------- util function ---------------------
export function getHeaders(timestamp: string, method: string, requestPath: string, queryString = "") {
// Check https://web3.okx.com/zh-hans/web3/build/docs/waas/rest-authentication for api-key
const apiKey = process.env.OKX_API_KEY;
const secretKey = process.env.OKX_SECRET_KEY;
const apiPassphrase = process.env.OKX_API_PASSPHRASE;
const projectId = process.env.OKX_PROJECT_ID;
if (!apiKey || !secretKey || !apiPassphrase || !projectId) {
throw new Error("Missing required environment variables");
}
const stringToSign = timestamp + method + requestPath + queryString;
return {
"Content-Type": "application/json",
"OK-ACCESS-KEY": apiKey,
"OK-ACCESS-SIGN": CryptoJS.enc.Base64.stringify(
CryptoJS.HmacSHA256(stringToSign, secretKey)
),
"OK-ACCESS-TIMESTAMP": timestamp,
"OK-ACCESS-PASSPHRASE": apiPassphrase,
"OK-ACCESS-PROJECT": projectId,
};
};
2. Check Allowance#
You need to check if the token has been approved for the DEX to spend. This step is only needed for ERC20 tokens, not for native tokens like ETH.
/**
* Check token allowance for DEX
* @param tokenAddress - Token contract address
* @param ownerAddress - Your wallet address
* @param spenderAddress - DEX spender address
* @returns Allowance amount
*/
async function checkAllowance(
tokenAddress: string,
ownerAddress: string,
spenderAddress: string
): Promise<bigint> {
const tokenABI = [
{
"constant": true,
"inputs": [
{ "name": "_owner", "type": "address" },
{ "name": "_spender", "type": "address" }
],
"name": "allowance",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
}
];
const tokenContract = new web3.eth.Contract(tokenABI, tokenAddress);
try {
const allowance = await tokenContract.methods.allowance(ownerAddress, spenderAddress).call();
return BigInt(String(allowance));
} catch (error) {
console.error('Failed to query allowance:', error);
throw error;
}
}
3. Check the Approval Parameters and Initiate the Approval#
If the allowance is lower than the amount you want to swap, you need to approve the token.
3.1 Define your transaction approval parameters
const getApproveTransactionParams = {
chainId: chainId,
tokenContractAddress: tokenAddress,
approveAmount: amount
};
3.2 Define helper functions
async function getApproveTransaction(
tokenAddress: string,
amount: string
): Promise<any> {
try {
const path = 'dex/aggregator/approve-transaction';
const url = `${baseUrl}${path}`;
const params = {
chainId: chainId,
tokenContractAddress: tokenAddress,
approveAmount: amount
};
// Prepare authentication
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, 'GET', requestPath, queryString);
const response = await axios.get(url, { params, headers });
if (response.data.code === '0') {
return response.data.data[0];
} else {
throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to get approval transaction data:', (error as Error).message);
throw error;
}
}
3.3 Create Compute gasLimit utility function
/**
* Get transaction gas limit from Onchain gateway API
* @param fromAddress - Sender address
* @param toAddress - Target contract address
* @param txAmount - Transaction amount (0 for approvals)
* @param inputData - Transaction calldata
* @returns Estimated gas limit
*/
async function getGasLimit(
fromAddress: string,
toAddress: string,
txAmount: string = '0',
inputData: string = ''
): Promise<string> {
try {
const path = 'dex/pre-transaction/gas-limit';
const url = `https://web3.okx.com/api/v5/${path}`;
const body = {
chainIndex: chainId,
fromAddress: fromAddress,
toAddress: toAddress,
txAmount: txAmount,
extJson: {
inputData: inputData
}
};
// Prepare authentication with body included in signature
const bodyString = JSON.stringify(body);
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const headers = getHeaders(timestamp, 'POST', requestPath, bodyString);
const response = await axios.post(url, body, { headers });
if (response.data.code === '0') {
return response.data.data[0].gasLimit;
} else {
throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to get gas limit:', (error as Error).message);
throw error;
}
}
3.4 Get transaction information and send approveTransaction
/**
* Sign and send approve transaction
* @param tokenAddress - Token to approve
* @param amount - Amount to approve
* @returns Order ID of the approval transaction
*/
async function approveToken(tokenAddress: string, amount: string): Promise<string | null> {
const spenderAddress = '0x3b3ae790Df4F312e745D270119c6052904FB6790'; // Ethereum Mainnet DEX spender
// See Router addresses at: https://web3.okx.com/build/docs/waas/dex-smart-contract
const currentAllowance = await checkAllowance(tokenAddress, WALLET_ADDRESS, spenderAddress);
if (currentAllowance >= BigInt(amount)) {
console.log('Sufficient allowance already exists');
return null;
}
console.log('Insufficient allowance, approving tokens...');
// Get approve transaction data from OKX DEX API
const approveData = await getApproveTransaction(tokenAddress, amount);
// Get accurate gas limit using Onchain gateway API
const gasLimit = await getGasLimit(WALLET_ADDRESS, tokenAddress, '0', approveData.data);
// Get current gas price (can also use Onchain gateway API)
const gasPrice = await web3.eth.getGasPrice();
const adjustedGasPrice = BigInt(gasPrice) * BigInt(15) / BigInt(10); // 1.5x for faster confirmation
// Get current nonce
const nonce = await web3.eth.getTransactionCount(WALLET_ADDRESS, 'latest');
// Create transaction object
const txObject = {
from: WALLET_ADDRESS,
to: tokenAddress,
data: approveData.data,
value: '0',
gas: gasLimit,
gasPrice: adjustedGasPrice.toString(),
nonce: nonce
};
// Sign transaction
const {signedTx} = await web3.eth.accounts.signTransaction(txObject, PRIVATE_KEY);
await web3.eth.sendSignedTransaction(signedTx);
}
4. Get Quote Data#
4.1 Define quote parameters
const quoteParams = {
amount: fromAmount,
chainId: chainId,
toTokenAddress: toTokenAddress,
fromTokenAddress: fromTokenAddress,
};
4.2 Define helper functions
/**
* Get swap quote from DEX API
* @param fromTokenAddress - Source token address
* @param toTokenAddress - Destination token address
* @param amount - Amount to swap
* @param slippage - Maximum slippage (e.g., "0.5" for 0.5%)
* @returns Swap quote
*/
async function getSwapQuote(
fromTokenAddress: string,
toTokenAddress: string,
amount: string,
slippage: string = '0.5'
): Promise<any> {
try {
const path = 'dex/aggregator/quote';
const url = `${baseUrl}${path}`;
const params = {
chainId: chainId,
fromTokenAddress,
toTokenAddress,
amount,
slippage
};
// Prepare authentication
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, 'GET', requestPath, queryString);
const response = await axios.get(url, { params, headers });
if (response.data.code === '0') {
return response.data.data[0];
} else {
throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to get swap quote:', (error as Error).message);
throw error;
}
}
5. Prepare Transaction#
5.1 Define swap parameters
const swapParams = {
chainId: chainId,
fromTokenAddress,
toTokenAddress,
amount,
userWalletAddress: userAddress,
slippage
};
5.2 Request swap transaction data
/**
* Get swap quote from DEX API
* @param fromTokenAddress - Source token address
* @param toTokenAddress - Destination token address
* @param amount - Amount to swap
* @param userAddress - User wallet address
* @param slippage - Maximum slippage (e.g., "0.5" for 0.5%)
* @returns Swap quote
*/
async function getSwapQuote(
fromTokenAddress: string,
toTokenAddress: string,
amount: string,
userAddress: string,
slippage: string = '0.5'
): Promise<any> {
try {
const path = 'dex/aggregator/swap';
const url = `${baseUrl}${path}`;
const params = {
chainId: chainId,
fromTokenAddress,
toTokenAddress,
amount,
userWalletAddress: userAddress,
slippage
};
// Prepare authentication
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, 'GET', requestPath, queryString);
const response = await axios.get(url, { params, headers });
if (response.data.code === '0') {
return response.data.data[0];
} else {
throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to get swap quote:', (error as Error).message);
throw error;
}
}
5.3 Build transaction
/**
* Execute token swap
* @param fromTokenAddress - Source token address
* @param toTokenAddress - Destination token address
* @param amount - Amount to swap
* @param slippage - Maximum slippage
*/
async function executeSwap(
fromTokenAddress: string,
toTokenAddress: string,
amount: string,
slippage: string = '0.5'
): Promise<string> {
// 1. Check allowance and approve if necessary (skip for native token)
if (fromTokenAddress !== ETH_ADDRESS) {
await approveToken(fromTokenAddress, amount);
}
// 2. Get swap transaction data
const swapData = await getSwapTransaction(fromTokenAddress, toTokenAddress, amount, WALLET_ADDRESS, slippage);
const txData = swapData.tx;
console.log("Swap TX data received");
// 3. Get accurate gas limit using Onchain gateway API
const gasLimit = await getGasLimit(
WALLET_ADDRESS,
txData.to,
txData.value || '0',
txData.data
);
console.log("Gas limit received");
// 4. Get current nonce
const nonce = await web3.eth.getTransactionCount(WALLET_ADDRESS, 'latest');
console.log("Nonce received");
// 5. Get current gas price and adjust for faster confirmation
const gasPrice = await web3.eth.getGasPrice();
const adjustedGasPrice = BigInt(gasPrice) * BigInt(15) / BigInt(10); // 1.5x for faster confirmation
console.log("Gas price received");
// 6. Create transaction object
const txObject = {
from: WALLET_ADDRESS,
to: txData.to,
data: txData.data,
value: txData.value || '0',
gas: gasLimit,
gasPrice: adjustedGasPrice.toString(),
nonce: nonce
};
console.log("TX build complete");
// 7. Sign transaction
const {signedTx} = await web3.eth.accounts.signTransaction(txObject, PRIVATE_KEY);
console.log("TX signed");
}
6. Broadcast Transaction#
6.1 With RPC
// 8. Broadcast transaction using RPC
const chainTxInfo = await web3.eth.sendSignedTransaction(signedTx);
console.log('chainTxInfo:', chainTxInfo);
6.2 With Onchain gateway API
// 8. Broadcast transaction using Onchain gateway API
try {
const path = 'dex/pre-transaction/broadcast-transaction';
const url = `https://web3.okx.com/api/v5/${path}`;
const broadcastData = {
signedTx: signedTx.rawTransaction,
chainIndex: chainId,
address: WALLET_ADDRESS
};
// Prepare authentication with body included in signature
const bodyString = JSON.stringify(broadcastData);
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const headers = getHeaders(timestamp, 'POST', requestPath, bodyString);
const response = await axios.post(url, broadcastData, { headers });
if (response.data.code === '0') {
const orderId = response.data.data[0].orderId;
console.log(`Swap transaction broadcast successfully, Order ID: ${orderId}`);
} else {
throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to broadcast swap transaction:', error);
throw error;
}
7. Track Transaction#
Finally, create a transaction tracking system:
With the Onchain gateway API
// Define error info interface
interface TxErrorInfo {
error: string;
message: string;
action: string;
}
/**
* Tracking transaction confirmation status using the Onchain gateway API
* @param orderId - Order ID from broadcast response
* @param intervalMs - Polling interval in milliseconds
* @param timeoutMs - Maximum time to wait
* @returns Final transaction confirmation status
*/
async function trackTransaction(
orderId: string,
intervalMs: number = 5000,
timeoutMs: number = 300000
): Promise<any> {
console.log(`Tracking transaction with Order ID: ${orderId}`);
const startTime = Date.now();
let lastStatus = '';
while (Date.now() - startTime < timeoutMs) {
// Get transaction status
try {
const path = 'dex/post-transaction/orders';
const url = `https://web3.okx.com/api/v5/${path}`;
const params = {
orderId: orderId,
chainIndex: chainId,
address: WALLET_ADDRESS,
limit: '1'
};
// Prepare authentication
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, 'GET', requestPath, queryString);
const response = await axios.get(url, { params, headers });
if (response.data.code === '0' && response.data.data && response.data.data.length > 0) {
if (response.data.data[0].orders && response.data.data[0].orders.length > 0) {
const txData = response.data.data[0].orders[0];
// Use txStatus to match the API response
const status = txData.txStatus;
// Only log when status changes
if (status !== lastStatus) {
lastStatus = status;
if (status === '1') {
console.log(`Transaction pending: ${txData.txHash || 'Hash not available yet'}`);
} else if (status === '2') {
console.log(`Transaction successful: https://web3.okx.com/explorer/base/tx/${txData.txHash}`);
return txData;
} else if (status === '3') {
const failReason = txData.failReason || 'Unknown reason';
const errorMessage = `Transaction failed: ${failReason}`;
console.error(errorMessage);
const errorInfo = handleTransactionError(txData);
console.log(`Error type: ${errorInfo.error}`);
console.log(`Suggested action: ${errorInfo.action}`);
throw new Error(errorMessage);
}
}
} else {
console.log(`No orders found for Order ID: ${orderId}`);
}
}
} catch (error) {
console.warn('Error checking transaction status:', (error as Error).message);
}
// Wait before next check
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
throw new Error('Transaction tracking timed out');
}
/**
* Comprehensive error handling with failReason
* @param txData - Transaction data from post-transaction/orders
* @returns Structured error information
*/
function handleTransactionError(txData: any): TxErrorInfo {
const failReason = txData.failReason || 'Unknown reason';
// Log the detailed error
console.error(`Transaction failed with reason: ${failReason}`);
// Default error handling
return {
error: 'TRANSACTION_FAILED',
message: failReason,
action: 'Try again or contact support'
};
}
For more detailed swap-specific information, you can use the SWAP API:
/**
* Track transaction using SWAP API
* @param chainId - Chain ID (e.g., 1 for Ethereum Mainnet)
* @param txHash - Transaction hash
* @returns Transaction details
*/
async function trackTransactionWithSwapAPI(chainId: string, txHash: string): Promise<any> {
try {
const path = 'dex/aggregator/history';
const url = `${baseUrl}${path}`;
const params = {
chainId: chainId,
txHash: txHash,
isFromMyProject: 'true'
};
// Prepare authentication
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, 'GET', requestPath, queryString);
const response = await axios.get(url, { params, headers });
if (response.data.code === '0') {
const txData = response.data.data;
const status = txData.status;
if (status === 'pending') {
console.log(`Transaction is still pending: ${txHash}`);
return { status: 'pending', details: txData };
} else if (status === 'success') {
console.log(`Transaction successful!`);
console.log(`From: ${txData.fromTokenDetails.symbol} - Amount: ${txData.fromTokenDetails.amount}`);
console.log(`To: ${txData.toTokenDetails.symbol} - Amount: ${txData.toTokenDetails.amount}`);
console.log(`Transaction Fee: ${txData.txFee}`);
console.log(`Explorer URL: https://web3.okx.com/explorer/base/tx/${txHash}`);
return { status: 'success', details: txData };
} else if (status === 'failure') {
console.error(`Transaction failed: ${txData.errorMsg || 'Unknown reason'}`);
return { status: 'failure', details: txData };
}
return txData;
} else {
throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to track transaction status:', (error as Error).message);
throw error;
}
}
The Onchain gateway API provides transaction tracking capabilities through the /wallet/post-transaction/orders
endpoint. This functionality enables tracking transactions as they progress through OKX's systems using order IDs and simple status codes (1: Pending, 2: Success, 3: Failed).
SWAP API transaction tracking provides comprehensive swap execution details using the /dex/aggregator/history
endpoint. It offers token-specific information (symbols, amounts), fees paid, and detailed blockchain data. Use this when you need complete swap insight with token-level details.
Choose the first for basic transaction confirmation status, and the second when you need detailed information about the swap execution itself.
8. Complete Implementation#
Here's a complete implementation example:
// Token Swap using OKX Onchain gateway API on Base
// Required packages
import { Web3 } from 'web3';
import axios from 'axios';
import * as dotenv from 'dotenv';
import CryptoJS from 'crypto-js';
// Load environment variables
dotenv.config();
// Connect to Base network
const web3 = new Web3('https://mainnet.base.org');
// Your wallet information - REPLACE WITH YOUR OWN VALUES
const WALLET_ADDRESS: string = process.env.EVM_WALLET_ADDRESS || '0xYourWalletAddress';
const PRIVATE_KEY: string = process.env.EVM_PRIVATE_KEY || 'YourPrivateKey'; // Without 0x prefix
// Token addresses for swap on Base
const ETH_ADDRESS: string = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; // Native ETH
const USDC_ADDRESS: string = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // Base USDC
// Chain ID for Base
const chainId: string = '8453';
// API URL
// const baseUrl: string = 'https://web3.okx.com/api/v5/';
const baseUrl: string = 'https://beta.okex.org/api/v5/'
// Amount to swap in smallest unit (approx $1 of ETH)
// 1 ETH = 10^18 wei, so 0.0005 ETH = 5 * 10^14 wei at ~$2000/ETH
const SWAP_AMOUNT: string = '500000000000000'; // 0.0005 ETH (approx $1)
const SLIPPAGE: string = '0.5'; // 0.5% slippage tolerance
// Define type for error handling
interface TxErrorInfo {
error: string;
message: string;
action: string;
}
// ===== Get Header Function =====
export function getHeaders(timestamp: string, method: string, requestPath: string, queryString = "") {
const apiKey = process.env.OKX_API_KEY;
const secretKey = process.env.OKX_SECRET_KEY;
const apiPassphrase = process.env.OKX_API_PASSPHRASE;
const projectId = process.env.OKX_PROJECT_ID;
if (!apiKey || !secretKey || !apiPassphrase || !projectId) {
throw new Error("Missing required environment variables");
}
const stringToSign = timestamp + method + requestPath + queryString;
return {
"Content-Type": "application/json",
"OK-ACCESS-KEY": apiKey,
"OK-ACCESS-SIGN": CryptoJS.enc.Base64.stringify(
CryptoJS.HmacSHA256(stringToSign, secretKey)
),
"OK-ACCESS-TIMESTAMP": timestamp,
"OK-ACCESS-PASSPHRASE": apiPassphrase,
"OK-ACCESS-PROJECT": projectId,
};
}
// ===== Token Approval Functions =====
/**
* Check token allowance for DEX
* @param tokenAddress - Token contract address
* @param ownerAddress - Your wallet address
* @param spenderAddress - DEX spender address
* @returns Allowance amount
*/
async function checkAllowance(
tokenAddress: string,
ownerAddress: string,
spenderAddress: string
): Promise<bigint> {
const tokenABI = [
{
"constant": true,
"inputs": [
{ "name": "_owner", "type": "address" },
{ "name": "_spender", "type": "address" }
],
"name": "allowance",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
}
];
const tokenContract = new web3.eth.Contract(tokenABI, tokenAddress);
try {
const allowance = await tokenContract.methods.allowance(ownerAddress, spenderAddress).call();
return BigInt(String(allowance));
} catch (error) {
console.error('Failed to query allowance:', error);
throw error;
}
}
/**
* Get approve transaction data from OKX DEX API
* @param tokenAddress - Token contract address
* @param amount - Amount to approve
* @returns Approval transaction data
*/
async function getApproveTransaction(
tokenAddress: string,
amount: string
): Promise<any> {
try {
const path = 'dex/aggregator/approve-transaction';
const url = `${baseUrl}${path}`;
const params = {
chainId: chainId,
tokenContractAddress: tokenAddress,
approveAmount: amount
};
// Prepare authentication
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, 'GET', requestPath, queryString);
const response = await axios.get(url, { params, headers });
if (response.data.code === '0') {
return response.data.data[0];
} else {
throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to get approval transaction data:', (error as Error).message);
throw error;
}
}
/**
* Get transaction gas limit from Onchain gateway API
* @param fromAddress - Sender address
* @param toAddress - Target contract address
* @param txAmount - Transaction amount (0 for approvals)
* @param inputData - Transaction calldata
* @returns Estimated gas limit
*/
async function getGasLimit(
fromAddress: string,
toAddress: string,
txAmount: string = '0',
inputData: string = ''
): Promise<string> {
try {
const path = 'dex/pre-transaction/gas-limit';
const url = `https://web3.okx.com/api/v5/${path}`;
console.log({path: path, url:url})
const body = {
chainIndex: chainId,
fromAddress: fromAddress,
toAddress: toAddress,
txAmount: txAmount,
extJson: {
inputData: inputData
}
};
// Prepare authentication with body included in signature
const bodyString = JSON.stringify(body);
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const headers = getHeaders(timestamp, 'POST', requestPath, bodyString);
const response = await axios.post(url, body, { headers });
if (response.data.code === '0') {
return response.data.data[0].gasLimit;
} else {
throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to get gas limit:', (error as Error).message);
throw error;
}
}
/**
* Sign and send approve transaction
* @param tokenAddress - Token to approve
* @param amount - Amount to approve
* @returns Order ID of the approval transaction
*/
async function approveToken(tokenAddress: string, amount: string): Promise<string | null> {
const spenderAddress = '0x6b2C0c7be2048Daa9b5527982C29f48062B34D58'; // Base DEX spender
const currentAllowance = await checkAllowance(tokenAddress, WALLET_ADDRESS, spenderAddress);
if (currentAllowance >= BigInt(amount)) {
console.log('Sufficient allowance already exists');
return null;
}
console.log('Insufficient allowance, approving tokens...');
// Get approve transaction data from OKX DEX API
const approveData = await getApproveTransaction(tokenAddress, amount);
// Get accurate gas limit using Onchain gateway API
const gasLimit = await getGasLimit(WALLET_ADDRESS, tokenAddress, '0', approveData.data);
// Get current gas price (can also use Onchain gateway API)
const gasPrice = await web3.eth.getGasPrice();
const adjustedGasPrice = BigInt(gasPrice) * BigInt(15) / BigInt(10); // 1.5x for faster confirmation
// Get current nonce
const nonce = await web3.eth.getTransactionCount(WALLET_ADDRESS, 'latest');
// Create transaction object
const txObject = {
from: WALLET_ADDRESS,
to: tokenAddress,
data: approveData.data,
value: '0',
gas: gasLimit,
gasPrice: adjustedGasPrice.toString(),
nonce: nonce
};
// Sign transaction
const signedTx = await web3.eth.accounts.signTransaction(txObject, PRIVATE_KEY);
// Broadcast transaction using Onchain gateway API
try {
const path = 'dex/pre-transaction/broadcast-transaction';
const url = `${baseUrl}${path}`;
const broadcastData = {
signedTx: signedTx.rawTransaction,
chainIndex: chainId,
address: WALLET_ADDRESS
};
// Prepare authentication with body included in signature
const bodyString = JSON.stringify(broadcastData);
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const headers = getHeaders(timestamp, 'POST', requestPath, bodyString);
const response = await axios.post(url, broadcastData, { headers });
if (response.data.code === '0') {
const orderId = response.data.data[0].orderId;
console.log(`Approval transaction broadcast successfully, Order ID: ${orderId}`);
// Track transaction confirmation status
await trackTransaction(orderId);
return orderId;
} else {
throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to broadcast approval transaction:', error);
throw error;
}
}
// ===== Swap Functions =====
/**
* Get swap quote from DEX API
* @param fromTokenAddress - Source token address
* @param toTokenAddress - Destination token address
* @param amount - Amount to swap
* @param userAddress - User wallet address
* @param slippage - Maximum slippage (e.g., "0.5" for 0.5%)
* @returns Swap quote
*/
async function getSwapQuote(
fromTokenAddress: string,
toTokenAddress: string,
amount: string,
userAddress: string,
slippage: string = '0.5'
): Promise<any> {
try {
const path = 'dex/aggregator/swap';
const url = `${baseUrl}${path}`;
const params = {
chainId: chainId,
fromTokenAddress,
toTokenAddress,
amount,
userWalletAddress: userAddress,
slippage
};
// Prepare authentication
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, 'GET', requestPath, queryString);
const response = await axios.get(url, { params, headers });
if (response.data.code === '0') {
return response.data.data[0];
} else {
throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to get swap quote:', (error as Error).message);
throw error;
}
}
/**
* Get swap transaction data from DEX API
* @param fromTokenAddress - Source token address
* @param toTokenAddress - Destination token address
* @param amount - Amount to swap
* @param userAddress - User wallet address
* @param slippage - Maximum slippage
* @returns Swap transaction data
*/
async function getSwapTransaction(
fromTokenAddress: string,
toTokenAddress: string,
amount: string,
userAddress: string,
slippage: string = '0.5'
): Promise<any> {
try {
const path = 'dex/aggregator/swap';
const url = `${baseUrl}${path}`;
const params = {
chainId: chainId,
fromTokenAddress,
toTokenAddress,
amount,
userWalletAddress: userAddress,
slippage
};
// Prepare authentication
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, 'GET', requestPath, queryString);
const response = await axios.get(url, { params, headers });
if (response.data.code === '0') {
return response.data.data[0];
} else {
throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to get swap transaction data:', (error as Error).message);
throw error;
}
}
/**
* Execute token swap
* @param fromTokenAddress - Source token address
* @param toTokenAddress - Destination token address
* @param amount - Amount to swap
* @param slippage - Maximum slippage
* @returns Order ID of the swap transaction
*/
async function executeSwap(
fromTokenAddress: string,
toTokenAddress: string,
amount: string,
slippage: string = '0.5'
): Promise<string> {
// 1. Check allowance and approve if necessary (skip for native token)
if (fromTokenAddress !== ETH_ADDRESS) {
await approveToken(fromTokenAddress, amount);
}
// 2. Get swap transaction data
const swapData = await getSwapTransaction(fromTokenAddress, toTokenAddress, amount, WALLET_ADDRESS, slippage);
const txData = swapData.tx;
console.log("Swap TX data received");
// 3. Get accurate gas limit using Onchain gateway API
const gasLimit = await getGasLimit(
WALLET_ADDRESS,
txData.to,
txData.value || '0',
txData.data
);
console.log("Gas limit received");
// 4. Get current nonce
const nonce = await web3.eth.getTransactionCount(WALLET_ADDRESS, 'latest');
console.log("Nonce received");
// 5. Get current gas price and adjust for faster confirmation
const gasPrice = await web3.eth.getGasPrice();
const adjustedGasPrice = BigInt(gasPrice) * BigInt(15) / BigInt(10); // 1.5x for faster confirmation
console.log("Gas price received");
// 6. Create transaction object
const txObject = {
from: WALLET_ADDRESS,
to: txData.to,
data: txData.data,
value: txData.value || '0',
gas: gasLimit,
gasPrice: adjustedGasPrice.toString(),
nonce: nonce
};
console.log("TX build complete");
// 7. Sign transaction
const signedTx = await web3.eth.accounts.signTransaction(txObject, PRIVATE_KEY);
console.log("TX signed");
// 8. Broadcast transaction using Onchain gateway API
try {
const path = 'dex/pre-transaction/broadcast-transaction';
const url = `https://web3.okx.com/api/v5/${path}`;
const broadcastData = {
signedTx: signedTx.rawTransaction,
chainIndex: chainId,
address: WALLET_ADDRESS
};
// Prepare authentication with body included in signature
const bodyString = JSON.stringify(broadcastData);
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const headers = getHeaders(timestamp, 'POST', requestPath, bodyString);
const response = await axios.post(url, broadcastData, { headers });
if (response.data.code === '0') {
const orderId = response.data.data[0].orderId;
console.log(`Swap transaction broadcast successfully, Order ID: ${orderId}`);
// 9. Track transaction confirmation status
await trackTransaction(orderId);
return orderId;
} else {
throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to broadcast swap transaction:', error);
throw error;
}
}
// ===== Transaction Tracking =====
/**
* Track transaction confirmation status using Onchain gateway API
* @param orderId - Order ID from broadcast response
* @param intervalMs - Polling interval in milliseconds
* @param timeoutMs - Maximum time to wait
* @returns Final transaction confirmation status
*/
async function trackTransaction(
orderId: string,
intervalMs: number = 5000,
timeoutMs: number = 300000
): Promise<any> {
console.log(`Tracking transaction with Order ID: ${orderId}`);
const startTime = Date.now();
let lastStatus = '';
while (Date.now() - startTime < timeoutMs) {
// Get transaction status
try {
const path = 'dex/post-transaction/orders';
const url = `https://web3.okx.com/api/v5/${path}`;
const params = {
orderId: orderId,
chainIndex: chainId,
address: WALLET_ADDRESS,
limit: '1'
};
// Prepare authentication
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, 'GET', requestPath, queryString);
const response = await axios.get(url, { params, headers });
if (response.data.code === '0' && response.data.data && response.data.data.length > 0) {
if (response.data.data[0].orders && response.data.data[0].orders.length > 0) {
const txData = response.data.data[0].orders[0];
// Use txStatus to match the API response
const status = txData.txStatus;
// Only log when status changes
if (status !== lastStatus) {
lastStatus = status;
if (status === '1') {
console.log(`Transaction pending: ${txData.txHash || 'Hash not available yet'}`);
} else if (status === '2') {
console.log(`Transaction successful: https://web3.okx.com/explorer/base/tx/${txData.txHash}`);
return txData;
} else if (status === '3') {
const failReason = txData.failReason || 'Unknown reason';
const errorMessage = `Transaction failed: ${failReason}`;
console.error(errorMessage);
const errorInfo = handleTransactionError(txData);
console.log(`Error type: ${errorInfo.error}`);
console.log(`Suggested action: ${errorInfo.action}`);
throw new Error(errorMessage);
}
}
} else {
console.log(`No orders found for Order ID: ${orderId}`);
}
}
} catch (error) {
console.warn('Error checking transaction status:', (error as Error).message);
}
// Wait before next check
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
throw new Error('Transaction tracking timed out');
}
/**
* Error handling with failReason
* @param txData - Transaction data from post-transaction/orders
* @returns Structured error information
*/
function handleTransactionError(txData: any): TxErrorInfo {
const failReason = txData.failReason || 'Unknown reason';
// Log the detailed error
console.error(`Transaction failed with reason: ${failReason}`);
// Default error handling
return {
error: 'TRANSACTION_FAILED',
message: failReason,
action: 'Try again or contact support'
};
}
// ======== Main Swap Execution Function ========
/**
* Main function to execute ETH to USDC swap on Base
*/
async function main(): Promise<void> {
try {
console.log('Starting ETH to USDC swap on Base using Onchain gateway API');
// Execute swap from ETH to USDC on Base
const orderId = await executeSwap(
ETH_ADDRESS, // From ETH on Base
USDC_ADDRESS, // To USDC on Base
SWAP_AMOUNT, // Amount in ETH's smallest unit (0.0005 ETH ≈ $1)
SLIPPAGE // 0.5% slippage
);
// Track transaction confirmation status
try {
const path = 'dex/post-transaction/orders';
const url = `https://web3.okx.com/api/v5/${path}`;
const params = {
orderId: orderId,
chainIndex: chainId
};
// Prepare authentication
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, 'GET', requestPath, queryString);
const response = await axios.get(url, { params, headers });
if (response.data.code === '0' && response.data.data && response.data.data.length > 0) {
console.log('Transaction Hash:', response.data.data[0].txHash);
}
} catch (error) {
console.error('Failed to get final transaction details:', (error as Error).message);
}
} catch (error) {
console.error('Swap failed:', (error as Error).message);
}
}
// Execute the main function
main();
Method 2: SDK approach#
Using the OKX DEX SDK provides a much simpler developer experience while retaining all the functionality of the API-first approach. The SDK handles many implementation details for you, including retry logic, error handling, and transaction management.
1. Install the SDK#
npm install @okx-dex/okx-dex-sdk
# or
yarn add @okx-dex/okx-dex-sdk
# or
pnpm add @okx-dex/okx-dex-sdk
2. Setup Your Environment#
Create a .env file with your API credentials and wallet information:
# OKX API Credentials
OKX_API_KEY=your_api_key
OKX_SECRET_KEY=your_secret_key
OKX_API_PASSPHRASE=your_passphrase
OKX_PROJECT_ID=your_project_id
# EVM Configuration
EVM_RPC_URL=your_evm_rpc_url
EVM_WALLET_ADDRESS=your_evm_wallet_address
EVM_PRIVATE_KEY=your_evm_private_key
3. Initialize the Client#
Create a file for your DEX client (e.g., DexClient.ts):
// DexClient.ts
import { OKXDexClient } from '@okx-dex/okx-dex-sdk';
import 'dotenv/config';
// Validate environment variables
const requiredEnvVars = [
'OKX_API_KEY',
'OKX_SECRET_KEY',
'OKX_API_PASSPHRASE',
'OKX_PROJECT_ID',
'EVM_WALLET_ADDRESS',
'EVM_PRIVATE_KEY',
'EVM_RPC_URL'
];
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`);
}
}
// Initialize the client
export const client = new OKXDexClient({
apiKey: process.env.OKX_API_KEY!,
secretKey: process.env.OKX_SECRET_KEY!,
apiPassphrase: process.env.OKX_API_PASSPHRASE!,
projectId: process.env.OKX_PROJECT_ID!,
evm: {
connection: {
rpcUrl: process.env.EVM_RPC_URL!,
},
walletAddress: process.env.EVM_WALLET_ADDRESS!,
privateKey: process.env.EVM_PRIVATE_KEY!,
}
});
4. Token Approval With the SDK#
Create an approval utility function:
// approval.ts
import { client } from './DexClient';
// Helper function to convert human-readable amounts to base units
export function toBaseUnits(amount: string, decimals: number): string {
// Remove any decimal point and count the decimal places
const [integerPart, decimalPart = ''] = amount.split('.');
const currentDecimals = decimalPart.length;
// Combine integer and decimal parts, removing the decimal point
let result = integerPart + decimalPart;
// Add zeros if you need more decimal places
if (currentDecimals < decimals) {
result = result + '0'.repeat(decimals - currentDecimals);
}
// Remove digits if you have too many decimal places
else if (currentDecimals > decimals) {
result = result.slice(0, result.length - (currentDecimals - decimals));
}
// Remove leading zeros
result = result.replace(/^0+/, '') || '0';
return result;
}
/**
* Example: Approve a token for swapping
*/
async function executeApproval(tokenAddress: string, amount: string) {
try {
// Get token information using quote
console.log("Getting token information...");
const tokenInfo = await client.dex.getQuote({
chainId: '8453', // Base Chain
fromTokenAddress: tokenAddress,
toTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', // Native token
amount: '1000000', // Use a reasonable amount for quote
slippage: '0.5'
});
const tokenDecimals = parseInt(tokenInfo.data[0].fromToken.decimal);
const rawAmount = toBaseUnits(amount, tokenDecimals);
console.log(`\nApproval Details:`);
console.log(`--------------------`);
console.log(`Token: ${tokenInfo.data[0].fromToken.tokenSymbol}`);
console.log(`Amount: ${amount} ${tokenInfo.data[0].fromToken.tokenSymbol}`);
console.log(`Amount in base units: ${rawAmount}`);
// Execute the approval
console.log("\nExecuting approval...");
const result = await client.dex.executeApproval({
chainId: '8453', // Base Chain
tokenContractAddress: tokenAddress,
approveAmount: rawAmount
});
if ('alreadyApproved' in result) {
console.log("\nToken already approved for the requested amount!");
return { success: true, alreadyApproved: true };
} else {
console.log("\nApproval completed successfully!");
console.log("Transaction Hash:", result.transactionHash);
console.log("Explorer URL:", result.explorerUrl);
return result;
}
} catch (error) {
if (error instanceof Error) {
console.error('Error executing approval:', error.message);
}
throw error;
}
}
// Run if this file is executed directly
if (require.main === module) {
// Example usage: ts-node approval.ts 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 1000
const args = process.argv.slice(2);
if (args.length !== 2) {
console.log("Usage: ts-node approval.ts <tokenAddress> <amountToApprove>");
console.log("\nExamples:");
console.log(" # Approve 1000 USDC");
console.log(` ts-node approval.ts 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 1000`);
process.exit(1);
}
const [tokenAddress, amount] = args;
executeApproval(tokenAddress, amount)
.then(() => process.exit(0))
.catch((error) => {
console.error('Error:', error);
process.exit(1);
});
}
export { executeApproval };
5. Execute a Swap With the SDK#
Create a swap execution file:
// swap.ts
import { client } from './DexClient';
/**
* Example: Execute a swap from ETH to USDC on Base chain
*/
async function executeSwap() {
try {
if (!process.env.EVM_PRIVATE_KEY) {
throw new Error('Missing EVM_PRIVATE_KEY in .env file');
}
// You can change this to any EVM chain
// For example, for Base, use chainId: '8453'
// For example, for baseSepolia, use chainId: '84532'
// You can also use SUI, use chainId: '784'
// When using another Chain, you need to change the fromTokenAddress and toTokenAddress to the correct addresses for that chain
const swapResult = await client.dex.executeSwap({
chainId: '8453', // Base chain ID
fromTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', // Native ETH
toTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
amount: String(10 * 10 ** 14), // .0001 ETH
slippage: '0.5', // 0.5% slippage
userWalletAddress: process.env.EVM_WALLET_ADDRESS!
});
console.log('Swap executed successfully:');
console.log(JSON.stringify(swapResult, null, 2));
return swapResult;
} catch (error) {
if (error instanceof Error) {
console.error('Error executing swap:', error.message);
// API errors include details in the message
if (error.message.includes('API Error:')) {
const match = error.message.match(/API Error: (.*)/);
if (match) console.error('API Error Details:', match[1]);
}
}
throw error;
}
}
// Run if this file is executed directly
if (require.main === module) {
executeSwap()
.then(() => process.exit(0))
.catch((error) => {
console.error('Error:', error);
process.exit(1);
});
}
export { executeSwap };
6. Additional SDK Functionality#
The SDK provides additional methods that simplify development:
Get a quote for a token pair
const quote = await client.dex.getQuote({
chainId: '8453', // Base Chain
fromTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC
toTokenAddress: '0x4200000000000000000000000000000000000006', // WETH
amount: '1000000', // 1 USDC (in smallest units)
slippage: '0.5' // 0.5%
});
- Method 1: API-First Approach1. Set Up Your Environment2. Check Allowance3. Check the Approval Parameters and Initiate the Approval4. Get Quote Data5. Prepare Transaction6. Broadcast Transaction7. Track Transaction8. Complete ImplementationMethod 2: SDK approach1. Install the SDK2. Setup Your Environment3. Initialize the Client4. Token Approval With the SDK5. Execute a Swap With the SDK6. Additional SDK Functionality