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:
| Field | Use in this guide |
|---|---|
srcAmountIn | Confirms the raw input amount used for the quote |
destAmountOut | Feeds the next leg and final comparison |
destAmountOutMin | Shows the minimum output after slippage |
priceImpactPct | Helps reject obviously poor routes |
routePlan | Shows 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 field | Why it matters |
|---|---|
| input mint, output mint, and raw amount | Reproduces the exact quote path |
destAmountOut from each leg | Shows where the apparent edge came from |
routePlan from each leg | Identifies which venues or route split Q1 selected |
priceImpactPct | Helps reject routes that look profitable only because the trade is too large |
| response time per quote | Separates route quality from latency problems |
| timestamp and app request ID | Lets 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:
- Add
user_accountto the Q1 request only when you need an executabletxn. - Deserialize and sign the returned transaction on the approved signer boundary.
- Submit through Carbium RPC.
- 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_bpsvalues 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.
Updated 27 days ago
