Build a Solana Arbitrage Quote Engine

Build a Solana quote scanner that checks round-trip routes with Carbium Q1 before you decide whether execution is worth wiring in.

Build a Solana Arbitrage Quote Engine

Arbitrage starts as a measurement problem, not an execution problem.

Before you sign transactions, tune retries, or add private routing, you need a loop that can ask a simple question consistently:

If I quote SOL -> USDC -> SOL right now, does the round trip return more raw SOL than it started with?

This page builds that quote scanner with Carbium Q1. It stays intentionally on the quote side of the workflow. When you are ready to turn a candidate into a signed transaction, move to Quote to Swap Integration Guide.

Part of the Carbium Solana infrastructure stack.


What this page owns

Use this page when you want to:

  • request two current Q1 quotes from https://api.carbium.io/api/v2/quote
  • compare raw output amounts with integer-safe arithmetic
  • keep quote scanning separate from signing and submission
  • decide whether a route is worth deeper investigation

This page does not promise profitable execution. Quotes move, fees matter, and a profitable quote pair can disappear before a transaction lands. For production bot architecture, use Carbium for Trading Bots.


The quote loop

The scanner uses a two-leg round trip:

flowchart TD
    A["Start with SOL<br/>raw lamports"] --> B["Q1 quote<br/>SOL -> USDC"]
    B --> C["Use destAmountOut<br/>as leg 2 input"]
    C --> D["Q1 quote<br/>USDC -> SOL"]
    D --> E{"Final SOL > start SOL?"}
    E -->|Yes| F["Candidate opportunity<br/>investigate execution"]
    E -->|No| G["No candidate<br/>keep scanning or stop"]

Q1 responses use fields such as:

FieldUse in this guide
srcAmountInConfirms the raw input amount used for the quote
destAmountOutFeeds the next leg and final comparison
destAmountOutMinShows the minimum output after slippage
priceImpactPctHelps reject obviously poor routes
routePlanShows which route Q1 selected

For the full request and response shape, use Q1.


Prerequisites

  • a Carbium Swap API key from carbium.io
  • Node.js 18+ for the TypeScript example, or Python 3.10+ for the Python example
  • a dedicated environment variable named CARBIUM_API_KEY
export CARBIUM_API_KEY="YOUR_API_KEY"

The examples use quote-only requests. They do not include user_account, so they do not request an executable transaction.


TypeScript quote scanner

This example avoids floating-point math for the quote comparison. Token amounts arrive as raw integer strings, so the scanner compares them with BigInt.

const QUOTE_URL = "https://api.carbium.io/api/v2/quote";

const SOL_MINT = "So11111111111111111111111111111111111111112";
const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";

type CarbiumQuote = {
  srcAmountIn?: string;
  destAmountOut?: string;
  destAmountOutMin?: string;
  priceImpactPct?: string;
  routePlan?: Array<{ swap?: string; percent?: number }>;
};

async function getQuote(params: {
  srcMint: string;
  dstMint: string;
  amountIn: bigint;
  slippageBps?: number;
}): Promise<CarbiumQuote> {
  const url = new URL(QUOTE_URL);
  url.searchParams.set("src_mint", params.srcMint);
  url.searchParams.set("dst_mint", params.dstMint);
  url.searchParams.set("amount_in", params.amountIn.toString());
  url.searchParams.set("slippage_bps", String(params.slippageBps ?? 10));

  const response = await fetch(url, {
    headers: {
      "X-API-KEY": process.env.CARBIUM_API_KEY ?? "",
      "Accept": "application/json",
    },
  });

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

  const quote = (await response.json()) as CarbiumQuote;

  if (!quote.destAmountOut) {
    throw new Error("Quote response did not include destAmountOut");
  }

  return quote;
}

async function checkRoundTrip(startLamports: bigint) {
  const leg1 = await getQuote({
    srcMint: SOL_MINT,
    dstMint: USDC_MINT,
    amountIn: startLamports,
    slippageBps: 10,
  });

  const usdcOut = BigInt(leg1.destAmountOut!);

  const leg2 = await getQuote({
    srcMint: USDC_MINT,
    dstMint: SOL_MINT,
    amountIn: usdcOut,
    slippageBps: 10,
  });

  const finalLamports = BigInt(leg2.destAmountOut!);
  const profitLamports = finalLamports - startLamports;

  return {
    startLamports: startLamports.toString(),
    finalLamports: finalLamports.toString(),
    profitLamports: profitLamports.toString(),
    profitableBeforeFees: profitLamports > 0n,
    leg1Route: leg1.routePlan,
    leg2Route: leg2.routePlan,
  };
}

