Overview
Solana Swap Execution - Alpha ModeSwap execution on Solana is currently in alpha mode. The feature is functional but may have limitations. EVM chains are fully stable and production-ready. Please report any Solana-related issues to the Mobula team.
This guide walks you through the complete process of executing a token swap using Mobula’s Swap API. You’ll learn how to:
- Get an optimized swap quote
- Sign the transaction with your wallet
- Broadcast the transaction to the blockchain
- Monitor the transaction status
The Mobula Swap API provides optimal routing across multiple DEXs and supports both EVM chains and Solana.
Supported EVM Chains
All EVM swaps are executed through MobulaRouter, a smart contract deployed on:
| Chain | Chain ID | Native Token |
|---|
| Ethereum | evm:1 | ETH |
| Optimism | evm:10 | ETH |
| BNB Chain | evm:56 | BNB |
| Polygon | evm:137 | MATIC |
| Base | evm:8453 | ETH |
| Arbitrum | evm:42161 | ETH |
| Avalanche | evm:43114 | AVAX |
MobulaRouter automatically routes to the best available aggregator for optimal pricing across all DEX liquidity on each chain.
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
- EVM native tokens: Use
0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE for native tokens on any EVM chain (ETH, BNB, MATIC, AVAX)
- Solana native: Use
So11111111111111111111111111111111111111112 for SOL
- Token amounts: Always use the smallest unit (wei for EVM, lamports for Solana)
EVM Chains
All EVM swap transactions go through MobulaRouter. The response provides a ready-to-sign transaction in the evm.transaction field:
const { evm, requestId } = response.data;
// Sign and send using ethers.js v6
const tx = await signer.sendTransaction({
to: evm.transaction.to, // MobulaRouter address
data: evm.transaction.data, // Encoded executeAggregatorSwap calldata
value: evm.transaction.value, // Native token amount (0 for ERC-20 input)
chainId: evm.transaction.chainId,
});
await tx.wait();
console.log('Swap confirmed:', tx.hash);
For native token swaps (ETH, BNB, MATIC, AVAX), use 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE as tokenIn. The API will set the correct value field in the transaction.
For ERC-20 token swaps, you must first approve the MobulaRouter contract to spend your tokens. The router address is returned in evm.transaction.to.
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
-
“Slippage too high”
- Increase slippage tolerance
- Reduce swap amount
- Wait for better market conditions
-
“No route found”
- Check token addresses are correct
- Verify tokens have liquidity on the chain
- Try without protocol restrictions
-
“Transaction simulation failed”
- Insufficient balance (including gas fees)
- Token approval may be needed (for non-native tokens)
- Slippage tolerance too low
-
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: