Trade API

Build Swap Applications on Solana#

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

  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 SOL to USDC on Solana Mainnet.

1. Set Up Your Environment#

Import the necessary Node.js libraries and set up your environment variables:

// Required libraries
import base58 from "bs58";
import BN from "bn.js";
import * as solanaWeb3 from "@solana/web3.js";
import { Connection } from "@solana/web3.js";
import cryptoJS from "crypto-js";
import axios from "axios";
import dotenv from 'dotenv';

dotenv.config();

// Environment variables
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;
const userAddress = process.env.WALLET_ADDRESS;
const userPrivateKey = process.env.PRIVATE_KEY;
const solanaRpcUrl = process.env.SOLANA_RPC_URL;

// Constants
const SOLANA_CHAIN_ID = "501";
const COMPUTE_UNITS = 300000;
const MAX_RETRIES = 3;

// Initialize Solana connection
const connection = new Connection(`${solanaRpcUrl}`, {
    confirmTransactionInitialTimeout: 5000
});
// Utility function for OKX API authentication
function getHeaders(timestamp: string, method: string, requestPath: string, queryString = "", body = "") {

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

2. Get Swap Data#

Solana's native token address is 11111111111111111111111111111111. Use the /swap endpoint to retrieve detailed swap information:

async function getSwapData(
    fromTokenAddress: string, 
    toTokenAddress: string, 
    amount: string, 
    slippage = '0.5'
) {
    const timestamp = new Date().toISOString();
    const requestPath = "/api/v5/dex/aggregator/swap";

    const params = {
        amount: amount,
        chainId: SOLANA_CHAIN_ID,
        fromTokenAddress: fromTokenAddress,
        toTokenAddress: toTokenAddress,
        userWalletAddress: userAddress,
        slippage: slippage
    };

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

    try {
        const response = await axios.get(
            `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);
        throw error;
    }
}

3. Prepare Transaction#

async function prepareTransaction(callData: string) {
    try {
        // Decode the base58 encoded transaction data
        const decodedTransaction = base58.decode(callData);
        
        // Get the latest blockhash
        const recentBlockHash = await connection.getLatestBlockhash();
        console.log("Got blockhash:", recentBlockHash.blockhash);
        
        let tx;
        
        // Try to deserialize as a versioned transaction first
        try {
            tx = solanaWeb3.VersionedTransaction.deserialize(decodedTransaction);
            console.log("Successfully created versioned transaction");
            tx.message.recentBlockhash = recentBlockHash.blockhash;
        } catch (e) {
            // Fall back to legacy transaction if versioned fails
            console.log("Versioned transaction failed, trying legacy:", e);
            tx = solanaWeb3.Transaction.from(decodedTransaction);
            console.log("Successfully created legacy transaction");
            tx.recentBlockhash = recentBlockHash.blockhash;
        }
        
        return {
            transaction: tx,
            recentBlockHash
        };
    } catch (error) {
        console.error("Error preparing transaction:", error);
        throw error;
    }
}

4. Broadcast Transaction#

With RPC

async function signTransaction(tx: solanaWeb3.Transaction | solanaWeb3.VersionedTransaction) {
    if (!userPrivateKey) {
        throw new Error("Private key not found");
    }
    
    const feePayer = solanaWeb3.Keypair.fromSecretKey(
        base58.decode(userPrivateKey)
    );
    
    if (tx instanceof solanaWeb3.VersionedTransaction) {
        tx.sign([feePayer]);
    } else {
        tx.partialSign(feePayer);
    }
    const txId = await connection.sendRawTransaction(tx.serialize());
    console.log('Transaction ID:', txId);

    // Wait for confirmation
    await connection.confirmTransaction(txId);
    console.log(`Transaction confirmed: https://solscan.io/tx/${txId}`);

    return txId;
}

With Onchain gateway API

async function broadcastTransaction(
    signedTx: solanaWeb3.Transaction | solanaWeb3.VersionedTransaction
) {
    try {
        const serializedTx = signedTx.serialize();
        const encodedTx = base58.encode(serializedTx);
        
        const path = "dex/pre-transaction/broadcast-transaction";
        const url = `https://web3.okx.com/api/v5/${path}`;
        
        const broadcastData = {
            signedTx: encodedTx,
            chainIndex: SOLANA_CHAIN_ID,
            address: userAddress
        };
        
        // 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(`Transaction broadcast successfully, Order ID: ${orderId}`);
            
            return orderId;
        } else {
            throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
        }
    } catch (error) {
        console.error('Failed to broadcast transaction:', error);
        throw error;
    }
}

5. Track Transaction#

Finally, create a transaction tracking system:

With the Onchain gateway API

// Define transaction status 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: SOLANA_CHAIN_ID,
                address: userAddress,
                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://solscan.io/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 instanceof Error ? error.message : "Unknown error"));
        }

        // 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 info
    let errorInfo: TxErrorInfo = {
        error: 'TRANSACTION_FAILED',
        message: failReason,
        action: 'Try again or contact support'
    };

    // More specific error handling based on the failure reason
    if (failReason.includes('insufficient funds')) {
        errorInfo = {
            error: 'INSUFFICIENT_FUNDS',
            message: 'Your wallet does not have enough funds to complete this transaction',
            action: 'Add more SOL to your wallet to cover the transaction'
        };
    } else if (failReason.includes('blockhash')) {
        errorInfo = {
            error: 'BLOCKHASH_EXPIRED',
            message: 'The transaction blockhash has expired',
            action: 'Try again with a fresh transaction'
        };
    } else if (failReason.includes('compute budget')) {
        errorInfo = {
            error: 'COMPUTE_BUDGET_EXCEEDED',
            message: 'Transaction exceeded compute budget',
            action: 'Increase compute units or simplify the transaction'
        };
    }

    return errorInfo;
}

