Learn how to swap SOL to USDC on Arbitrum using OneBalance APIs.

Complete Code Repository

Get the full working example with setup instructions. 100% free and ready to run.

Setup

Install packages and create environment file:
pnpm install @solana/web3.js bs58 dotenv viem
.env
SOLANA_PRIVATE_KEY=your_base58_private_key_here
ONEBALANCE_API_KEY=your_api_key_here
ARBITRUM_RECIPIENT=0x895Cf62399bF1F8b88195E741b64278b41EB7F09

Example

swap.ts
import { Keypair, MessageV0, VersionedTransaction, PublicKey } from '@solana/web3.js';
import { formatUnits, parseUnits } from 'viem';
import bs58 from 'bs58';
import dotenv from 'dotenv';

// Load environment variables
dotenv.config();

// Configuration from environment
const SOLANA_PRIVATE_KEY = process.env.SOLANA_PRIVATE_KEY!;
const ARBITRUM_RECIPIENT = process.env.ARBITRUM_RECIPIENT!;
const API_KEY = process.env.ONEBALANCE_API_KEY || "42bb629272001ee1163ca0dbbbc07bcbb0ef57a57baf16c4b1d4672db4562c11";

// Create Solana keypair from private key
const keypair = Keypair.fromSecretKey(bs58.decode(SOLANA_PRIVATE_KEY));
const SOLANA_ACCOUNT = keypair.publicKey.toString();

// Solana asset IDs
const SOL_ASSET_ID = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501";
const USDC_ARB_ASSET_ID = "eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831";

console.log(`Using Solana account: ${SOLANA_ACCOUNT}`);
console.log(`Arbitrum recipient: ${ARBITRUM_RECIPIENT}`);

/**
 * Check SOL balance before swapping
 */
async function checkSOLBalance() {
  try {
    console.log('Checking SOL balance...');
    
    const response = await fetch(
      `https://be.onebalance.io/api/v3/balances/aggregated-balance?account=solana:${SOLANA_ACCOUNT}&aggregatedAssetId=ds:sol`,
      { headers: { 'x-api-key': API_KEY } }
    );
    
    if (!response.ok) {
      throw new Error('Failed to fetch balance');
    }
    
    const balanceData = await response.json();
    const solBalance = balanceData.balanceByAggregatedAsset?.find(
      (asset: any) => asset.aggregatedAssetId === 'ds:sol'
    );
    
    if (!solBalance) {
      throw new Error('No SOL balance found');
    }
    
    const balanceInSOL = parseFloat(formatUnits(BigInt(solBalance.balance), 9));
    console.log(`Available SOL: ${balanceInSOL.toFixed(4)} SOL`);
    
    return balanceInSOL;
    
  } catch (error) {
    console.error('Balance check failed:', error);
    throw error;
  }
}

/**
 * Swap SOL to USDC on Arbitrum (cross-chain)
 */
async function swapSOLtoUSDC() {
  try {
    console.log('Starting SOL → USDC (Arbitrum) swap...\n');
    
    // Validate Solana address format
    try {
      new PublicKey(SOLANA_ACCOUNT);
    } catch (error) {
      throw new Error('Invalid Solana address format');
    }
    
    // Check balance
    const balance = await checkSOLBalance();
    const swapAmount = 0.01; // 0.01 SOL
    
    if (balance < swapAmount) {
      throw new Error(`Insufficient balance. Need ${swapAmount} SOL, have ${balance.toFixed(4)} SOL`);
    }
    
    console.log(`Swapping ${swapAmount} SOL to USDC on Arbitrum...`);
    
    // Step 1: Get quote
    console.log('Getting quote...');
    
    const quoteRequest = {
      from: {
        accounts: [{
          type: "solana",
          accountAddress: SOLANA_ACCOUNT
        }],
        asset: {
          assetId: SOL_ASSET_ID
        },
        amount: parseUnits(swapAmount.toString(), 9).toString() // SOL has 9 decimals
      },
      to: {
        asset: {
          assetId: USDC_ARB_ASSET_ID // USDC on Arbitrum
        },
        account: `eip155:42161:${ARBITRUM_RECIPIENT}`
      }
    };

    const quoteResponse = await fetch('https://be.onebalance.io/api/v3/quote', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': API_KEY
      },
      body: JSON.stringify(quoteRequest)
    });

    if (!quoteResponse.ok) {
      const error = await quoteResponse.json();
      throw new Error(`Quote failed: ${error.message}`);
    }

    const quote = await quoteResponse.json();
    console.log('Quote received:', {
      id: quote.id,
      willReceive: `${formatUnits(BigInt(quote.destinationToken.amount), 6)} USDC`,
      fiatValue: `$${quote.destinationToken.fiatValue}`,
      fees: `$${quote.fees.cumulativeUSD}`
    });

    // Step 2: Sign the Solana operation
    console.log('Signing Solana transaction...');
    
    const solanaOperation = quote.originChainsOperations.find((op: any) => op.type === 'solana');
    if (!solanaOperation) {
      throw new Error('No Solana operation found in quote');
    }
    
    const signedOperation = signSolanaOperation(solanaOperation);
    console.log('Transaction signed successfully');
    
    const signedQuote = {
      ...quote,
      originChainsOperations: quote.originChainsOperations.map((op: any) => 
        op.type === 'solana' ? signedOperation : op
      )
    };

    // Step 3: Execute the swap
    console.log('Executing cross-chain swap...');
    
    const executeResponse = await fetch('https://be.onebalance.io/api/v3/quote/execute', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': API_KEY
      },
      body: JSON.stringify(signedQuote)
    });

    if (!executeResponse.ok) {
      const error = await executeResponse.json();
      throw new Error(`Execution failed: ${error.message}`);
    }

    const result = await executeResponse.json();
    console.log('Swap submitted successfully!');
    
    // Step 4: Monitor transaction completion
    console.log('Monitoring transaction...');
    await monitorCompletion(quote.id);
    
    console.log('Cross-chain swap completed successfully!');
    console.log(`USDC delivered to Arbitrum address: ${ARBITRUM_RECIPIENT}`);
    
    return result;

  } catch (error) {
    console.error('Swap failed:', (error as Error).message);
    throw error;
  }
}

// Real Solana signing implementation
function signSolanaOperation(chainOp: any): any {
  try {
    const msgBuffer = Buffer.from(chainOp.dataToSign, 'base64');
    const message = MessageV0.deserialize(msgBuffer);
    const transaction = new VersionedTransaction(message);
    
    // Sign with keypair
    transaction.sign([keypair]);
    
    // Extract signature
    const signature = bs58.encode(Buffer.from(transaction.signatures[transaction.signatures.length - 1]));
    
    return {
      ...chainOp,
      signature,
    };
    
  } catch (error) {
    console.error('Signing failed:', error);
    throw new Error(`Failed to sign Solana transaction: ${(error as Error).message}`);
  }
}

// Monitor transaction completion
async function monitorCompletion(quoteId: string): Promise<void> {
  const timeout = 60_000; // 60 seconds
  const startTime = Date.now();
  
  while (Date.now() - startTime < timeout) {
    try {
      const statusResponse = await fetch(
        `https://be.onebalance.io/api/v3/status/get-execution-status?quoteId=${quoteId}`,
        { headers: { 'x-api-key': API_KEY } }
      );
      
      if (statusResponse.ok) {
        const status = await statusResponse.json();
        console.log(`Status: ${status.status.status}`);

        if (status.status.status === 'COMPLETED') {
          console.log('Transaction completed successfully!');
          return;
        } else if (status.status.status === 'FAILED') {
          throw new Error('Transaction failed');
        }
      }
    } catch (error) {
      console.log('Status check error (retrying):', (error as Error).message);
    }

    // Wait 3 seconds before next check
    await new Promise(resolve => setTimeout(resolve, 3000));
  }
  
  throw new Error('Transaction monitoring timeout');
}

// Run the swap
async function main() {
  try {
    await swapSOLtoUSDC();
  } catch (error) {
    console.error('Failed:', error);
    process.exit(1);
  }
}

main();

How It Works

The script performs a complete cross-chain swap with validation and monitoring:
  1. Balance Check: Verify you have sufficient SOL for the swap
  2. Request Quote: Get pricing for 0.01 SOL → USDC on Arbitrum
  3. Sign Transaction: Sign the Solana operation with your private key
  4. Execute Swap: Submit the signed transaction to OneBalance
  5. Monitor Progress: Track the transaction until completion
Expected Result: ~0.015 USDC delivered to your Arbitrum address with full monitoring

Run the Example

Set your environment variables and run:
npx tsx swap.ts
Make sure your Solana account has at least 0.01 SOL for the swap.

Next Steps

Got stuck? Check the Solana Troubleshooting Guide for common issues and solutions.