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…

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.

If the request fails before you reach signing or confirmation, use Swap API Errors Reference first. This page assumes the quote request itself is succeeding.

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