Building Arbitrage Bots on Solana
Architecture, implementation, and optimization strategies for cross-DEX arbitrage on Solana.
Arbitrage on Solana
Arbitrage — profiting from price differences across markets — is one of the most technically demanding and competitive applications on Solana. The combination of fast block times, low fees, and a liquid DEX ecosystem creates abundant arbitrage opportunities, but the competition is fierce. Successful arbitrage bots require sub-millisecond decision making, optimal transaction construction, and sophisticated infrastructure.
Types of Arbitrage
| Type | Description | Complexity | Capital Required |
|---|---|---|---|
| Simple DEX arbitrage | Buy on DEX A, sell on DEX B | Low | Medium |
| Triangular arbitrage | A→B→C→A cycle | Medium | Medium |
| Flash loan arbitrage | Borrow, arb, repay in one tx | High | None |
| CEX-DEX arbitrage | Exploit CEX/DEX price gaps | Very High | High |
| Liquidation arbitrage | Liquidate undercollateralized positions | High | High |
Architecture Overview
A production arbitrage bot consists of several components working in concert. The price monitor tracks prices across all relevant DEXes in real time using gRPC streaming. The opportunity detector calculates potential profits from price differences, accounting for fees and slippage. The transaction builder constructs optimized transactions. The executor submits transactions with appropriate priority fees.
Price Monitoring with gRPC
import Client, { CommitmentLevel } from "@triton-one/yellowstone-grpc";
const RAYDIUM_AMM = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8";
const ORCA_WHIRLPOOL = "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc";
interface PoolState {
address: string;
tokenA: string;
tokenB: string;
reserveA: bigint;
reserveB: bigint;
lastUpdate: number;
}
const pools = new Map<string, PoolState>();
async function startPriceMonitor(grpcEndpoint: string, token: string) {
const client = new Client(grpcEndpoint, token, {});
const stream = await client.subscribe();
stream.write({
accounts: {
poolAccounts: {
owner: [RAYDIUM_AMM, ORCA_WHIRLPOOL],
filters: [],
},
},
commitment: CommitmentLevel.PROCESSED,
});
stream.on("data", (data) => {
if (data.account) {
const update = data.account.account;
const poolAddress = update?.pubkey?.toString();
if (poolAddress) {
const state = parsePoolState(update.data);
pools.set(poolAddress, state);
checkArbitrageOpportunity(poolAddress, state);
}
}
});
}
function checkArbitrageOpportunity(updatedPool: string, state: PoolState) {
// Find pools with the same token pair
for (const [address, pool] of pools) {
if (address === updatedPool) continue;
if (pool.tokenA === state.tokenA && pool.tokenB === state.tokenB) {
const priceA = Number(state.reserveB) / Number(state.reserveA);
const priceB = Number(pool.reserveB) / Number(pool.reserveA);
const spread = Math.abs(priceA - priceB) / Math.min(priceA, priceB);
if (spread > 0.003) { // 0.3% minimum spread after fees
executeArbitrage(updatedPool, address, state, pool, spread);
}
}
}
}Jupiter Integration for Optimal Routing
import { createJupiterApiClient } from "@jup-ag/api";
const jupiterApi = createJupiterApiClient();
async function getArbitrageRoute(
inputMint: string,
outputMint: string,
amount: number
) {
// Get quote for forward swap
const forwardQuote = await jupiterApi.quoteGet({
inputMint,
outputMint,
amount,
slippageBps: 50, // 0.5% slippage
onlyDirectRoutes: false,
});
// Get quote for reverse swap
const reverseQuote = await jupiterApi.quoteGet({
inputMint: outputMint,
outputMint: inputMint,
amount: Number(forwardQuote.outAmount),
slippageBps: 50,
onlyDirectRoutes: false,
});
const profit = Number(reverseQuote.outAmount) - amount;
const profitBps = (profit / amount) * 10000;
return { forwardQuote, reverseQuote, profit, profitBps };
}Priority Fees and Transaction Optimization
In competitive arbitrage, transaction inclusion speed is critical. Solana's priority fee mechanism allows transactions to pay more to jump ahead in the queue. Setting the right priority fee is a balance: too low and your transaction may be delayed or dropped; too high and you erode your profit margin.
import { ComputeBudgetProgram, TransactionMessage, VersionedTransaction } from "@solana/web3.js";
async function buildOptimizedTransaction(
instructions: TransactionInstruction[],
payer: PublicKey,
connection: Connection
) {
// Get recent priority fees for similar transactions
const recentFees = await connection.getRecentPrioritizationFees({
lockedWritableAccounts: instructions.flatMap(ix =>
ix.keys.filter(k => k.isWritable).map(k => k.pubkey)
),
});
// Use 75th percentile fee for competitive inclusion
const sortedFees = recentFees.map(f => f.prioritizationFee).sort((a, b) => a - b);
const p75Fee = sortedFees[Math.floor(sortedFees.length * 0.75)] || 1000;
const computeBudgetIx = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: p75Fee,
});
const computeLimitIx = ComputeBudgetProgram.setComputeUnitLimit({
units: 200_000, // Estimate compute units needed
});
const { blockhash } = await connection.getLatestBlockhash('processed');
const message = new TransactionMessage({
payerKey: payer,
recentBlockhash: blockhash,
instructions: [computeBudgetIx, computeLimitIx, ...instructions],
}).compileToV0Message();
return new VersionedTransaction(message);
}Jito Bundles for Atomic Execution
Jito's block engine allows submitting bundles of transactions that execute atomically — either all succeed or all fail. This is essential for arbitrage strategies that require multiple transactions to be profitable. Jito also provides MEV protection and allows tipping validators for priority inclusion.
Risk Management
Arbitrage bots face several risks beyond technical execution. Slippage can eliminate profits if pool state changes between opportunity detection and execution. Failed transactions still consume priority fees. Network congestion can delay execution. Implement strict profit thresholds, maximum position sizes, and circuit breakers to protect capital.