Solana DEX Arbitrage in One Transaction: How It Works

How Solana arbitrage works in one transaction: buy on one DEX pool, sell on another, the minimum-output floor, the costs, speed and a Rust sketch.

  • October 29, 2026
  • Bots
  • AllenHark Team
A quiet stack of pagesAn abstract drawing of light on the page's dark ground. A neat stack of thin, solid pages, the top one lit, with faint streams of light drifting toward it from the left.A quiet stack of pagesAn abstract drawing of light on the page's dark ground. A neat stack of thin, solid pages, the top one lit, with faint streams of light drifting toward it from the left.

The same token often trades on several Solana pools at once, and their prices don't always match. DEX arbitrage buys the token where it is cheaper and sells it where it is dearer. Done in one transaction, both swaps happen together or not at all.

Below: the atomic two-leg transaction and the minimum-output check that protects it, a worked example with constant-product math, the costs, why speed decides who gets the gap, and a Rust sketch. No profit promises: margins are thin and the competition is real.

What DEX arbitrage on Solana is

A popular token can have a PumpSwap pool, a Raydium CPMM pool and a Meteora pool, each with its own reserves of SOL and the token, and each pricing from its own reserves only. When someone buys a lot on one pool, its price rises and the others stay put. For a moment, the token is cheaper on one pool than on another.

An arbitrage trade closes that gap: buy on the cheaper pool, sell on the dearer one. Your buy pushes the cheap pool's price up, your sell pushes the dear pool's down, and the two meet. The round trip starts and ends in SOL, so the result is counted in SOL, not in a token you are left holding.

Why one transaction: atomic two-leg arbitrage

A Solana transaction is atomic: if any of its instructions fails, the whole transaction fails and none of its changes stick. Put the buy and the sell in one transaction and they land together or not at all.

With two separate transactions, the buy can land while the sell fails, or lands a block later at a worse price, and you hold a token you never wanted. In one transaction, you never hold the token between the legs.

The minimum-output check

A swap instruction takes a minimum output: the least it may return, or it fails. Arbitrage sets the sell leg's minimum output to:

SOL in + every cost + your minimum profit

If the pools are where you priced them, the sell returns more than that and the transaction lands. If someone traded first and the gap is smaller, the sell returns less, the pool's program rejects it, and the whole transaction fails, buy included. You keep your SOL, less the network and priority fee: on Solana, a transaction that lands and fails still pays those.

The buy leg can carry a minimum too, the token amount the sell expects, so a short buy fails on the spot. If your legs can't fit in one transaction, a Jito bundle runs several transactions all-or-nothing instead: see how to send Jito bundles on Solana.

A worked example (illustrative numbers)

These numbers are illustrative, made up to show the math. They are not a real trade, real pools or an expected profit.

A constant-product pool holds reserves x and y with x · y = k. The fee comes off the input first, then the invariant sets the output, as Raydium's constant-product docs write it. For an amount a in, a reserve x of what you put in, a reserve y of what you take out and a fee f:

out = y × a × (1 − f) / (x + a × (1 − f))

Two pools of the same token, each with a 0.25 % fee:

Pool A (cheaper)Pool B (dearer)
SOL reserve1,000 SOL1,020 SOL
Token reserve100,000,000 tokens100,000,000 tokens
Price10 SOL per million tokens10.2 SOL per million tokens
Pool fee0.25 %0.25 %

Pool B's price is 2 % higher. Here is a 1 SOL round trip:

  1. Buy on pool A. The fee is 0.0025 SOL, so 0.9975 SOL goes into the formula: 100,000,000 × 0.9975 / (1,000 + 0.9975) = 99,650.60 tokens.
  2. Sell on pool B. The fee is 0.25 % of those tokens, so 99,401.47 go into the formula: 1,020 × 99,401.47 / (100,000,000 + 99,401.47) = 1.012888 SOL.
  3. Before transaction costs, that is 0.012888 SOL more than went in. Without the pool fees it would have been 0.017964 SOL.
  4. Transaction costs. Say the network and priority fee is 0.000105 SOL and the relay tip 0.001 SOL (both illustrative): 0.001105 SOL in all. That leaves 0.011783 SOL.

