Sending Transactions through Carbium RPC

This guide explains how to get unsigned transactions from the Carbium Quote API and submit them using Carbium RPC for fast, reliable execution on Solana.

Overview

The Carbium workflow:

  1. Get Quote with Transaction - Fetch a quote with user_account parameter to receive an unsigned transaction
  2. Deserialize & Sign - Decode the base64 transaction and sign with your wallet
  3. Send via Carbium RPC - Submit the signed transaction through Carbium's optimized RPC

Prerequisites

npm install @solana/web3.js bs58 undici dotenv

Environment variables (.env):

CARBIUM_API_KEY=your-api-key
CARBIUM_RPC_API_KEY=your-rpc-key
SOLANA_PRIVATE_KEY=your-base58-private-key

Step 1: Get a Quote with Unsigned Transaction

When you include the user_account parameter, Carbium returns the quote plus a ready-to-sign transaction in the txn field.

import 'dotenv/config';
import { fetch } from 'undici';

const CARBIUM_API_KEY = process.env.CARBIUM_API_KEY;
const WALLET_ADDRESS = 'YOUR_WALLET_PUBLIC_KEY';

// Token mints
const SOL = 'So11111111111111111111111111111111111111112';
const USDC = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';

async function getQuoteWithTransaction(srcMint, dstMint, amountIn, slippageBps = 50) {
  const url = new URL('https://api.carbium.io/api/v2/quote');
  url.searchParams.set('src_mint', srcMint);
  url.searchParams.set('dst_mint', dstMint);
  url.searchParams.set('amount_in', String(amountIn));
  url.searchParams.set('slippage_bps', String(slippageBps));
  url.searchParams.set('user_account', WALLET_ADDRESS);  // Required for transaction

  const response = await fetch(url, {
    headers: {
      'X-API-KEY': CARBIUM_API_KEY,
      'Accept': 'application/json'
    }
  });

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

  const quote = await response.json();

  // quote.txn contains the base64-encoded unsigned transaction
  if (!quote.txn) {
    throw new Error('Quote missing transaction - ensure user_account is provided');
  }

  return quote;
}

// Example: Get quote for 0.1 SOL -> USDC with 50 bps slippage
const quote = await getQuoteWithTransaction(SOL, USDC, 100_000_000, 50);
console.log('Expected output:', quote.destAmountOut);
console.log('Transaction ready:', quote.txn ? 'YES' : 'NO');

Quote Response Structure

{
  "srcMint": "So11111111111111111111111111111111111111112",
  "dstMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
  "srcAmountIn": "100000000",
  "destAmountOut": "18950000",
  "slippageBps": 50,
  "txn": "AQAAAA...base64-encoded-transaction..."
}

Step 2: Deserialize and Sign the Transaction

The txn field contains a Solana VersionedTransaction encoded in base64. Deserialize it, sign with your wallet, then prepare for sending.

import { Connection, Keypair, VersionedTransaction } from '@solana/web3.js';
import bs58 from 'bs58';

const CARBIUM_RPC_URL = `https://rpc-service.carbium.io/?apiKey=${process.env.CARBIUM_RPC_API_KEY}`;
const PRIVATE_KEY = process.env.SOLANA_PRIVATE_KEY;

// Initialize wallet from base58 private key
function initWallet(privateKeyB58) {
  const privateKeyBytes = bs58.decode(privateKeyB58);
  return Keypair.fromSecretKey(privateKeyBytes);
}

// Deserialize and sign the transaction
function signTransaction(txnBase64, wallet) {
  // Decode from base64
  const txnBuffer = Buffer.from(txnBase64, 'base64');

  // Deserialize as VersionedTransaction
  const transaction = VersionedTransaction.deserialize(txnBuffer);

  // Sign with wallet
  transaction.sign([wallet]);

  return transaction;
}

const wallet = initWallet(PRIVATE_KEY);
const signedTx = signTransaction(quote.txn, wallet);

Step 3: Send Transaction via Carbium RPC

Submit the signed transaction through Carbium RPC for optimized routing and fast confirmation.

async function sendTransaction(connection, signedTransaction) {
  const sendStart = Date.now();

  // Send with skipPreflight for maximum speed
  const signature = await connection.sendTransaction(signedTransaction, {
    skipPreflight: true,      // Skip simulation for speed
    maxRetries: 3,
    preflightCommitment: 'processed'
  });

  console.log(`TX sent in ${Date.now() - sendStart}ms: ${signature}`);
  return signature;
}

// Confirm using HTTP polling (more reliable than WebSocket)
async function confirmTransaction(connection, 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 { success: true, status: status.value.confirmationStatus };
    }

    // Poll every 500ms
    await new Promise(resolve => setTimeout(resolve, 500));
  }

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

Complete Example

Here's a full working example putting it all together:

import 'dotenv/config';
import { Connection, Keypair, VersionedTransaction } from '@solana/web3.js';
import { fetch } from 'undici';
import bs58 from 'bs58';

// Configuration
const CARBIUM_API_KEY = process.env.CARBIUM_API_KEY;
const CARBIUM_RPC_URL = `https://rpc-service.carbium.io/?apiKey=${process.env.CARBIUM_RPC_API_KEY}`;
const PRIVATE_KEY = process.env.SOLANA_PRIVATE_KEY;

