Skip to main content

Overview

This guide walks you through the complete process of executing a token swap using Mobula’s Swap API. You’ll learn how to:
  1. Get an optimized swap quote
  2. Sign the transaction with your wallet
  3. Broadcast the transaction to the blockchain
  4. Monitor the transaction status
The Mobula Swap API provides optimal routing across multiple DEXs and supports both EVM chains (Ethereum, BSC, Base, etc.) and Solana.

Prerequisites

  • Node.js 18+ installed
  • A Mobula API key (get one at admin.mobula.io)
  • Basic knowledge of TypeScript/JavaScript
  • A wallet with funds on the blockchain you want to swap on

Installation

First, install the required dependencies:
npm install @solana/web3.js @solana/wallet-adapter-base ethers

Complete Swap Flow

Step 1: Get a Swap Quote

The first step is to request a swap quote. The API will return the estimated output amount and a serialized transaction ready to be signed.
interface SwapQuoteParams {
  chainId: string;
  tokenIn: string;
  tokenOut: string;
  amount: string;
  walletAddress: string;
  slippage?: string;
  onlyProtocols?: string;
  excludedProtocols?: string;
}

interface SwapQuoteResponse {
  data: {
    transaction?: {
      serialized: string;
      type: 'legacy' | 'versioned';
    };
    estimatedAmountOut?: string;
    estimatedSlippage?: number;
    requestId: string;
  };
  error?: string;
}

async function getSwapQuote(params: SwapQuoteParams): Promise<SwapQuoteResponse> {
  const queryParams = new URLSearchParams({
    chainId: params.chainId,
    tokenIn: params.tokenIn,
    tokenOut: params.tokenOut,
    amount: params.amount,
    walletAddress: params.walletAddress,
    slippage: params.slippage || '1',
  });

  if (params.onlyProtocols) {
    queryParams.append('onlyProtocols', params.onlyProtocols);
  }

  if (params.excludedProtocols) {
    queryParams.append('excludedProtocols', params.excludedProtocols);
  }

  const response = await fetch(
    `https://api.mobula.io/api/2/swap/quoting?${queryParams}`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.MOBULA_API_KEY}`,
      },
    }
  );

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return await response.json();
}

Step 2: Sign the Transaction

Once you have the quote, you need to sign the transaction with your wallet. The process differs between Solana and EVM chains.

Solana Transaction Signing

import { VersionedTransaction, Transaction } from '@solana/web3.js';
import type { WalletAdapter } from '@solana/wallet-adapter-base';

async function signSolanaTransaction(
  serializedTransaction: string,
  transactionType: 'legacy' | 'versioned',
  wallet: WalletAdapter
): Promise<string> {
  // Decode the base64 transaction
  const transactionBuffer = Buffer.from(serializedTransaction, 'base64');

  if (transactionType === 'versioned') {
    // Handle versioned transaction (most common for Solana)
    const transaction = VersionedTransaction.deserialize(transactionBuffer);
    
    // Sign the transaction with the wallet
    const signedTx = await wallet.signTransaction(transaction);
    
    // Serialize the signed transaction back to base64
    return Buffer.from(signedTx.serialize()).toString('base64');
  } else {
    // Handle legacy transaction
    const transaction = Transaction.from(transactionBuffer);
    
    // Sign the transaction
    const signedTx = await wallet.signTransaction(transaction);
    
    // Serialize and encode
    return Buffer.from(
      signedTx.serialize({ requireAllSignatures: false })
    ).toString('base64');
  }
}

EVM Transaction Signing

import { ethers } from 'ethers';

async function signEvmTransaction(
  serializedTransaction: string,
  wallet: ethers.Wallet
): Promise<string> {
  // Decode the transaction
  const transactionBuffer = Buffer.from(serializedTransaction, 'base64');
  
  // Parse the transaction
  const transaction = ethers.utils.parseTransaction(transactionBuffer);
  
  // Sign the transaction
  const signedTx = await wallet.signTransaction({
    to: transaction.to,
    data: transaction.data,
    value: transaction.value,
    gasLimit: transaction.gasLimit,
    gasPrice: transaction.gasPrice,
    nonce: transaction.nonce,
    chainId: transaction.chainId,
  });
  
  // Return as base64
  return Buffer.from(ethers.utils.arrayify(signedTx)).toString('base64');
}

Step 3: Send the Signed Transaction

After signing, broadcast the transaction to the blockchain.
interface SwapSendParams {
  chainId: string;
  signedTransaction: string;
}

interface SwapSendResponse {
  data: {
    success: boolean;
    transactionHash?: string;
    requestId: string;
  };
  error?: string;
}

async function sendSwapTransaction(params: SwapSendParams): Promise<SwapSendResponse> {
  const response = await fetch('https://api.mobula.io/api/2/swap/send', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.MOBULA_API_KEY}`,
    },
    body: JSON.stringify(params),
  });

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return await response.json();
}

