Trade API

Build Swap Applications on EVM#

There are two approaches to building swap applications with OKX DEX on EVM networks:

  1. The API-first approach - directly interacting with OKX DEX API endpoints
  2. 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 Base Chain
const ETH_ADDRESS: string = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; // Native ETH
const USDC_ADDRESS: string = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // USDC on Base

// Chain ID for Base Chain
const chainId: string = '8453';

// API URL
const baseUrl: string = 'https://web3.okx.com/api/v5/';

// Amount to swap in smallest unit (0.0005 ETH)
const SWAP_AMOUNT: string = '500000000000000'; // 0.0005 ETH
const SLIPPAGE: string = '0.005'; // 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

Using the Onchain gateway API to get the gas limit.

This function uses the Onchain gateway API. This API is available to our enterprise customers only. If you are interested, please contact us dexapi@okx.com.

/**
 * 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;
  }
}

Using RPC to get the gas limit.

const gasLimit = await web3.eth.estimateGas({
  from: WALLET_ADDRESS,
  to: tokenAddress,
  value: '0',
  data: approveData.data
});

// Add 20% buffer
const gasLimit = (BigInt(gasLimit) * BigInt(12) / BigInt(10)).toString();

3.4 Get transaction information and send approveTransaction

/**
 * Sign and send approve transaction
 * @param tokenAddress - Token to approve
 * @param amount - Amount to approve
 * @returns Transaction hash 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 RPC
  const gasLimit = await web3.eth.estimateGas({
    from: WALLET_ADDRESS,
    to: tokenAddress,
    value: '0',
    data: approveData.data
  });

  // Get accurate gas limit using Onchain gateway API
//   const gasLimit = await getGasLimit(WALLET_ADDRESS, tokenAddress, '0', approveData.data);

  // Get current gas price
  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 and broadcast transaction
  const signedTx = await web3.eth.accounts.signTransaction(txObject, PRIVATE_KEY);
  const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
  
  console.log(`Approval transaction successful: ${receipt.transactionHash}`);
  return receipt.transactionHash;
}

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.005" for 0.5%)
 * @returns Swap quote
 */
async function getSwapQuote(
  fromTokenAddress: string,
  toTokenAddress: string,
  amount: string,
  slippage: string = '0.005'
): 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 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 (e.g., "0.005" for 0.5%)
 * @returns Swap transaction data
 */
async function getSwapTransaction(
  fromTokenAddress: string,
  toTokenAddress: string,
  amount: string,
  userAddress: string,
  slippage: string = '0.005'
): 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;
  }
}

6. Simulate Transaction#

Before executing the actual swap, it's crucial to simulate the transaction to ensure it will succeed and to identify any potential issues:

This function uses the Onchain gateway API. This API is available to our enterprise customers only. If you are interested, please contact us dexapi@okx.com.

async function simulateTransaction(swapData: any) {
    try {
        if (!swapData.tx) {
            throw new Error('Invalid swap data format - missing transaction data');
        }

        const tx = swapData.tx;
        const params: any = {
            fromAddress: tx.from,
            toAddress: tx.to,
            txAmount: tx.value || '0',
            chainIndex: chainId,
            extJson: {
                inputData: tx.data
            },
            includeDebug: true
        };

        const timestamp = new Date().toISOString();
        const requestPath = "/api/v5/dex/pre-transaction/simulate";
        const requestBody = JSON.stringify(params);
        const headers = getHeaders(timestamp, "POST", requestPath, "", requestBody);

        console.log('Simulating transaction...');
        const response = await axios.post(
            `https://web3.okx.com${requestPath}`, 
            params, 
            { headers }
        );

        if (response.data.code !== "0") {
            throw new Error(`Simulation failed: ${response.data.msg || "Unknown simulation error"}`);
        }

        const simulationResult = response.data.data[0];
        
        // Check simulation success
        if (simulationResult.success === false) {
            console.error('Transaction simulation failed:', simulationResult.error);
            throw new Error(`Transaction would fail: ${simulationResult.error}`);
        }

        console.log('Transaction simulation successful');
        console.log(`Estimated gas used: ${simulationResult.gasUsed || 'N/A'}`);
        
        if (simulationResult.logs) {
            console.log('Simulation logs:', simulationResult.logs);
        }

        return simulationResult;
    } catch (error) {
        console.error("Error simulating transaction:", error);
        throw error;
    }
}

7. Broadcast Transaction#

With RPC

/**
 * Execute token swap
 * @param fromTokenAddress - Source token address
 * @param toTokenAddress - Destination token address
 * @param amount - Amount to swap
 * @param slippage - Maximum slippage
 * @returns Transaction hash
 */