For more detailed swap-specific information, you can use the SWAP API:

/**
 * Track transaction using SWAP API
 * @param chainId - Chain ID (e.g., 501 for Solana)
 * @param txHash - Transaction hash
 * @returns Transaction details
 */
async function trackTransactionWithSwapAPI(
    txHash: string
): Promise<any> {
    try {
        const path = 'dex/aggregator/history';
        const url = `https://web3.okx.com/api/v5/${path}`;

        const params = {
            chainId: SOLANA_CHAIN_ID,
            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://solscan.io/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 instanceof Error ? error.message : "Unknown error"));
        throw error;
    }
}

The Onchain gateway API provides transaction tracking capabilities through the /dex/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.

6. Complete Implementation#

Here's a complete implementation example:

// solana-swap.ts
import base58 from "bs58";
import BN from "bn.js";
import * as solanaWeb3 from "@solana/web3.js";
import { Connection } from "@solana/web3.js";
import cryptoJS from "crypto-js";
import axios from "axios";
import dotenv from 'dotenv';

dotenv.config();

// Environment variables
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;
const userAddress = process.env.WALLET_ADDRESS;
const userPrivateKey = process.env.PRIVATE_KEY;
const solanaRpcUrl = process.env.SOLANA_RPC_URL;

// Constants
const SOLANA_CHAIN_ID = "501";  // Solana Mainnet
const COMPUTE_UNITS = 300000;
const MAX_RETRIES = 3;
const CONFIRMATION_TIMEOUT = 60000;
const POLLING_INTERVAL = 5000;
const BASE_URL = "https://web3.okx.com";
const DEX_PATH = "api/v5/dex";

// Initialize Solana connection
const connection = new Connection(solanaRpcUrl || "https://api.mainnet-beta.solana.com", {
    confirmTransactionInitialTimeout: 30000
});

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

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

/**
 * Convert human-readable amount to the smallest token units
 */
function convertAmount(amount: string, decimals: number): string {
    try {
        if (!amount || isNaN(parseFloat(amount))) {
            throw new Error("Invalid amount");
        }
        
        const value = parseFloat(amount);
        if (value <= 0) {
            throw new Error("Amount must be greater than 0");
        }
        
        return new BN(value * Math.pow(10, decimals)).toString();
    } catch (err) {
        console.error("Amount conversion error:", err);
        throw new Error("Invalid amount format");
    }
}

/**
 * Get token information from the API
 */
async function getTokenInfo(fromTokenAddress: string, toTokenAddress: string) {
    const timestamp = new Date().toISOString();
    const path = `dex/aggregator/quote`;
    const requestPath = `/api/v5/${path}`;
    
    const params: Record<string, string> = {
        chainId: SOLANA_CHAIN_ID,
        fromTokenAddress,
        toTokenAddress,
        amount: "1000000", // Small amount just to get token info
        slippage: "0.5",
    };
    
    const queryString = "?" + new URLSearchParams(params).toString();
    const headers = getHeaders(timestamp, "GET", requestPath, queryString);
    
    try {
        const response = await axios.get(
            `${BASE_URL}${requestPath}${queryString}`,
            { headers }
        );
        
        if (response.data.code !== "0" || !response.data.data?.[0]) {
            throw new Error("Failed to get token information");
        }
        
        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);
        throw error;
    }
}

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