Step 4: Monitor Transaction Status

After broadcasting, monitor the transaction on the blockchain.
import { Connection, PublicKey } from '@solana/web3.js';
import { ethers } from 'ethers';

// For Solana
async function waitForSolanaConfirmation(
  signature: string,
  connection: Connection,
  maxAttempts: number = 30
): Promise<boolean> {
  for (let i = 0; i < maxAttempts; i++) {
    const status = await connection.getSignatureStatus(signature);
    
    if (status.value?.confirmationStatus === 'confirmed' || 
        status.value?.confirmationStatus === 'finalized') {
      console.log(`✅ Transaction confirmed: ${signature}`);
      return true;
    }
    
    if (status.value?.err) {
      console.error(`❌ Transaction failed:`, status.value.err);
      return false;
    }
    
    // Wait 2 seconds before checking again
    await new Promise(resolve => setTimeout(resolve, 2000));
  }
  
  console.warn(`⚠️ Transaction not confirmed after ${maxAttempts} attempts`);
  return false;
}

// For EVM
async function waitForEvmConfirmation(
  txHash: string,
  provider: ethers.providers.Provider,
  confirmations: number = 1
): Promise<boolean> {
  try {
    const receipt = await provider.waitForTransaction(txHash, confirmations);
    
    if (receipt.status === 1) {
      console.log(`✅ Transaction confirmed: ${txHash}`);
      return true;
    } else {
      console.error(`❌ Transaction failed: ${txHash}`);
      return false;
    }
  } catch (error) {
    console.error(`❌ Error waiting for transaction:`, error);
    return false;
  }
}

Complete Examples

Example 1: Swap SOL to USDC on Solana

import { Connection, PublicKey } from '@solana/web3.js';
import { WalletAdapter } from '@solana/wallet-adapter-base';