With a minimum profit of 0.002 SOL, the sell's floor is 1 + 0.001105 + 0.002 = 1.003105 SOL. The quote, 1.012888 SOL, clears it, so the round trip is worth sending.

Now suppose someone sells 1,000,000 tokens into pool B just before your transaction lands. Pool B's price falls to about 9.999 SOL per million, below pool A's. Your sell would return about 0.992965 SOL, under the floor, so the transaction fails. The buy and the tip transfer are undone, and you pay only the 0.000105 SOL network and priority fee.

Why bigger isn't better: price impact and the best size

Every SOL you put in moves both prices, so the gap shrinks as the trade grows. The same pools at different sizes:

SOL inSOL backBefore transaction costsAfter 0.001105 SOL of transaction costs
11.012888+0.012888+0.011783
22.021756+0.021756+0.020651
44.027526+0.027526+0.026421
66.017499+0.017499+0.016394
87.991860−0.008140−0.009245
109.950794−0.049206−0.050311

The best size here is about 3.7 SOL, for about 0.0277 SOL before transaction costs. There, pool A ends at about 10.074 and pool B at about 10.125 SOL per million tokens: the gap left, about 0.5 %, is what the two pool fees take, so one more SOL would lose. At 8 SOL the round trip loses money.

The size of the gap is everything. With a 1 % gap (pool B at 1,010 SOL), the best size drops to about 1.24 SOL and the most it makes before transaction costs to about 0.0031 SOL, and the same 0.001105 SOL of transaction costs takes more than a third of that.

Real pools vary. PumpSwap's docs call it a constant-product AMM that prices against effective quote reserves: the vault balance plus a virtual amount. Raydium CPMM and AMM v4 are constant product; AMM v4's fee is 0.25 %, and each CPMM pool picks a fee tier. Meteora DLMM holds liquidity in price bins, and a swap inside the active bin trades at that bin's fixed price, so its math differs. Read each pool's fee and state on chain.

The costs that eat the edge

CostWhat it is
Pool feesEach pool takes its fee on its leg, so a round trip pays two. A gap smaller than both fees together is no gap.
Priority feeYour compute-unit price times the compute-unit limit you ask for. Paid whether the transaction succeeds or fails.
Relay tipA transfer inside the transaction to the relay that sends it; AllenHark Relay's page gives its current minimum. Paid only when the transaction succeeds.
Failed transactionsA transaction that lands and fails still pays its network and priority fee.
Other searchersMany bots see the same gap. One closes it; the rest fail on their minimum output and pay their fees.
Stale pricesA quote made from old pool state can be wrong by the time it lands.

Failed transactions are normal in arbitrage. What counts is whether the round trips that land pay for all the ones that don't.

Speed: see the update first, land in the same block

A gap opens when a trade moves one pool and closes when the first arbitrage lands. A Solana slot is about 200 ms today, and each slot's leader decides what goes in its block. Every searcher who saw the gap races for the same moment; once one round trip lands, the others fail on their minimum output.

Speed has two halves:

Speed doesn't make a gap bigger. A gap smaller than its costs is still a loss, however fast you are.

A Rust sketch: amount out and the two minimums

If you're building a Solana arbitrage bot in Rust, this is the core math. It is a sketch, not the AllenHark bot's code. It uses integers (lamports and token base units) and u128 for the products, so nothing overflows.

Rust
// A sketch to show the math. Not the AllenHark bot's code.

/// A constant-product pool: SOL reserve in lamports, token reserve in base units.
#[derive(Clone, Copy)]
struct Pool {
    sol: u64,
    token: u64,
    fee_bps: u64, // 25 = 0.25 %, taken from the input
}

