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:
- Get Quote with Wallet → Returns quote + executable transaction
- Sign Transaction → Sign with your wallet keypair
- Send Transaction → Submit to the Solana network
- Confirm → Poll for confirmation status
API Reference
Quote Endpoint (with Transaction)
GET https://api.carbium.io/api/v2/quote
| Parameter | Type | Required | Description |
|---|---|---|---|
src_mint | string | Yes | Source token mint address |
dst_mint | string | Yes | Destination token mint address |
amount_in | number | Yes | Input amount in smallest units (lamports) |
slippage_bps | number | No | Slippage tolerance in basis points (default: 10) |
user_account | string | Yes* | 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..."
}
| Field | Description |
|---|---|
srcAmountIn | Input amount in smallest units |
destAmountOut | Expected output amount in smallest units |
destAmountOutMin | Minimum output after slippage |
priceImpactPct | Price impact percentage |
routePlan | Array of swap route details |
txn | Base64-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 Case | Recommended Slippage |
|---|---|
| Stablecoin swaps | 5-10 bps (0.05-0.1%) |
| Major pairs (SOL/USDC) | 10-50 bps (0.1-0.5%) |
| Volatile tokens | 50-100 bps (0.5-1%) |
| Arbitrage | 10 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
| Error | Cause | Solution |
|---|---|---|
Quote missing transaction | user_account not provided or empty | Include wallet address in quote request |
Transaction failed: InsufficientFunds | Not enough tokens | Check balance before swap |
Transaction failed: SlippageExceeded | Price moved too much | Increase slippage or retry |
Transaction not confirmed | Network congestion | Increase 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
- Always provide user_account - Required to receive executable transaction
- Use HTTP polling - More reliable than WebSocket for confirmations
- Skip preflight for speed - Quote already validates the route
- Handle partial failures - In multi-leg trades, track intermediate state
- Clear quote cache after execution - Prevents stale re-execution
Updated about 9 hours ago