async function swapSolToUsdc(
  wallet: WalletAdapter,
  amountInSol: number,
  slippagePercent: number = 1
) {
  const connection = new Connection('https://api.mainnet-beta.solana.com');
  
  console.log(`🔄 Starting swap: ${amountInSol} SOL → USDC`);
  
  try {
    // Step 1: Get quote
    console.log('📊 Fetching quote...');
    const quote = await getSwapQuote({
      chainId: 'solana',
      tokenIn: 'So11111111111111111111111111111111111111112', // SOL
      tokenOut: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
      amount: (amountInSol * 1e9).toString(), // Convert to lamports
      walletAddress: wallet.publicKey!.toString(),
      slippage: slippagePercent.toString(),
    });
    
    if (quote.error || !quote.data.transaction) {
      throw new Error(quote.error || 'No transaction returned');
    }
    
    console.log(`💰 Estimated output: ${quote.data.estimatedAmountOut} USDC`);
    console.log(`📉 Estimated slippage: ${quote.data.estimatedSlippage}%`);
    
    // Step 2: Sign transaction
    console.log('✍️  Signing transaction...');
    const signedTx = await signSolanaTransaction(
      quote.data.transaction.serialized,
      quote.data.transaction.type,
      wallet
    );
    
    // Step 3: Send transaction
    console.log('📤 Broadcasting transaction...');
    const result = await sendSwapTransaction({
      chainId: 'solana',
      signedTransaction: signedTx,
    });
    
    if (!result.data.success || !result.data.transactionHash) {
      throw new Error(result.error || 'Transaction failed');
    }
    
    console.log(`🔗 Transaction hash: ${result.data.transactionHash}`);
    
    // Step 4: Wait for confirmation
    console.log('⏳ Waiting for confirmation...');
    const confirmed = await waitForSolanaConfirmation(
      result.data.transactionHash,
      connection
    );
    
    if (confirmed) {
      console.log('🎉 Swap completed successfully!');
      return result.data.transactionHash;
    } else {
      throw new Error('Transaction not confirmed');
    }
  } catch (error) {
    console.error('❌ Swap failed:', error);
    throw error;
  }
}

// Usage
const amountToSwap = 0.1; // 0.1 SOL
const maxSlippage = 1; // 1%

swapSolToUsdc(wallet, amountToSwap, maxSlippage)
  .then(txHash => console.log(`Success! TX: ${txHash}`))
  .catch(error => console.error('Failed:', error));

Example 2: Swap ETH to USDC on Ethereum

import { ethers } from 'ethers';

async function swapEthToUsdc(
  wallet: ethers.Wallet,
  amountInEth: number,
  slippagePercent: number = 1
) {
  const provider = wallet.provider;
  
  console.log(`🔄 Starting swap: ${amountInEth} ETH → USDC`);
  
  try {
    // Step 1: Get quote
    console.log('📊 Fetching quote...');
    const quote = await getSwapQuote({
      chainId: 'ethereum',
      tokenIn: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', // ETH
      tokenOut: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
      amount: ethers.utils.parseEther(amountInEth.toString()).toString(),
      walletAddress: wallet.address,
      slippage: slippagePercent.toString(),
    });
    
    if (quote.error || !quote.data.transaction) {
      throw new Error(quote.error || 'No transaction returned');
    }
    
    const estimatedUsdc = parseFloat(quote.data.estimatedAmountOut || '0') / 1e6;
    console.log(`💰 Estimated output: ${estimatedUsdc.toFixed(2)} USDC`);
    console.log(`📉 Estimated slippage: ${quote.data.estimatedSlippage}%`);
    
    // Step 2: Sign transaction
    console.log('✍️  Signing transaction...');
    const signedTx = await signEvmTransaction(
      quote.data.transaction.serialized,
      wallet
    );
    
    // Step 3: Send transaction
    console.log('📤 Broadcasting transaction...');
    const result = await sendSwapTransaction({
      chainId: 'ethereum',
      signedTransaction: signedTx,
    });
    
    if (!result.data.success || !result.data.transactionHash) {
      throw new Error(result.error || 'Transaction failed');
    }
    
    console.log(`🔗 Transaction hash: ${result.data.transactionHash}`);
    
    // Step 4: Wait for confirmation
    console.log('⏳ Waiting for confirmation...');
    const confirmed = await waitForEvmConfirmation(
      result.data.transactionHash,
      provider!,
      1
    );
    
    if (confirmed) {
      console.log('🎉 Swap completed successfully!');
      return result.data.transactionHash;
    } else {
      throw new Error('Transaction not confirmed');
    }
  } catch (error) {
    console.error('❌ Swap failed:', error);
    throw error;
  }
}

// Usage with ethers wallet
const provider = new ethers.providers.JsonRpcProvider('https://eth.llamarpc.com');
const wallet = new ethers.Wallet('YOUR_PRIVATE_KEY', provider);