async function executeSwap(
  fromTokenAddress: string,
  toTokenAddress: string,
  amount: string,
  slippage: string = '0.005'
): 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
  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 and broadcast transaction using RPC
  const signedTx = await web3.eth.accounts.signTransaction(txObject, PRIVATE_KEY);
  console.log("TX signed");
  
  const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
  console.log(`Transaction successful: ${receipt.transactionHash}`);
  
  return receipt.transactionHash;
}

With Onchain gateway API

This function uses the Onchain gateway API. This API is available to our enterprise customers only. If you are interested, please contact us dexapi@okx.com.

/**
 * Broadcast transaction using Onchain gateway API
 * @param signedTx - Signed transaction
 * @param chainId - Chain ID
 * @param walletAddress - Wallet address
 * @returns Order ID
 */
async function broadcastTransaction(signedTx: any, chainId: string, walletAddress: string): Promise<string> {
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;
}

8. Track Transaction#

Using Onchain gateway API

This function uses the Onchain gateway API. This API is available to our enterprise customers only. If you are interested, please contact us dexapi@okx.com.

// 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'
  };
}

Track transaction using 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[0];
      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://basescan.org/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;
  }
}

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.

9. Complete Implementation#

Here's a complete implementation example:

// evm-swap.ts
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(process.env.EVM_RPC_URL || 'https://mainnet.base.org');

// Your wallet information - REPLACE WITH YOUR OWN VALUES
const WALLET_ADDRESS: string = process.env.EVM_WALLET_ADDRESS || '';
const PRIVATE_KEY: string = process.env.EVM_PRIVATE_KEY || '';

// Token addresses for swap on Base Chain
const ETH_ADDRESS: string = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; // Native ETH

// Chain ID for Base Chain
const chainId: string = '8453';

// API URL
const baseUrl: string = 'https://web3.okx.com/api/v5/';

// Define interfaces
interface TokenInfo {
    tokenSymbol: string;
    decimal: string;
    tokenUnitPrice: string;
}

interface QuoteData {
    fromToken: TokenInfo;
    toToken: TokenInfo;
}

interface SwapData {
    tx: {
        from: string;
        to: string;
        data: string;
        value?: string;
    };
}

interface TransactionData {
    chainId: string;
    fromTokenDetails: {
        amount: string;
        symbol: string;
        tokenAddress: string;
    };
    status: 'pending' | 'success' | 'fail';
    toTokenDetails: {
        amount: string;
        symbol: string;
        tokenAddress: string;
    };
    txFee: string;
    txHash: string;
    errorMsg?: string;
}

// ======== Utility Functions ========

// Add delay function at the top with other utility functions
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

/**
 * Generate API authentication headers
 */
function getHeaders(timestamp: string, method: string, requestPath: string, queryString = "", body = "") {
    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 for API authentication");
    }

    const stringToSign = timestamp + method + requestPath + (queryString || body);

    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,
    };
}

/**
 * Check token allowance for DEX
 */
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 token information from the API
 */
async function getTokenInfo(fromTokenAddress: string, toTokenAddress: string) {
    await delay(1000); // Add 1 second delay
    const timestamp = new Date().toISOString();
    const path = `dex/aggregator/quote`;
    const requestPath = `/api/v5/${path}`;

    const params: Record<string, string> = {
        chainId,
        fromTokenAddress,
        toTokenAddress,
        amount: "1000000000000000000", // 1 ETH in wei for better quote
        slippage: "0.005",
    };

    const queryString = "?" + new URLSearchParams(params).toString();
    const headers = getHeaders(timestamp, "GET", requestPath, queryString);

    try {
        const response = await axios.get<{code: string, data: QuoteData[], msg?: string}>(
            `https://web3.okx.com${requestPath}${queryString}`,
            { headers }
        );

        if (response.data.code !== "0") {
            throw new Error(`API Error: ${response.data.msg || "Failed to get token information"}`);
        }

        if (!response.data.data?.[0]) {
            throw new Error("No token information returned from API");
        }

        const quoteData = response.data.data[0];

        return {
            fromToken: {
                symbol: quoteData.fromToken.tokenSymbol,
                decimals: parseInt(quoteData.fromToken.decimal),
                price: quoteData.fromToken.tokenUnitPrice
            },
            toToken: {
                symbol: quoteData.toToken.tokenSymbol,
                decimals: parseInt(quoteData.toToken.decimal),
                price: quoteData.toToken.tokenUnitPrice
            }
        };
    } catch (error) {
        console.error("Error fetching token information:", error instanceof Error ? error.message : "Unknown error");
        throw error;
    }
}

// ===== Token Approval Functions =====

/**
 * Get approve transaction data from OKX DEX API
 */
async function getApproveTransaction(tokenAddress: string, amount: string): Promise<any> {
    await delay(1000); // Add 1 second delay
    try {
        const path = 'dex/aggregator/approve-transaction';
        const url = `${baseUrl}${path}`;
        const params = {
            chainId,
            tokenContractAddress: tokenAddress,
            approveAmount: amount
        };

        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<{code: string, data: any[], msg?: string}>(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;
    }
}

/**
 * Sign and send approve transaction
 */
async function approveToken(tokenAddress: string, amount: string): Promise<string | null> {
    const spenderAddress = '0x3b3ae790Df4F312e745D270119c6052904FB6790'; // Ethereum Mainnet 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 current gas price
    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: '100000', // Standard gas limit for approvals
        gasPrice: adjustedGasPrice.toString(),
        nonce: nonce
    };

    // Sign and broadcast transaction
    const signedTx = await web3.eth.accounts.signTransaction(txObject, PRIVATE_KEY);
    const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);

    console.log(`Approval transaction successful: https://basescan.org/tx/${receipt.transactionHash}`);
    return receipt.transactionHash.toString();
}

// ===== Swap Functions =====

/**
 * Get swap data from the API
 */
async function getSwapData(
    fromTokenAddress: string,
    toTokenAddress: string,
    amount: string,
    slippage = '0.005'
) {
    await delay(1000); // Add 1 second delay
    const timestamp = new Date().toISOString();
    const path = `dex/aggregator/swap`;
    const requestPath = `/api/v5/${path}`;

    const params: Record<string, string> = {
        amount,
        chainId,
        fromTokenAddress,
        toTokenAddress,
        slippage,
        userWalletAddress: WALLET_ADDRESS
    };

    const queryString = "?" + new URLSearchParams(params).toString();
    const headers = getHeaders(timestamp, "GET", requestPath, queryString);

    try {
        const response = await axios.get<{code: string, data: SwapData[], msg?: string}>(
            `https://web3.okx.com${requestPath}${queryString}`,
            { headers }
        );

        if (response.data.code !== "0" || !response.data.data?.[0]) {
            throw new Error(`API Error: ${response.data.msg || "Failed to get swap data"}`);
        }

        return response.data.data[0];
    } catch (error) {
        console.error("Error fetching swap data:", error instanceof Error ? error.message : "Unknown error");
        throw error;
    }
}

/**
 * Sign and broadcast transaction using RPC
 */
async function signAndBroadcastTransaction(
    fromTokenAddress: string,
    toTokenAddress: string,
    amount: string,
    slippage: string = '0.005'
): 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 getSwapData(fromTokenAddress, toTokenAddress, amount, slippage);
    const txData = swapData.tx;
    console.log("Swap TX data received");

    // 3. 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

    // 4. Get current nonce
    const nonce = await web3.eth.getTransactionCount(WALLET_ADDRESS, 'latest');

    // 5. Create transaction object
    const txObject: {
        from: string;
        to: string;
        data: string;
        value: string;
        gasPrice: string;
        nonce: string | number;
        gas?: string;
    } = {
        from: WALLET_ADDRESS,
        to: txData.to,
        data: txData.data,
        value: txData.value || '0',
        gasPrice: adjustedGasPrice.toString(),
        nonce: nonce.toString()
    };

    // 6. Estimate gas with a higher buffer
    try {
        const estimatedGas = await web3.eth.estimateGas(txObject);
        txObject.gas = (BigInt(estimatedGas) * BigInt(12) / BigInt(10)).toString(); // Add 20% buffer
    } catch (error) {
        console.log("Gas estimation failed, using default gas limit");
        txObject.gas = '300000'; // Higher default gas limit
    }

    console.log("Transaction object:", {
        ...txObject,
        gasPrice: web3.utils.fromWei(txObject.gasPrice, 'gwei') + ' gwei',
        gas: txObject.gas
    });

    // 7. Sign and broadcast transaction using RPC
    const signedTx = await web3.eth.accounts.signTransaction(txObject, PRIVATE_KEY);
    const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);

    console.log(`Transaction successful: ${receipt.transactionHash}`);
    return receipt.transactionHash.toString();
}

/**
 * Track transaction using SWAP API
 */
async function trackTransactionWithSwapAPI(txHash: string): Promise<any> {
    await delay(1000); // Add 1 second delay
    try {
        const path = 'dex/aggregator/history';
        const url = `${baseUrl}${path}`;

        const params = {
            chainId,
            txHash: txHash,
            isFromMyProject: 'false'
        };

        const timestamp = new Date().toISOString();
        const requestPath = `/api/v5/${path}`;
        const queryString = "?" + new URLSearchParams(params).toString();
        const headers = getHeaders(timestamp, 'GET', requestPath, queryString);

        console.log('Fetching transaction status...');
        const response = await axios.get<{code: string, data: TransactionData[], msg?: string}>(url, { params, headers });

        if (!response.data) {
            throw new Error('No response data received from API');
        }

        if (response.data.code !== '0') {
            throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
        }

        if (!response.data.data || !Array.isArray(response.data.data) || response.data.data.length === 0) {
            console.log('Transaction not found in history yet, might be too recent');
            return { status: 'pending', details: null };
        }

        const txData = response.data.data[0];
        if (!txData) {
            console.log('Transaction data not available yet');
            return { status: 'pending', details: null };
        }

        const status = txData.status;
        console.log(`Transaction status: ${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://etherscan.io/tx/${txHash}`);
            return { status: 'success', details: txData };
        } else if (status === 'fail') {
            const errorMsg = txData.errorMsg || 'Unknown reason';
            console.error(`Transaction failed: ${errorMsg}`);
            return { status: 'failure', details: txData, error: errorMsg };
        }

        return { status: 'unknown', details: txData };
    } catch (error) {
        console.error('Failed to track transaction status:', (error instanceof Error ? error.message : "Unknown error"));
        // Return a pending status instead of throwing to allow for retries
        return { status: 'pending', details: null, error: error instanceof Error ? error.message : "Unknown error" };
    }
}

// ======== Main Swap Execution Function ========

/**
 * Execute a token swap on Ethereum
 */
async function executeSwap(
    fromTokenAddress: string,
    toTokenAddress: string,
    amount: string,
    slippage: string = '0.005'
): Promise<any> {
    try {
        if (!PRIVATE_KEY) {
            throw new Error("Missing private key");
        }

        if (!WALLET_ADDRESS) {
            throw new Error("Missing wallet address");
        }

        // Step 1: Get swap data from OKX DEX API
        console.log("Getting swap data...");
        const swapData = await getSwapData(fromTokenAddress, toTokenAddress, amount, slippage);
        console.log("Swap route obtained");

        // Step 2: Check allowance and approve if necessary (skip for native token)
        if (fromTokenAddress !== ETH_ADDRESS) {
            await approveToken(fromTokenAddress, amount);
        }

        // Step 3: Sign and broadcast the transaction using RPC
        const txHash = await signAndBroadcastTransaction(fromTokenAddress, toTokenAddress, amount, slippage);

        // Step 4: Track transaction using SWAP API
        const txStatus = await trackTransactionWithSwapAPI(txHash);

        return {
            success: true,
            txHash,
            status: txStatus.status,
            details: txStatus.details
        };
    } catch (error) {
        console.error("Error during swap:", error);
        return {
            success: false,
            error: error instanceof Error ? error.message : "Unknown error"
        };
    }
}

// ======== Command Line Interface ========

async function main() {
    try {
        const args = process.argv.slice(2);
        if (args.length < 3) {
            console.log("Usage: ts-node evm-swap.ts <amount> <fromTokenAddress> <toTokenAddress> [<slippage>]");
            console.log("Example: ts-node evm-swap.ts 0.0005 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 0.005");
            process.exit(1);
        }

        const [amountStr, fromTokenAddress, toTokenAddress, slippage = '0.005'] = args;

        // Get token information
        console.log("Getting token information...");
        const tokenInfo = await getTokenInfo(fromTokenAddress, toTokenAddress);
        console.log(`From: ${tokenInfo.fromToken.symbol} (${tokenInfo.fromToken.decimals} decimals)`);
        console.log(`To: ${tokenInfo.toToken.symbol} (${tokenInfo.toToken.decimals} decimals)`);

        // Convert amount using fetched decimals
        const rawAmount = (parseFloat(amountStr) * Math.pow(10, tokenInfo.fromToken.decimals)).toString();
        console.log(`Amount in ${tokenInfo.fromToken.symbol} base units:`, rawAmount);

        // Execute swap
        console.log("\nExecuting swap...");
        const result = await executeSwap(fromTokenAddress, toTokenAddress, rawAmount, slippage);

        if (result.success) {
            console.log("\nSwap completed successfully!");
            console.log("Transaction Hash:", result.txHash);
            console.log("Status:", result.status);
            if (result.details) {
                console.log("Transaction Details:", JSON.stringify(result.details, null, 2));
            }
        } else {
            console.error("\nSwap failed:", result.error);
        }

        process.exit(result.success ? 0 : 1);
    } catch (error) {
        console.error("Error:", error instanceof Error ? error.message : "Unknown error");
        process.exit(1);
    }
}

// Execute main function if run directly
if (require.main === module) {
    main();
}

// Export functions for modular usage
export {
    executeSwap,
    signAndBroadcastTransaction,
    trackTransactionWithSwapAPI,
    getSwapData
};

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.005'
        });
        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.005', // 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.005'     // 0.5%
});