checkRoundTrip(1_000_000_000n)
  .then((result) => console.log(JSON.stringify(result, null, 2)))
  .catch((error) => {
    console.error(error);
    process.exitCode = 1;
  });

If this prints profitableBeforeFees: true, treat it as a candidate signal only. You still need to account for transaction fees, execution latency, slippage, confirmation risk, and duplicate-send controls before using capital.


Python quote scanner

The Python version uses int for raw amounts and keeps the same quote-only boundary.

import os
import requests

QUOTE_URL = "https://api.carbium.io/api/v2/quote"

SOL_MINT = "So11111111111111111111111111111111111111112"
USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"


def get_quote(src_mint: str, dst_mint: str, amount_in: int, slippage_bps: int = 10):
    response = requests.get(
        QUOTE_URL,
        params={
            "src_mint": src_mint,
            "dst_mint": dst_mint,
            "amount_in": str(amount_in),
            "slippage_bps": str(slippage_bps),
        },
        headers={
            "X-API-KEY": os.environ["CARBIUM_API_KEY"],
            "Accept": "application/json",
        },
        timeout=10,
    )
    response.raise_for_status()
    quote = response.json()

    if "destAmountOut" not in quote:
        raise RuntimeError("Quote response did not include destAmountOut")

    return quote


def check_round_trip(start_lamports: int):
    leg1 = get_quote(SOL_MINT, USDC_MINT, start_lamports)
    usdc_out = int(leg1["destAmountOut"])

    leg2 = get_quote(USDC_MINT, SOL_MINT, usdc_out)
    final_lamports = int(leg2["destAmountOut"])

    profit_lamports = final_lamports - start_lamports

    return {
        "startLamports": start_lamports,
        "finalLamports": final_lamports,
        "profitLamports": profit_lamports,
        "profitableBeforeFees": profit_lamports > 0,
        "leg1Route": leg1.get("routePlan"),
        "leg2Route": leg2.get("routePlan"),
    }


if __name__ == "__main__":
    print(check_round_trip(1_000_000_000))

Make the scanner useful before adding execution

A round-trip scanner becomes more useful when it records enough context to explain why a candidate appeared.

Log fieldWhy it matters
input mint, output mint, and raw amountReproduces the exact quote path
destAmountOut from each legShows where the apparent edge came from
routePlan from each legIdentifies which venues or route split Q1 selected
priceImpactPctHelps reject routes that look profitable only because the trade is too large
response time per quoteSeparates route quality from latency problems
timestamp and app request IDLets you correlate scanner logs with later execution attempts

Do not skip this layer. Without it, a bot team can see a green signal and still have no idea whether the edge came from route selection, stale local assumptions, or a transient market move.


When to move from quotes to execution

Move to execution only after the scanner has shown repeated, explainable candidate routes and your app has a safe signing boundary.

Use this handoff:

  1. Add user_account to the Q1 request only when you need an executable txn.
  2. Deserialize and sign the returned transaction on the approved signer boundary.
  3. Submit through Carbium RPC.
  4. Confirm the signature before rebuilding or replaying the trade.

That workflow belongs in Quote to Swap Integration Guide. Keep this page focused on finding candidate routes.

📘

For automated strategies, keep scanner keys, execution keys, and signing material separate. The scanner only needs the Swap API key. Execution usually also needs an RPC key and a signer.


Guardrails for arbitrage builders

  • compare raw integer amounts, not rounded UI token values
  • use tight but realistic slippage_bps values for the strategy you are testing
  • reject candidates that depend on a route you cannot explain from routePlan
  • log no-route and auth failures separately from unprofitable quotes
  • never treat a quote-only profit as a landed trade
  • check confirmation state before any retry after submission
🔶

Use Carbium Q1 to scan the route, then use the execution guides only when the signal is strong enough to justify signing. For product access and setup, start at carbium.io.