const amountToSwap = 0.1; // 0.1 ETH
const maxSlippage = 1; // 1%

swapEthToUsdc(wallet, amountToSwap, maxSlippage)
  .then(txHash => console.log(`Success! TX: ${txHash}`))
  .catch(error => console.error('Failed:', error));

Example 3: Advanced Swap with Protocol Restrictions

async function advancedSwap(
  wallet: WalletAdapter,
  tokenIn: string,
  tokenOut: string,
  amount: string,
  options: {
    slippage?: number;
    onlyProtocols?: string[];
    excludedProtocols?: string[];
  } = {}
) {
  try {
    // Get quote with advanced options
    const quote = await getSwapQuote({
      chainId: 'solana',
      tokenIn,
      tokenOut,
      amount,
      walletAddress: wallet.publicKey!.toString(),
      slippage: options.slippage?.toString() || '1',
      onlyProtocols: options.onlyProtocols?.join(','),
      excludedProtocols: options.excludedProtocols?.join(','),
    });
    
    if (quote.error) {
      throw new Error(quote.error);
    }
    
    console.log('Quote received:', {
      estimatedOut: quote.data.estimatedAmountOut,
      slippage: quote.data.estimatedSlippage,
      requestId: quote.data.requestId,
    });
    
    // ... continue with signing and sending
    
  } catch (error) {
    console.error('Swap error:', error);
    throw error;
  }
}

// Usage: Only use Raydium and Orca
advancedSwap(
  wallet,
  'So11111111111111111111111111111111111111112', // SOL
  'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
  '100000000', // 0.1 SOL
  {
    slippage: 2,
    onlyProtocols: ['raydium-amm', 'orca-whirlpool'],
  }
);

// Usage: Exclude specific protocol
advancedSwap(
  wallet,
  'So11111111111111111111111111111111111111112',
  'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
  '100000000',
  {
    excludedProtocols: ['0xSomeFactoryAddress'],
  }
);

Error Handling Best Practices

enum SwapErrorType {
  QUOTE_FAILED = 'QUOTE_FAILED',
  SIGNING_FAILED = 'SIGNING_FAILED',
  BROADCAST_FAILED = 'BROADCAST_FAILED',
  CONFIRMATION_TIMEOUT = 'CONFIRMATION_TIMEOUT',
  INSUFFICIENT_BALANCE = 'INSUFFICIENT_BALANCE',
  SLIPPAGE_EXCEEDED = 'SLIPPAGE_EXCEEDED',
}

class SwapError extends Error {
  constructor(
    public type: SwapErrorType,
    message: string,
    public originalError?: unknown
  ) {
    super(message);
    this.name = 'SwapError';
  }
}