// Token mints
const TOKENS = {
  SOL: 'So11111111111111111111111111111111111111112',
  USDC: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
  USDT: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'
};

class CarbiumSwapExecutor {
  constructor() {
    this.connection = new Connection(CARBIUM_RPC_URL, 'confirmed');
    this.wallet = Keypair.fromSecretKey(bs58.decode(PRIVATE_KEY));
    this.walletAddress = this.wallet.publicKey.toBase58();
  }

  async getQuoteWithTransaction(srcMint, dstMint, amountIn, slippageBps = 50) {
    const url = new URL('https://api.carbium.io/api/v2/quote');
    url.searchParams.set('src_mint', srcMint);
    url.searchParams.set('dst_mint', dstMint);
    url.searchParams.set('amount_in', String(amountIn));
    url.searchParams.set('slippage_bps', String(slippageBps));
    url.searchParams.set('user_account', this.walletAddress);

    const response = await fetch(url, {
      headers: {
        'X-API-KEY': CARBIUM_API_KEY,
        'Accept': 'application/json'
      }
    });

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

    const quote = await response.json();

    if (!quote.txn) {
      throw new Error('Quote missing transaction');
    }

    return quote;
  }

  signTransaction(txnBase64) {
    const transaction = VersionedTransaction.deserialize(
      Buffer.from(txnBase64, 'base64')
    );
    transaction.sign([this.wallet]);
    return transaction;
  }

  async sendAndConfirm(signedTransaction, timeout = 60000) {
    // Send transaction
    const signature = await this.connection.sendTransaction(signedTransaction, {
      skipPreflight: true,
      maxRetries: 3,
      preflightCommitment: 'processed'
    });

    console.log(`TX sent: ${signature}`);

    // Confirm via HTTP polling
    const startTime = Date.now();

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

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

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

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

    throw new Error('Transaction confirmation timeout');
  }

  async executeSwap(srcMint, dstMint, amountIn, slippageBps = 50) {
    console.log(`Swapping ${amountIn} ${srcMint.slice(0,4)}... -> ${dstMint.slice(0,4)}...`);

    // Step 1: Get quote with transaction
    const quote = await this.getQuoteWithTransaction(srcMint, dstMint, amountIn, slippageBps);
    console.log(`Expected output: ${quote.destAmountOut}`);

    // Step 2: Sign transaction
    const signedTx = this.signTransaction(quote.txn);

    // Step 3: Send and confirm
    const result = await this.sendAndConfirm(signedTx);

    console.log(`Swap complete: ${result.signature}`);
    return result;
  }
}

// Usage
async function main() {
  const executor = new CarbiumSwapExecutor();

  // Swap 0.01 SOL to USDC (10_000_000 lamports = 0.01 SOL)
  const result = await executor.executeSwap(
    TOKENS.SOL,
    TOKENS.USDC,
    10_000_000,  // 0.01 SOL in lamports
    50           // 0.5% slippage
  );

  console.log('Success:', result);
}

main().catch(console.error);

API Reference

Quote Endpoint

GET https://api.carbium.io/api/v2/quote
ParameterRequiredDescription
src_mintYesSource token mint address
dst_mintYesDestination token mint address
amount_inYesInput amount in smallest units (lamports/decimals)
slippage_bpsNoSlippage tolerance in basis points (default: 50)
user_accountYes*Wallet public key (*required to get transaction)

Headers:

X-API-KEY: your-api-key
Accept: application/json

Carbium RPC Endpoint

https://rpc-service.carbium.io/?apiKey=your-rpc-key

Standard Solana JSON-RPC compatible. Optimized for:

  • Low-latency transaction submission
  • Reliable confirmation
  • Geographic routing

Best Practices

Speed Optimization

// Use skipPreflight for faster execution (you already have a valid quote)
const signature = await connection.sendTransaction(signedTx, {
  skipPreflight: true,
  maxRetries: 3
});

Error Handling

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

      if (i === maxRetries - 1) throw error;

      // Wait before retry
      await new Promise(r => setTimeout(r, 1000 * (i + 1)));
    }
  }
}

Rate Limiting

The Carbium API has rate limits. For high-frequency usage, implement a token bucket:

class RateLimiter {
  constructor(maxRPS = 3) {
    this.tokens = maxRPS;
    this.maxTokens = maxRPS;
    this.lastRefill = Date.now();
  }

  async acquire() {
    const now = Date.now();
    const elapsed = (now - this.lastRefill) / 1000;
    this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.maxTokens);
    this.lastRefill = now;

    if (this.tokens >= 1) {
      this.tokens--;
      return;
    }

    const waitMs = ((1 - this.tokens) / this.maxTokens) * 1000;
    await new Promise(r => setTimeout(r, waitMs));
    this.tokens = 0;
  }
}

Common Token Mints

TokenMint Address
SOLSo11111111111111111111111111111111111111112
USDCEPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
USDTEs9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB

Troubleshooting

ErrorCauseSolution
Quote missing transactionuser_account not providedAdd user_account parameter
Transaction failedInsufficient balance or slippageCheck balance, increase slippage
Confirmation timeoutNetwork congestionRetry with higher priority fee
401 UnauthorizedInvalid API keyCheck X-API-KEY header