/**
 * Get swap data from the API
 */
async function getSwapData(
    fromTokenAddress: string,
    toTokenAddress: string,
    amount: string,
    slippage = '0.5'
) {
    const timestamp = new Date().toISOString();
    const path = `dex/aggregator/swap`;
    const requestPath = `/api/v5/${path}`;
    
    // Ensure all parameters are defined before creating URLSearchParams
    const params: Record<string, string> = {
        amount,
        chainId: SOLANA_CHAIN_ID,
        fromTokenAddress,
        toTokenAddress,
        slippage
    };
    
    // Only add userWalletAddress if it's defined
    if (userAddress) {
        params.userWalletAddress = userAddress;
    }
    
    const queryString = "?" + new URLSearchParams(params).toString();
    const headers = getHeaders(timestamp, "GET", requestPath, queryString);
    
    try {
        const response = await axios.get(
            `${BASE_URL}${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);
        throw error;
    }
}

/**
 * Prepare the transaction with the latest blockhash and compute units
 */
async function prepareTransaction(callData: string) {
    try {
        // Decode the base58 encoded transaction data
        const decodedTransaction = base58.decode(callData);
        
        // Get the latest blockhash for transaction freshness
        const recentBlockHash = await connection.getLatestBlockhash();
        console.log("Got blockhash:", recentBlockHash.blockhash);
        
        let tx;
        
        // Try to deserialize as a versioned transaction first (Solana v0 transaction format)
        try {
            tx = solanaWeb3.VersionedTransaction.deserialize(decodedTransaction);
            console.log("Successfully created versioned transaction");
            tx.message.recentBlockhash = recentBlockHash.blockhash;
        } catch (e) {
            // Fall back to legacy transaction if versioned fails
            console.log("Versioned transaction failed, trying legacy format");
            tx = solanaWeb3.Transaction.from(decodedTransaction);
            console.log("Successfully created legacy transaction");
            tx.recentBlockhash = recentBlockHash.blockhash;
            
            // Add compute budget instruction for complex swaps (only for legacy transactions)
            // For versioned transactions, this would already be included in the message
            const computeBudgetIx = solanaWeb3.ComputeBudgetProgram.setComputeUnitLimit({
                units: COMPUTE_UNITS
            });
            
            tx.add(computeBudgetIx);
        }
        
        return {
            transaction: tx,
            recentBlockHash
        };
    } catch (error) {
        console.error("Error preparing transaction:", error);
        throw error;
    }
}

/**
 * Sign the transaction with user's private key
 */
async function signTransaction(tx: solanaWeb3.Transaction | solanaWeb3.VersionedTransaction) {
    if (!userPrivateKey) {
        throw new Error("Private key not found");
    }
    
    const feePayer = solanaWeb3.Keypair.fromSecretKey(
        base58.decode(userPrivateKey)
    );
    
    if (tx instanceof solanaWeb3.VersionedTransaction) {
        tx.sign([feePayer]);
    } else {
        tx.partialSign(feePayer);
    }
    
    return tx;
}

/**
 * Broadcast transaction using Onchain gateway API
 */
async function broadcastTransaction(
    signedTx: solanaWeb3.Transaction | solanaWeb3.VersionedTransaction
) {
    try {
        const serializedTx = signedTx.serialize();
        const encodedTx = base58.encode(serializedTx);
        
        const path = `dex/pre-transaction/broadcast-transaction`;
        const requestPath = `/api/v5/${path}`;
        
        // Ensure all parameters are defined
        const broadcastData: Record<string, string> = {
            signedTx: encodedTx,
            chainIndex: SOLANA_CHAIN_ID
        };
        
        // Only add address if it's defined
        if (userAddress) {
            broadcastData.address = userAddress;
        }
        
        // Prepare authentication with body included in signature
        const bodyString = JSON.stringify(broadcastData);
        const timestamp = new Date().toISOString();
        const headers = getHeaders(timestamp, 'POST', requestPath, "", bodyString);
        
        const response = await axios.post(`${BASE_URL}${requestPath}`, broadcastData, { headers });
        
        if (response.data.code === '0') {
            const orderId = response.data.data[0].orderId;
            console.log(`Transaction broadcast successfully, Order ID: ${orderId}`);
            return orderId;
        } else {
            throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
        }
    } catch (error) {
        console.error('Failed to broadcast transaction:', error);
        throw error;
    }
}

// ======== Transaction Tracking ========

// Define error info interface
interface TxErrorInfo {
    error: string;
    message: string;
    action: string;
}

/**
 * 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 Transaction confirmation status
 */
async function trackTransaction(
    orderId: string,
    intervalMs: number = POLLING_INTERVAL,
    timeoutMs: number = CONFIRMATION_TIMEOUT
): Promise<any> {
    console.log(`Track 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 requestPath = `/api/v5/${path}`;
            
            // Ensure all parameters are defined before creating URLSearchParams
            const params: Record<string, string> = {
                orderId: orderId,
                chainIndex: SOLANA_CHAIN_ID,
                limit: '1'
            };
            
            // Only add address if it's defined
            if (userAddress) {
                params.address = userAddress;
            }

            // Prepare authentication
            const timestamp = new Date().toISOString();
            const queryString = "?" + new URLSearchParams(params).toString();
            const headers = getHeaders(timestamp, 'GET', requestPath, queryString);

            const response = await axios.get(`${BASE_URL}${requestPath}${queryString}`, { 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://solscan.io/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 instanceof Error ? error.message : "Unknown error"));
        }

        // 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 ========

/**
 * Execute a token swap on Solana
 */
async function executeSwap(
    fromTokenAddress: string,
    toTokenAddress: string,
    amount: string,
    slippage: string = '0.5'
): Promise<any> {
    try {
        // Validate inputs
        if (!userPrivateKey) {
            throw new Error("Missing private key");
        }
        
        if (!userAddress) {
            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: Get the transaction data
        const callData = swapData.tx.data;
        if (!callData) {
            throw new Error("Invalid transaction data received from API");
        }

        // Step 3: Prepare the transaction with compute units
        const { transaction, recentBlockHash } = await prepareTransaction(callData);
        console.log("Transaction prepared with compute unit limit:", COMPUTE_UNITS);

        // Step 4: Sign the transaction
        const signedTx = await signTransaction(transaction);
        console.log("Transaction signed");

        // Step 5: Broadcast the transaction using Onchain gateway API
        const orderId = await broadcastTransaction(signedTx);
        console.log(`Transaction broadcast successful with order ID: ${orderId}`);

        // Step 6: Track transaction confirmation status
        console.log("Tracking transaction...");
        const txStatus = await trackTransaction(orderId);
        
        return {
            success: true,
            orderId,
            txHash: txStatus.txHash,
            status: txStatus.txStatus === '2' ? 'SUCCESS' : 'PENDING'
        };
    } 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 solana-swap.ts <amount> <fromTokenAddress> <toTokenAddress> [<slippage>]");
            console.log("Example: ts-node solana-swap.ts 0.1 11111111111111111111111111111111 EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v 0.5");
            process.exit(1);
        }
        
        const [amountStr, fromTokenAddress, toTokenAddress, slippage = '0.5'] = 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 = convertAmount(amountStr, tokenInfo.fromToken.decimals);
        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("Order ID:", result.orderId);
            if (result.txHash) {
                console.log("Transaction ID:", result.txHash);
                console.log("Explorer URL:", `https://solscan.io/tx/${result.txHash}`);
            }
        } 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,
    broadcastTransaction,
    trackTransaction,
    prepareTransaction
};

7. MEV Protection#

The first line of defense uses dynamic priority fees - think of it as your bid in an auction against MEV bots:

const MEV_PROTECTION = {
    // Trade Protection
    MAX_PRICE_IMPACT: "0.05",        // 5% max price impact
    SLIPPAGE: "0.05",                // 5% slippage tolerance
    MIN_ROUTES: 2,                   // Minimum DEX routes

    // Priority Fees
    MIN_PRIORITY_FEE: 10_000,
    MAX_PRIORITY_FEE: 1_000_000,
    PRIORITY_MULTIPLIER: 2,

    // TWAP Settings
    TWAP_ENABLED: true,
    TWAP_INTERVALS: 4,               // Split into 4 parts
    TWAP_DELAY_MS: 2000,            // 2s between trades

    // Transaction Settings
    COMPUTE_UNITS: 300_000,
    MAX_RETRIES: 3,
    CONFIRMATION_TIMEOUT: 60_000,

    // Block Targeting
    TARGET_SPECIFIC_BLOCKS: true,
    PREFERRED_SLOT_OFFSET: 2,        // Target blocks with slot % 4 == 2
} as const;

static async getPriorityFee() {
    const recentFees = await connection.getRecentPrioritizationFees();
    const maxFee = Math.max(...recentFees.map(fee => fee.prioritizationFee));
    return Math.min(maxFee * 1.5, MEV_PROTECTION.MAX_PRIORITY_FEE);
}

For larger trades, you can enable TWAP (Time-Weighted Average Price). Instead of making one big splash that MEV bots love to target, your trade gets split into smaller pieces:

// Define the TWAPExecution class outside of the if block
class TWAPExecution {
    static async splitTrade(
        totalAmount: string,
        fromTokenAddress: string,
        toTokenAddress: string
    ): Promise<TradeChunk[]> {
        const amount = new BN(totalAmount);
        const chunkSize = amount.divn(MEV_PROTECTION.TWAP_INTERVALS);

        return Array(MEV_PROTECTION.TWAP_INTERVALS)
            .fill(null)
            .map(() => ({
                amount: chunkSize.toString(),
                fromTokenAddress,
                toTokenAddress,
                minAmountOut: "0" // Will be calculated per chunk
            }));
    }
}

// Then use it in the if block
if (MEV_PROTECTION.TWAP_ENABLED) {
    const chunks = await TWAPExecution.splitTrade(
        rawAmount,
        fromTokenAddress,
        toTokenAddress
    );
}

Protection in Action#

When you execute a trade with this implementation, several things happen:

  1. Pre-Trade Checks:
  • The token you're buying gets checked for honeypot characteristics
  • Network fees are analyzed to set competitive priority
  • Your trade size determines if it should be split up
  1. During the Trade:
  • Large trades can be split into parts with randomized timing
  • Each piece gets its own priority fee based on market conditions
  • Specific block targeting helps reduce exposure
  1. Transaction Safety:
  • Each transaction runs through simulation first
  • Built-in confirmation tracking
  • Automatic retry logic if something goes wrong

While MEV on Solana can't be completely eliminated, these protections make life harder for MEV bots.

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
# Solana Configuration
SOLANA_RPC_URL=your_solana_rpc_url
SOLANA_WALLET_ADDRESS=your_solana_wallet_address
SOLANA_PRIVATE_KEY=your_solana_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',
    'SOLANA_WALLET_ADDRESS',
    'SOLANA_PRIVATE_KEY',
    'SOLANA_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!,
    solana: {
        connection: {
            rpcUrl: process.env.SOLANA_RPC_URL!,
            wsEndpoint: process.env.SOLANA_WS_URL,
            confirmTransactionInitialTimeout: 5000
        },
        walletAddress: process.env.SOLANA_WALLET_ADDRESS!,
        privateKey: process.env.SOLANA_PRIVATE_KEY!,
        computeUnits: 300000,
        maxRetries: 3
    }
});

4. Execute a Swap With the SDK#

Create a swap execution file:

// swap.ts
import { client } from './DexClient';
/**
 * Example: Execute a swap from SOL to USDC
 */
async function executeSwap() {
  try {
    if (!process.env.SOLANA_PRIVATE_KEY) {
      throw new Error('Missing SOLANA_PRIVATE_KEY in .env file');
    }
    // Get quote to fetch token information
    console.log("Getting token information...");
    const quote = await client.dex.getQuote({
        chainId: '501',
        fromTokenAddress: '11111111111111111111111111111111', // SOL
        toTokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
        amount: '1000000', // Small amount for quote
        slippage: '0.5'
    });
    const tokenInfo = {
        fromToken: {
            symbol: quote.data[0].fromToken.tokenSymbol,
            decimals: parseInt(quote.data[0].fromToken.decimal),
            price: quote.data[0].fromToken.tokenUnitPrice
        },
        toToken: {
            symbol: quote.data[0].toToken.tokenSymbol,
            decimals: parseInt(quote.data[0].toToken.decimal),
            price: quote.data[0].toToken.tokenUnitPrice
        }
    };
    // Convert amount to base units (for display purposes)
    const humanReadableAmount = 0.1; // 0.1 SOL
    const rawAmount = (humanReadableAmount * Math.pow(10, tokenInfo.fromToken.decimals)).toString();
    console.log("\nSwap Details:");
    console.log("--------------------");
    console.log(`From: ${tokenInfo.fromToken.symbol}`);
    console.log(`To: ${tokenInfo.toToken.symbol}`);
    console.log(`Amount: ${humanReadableAmount} ${tokenInfo.fromToken.symbol}`);
    console.log(`Amount in base units: ${rawAmount}`);
    console.log(`Approximate USD value: $${(humanReadableAmount * parseFloat(tokenInfo.fromToken.price)).toFixed(2)}`);
    // Execute the swap
    console.log("\nExecuting swap...");
    const swapResult = await client.dex.executeSwap({
      chainId: '501', // Solana chain ID
      fromTokenAddress: '11111111111111111111111111111111', // SOL
      toTokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
      amount: rawAmount,
      slippage: '0.5', // 0.5% slippage
      userWalletAddress: process.env.SOLANA_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 };

5. 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: '501',  // Solana
    fromTokenAddress: '11111111111111111111111111111111', // SOL
    toTokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
    amount: '100000000',  // 0.1 SOL (in lamports)
    slippage: '0.5'     // 0.5%
});