async function executeSwapWithErrorHandling(
  params: SwapQuoteParams,
  wallet: WalletAdapter
) {
  try {
    // Get quote
    const quote = await getSwapQuote(params);
    
    if (quote.error) {
      if (quote.error.includes('slippage')) {
        throw new SwapError(
          SwapErrorType.SLIPPAGE_EXCEEDED,
          'Slippage tolerance exceeded. Try increasing slippage or reducing amount.',
          quote.error
        );
      }
      
      if (quote.error.includes('liquidity')) {
        throw new SwapError(
          SwapErrorType.QUOTE_FAILED,
          'Insufficient liquidity for this swap.',
          quote.error
        );
      }
      
      throw new SwapError(
        SwapErrorType.QUOTE_FAILED,
        `Failed to get quote: ${quote.error}`,
        quote.error
      );
    }
    
    if (!quote.data.transaction) {
      throw new SwapError(
        SwapErrorType.QUOTE_FAILED,
        'No transaction returned from quote'
      );
    }
    
    // Sign transaction
    let signedTx: string;
    try {
      signedTx = await signSolanaTransaction(
        quote.data.transaction.serialized,
        quote.data.transaction.type,
        wallet
      );
    } catch (error) {
      throw new SwapError(
        SwapErrorType.SIGNING_FAILED,
        'Failed to sign transaction. User may have rejected.',
        error
      );
    }
    
    // Send transaction
    const result = await sendSwapTransaction({
      chainId: params.chainId,
      signedTransaction: signedTx,
    });
    
    if (!result.data.success) {
      throw new SwapError(
        SwapErrorType.BROADCAST_FAILED,
        result.error || 'Failed to broadcast transaction',
        result.error
      );
    }
    
    return {
      success: true,
      transactionHash: result.data.transactionHash,
      estimatedAmountOut: quote.data.estimatedAmountOut,
    };
    
  } catch (error) {
    if (error instanceof SwapError) {
      // Handle specific swap errors
      switch (error.type) {
        case SwapErrorType.SLIPPAGE_EXCEEDED:
          console.error('💥 Slippage too high. Consider:');
          console.error('   - Increasing slippage tolerance');
          console.error('   - Reducing swap amount');
          console.error('   - Trying again in a few moments');
          break;
          
        case SwapErrorType.SIGNING_FAILED:
          console.error('✍️ Transaction signing failed');
          console.error('   - User may have rejected the transaction');
          console.error('   - Check wallet connection');
          break;
          
        case SwapErrorType.BROADCAST_FAILED:
          console.error('📡 Failed to broadcast transaction');
          console.error('   - Check network connection');
          console.error('   - Verify wallet has sufficient balance for fees');
          break;
          
        default:
          console.error('❌ Swap error:', error.message);
      }
      
      throw error;
    }
    
    // Unknown error
    console.error('❌ Unexpected error:', error);
    throw new SwapError(
      SwapErrorType.QUOTE_FAILED,
      'An unexpected error occurred',
      error
    );
  }
}

Important Notes

Token Addresses

  • Native tokens: Use 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE for ETH, BNB, MATIC, etc.
  • Solana native: Use So11111111111111111111111111111111111111112 for SOL
  • Token amounts: Always use the smallest unit (wei for EVM, lamports for Solana)

Amount Conversion

// EVM chains (18 decimals for most tokens)
const ethAmount = '1.5'; // 1.5 ETH
const amountInWei = ethers.utils.parseEther(ethAmount).toString();

// For tokens with different decimals
const usdcAmount = '100'; // 100 USDC
const amountInSmallestUnit = (parseFloat(usdcAmount) * 1e6).toString(); // USDC has 6 decimals

// Solana (9 decimals for most tokens)
const solAmount = 0.5; // 0.5 SOL
const amountInLamports = (solAmount * 1e9).toString();

Protocol Filtering

  • onlyProtocols: Restricts routing to specific tradable protocols. Only valid tradable pool types will be considered.
    onlyProtocols: 'uniswap-v3,sushiswap-v2'
    
  • excludedProtocols: Excludes specific factory addresses from routing.
    excludedProtocols: '0xfactoryAddress1,0xfactoryAddress2'
    

Slippage Recommendations

  • Stablecoins: 0.1% - 0.5%
  • Major tokens: 0.5% - 1%
  • Low liquidity tokens: 2% - 5%
  • Memecoins: 5% - 10%
Higher slippage increases success rate but may result in worse execution prices.

Troubleshooting

Common Issues

  1. “Slippage too high”
    • Increase slippage tolerance
    • Reduce swap amount
    • Wait for better market conditions
  2. “No route found”
    • Check token addresses are correct
    • Verify tokens have liquidity on the chain
    • Try without protocol restrictions
  3. “Transaction simulation failed”
    • Insufficient balance (including gas fees)
    • Token approval may be needed (for non-native tokens)
    • Slippage tolerance too low
  4. Transaction not confirming
    • Network congestion (especially on Ethereum)
    • Gas price too low (EVM chains)
    • Check blockchain explorer with transaction hash

Next Steps

Support

Need help? Join our community: