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:
- 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 20 hours ago