/// Constant-product output for `amount_in`, after a fee in basis points:
/// out = reserve_out * in_after_fee / (reserve_in + in_after_fee), rounded down.
fn amount_out(amount_in: u64, reserve_in: u64, reserve_out: u64, fee_bps: u64) -> Option<u64> {
    if amount_in == 0 || reserve_in == 0 || reserve_out == 0 || fee_bps >= 10_000 {
        return None;
    }
    // u128: a product of two u64 values always fits.
    let in_after_fee = amount_in as u128 * (10_000 - fee_bps) as u128 / 10_000;
    let out = in_after_fee * reserve_out as u128 / (reserve_in as u128 + in_after_fee);
    // out is always below reserve_out, so it fits back in a u64.
    Some(out as u64)
}

/// What the transaction carries: the SOL in and each leg's minimum output.
struct Plan {
    sol_in: u64,
    min_tokens: u64,  // leg 1 (buy) must return at least this
    min_sol_out: u64, // leg 2 (sell) must return at least this
}

/// Price a round trip: buy on `cheap`, sell on `dear`. None if it can't clear the floor.
fn plan(sol_in: u64, cheap: &Pool, dear: &Pool, costs: u64, min_profit: u64) -> Option<Plan> {
    let tokens = amount_out(sol_in, cheap.sol, cheap.token, cheap.fee_bps)?;
    let sol_out = amount_out(tokens, dear.token, dear.sol, dear.fee_bps)?;
    // The floor: SOL in + every cost + your minimum profit.
    let floor = sol_in.checked_add(costs)?.checked_add(min_profit)?;
    (sol_out >= floor).then_some(Plan { sol_in, min_tokens: tokens, min_sol_out: floor })
}

/// What the chain does when it lands, against the pools as they are then.
/// Either leg short of its minimum fails the whole transaction.
fn lands(p: &Plan, cheap: &Pool, dear: &Pool) -> bool {
    let tokens = amount_out(p.sol_in, cheap.sol, cheap.token, cheap.fee_bps).unwrap_or(0);
    if tokens < p.min_tokens {
        return false; // leg 1 short
    }
    let sol_out = amount_out(p.min_tokens, dear.token, dear.sol, dear.fee_bps).unwrap_or(0);
    sol_out >= p.min_sol_out // false: leg 2 short
}

fn main() {
    const SOL: u64 = 1_000_000_000; // lamports
    const TOK: u64 = 1_000_000; // a token with 6 decimals
    // The illustrative pools from the worked example.
    let a = Pool { sol: 1_000 * SOL, token: 100_000_000 * TOK, fee_bps: 25 };
    let b = Pool { sol: 1_020 * SOL, token: 100_000_000 * TOK, fee_bps: 25 };
    let costs = 105_000 + 1_000_000; // network and priority fee, relay tip
    let min_profit = 2_000_000; // 0.002 SOL

    let Some(p) = plan(SOL, &a, &b, costs, min_profit) else {
        println!("skip: under the floor");
        return;
    };
    println!("send: min tokens {}, min SOL out {}", p.min_tokens, p.min_sol_out);
    println!("lands on the pools as priced: {}", lands(&p, &a, &b));

    // Someone sells 1,000,000 tokens into pool B first, and its price drops.
    let sold = 1_000_000 * TOK;
    let taken = amount_out(sold, b.token, b.sol, b.fee_bps).unwrap();
    let b_after = Pool { sol: b.sol - taken, token: b.token + sold, ..b };
    println!("lands after that sell: {}", lands(&p, &a, &b_after));
}

Run it and it prints the plan (at least 99,650,598,527 token base units, at least 1,003,105,000 lamports), then true for the pools as priced and false after the sell into pool B: the second leg comes up short, so the whole round trip fails.

  • Rounding. The amount after the fee rounds down, so the fee rounds up, and the output rounds down, as Raydium's docs describe. Check each program's own rounding.
  • Overflow. Products are in u128 and the floor uses checked_add, so bad inputs return None instead of wrapping.
  • What's missing. A real bot also reads each venue's own state (fee tiers, protocol and creator fees, PumpSwap's virtual reserves, DLMM bins, Token-2022 transfer fees), builds each program's swap instruction, sets a compute budget and simulates before sending.

How the AllenHark Solana Arbitrage Bot does it

The Solana Arbitrage Bot buys your token on one pool and sells it on another in one transaction, back to SOL, and like every AllenHark bot it detects in 0.2 ms. Its guide covers every step; in short:

  • Your watchlist. It trades only the tokens you add by CA, each against SOL; the list holds 100 by default.
  • Pools it finds itself. It learns each token's pools from the token's own transactions and checks each on chain before use. It trades PumpSwap, Raydium CPMM, Raydium AMM v4, Meteora DAMM v1, Meteora DAMM v2 and Meteora DLMM, not Orca or Raydium CLMM.
  • Routes through other tokens. A pool that prices your token in PUMP or USDC is traded through one of that token's SOL pools in the same transaction, so it still starts and ends in SOL.
  • Every cost counted, your minimum written in. It prices each round trip after both pools' fees, the network and priority fee, the relay tip and any token-account rent kept, sizes it between your smallest trade and your per-trade cap, and skips pools whose state is too old. The sell's floor is the SOL in plus every cost plus your minimum profit, and every live round trip is simulated first.
  • Tips only when worth it. A round trip pays the relay tip only when it would make more than the tip plus your minimum profit. A smaller one goes tip-free through AllenHark Relay if your deployment's relay key sends tip-free, or is skipped.
  • No token held, no sandwiching. The token account a trade opens is closed in the same transaction, so there are no open positions or TP/SL. It never trades ahead of anyone's pending trade.
  • Tokens it refuses. Token-2022 mints with a transfer fee or transfer hook, or that are non-transferable, pausable, or make new accounts frozen by default.
  • Limits. A per-trade cap, daily caps on fees and tips and on failed trades' fees, trades per minute, and a stop after a run of failed trades.

It runs on your own server, and the wallet key stays there. A new bot starts in practice: it reads real pools, records what each round trip would have returned with virtual SOL, and sends nothing. Practice results are not indicative of live results, because practice can't see the other bots chasing the same gaps. Going live takes SOL in the bot wallet and your PIN.

Plans are monthly: see the bot's page for the current monthly price, the settings reference for every setting, and all AllenHark bots. Many round trips fail and failed trades still pay fees; read the risk disclosure before you add SOL.

FAQ

Is Solana arbitrage still profitable?

Sometimes, and often not. Margins are thin and competition is heavy: most gaps are smaller than their costs, many bots chase the rest, and failed round trips still pay fees. Nobody can promise a profit, and AllenHark doesn't. Practise first, set daily caps, and judge by your PnL after every fee and tip, failed trades included.

What happens if the price moves before my transaction lands?

The sell returns less than its minimum output, so the whole transaction fails and the buy is undone. You keep your SOL, less the network and priority fee, and never end up holding the token.

Do I need Rust to run a Solana arbitrage bot?

Not to run one: the AllenHark bot is set up from the app, with no code to write. To build your own, Rust is a common choice, since most Solana on-chain programs and much of its tooling are written in it, but the math works the same in any language.

Which pools does the AllenHark Solana Arbitrage Bot trade?

PumpSwap, Raydium CPMM, Raydium AMM v4, Meteora DAMM v1, Meteora DAMM v2 and Meteora DLMM, with routes through PUMP or USDC when a pool prices your token in one of them. Not Orca or Raydium CLMM.

Is arbitrage the same as sandwiching?

No. Arbitrage trades two pools against each other after a price has moved. A sandwich trades ahead of someone's pending trade and against it. The AllenHark bot never trades ahead of anyone's pending trade.