Executing Swaps

Execute token swaps on Solana using the Carbium DEX API. This guide covers the complete flow from getting an executable quote to confirming the transaction on-chain.

Overview

Carbium provides a streamlined swap execution flow:

  1. Get Quote with Wallet → Returns quote + executable transaction
  2. Sign Transaction → Sign with your wallet keypair
  3. Send Transaction → Submit to the Solana network
  4. Confirm → Poll for confirmation status

API Reference

Quote Endpoint (with Transaction)

GET https://api.carbium.io/api/v2/quote

ParameterTypeRequiredDescription
src_mintstringYesSource token mint address
dst_mintstringYesDestination token mint address
amount_innumberYesInput amount in smallest units (lamports)
slippage_bpsnumberNoSlippage tolerance in basis points (default: 10)
user_accountstringYes*Wallet address to receive executable transaction

*When user_account is provided, the response includes a txn field containing a base64-encoded VersionedTransaction ready for signing. Without it, txn returns as an empty string.

Response Example

{
  "srcAmountIn": "10000000",
  "destAmountOut": "1257091",
  "destAmountOutMin": "1257091",
  "slippage": "10",
  "priceImpactPct": "0",
  "routePlan": [
    {
      "swap": "Raydium",
      "percent": 100
    }
  ],
  "txn": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAHCw..."
}
FieldDescription
srcAmountInInput amount in smallest units
destAmountOutExpected output amount in smallest units
destAmountOutMinMinimum output after slippage
priceImpactPctPrice impact percentage
routePlanArray of swap route details
txnBase64-encoded VersionedTransaction (empty string if user_account not provided)

Implementation

JavaScript (Node.js)
import { Connection, Keypair, VersionedTransaction } from '@solana/web3.js';
import bs58 from 'bs58';
import dotenv from 'dotenv';

dotenv.config();

const API_KEY = process.env.CARBIUM_API_KEY;
const RPC_URL = process.env.SOLANA_RPC_URL;
const PRIVATE_KEY = process.env.PRIVATE_KEY;

// Initialize connection and wallet
const connection = new Connection(RPC_URL, 'confirmed');
const wallet = Keypair.fromSecretKey(bs58.decode(PRIVATE_KEY));

/**
 * Get executable quote from Carbium
 */
async function getExecutableQuote(srcMint, dstMint, amountIn, slippageBps = 10) {
  const params = new URLSearchParams({
    src_mint: srcMint,
    dst_mint: dstMint,
    amount_in: String(amountIn),
    slippage_bps: String(slippageBps),
    user_account: wallet.publicKey.toBase58()
  });

  const response = await fetch(
    `https://api.carbium.io/api/v2/quote?${params}`,
    { headers: { 'X-API-KEY': API_KEY } }
  );

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

  const data = await response.json();

  // API returns empty string when user_account not provided
  if (!data.txn || data.txn.length === 0) {
    throw new Error('Quote missing transaction - ensure user_account is provided');
  }

  return data;
}

/**
 * Confirm transaction using HTTP polling
 */
async function confirmTransaction(signature, timeout = 60000) {
  const startTime = Date.now();

  while (Date.now() - startTime < timeout) {
    const status = await connection.getSignatureStatus(signature);

    if (status.value?.err) {
      throw new Error(`Transaction failed: ${JSON.stringify(status.value.err)}`);
    }

    if (status.value?.confirmationStatus === 'confirmed' ||
        status.value?.confirmationStatus === 'finalized') {
      return true;
    }

    await new Promise(resolve => setTimeout(resolve, 500));
  }

  throw new Error(`Transaction not confirmed within ${timeout / 1000}s`);
}

/**
 * Execute a swap on Carbium
 */
async function executeSwap(srcMint, dstMint, amountIn) {
  console.log(`Executing swap: ${amountIn} ${srcMint.slice(0,4)} → ${dstMint.slice(0,4)}`);

  // Step 1: Get executable quote
  const quote = await getExecutableQuote(srcMint, dstMint, amountIn);
  console.log(`Expected output: ${quote.destAmountOut}`);

  // Step 2: Deserialize transaction
  const transaction = VersionedTransaction.deserialize(
    Buffer.from(quote.txn, 'base64')
  );

  // Step 3: Sign transaction
  transaction.sign([wallet]);

  // Step 4: Send transaction
  const signature = await connection.sendTransaction(transaction, {
    skipPreflight: true,  // Skip simulation for speed (quote already validated)
    maxRetries: 3
  });
  console.log(`Transaction sent: ${signature}`);

  // Step 5: Confirm transaction
  await confirmTransaction(signature);
  console.log(`Confirmed: https://solscan.io/tx/${signature}`);

  return {
    signature,
    inputAmount: amountIn,
    outputAmount: quote.destAmountOut
  };
}

// Example: Swap 0.01 SOL to USDC
const SOL = 'So11111111111111111111111111111111111111112';
const USDC = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';

const result = await executeSwap(SOL, USDC, 10_000_000); // 0.01 SOL
console.log('Swap completed:', result);
Python
import os
import asyncio
import base64
import aiohttp
from solders.keypair import Keypair
from solders.transaction import VersionedTransaction
from solana.rpc.async_api import AsyncClient
from dotenv import load_dotenv

load_dotenv()

API_KEY = os.getenv('CARBIUM_API_KEY')
RPC_URL = os.getenv('SOLANA_RPC_URL')
PRIVATE_KEY = os.getenv('PRIVATE_KEY')

# Initialize wallet from base58 private key
wallet = Keypair.from_base58_string(PRIVATE_KEY)

async def get_executable_quote(session, src_mint, dst_mint, amount_in, slippage_bps=10):
    """Get quote with executable transaction from Carbium"""
    params = {
        'src_mint': src_mint,
        'dst_mint': dst_mint,
        'amount_in': str(amount_in),
        'slippage_bps': str(slippage_bps),
        'user_account': str(wallet.pubkey())
    }
    headers = {'X-API-KEY': API_KEY}

    async with session.get(
        'https://api.carbium.io/api/v2/quote',
        params=params,
        headers=headers
    ) as resp:
        if resp.status != 200:
            raise Exception(f'Quote failed: {resp.status}')
        data = await resp.json()

        # API returns empty string when user_account not provided
        if not data.get('txn') or len(data['txn']) == 0:
            raise Exception('Quote missing transaction - ensure user_account is provided')

        return data

async def confirm_transaction(client, signature, timeout=60):
    """Confirm transaction using HTTP polling"""
    import time
    start_time = time.time()

    while time.time() - start_time < timeout:
        resp = await client.get_signature_statuses([signature])
        status = resp.value[0]

        if status:
            if status.err:
                raise Exception(f'Transaction failed: {status.err}')

            if status.confirmation_status in ['confirmed', 'finalized']:
                return True

        await asyncio.sleep(0.5)

    raise Exception(f'Transaction not confirmed within {timeout}s')

async def execute_swap(src_mint, dst_mint, amount_in):
    """Execute a swap on Carbium"""
    async with aiohttp.ClientSession() as session:
        async with AsyncClient(RPC_URL) as client:
            print(f'Executing swap: {amount_in} {src_mint[:4]} → {dst_mint[:4]}')

            # Step 1: Get executable quote
            quote = await get_executable_quote(session, src_mint, dst_mint, amount_in)
            print(f'Expected output: {quote["destAmountOut"]}')

            # Step 2: Deserialize transaction
            txn_bytes = base64.b64decode(quote['txn'])
            transaction = VersionedTransaction.from_bytes(txn_bytes)

            # Step 3: Sign transaction
            signed_tx = VersionedTransaction(transaction.message, [wallet])

            # Step 4: Send transaction
            resp = await client.send_raw_transaction(
                bytes(signed_tx),
                opts={'skip_preflight': True, 'max_retries': 3}
            )
            signature = str(resp.value)
            print(f'Transaction sent: {signature}')

            # Step 5: Confirm transaction
            await confirm_transaction(client, signature)
            print(f'Confirmed: https://solscan.io/tx/{signature}')

            return {
                'signature': signature,
                'input_amount': amount_in,
                'output_amount': quote['destAmountOut']
            }

# Example: Swap 0.01 SOL to USDC
async def main():
    SOL = 'So11111111111111111111111111111111111111112'
    USDC = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'

    result = await execute_swap(SOL, USDC, 10_000_000)  # 0.01 SOL
    print('Swap completed:', result)

asyncio.run(main())

Key Concepts

Transaction Types

Carbium returns VersionedTransactions (V0), which support:

  • Address Lookup Tables (ALTs) for more accounts per transaction
  • Efficient routing through multiple DEX pools
// Always use VersionedTransaction, not legacy Transaction
import { VersionedTransaction } from '@solana/web3.js';

const transaction = VersionedTransaction.deserialize(
  Buffer.from(quote.txn, 'base64')
);

Skip Preflight

For speed-critical execution, skip preflight simulation:

const signature = await connection.sendTransaction(transaction, {
  skipPreflight: true,  // Quote already validated the route
  maxRetries: 3
});

Note: Only skip preflight when you trust the quote source. The Carbium API validates routes before returning transactions.

HTTP Polling vs WebSocket

Use HTTP polling for transaction confirmation (more reliable):

// Recommended: HTTP polling
const status = await connection.getSignatureStatus(signature);

// Avoid: WebSocket confirmation (can fail with 405 errors on some RPCs)
// await connection.confirmTransaction(signature);

Slippage Settings

Use CaseRecommended Slippage
Stablecoin swaps5-10 bps (0.05-0.1%)
Major pairs (SOL/USDC)10-50 bps (0.1-0.5%)
Volatile tokens50-100 bps (0.5-1%)
Arbitrage10 bps (0.1%)
// Tight slippage for arbitrage
const quote = await getExecutableQuote(srcMint, dstMint, amount, 10);

// Looser slippage for volatile tokens
const quote = await getExecutableQuote(srcMint, dstMint, amount, 100);

Error Handling

Common Errors

ErrorCauseSolution
Quote missing transactionuser_account not provided or emptyInclude wallet address in quote request
Transaction failed: InsufficientFundsNot enough tokensCheck balance before swap
Transaction failed: SlippageExceededPrice moved too muchIncrease slippage or retry
Transaction not confirmedNetwork congestionIncrease timeout or add priority fee

Retry Logic

async function executeSwapWithRetry(srcMint, dstMint, amountIn, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await executeSwap(srcMint, dstMint, amountIn);
    } catch (error) {
      console.error(`Attempt ${attempt} failed: ${error.message}`);

      if (attempt === maxRetries) throw error;

      // Wait before retry (exponential backoff)
      await new Promise(r => setTimeout(r, 1000 * attempt));
    }
  }
}

Best Practices

  1. Always provide user_account - Required to receive executable transaction
  2. Use HTTP polling - More reliable than WebSocket for confirmations
  3. Skip preflight for speed - Quote already validates the route
  4. Handle partial failures - In multi-leg trades, track intermediate state
  5. Clear quote cache after execution - Prevents stale re-execution