V10 Instruction
Current V10 instruction of the SolanaMevBot onchain program, with bridge pools and bridge routes
The onchain program takes in mints and pools, and will calculate the most optimal arbitrage trade between them and execute the trades.
If you want maximum flexibility, you can write your own bot and just call the onchain program from there. This allow you to do whatever customization you want but still utilize the onchain program to get maximum arb opportunity.
Demo bot: https://github.com/Cetipoo/solana-onchain-arbitrage-bot
The program id is MEViEnscUm6tsQRoGd9h6nLQaQspKj7DB2M5FwM3Xvz. This page documents the current V10 instruction (opcode 61). It keeps everything V9 does and adds bridge pools, so the program can arbitrage a token that has no deep pool directly against SOL or USDC by routing through a second token.
The previous V9 instruction (opcode 28) is unchanged and keeps working. Existing integrations do not need to migrate.
NOTE: It's recommended to include this address lookup table as it has all the fixed keys needed: 4sKLJ1Qoudh8PJyqBeuKocYdsZvxTcRShUt9aKqwhgvC
NOTE: V10 requires an explicit compute unit limit in the instruction data between 1 and 1_400_000. Zero is rejected. A three-swap route typically needs at least 400_000 and a four-swap route with a conversion pool at least 500_000. Concentrated pools (CLMM, Whirlpool, DLMM) and Pump pools need more than AMM pools.
You can still check https://github.com/Cetipoo/solana-onchain-arbitrage-bot to see how to extract the keys for each pool type. The per-pool key sets are the same as V9; only the ordering and the header differ.
Current supported DEX
- Raydium
- Raydium CPMM
- Raydium CLMM
- Pumpfun Swap
- Meteora DLMM
- Meteora Dynamic Pool (DAMM V1)
- Meteora DAMM V2
- Orca whirlpool
- Pancake Swap
- Byreal
Every DEX above is accepted in every pool role: intermediate, bridge, direct and conversion. A full four-hop route such as S → T → B → Q → S can use any of them on any hop. Some DEXes only accept certain pools, see DEX requirements below.
The bridge pool concept
V9 works on one pair: a target token X and a base mint (SOL or USDC). Every pool you pass is X/base, and the program picks the best buy pool and sell pool between them.
V10 works on a group of three mints:
| Mint | Meaning |
|---|---|
| S (settlement) | The mint you start and end with. Must be WSOL or USDC. This is where profit is measured and fees are taken. |
| T (target) | The token you are arbitraging. Any SPL Token or Token-2022 mint. |
| B (bridge) | A second token that T is paired with. Any SPL Token or Token-2022 mint. B must differ from T and from S. |
And three pool roles:
| Role | Pair | Count per group | What it does |
|---|---|---|---|
| Intermediate | T/B | exactly 1 | The pool that connects the target to the bridge token. |
| Bridge | B/S | 1 or more | Pools that connect the bridge token back to settlement. |
| Direct | T/S | 1 or more | Pools that connect the target straight to settlement. Same as the pools you would give V9. |
From these the program considers three routes and executes the most profitable one:
S → T → S(direct buy, direct sell: identical to V9)S → T → B → S(buy T directly, sell T for B on the intermediate, sell B on a bridge)S → B → T → S(buy B on a bridge, buy T on the intermediate, sell T directly)
Only one route is executed per instruction. The same pool is never used twice in one route.
A common use: T is a meme coin whose main liquidity is against another token B (for example a T/USDC Pump pool with a USDC/SOL bridge, or a T/PUMP pool with PUMP/SOL bridges), while a thinner T/SOL pool exists on another DEX.
A real example
This mainnet transaction (fGgz…kor1) was sent with one V10 group and a conversion pool:
| Role | Mint / pool |
|---|---|
| S (settlement) | SOL |
| T (target) | Tulip 7vSG4GX8qz5V36noSde5Z9xV8xAXAGqivDyaNytPVDJf |
| B (bridge) | FLWS FLWSojG1gB5VStYR3Sb4nQFRt43UBYkqih1j2CpVLqgd |
| Q (other settlement, see conversion pool below) | USDC |
| Intermediate T/B | Raydium CPMM r9cBueKD9ZLzN2kXyNCzpX5iJmYLw5fWhDzCtbDCz6K (Tulip/FLWS) |
| Bridge B/Q | Raydium CLMM 62sCoQRHneRNWeDXvDvTz7s5uTNgBZD9JE3389DVZ9if (FLWS/USDC) |
| Direct T/S | Meteora DLMM 6DkVy4DPa3xPfksEnRHCtVoyvtRFNDYjerDSWnMKrhhR (Tulip/SOL) |
| Conversion Q/S | Raydium AMM 58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2 (SOL/USDC) |
Tulip has no deep SOL pool, but it trades against FLWS, and FLWS trades against USDC. The instruction offered both the plain SOL → Tulip → SOL round trip on the DLMM pool and the bridge routes. The executed route was SOL → Tulip → FLWS → USDC → SOL:
- Buy Tulip with SOL on the direct DLMM pool.
- Sell Tulip for FLWS on the intermediate CPMM pool. Tulip is a Token-2022 mint with a transfer fee, which is why slightly fewer Tulip arrive than were bought.
- Sell FLWS for USDC on the bridge CLMM pool.
- Convert USDC back to SOL on the conversion Raydium pool.
All four legs are one instruction. Three DLMM Tulip/SOL pools were supplied as direct candidates; 6DkV…rhhR served as the buy leg, while the sell side went through the bridge. The exact accounts and bytes are in the worked examples.
Optional conversion pool (SOL ↔ USDC)
In V9, supplying a USDC pool with a SOL base meant appending the fixed Raydium SOL/USDC pool. V10 makes this explicit.
If your bridge pools quote the other settlement mint (call it Q), you add one conversion pool Q/S, on any supported DEX, after the common accounts and set bit 0 of the flags byte. Bridges then become B/Q and the bridge routes get a fourth leg:
S → T → B → Q → SS → Q → B → T → S
Direct S → T → S still competes. T and B must also differ from Q. Setting bit 1 (constant_conversion) prices the conversion leg at the pool's current rate without accounting for slippage, which saves compute. Use it only when your trade size is small relative to the conversion pool. Bit 1 requires bit 0.
For the simple case SOL → USDC → T → SOL you do not need a conversion pool at all: make B = USDC and use a SOL/USDC pool such as Raydium 58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2 as the bridge.
Multiple groups
One instruction can carry up to 4 groups, each with its own T, B, intermediate, bridges and direct pools. Only one route across all groups is executed. All groups share the same S and, if present, the same conversion pool.
Direct-only groups
A group with bridge_count = 0 has no B mint and no intermediate pool. It is the V9 shape inside V10: at least two T/S pools, and the program picks the best buy and sell pair among them. With a conversion pool present, a direct-only group may also include T/Q pools (at least one pool must still be T/S), which gives S → T → Q → S and S → Q → T → S routes.
The account layout for a direct-only group is only the T mint accounts followed by its pool blocks. Direct-only and bridge groups cannot be mixed in the same instruction.
DEX requirements
Every supported DEX is accepted in every role, the conversion pool included. Some DEXes only accept certain pools:
- Raydium: the pool must be in swap-only status (status
6) and both mints must be SPL Token. Token-2022 Raydium pools are rejected. - Meteora Dynamic Pool: only enabled constant-product pools with SPL Token mints. Stable-curve pools are rejected.
- Pancake Swap and Byreal: same account block as Raydium CLMM, with their own program id.
The other DEXes listed on the V9 page (Solfi, Humidifi, Vertigo, Heaven, Futarchy) are not accepted by V10.
Limits
- At most 4 groups and 16 pool declarations per instruction (the conversion pool counts as one).
- Between 1 and 6 tick array / bin array accounts per concentrated pool (CLMM, Whirlpool, DLMM).
- Token-2022 mints with transfer fees are supported. Transfer hooks and confidential transfers are rejected.
- The transaction still has to fit 1232 bytes and 64 account locks. Use address lookup tables or fewer candidates.
Instruction data
The data starts with opcode 61. Offsets below are relative to the byte after the opcode. The first 24 bytes are identical to V9 (including the fixed trade size field), followed by the new V10 tail. Amounts are in base units of the settlement mint: lamports for WSOL, 1e-6 for USDC.
| Offset | Field |
|---|---|
| 0 | minimum_profit: u64, the trade must net more than this |
| 8 | compute_unit_limit: u32, must be 1..=1_400_000 |
| 12 | no_failure: u8, 1 = return success without trading when no route clears minimum_profit, instead of failing the transaction |
| 13 | additional_fee_bp: u16, maximum 8500 |
| 15 | use_flashloan: u8 |
| 16 | trade_size: u64, 0 enables automatic sizing |
| 24 | flags: u8. Bit 0: a conversion pool is present. Bit 1: constant-price conversion (requires bit 0). Other bits must be zero |
| 25 | group_count: u8, 1..=4 |
| 26 onward | If bit 0 is set: conversion_account_count: u16. Then per group: bridge_count: u8, direct_count: u8, then one u16 account count for each pool in order: intermediate, bridges, directs. A group with bridge_count = 0 is direct-only: it has no intermediate, direct_count must be at least 2, and the counts cover only its direct pools. Each count is the number of accounts in a pool block, including its program id and quote mint |
Counts are per pool because the same DEX can have a variable number of accounts (memo program, bitmap extension, tick arrays, Pump cashback accounts).
Account layout
0. Wallet (signer, writable)
1. S mint (WSOL or USDC)
2. Fee collector (writable)
Additional fee collector (writable) -- only when additional_fee_bp > 0
3. Wallet S token account (writable)
4. Token program
5. System program
6. Associated Token program
Flashloan vault authority -- only when use_flashloan
Flashloan vault token account (writable) -- only when use_flashloan
Q mint, Q token program, wallet Q account -- only when flags bit 0
Conversion pool block (Q/S) -- only when flags bit 0
For each bridge group:
T mint, T token program, wallet T account (writable)
B mint, B token program, wallet B account (writable)
Intermediate pool block (T/B)
Bridge pool block(s) (B/Q, or B/S without conversion)
Direct pool block(s) (T/S)
For each direct-only group (bridge_count = 0):
T mint, T token program, wallet T account (writable)
Direct pool block(s) (T/S, or T/Q when a conversion pool is present)Every pool block starts with the DEX program id and then the quote mint of that block. This replaces the "V9: Base mint" account: it is B for the intermediate block, Q (or S without conversion) for bridge blocks, and S for direct and conversion blocks. A pool may store the pair in either order on-chain. Always list its vaults in x/base order as described in the code below, regardless of the pool's own mint order.
The wallet T and B token accounts are created idempotently if missing. The wallet S account must already exist and be funded (or use a flashloan).
Example code to generate instruction
use solana_program::{
instruction::{AccountMeta, Instruction},
pubkey,
pubkey::Pubkey,
system_program,
};
use spl_associated_token_account::get_associated_token_address_with_program_id;
pub const MEMO_PROGRAM: Pubkey = pubkey!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
pub const TOKEN_2022: Pubkey = pubkey!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb");
pub const ASSOCIATED_TOKEN: Pubkey = pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
pub const PUMP_PROGRAM: Pubkey = pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA");
pub const PUMP_FEE_PROGRAM: Pubkey = pubkey!("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ");
pub const RAYDIUM_CLMM: Pubkey = pubkey!("CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK");
pub const PANCAKE_SWAP: Pubkey = pubkey!("HpNfyc2Saw7RKkQd8nEL4khUcuPhQ7WwY1B2qjx8jxFq");
pub const BYREAL: Pubkey = pubkey!("REALQqNEomY6cQGZJUGwywTBD2UmDT32rZcNnfxQ5N2");
pub const METEORA_VAULT_PROGRAM: Pubkey = pubkey!("24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi");
pub const WSOL: Pubkey = pubkey!("So11111111111111111111111111111111111111112");
pub const USDC: Pubkey = pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
/// A mint together with its token program and the wallet's token account for it.
#[derive(Clone, Copy)]
pub struct MintAccounts {
pub mint: Pubkey, // Mint
pub token_program: Pubkey, // Token program ID (Token or Token-2022)
pub wallet_account: Pubkey, // Wallet token account (ATA)
}
/// One pool declaration. `x` is the first mint of the role (T for intermediate and direct,
/// B for bridge, Q for conversion) and `base` is the second (B, S or Q). Vaults are given
/// in that orientation regardless of the pool's own mint order.
pub enum V10Pool {
RaydiumAmm {
// Pool must be in swap-only status (6) with SPL Token mints
pool: Pubkey, // Raydium AMM account
x_vault: Pubkey, // Raydium token X vault
base_vault: Pubkey, // Raydium base vault
},
RaydiumCp {
pool: Pubkey, // Raydium CP pool
amm_config: Pubkey, // Raydium CP amm config
x_vault: Pubkey, // Raydium CP token X vault
base_vault: Pubkey, // Raydium CP base vault
observation: Pubkey, // Raydium CP observation
},
Pump {
pool: Pubkey, // Pump pool
pool_base_mint: Pubkey, // Pump pool's own base mint
pool_quote: MintAccounts, // Pump pool's own quote mint and its token program
x_vault: Pubkey, // Pump token X account
base_vault: Pubkey, // Pump base account
fee_recipient: Pubkey, // Protocol fee recipient from Pump global config (use the Mayhem fee recipient for mayhem-mode pools)
coin_creator_vault_authority: Pubkey, // Pump coin creator vault authority
has_coin_creator: bool, // Pool has a coin creator set
is_cashback_coin: bool, // Pump is cashback coin
buyback_fee_recipient: Pubkey, // Buyback fee recipient from global config
},
Dlmm {
pair: Pubkey, // DLMM pair account
x_vault: Pubkey, // DLMM token X vault
base_vault: Pubkey, // DLMM base vault
oracle: Pubkey, // DLMM oracle
bitmap_extension: Option<Pubkey>, // DLMM bin array bitmap extension
bin_arrays: Vec<Pubkey>, // 1..=6 bin array accounts
},
Whirlpool {
pool: Pubkey, // Whirlpool pool
oracle: Pubkey, // Whirlpool oracle
x_vault: Pubkey, // Whirlpool token X vault
base_vault: Pubkey, // Whirlpool token base vault
tick_arrays: Vec<Pubkey>, // 1..=6 tick array accounts
},
Meteora {
// Enabled constant-product pools with SPL Token mints only
pool: Pubkey, // Meteora pool
x_vault: Pubkey, // Meteora pool token X vault
base_vault: Pubkey, // Meteora pool token base vault
x_token_vault: Pubkey, // Meteora pool token X token vault
base_token_vault: Pubkey, // Meteora pool token base token vault
x_lp_mint: Pubkey, // Meteora pool token X lp mint
base_lp_mint: Pubkey, // Meteora pool token base lp mint
x_pool_lp: Pubkey, // Meteora pool token X pool lp
base_pool_lp: Pubkey, // Meteora pool token base pool lp
x_admin_fee: Pubkey, // Meteora pool admin token fee X
base_admin_fee: Pubkey, // Meteora pool admin token fee base
},
RaydiumClmm {
// Also used for Pancake Swap and Byreal, with their program id
pool: Pubkey, // Raydium CLMM pool
amm_config: Pubkey, // Raydium CLMM amm config
observation_state: Pubkey, // Raydium CLMM observation state
bitmap_extension: Pubkey, // Raydium CLMM bitmap extension
x_vault: Pubkey, // Raydium CLMM token X vault
base_vault: Pubkey, // Raydium CLMM token base vault
tick_arrays: Vec<Pubkey>, // 1..=6 tick array accounts
},
DammV2 {
pool: Pubkey, // DAMM V2 pool
x_vault: Pubkey, // DAMM V2 token X vault
base_vault: Pubkey, // DAMM V2 token base vault
},
}
/// One group: target T, bridge B, and the pools in each role.
pub struct Group {
pub target: MintAccounts, // T
pub bridge: MintAccounts, // B
pub intermediate: V10Pool, // T/B
pub bridges: Vec<V10Pool>, // B/S (or B/Q with a conversion pool)
pub direct: Vec<V10Pool>, // T/S
}
fn pda(seeds: &[&[u8]], program: &Pubkey) -> Pubkey {
Pubkey::find_program_address(seeds, program).0
}
/// Builds one pool block. Every block starts with the DEX program id and the block's quote mint.
fn pool_block(
pool: &V10Pool,
program_id: &Pubkey,
block_quote_mint: &Pubkey,
x: &MintAccounts,
base: &MintAccounts,
wallet: &Pubkey,
) -> Vec<AccountMeta> {
let mut accounts = vec![
AccountMeta::new_readonly(*program_id, false),
AccountMeta::new_readonly(*block_quote_mint, false), // V10: quote mint of this block
];
let is_token_2022 = x.token_program == TOKEN_2022 || base.token_program == TOKEN_2022;
match pool {
V10Pool::RaydiumAmm { pool, x_vault, base_vault } => {
accounts.push(AccountMeta::new_readonly(pda(&[b"amm authority"], program_id), false));
accounts.push(AccountMeta::new(*pool, false));
accounts.push(AccountMeta::new(*x_vault, false));
accounts.push(AccountMeta::new(*base_vault, false));
}
V10Pool::RaydiumCp { pool, amm_config, x_vault, base_vault, observation } => {
accounts.push(AccountMeta::new_readonly(
pda(&[b"vault_and_lp_mint_auth_seed"], program_id),
false,
));
accounts.push(AccountMeta::new(*pool, false));
accounts.push(AccountMeta::new_readonly(*amm_config, false));
accounts.push(AccountMeta::new(*x_vault, false));
accounts.push(AccountMeta::new(*base_vault, false));
accounts.push(AccountMeta::new(*observation, false));
}
V10Pool::Pump {
pool,
pool_base_mint,
pool_quote,
x_vault,
base_vault,
fee_recipient,
coin_creator_vault_authority,
has_coin_creator,
is_cashback_coin,
buyback_fee_recipient,
} => {
// All fee token accounts are ATAs of the pool's own quote mint.
let quote_ata = |owner: &Pubkey| {
get_associated_token_address_with_program_id(
owner,
&pool_quote.mint,
&pool_quote.token_program,
)
};
let user_volume_accumulator =
pda(&[b"user_volume_accumulator", wallet.as_ref()], &PUMP_PROGRAM);
accounts.push(AccountMeta::new_readonly(pda(&[b"global_config"], &PUMP_PROGRAM), false));
accounts.push(AccountMeta::new_readonly(pda(&[b"__event_authority"], &PUMP_PROGRAM), false));
accounts.push(AccountMeta::new_readonly(*fee_recipient, false));
accounts.push(AccountMeta::new(*pool, false));
accounts.push(AccountMeta::new(*x_vault, false));
accounts.push(AccountMeta::new(*base_vault, false));
accounts.push(AccountMeta::new(quote_ata(fee_recipient), false)); // Pump fee token wallet
accounts.push(AccountMeta::new(quote_ata(coin_creator_vault_authority), false)); // Pump coin creator vault
accounts.push(AccountMeta::new_readonly(*coin_creator_vault_authority, false));
accounts.push(AccountMeta::new_readonly(
pda(&[b"global_volume_accumulator"], &PUMP_PROGRAM),
false,
));
accounts.push(AccountMeta::new(user_volume_accumulator, false));
accounts.push(AccountMeta::new_readonly(
pda(&[b"fee_config", PUMP_PROGRAM.as_ref()], &PUMP_FEE_PROGRAM),
false,
));
accounts.push(AccountMeta::new_readonly(PUMP_FEE_PROGRAM, false));
// Tail accounts. Order matters: cashback pair, then pool-v2, then buyback pair.
// The cashback pair repeats the user volume accumulator on purpose: the
// first copy above is used for buys, this second copy for sells.
if *is_cashback_coin {
accounts.push(AccountMeta::new(quote_ata(&user_volume_accumulator), false));
accounts.push(AccountMeta::new(user_volume_accumulator, false));
}
if *has_coin_creator {
accounts.push(AccountMeta::new_readonly(
pda(&[b"pool-v2", pool_base_mint.as_ref()], &PUMP_PROGRAM),
false,
));
}
accounts.push(AccountMeta::new_readonly(*buyback_fee_recipient, false));
accounts.push(AccountMeta::new(quote_ata(buyback_fee_recipient), false));
}
V10Pool::Meteora {
pool,
x_vault,
base_vault,
x_token_vault,
base_token_vault,
x_lp_mint,
base_lp_mint,
x_pool_lp,
base_pool_lp,
x_admin_fee,
base_admin_fee,
} => {
accounts.push(AccountMeta::new_readonly(METEORA_VAULT_PROGRAM, false));
accounts.push(AccountMeta::new(*pool, false));
accounts.push(AccountMeta::new(*x_vault, false));
accounts.push(AccountMeta::new(*base_vault, false));
accounts.push(AccountMeta::new(*x_token_vault, false));
accounts.push(AccountMeta::new(*base_token_vault, false));
accounts.push(AccountMeta::new(*x_lp_mint, false));
accounts.push(AccountMeta::new(*base_lp_mint, false));
accounts.push(AccountMeta::new(*x_pool_lp, false));
accounts.push(AccountMeta::new(*base_pool_lp, false));
accounts.push(AccountMeta::new(*x_admin_fee, false));
accounts.push(AccountMeta::new(*base_admin_fee, false));
}
V10Pool::Dlmm { pair, x_vault, base_vault, oracle, bitmap_extension, bin_arrays } => {
accounts.push(AccountMeta::new_readonly(pda(&[b"__event_authority"], program_id), false));
if is_token_2022 {
accounts.push(AccountMeta::new_readonly(MEMO_PROGRAM, false));
}
accounts.push(AccountMeta::new(*pair, false));
accounts.push(AccountMeta::new(*x_vault, false));
accounts.push(AccountMeta::new(*base_vault, false));
accounts.push(AccountMeta::new(*oracle, false));
if let Some(bitmap) = bitmap_extension {
accounts.push(AccountMeta::new(*bitmap, false));
}
accounts.extend(bin_arrays.iter().map(|a| AccountMeta::new(*a, false)));
}
V10Pool::Whirlpool { pool, oracle, x_vault, base_vault, tick_arrays } => {
accounts.push(AccountMeta::new_readonly(MEMO_PROGRAM, false)); // Always present for Whirlpool
accounts.push(AccountMeta::new(*pool, false));
accounts.push(AccountMeta::new(*oracle, false)); // Oracle NEEDS to be writable for Whirlpool
accounts.push(AccountMeta::new(*x_vault, false));
accounts.push(AccountMeta::new(*base_vault, false));
accounts.extend(tick_arrays.iter().map(|a| AccountMeta::new(*a, false)));
}
V10Pool::RaydiumClmm {
pool,
amm_config,
observation_state,
bitmap_extension,
x_vault,
base_vault,
tick_arrays,
} => {
if is_token_2022 {
accounts.push(AccountMeta::new_readonly(MEMO_PROGRAM, false));
}
accounts.push(AccountMeta::new(*pool, false));
accounts.push(AccountMeta::new_readonly(*amm_config, false));
accounts.push(AccountMeta::new(*observation_state, false));
accounts.push(AccountMeta::new(*bitmap_extension, false));
accounts.push(AccountMeta::new(*x_vault, false));
accounts.push(AccountMeta::new(*base_vault, false));
accounts.extend(tick_arrays.iter().map(|a| AccountMeta::new(*a, false)));
}
V10Pool::DammV2 { pool, x_vault, base_vault } => {
accounts.push(AccountMeta::new_readonly(pda(&[b"__event_authority"], program_id), false));
accounts.push(AccountMeta::new_readonly(pda(&[b"pool_authority"], program_id), false));
accounts.push(AccountMeta::new(*pool, false));
accounts.push(AccountMeta::new(*x_vault, false));
accounts.push(AccountMeta::new(*base_vault, false));
}
}
accounts
}
fn push_mint(accounts: &mut Vec<AccountMeta>, m: &MintAccounts) {
accounts.push(AccountMeta::new_readonly(m.mint, false));
accounts.push(AccountMeta::new_readonly(m.token_program, false));
accounts.push(AccountMeta::new(m.wallet_account, false));
}
pub fn generate_onchain_v10_instruction(
wallet: &Pubkey,
settlement: &MintAccounts, // S: WSOL or USDC, Token program only
conversion: Option<(MintAccounts, Pubkey, V10Pool)>, // (Q, DEX program id, Q/S pool)
groups: &[(Pubkey, Vec<Pubkey>, Vec<Pubkey>, Group)], // (intermediate DEX, bridge DEXes, direct DEXes, group)
program_id: &Pubkey,
minimum_profit: u64,
compute_unit_limit: u32, // 1..=1_400_000
no_failure_mode: bool,
use_flashloan: bool,
trade_size: u64, // 0 => automatic sizing, else base units of S
constant_conversion: bool, // requires a conversion pool
) -> Instruction {
// Fee collector must match the settlement mint
let fee_collector = if settlement.mint == USDC {
pubkey!("GzVRuLF349u78FHpr8KbqMhrZ1aDxnhSF59JWiZ6tbgt")
} else if use_flashloan {
pubkey!("6AGB9kqgSp2mQXwYpdrV4QVV8urvCaDS35U1wsLssy6H")
} else {
let fee_accounts = [
pubkey!("GPpkDpzCDmYJY5qNhYmM14c7rct1zmkjWc2CjR5g7RZ1"),
pubkey!("J6c7noBHvWju4mMA3wXt3igbBSp2m9ATbA6cjMtAUged"),
pubkey!("BjsfwxDu7GX7RRW6oSRTpMkASdXAgCcHnXEcatqSfuuY"),
];
fee_accounts[rand::random::<usize>() % fee_accounts.len()]
};
let mut accounts = vec![
AccountMeta::new(*wallet, true),
AccountMeta::new_readonly(settlement.mint, false),
AccountMeta::new(fee_collector, false),
// Additional fee collector goes here when additional_fee_bp > 0
AccountMeta::new(settlement.wallet_account, false),
AccountMeta::new_readonly(settlement.token_program, false),
AccountMeta::new_readonly(system_program::id(), false),
AccountMeta::new_readonly(ASSOCIATED_TOKEN, false),
];
// Add flashloan accounts if enabled
if use_flashloan {
accounts.push(AccountMeta::new_readonly(
pubkey!("5LFpzqgsxrSfhKwbaFiAEJ2kbc9QyimjKueswsyU4T3o"),
false,
));
accounts.push(AccountMeta::new(
pda(&[b"vault_token_account", settlement.mint.as_ref()], program_id),
false,
));
}
// Optional conversion pool: Q mint accounts, then the Q/S block
let mut conversion_account_count: Option<u16> = None;
let quote = if let Some((q, dex, pool)) = &conversion {
push_mint(&mut accounts, q);
let block = pool_block(pool, dex, &settlement.mint, q, settlement, wallet);
conversion_account_count = Some(block.len() as u16);
accounts.extend(block);
*q
} else {
*settlement
};
// Bridge groups: mints, then intermediate, bridges, directs.
// A direct-only group would instead push only the T mint accounts, then its
// pool blocks, and record (0, pool_count, counts).
let mut group_descriptors: Vec<(u8, u8, Vec<u16>)> = Vec::new();
for (intermediate_dex, bridge_dexes, direct_dexes, g) in groups {
push_mint(&mut accounts, &g.target);
push_mint(&mut accounts, &g.bridge);
let mut counts = Vec::new();
// Intermediate block (T/B): quote mint is B
let block = pool_block(
&g.intermediate,
intermediate_dex,
&g.bridge.mint,
&g.target,
&g.bridge,
wallet,
);
counts.push(block.len() as u16);
accounts.extend(block);
// Bridge blocks (B/Q, or B/S without conversion): quote mint is Q or S
for (pool, dex) in g.bridges.iter().zip(bridge_dexes) {
let block = pool_block(pool, dex, "e.mint, &g.bridge, "e, wallet);
counts.push(block.len() as u16);
accounts.extend(block);
}
// Direct blocks (T/S): quote mint is S
for (pool, dex) in g.direct.iter().zip(direct_dexes) {
let block = pool_block(pool, dex, &settlement.mint, &g.target, settlement, wallet);
counts.push(block.len() as u16);
accounts.extend(block);
}
group_descriptors.push((g.bridges.len() as u8, g.direct.len() as u8, counts));
}
// Create instruction data
let mut data = vec![61u8];
data.extend_from_slice(&minimum_profit.to_le_bytes());
data.extend_from_slice(&compute_unit_limit.to_le_bytes());
data.extend_from_slice(if no_failure_mode { &[1] } else { &[0] });
data.extend_from_slice(&0u16.to_le_bytes()); // additional_fee_bp
data.extend_from_slice(if use_flashloan { &[1] } else { &[0] });
data.extend_from_slice(&trade_size.to_le_bytes());
// V10 tail
let mut flags = 0u8;
if conversion_account_count.is_some() {
flags |= 1;
}
if constant_conversion {
flags |= 2;
}
data.push(flags);
data.push(group_descriptors.len() as u8);
if let Some(count) = conversion_account_count {
data.extend_from_slice(&count.to_le_bytes());
}
for (bridge_count, direct_count, counts) in &group_descriptors {
data.push(*bridge_count);
data.push(*direct_count);
for count in counts {
data.extend_from_slice(&count.to_le_bytes());
}
}
Instruction {
program_id: *program_id,
accounts,
data,
}
}Example group
Arbitrage T where its main pool is T/PUMP on Pumpfun Swap, PUMP has SOL pools on Pumpfun Swap and Meteora DLMM, and T/SOL exists on Meteora DLMM:
S = WSOL
T = <target mint>
B = PUMP
Intermediate: Pumpfun Swap T/PUMP
Bridges: Pumpfun Swap PUMP/SOL
Meteora DLMM PUMP/SOL
Direct: Meteora DLMM T/SOLThe program then compares SOL → T → SOL on the direct pool against SOL → T → PUMP → SOL and SOL → PUMP → T → SOL through the best bridge, and executes the most profitable one.
Worked examples
Three complete instructions: the two classic shapes and one bridge group. Wallet-owned accounts are placeholders; pools, mints and programs are real. The first two are built from pools of the two real V9 transactions on the V9 page, re-encoded as V10 direct-only groups. The third is a real V10 transaction, byte for byte as sent.
SOL → X → SOL
One direct-only group. T is URA URARfsinxCRw4JpvQhuT4CxavdZXZEMjv9ZwWmWpwag (Token-2022) with four Raydium CLMM URA/SOL pools. No bridge, no conversion pool.
Instruction data (37 bytes):
3d opcode 61
00 00 00 00 00 00 00 00 minimum_profit = 0
63 b1 07 00 compute_unit_limit = 504_163
00 no_failure = 0
00 00 additional_fee_bp = 0
01 use_flashloan = 1
00 00 00 00 00 00 00 00 trade_size = 0 (automatic sizing)
00 flags = 0 (no conversion pool)
01 group_count = 1
00 group 0: bridge_count = 0 (direct-only)
04 group 0: direct_count = 4
0b 00 pool 0 account count = 11
0a 00 pool 1 account count = 10
0b 00 pool 2 account count = 11
0a 00 pool 3 account count = 10Accounts (54):
[0] wallet (signer)
[1] S mint So11111111111111111111111111111111111111112
[2] fee collector (see below)
[3] wallet WSOL account
[4] Token program
[5] System program
[6] Associated Token program
[7] flashloan authority
5LFpzqgsxrSfhKwbaFiAEJ2kbc9QyimjKueswsyU4T3o
[8] flashloan vault
CHy11HNwixLjdhWDbQLsJ5C5otKxke1e37zVeqium9Uo
[9-11] T mint URAR…pwag, Token-2022 program, wallet URA
account
[12-22] Raydium CLMM block, URA/SOL pool Hgjk…Pgu4: program,
WSOL mint (block quote mint), memo, pool, amm config,
observation, bitmap extension, URA vault, SOL vault, 2
tick arrays
[23-32] Raydium CLMM block, URA/SOL pool 7uYx…rUd5 (1 tick
array)
[33-43] Raydium CLMM block, URA/SOL pool 3ihM…QZc6 (2 tick
arrays)
[44-53] Raydium CLMM block, URA/SOL pool AgZb…YouB (1 tick
array)Compared with the V9 transaction: no mixed mode accounts, the CLMM blocks are byte-for-byte the same, and the data carries the per-pool counts.
SOL → X → USDC → SOL
One direct-only group plus a conversion pool. T is SKR SKRbvo6Gf7GondiT3BbTfuRDPqLWei4j2Qy2NPGZhW3 with a DLMM SKR/SOL pool and a DLMM SKR/USDC pool. The SKR/USDC pool is a T/Q pool, so flags bit 0 is set and the Raydium SOL/USDC conversion block follows the common accounts. This gives the program SOL → USDC → SKR → SOL and SOL → SKR → USDC → SOL to choose from. A plain SOL → SKR → SOL would need a second SKR/SOL pool, because one pool cannot be used twice in the same route.
Instruction data (35 bytes):
3d opcode 61
00 00 00 00 00 00 00 00 minimum_profit = 0
09 aa 07 00 compute_unit_limit = 502_281
01 no_failure = 1
00 00 additional_fee_bp = 0
01 use_flashloan = 1
00 00 00 00 00 00 00 00 trade_size = 0
01 flags = 1 (conversion pool present)
01 group_count = 1
06 00 conversion pool account count = 6
00 group 0: bridge_count = 0 (direct-only)
02 group 0: direct_count = 2
0b 00 pool 0 account count = 11
0b 00 pool 1 account count = 11Accounts (43):
[0-6] wallet, WSOL mint, fee collector, wallet WSOL account,
Token program, System program, Associated Token
program
[7-8] flashloan authority, flashloan vault
[9-11] Q mint EPjF…Dt1v (USDC), Token program, wallet USDC
account
[12-17] conversion block, Raydium SOL/USDC pool 58oQ…YQo2:
program, WSOL mint (block quote mint), Raydium
authority 5Q54…e4j1, pool, USDC vault HLmq…vBBz, SOL
vault DQyr…vPpz
[18-20] T mint SKRb…ZhW3, Token program, wallet SKR account
[21-31] DLMM block, SKR/SOL pair BCv5…TjQ9: program, WSOL mint
(block quote mint), event authority, pair, SKR vault,
SOL vault, oracle, bin array bitmap extension, 3 bin
arrays
[32-42] DLMM block, SKR/USDC pair 3EFv…g6wB: program, USDC
mint (block quote mint), event authority, pair, SKR
vault, USDC vault, oracle, bin array bitmap extension,
3 bin arraysCompared with the V9 transaction: the eight mixed mode accounts become the three Q mint accounts plus a six-account conversion block, the instructions sysvar is gone, and each pool block names its own quote mint.
SOL → T → B → USDC → SOL (bridge group)
The real transaction fGgz…kor1 from the diagram above, exactly as sent. One bridge group with T = Tulip, B = FLWS, one intermediate, one bridge, three direct pools, and a conversion pool.
Instruction data (41 bytes), as sent:
3d opcode 61
a0 86 01 00 00 00 00 00 minimum_profit = 100_000
10 0c 09 00 compute_unit_limit = 592_912
00 no_failure = 0
00 00 additional_fee_bp = 0
01 use_flashloan = 1
00 00 00 00 00 00 00 00 trade_size = 0
01 flags = 1 (conversion pool present)
01 group_count = 1
06 00 conversion pool account count = 6
01 group 0: bridge_count = 1
03 group 0: direct_count = 3
08 00 intermediate account count = 8
0c 00 bridge 0 account count = 12
0b 00 direct 0 account count = 11
0b 00 direct 1 account count = 11
0b 00 direct 2 account count = 11Accounts (77):
[0-6] wallet, WSOL mint, fee collector, wallet WSOL account,
Token program, System program, Associated Token
program
[7-8] flashloan authority, flashloan vault
[9-11] Q mint EPjF…Dt1v (USDC), Token program, wallet USDC
account
[12-17] conversion block, Raydium SOL/USDC pool 58oQ…YQo2 (6
accounts, as above)
[18-20] T mint 7vSG…VDJf (Tulip), Token-2022 program, wallet
Tulip account
[21-23] B mint FLWS…Lqgd, Token-2022 program, wallet FLWS
account
[24-31] intermediate block, Raydium CPMM Tulip/FLWS pool
r9cB…Cz6K: program, FLWS mint (block quote mint),
authority, pool, amm config, Tulip vault, FLWS vault,
observation
[32-43] bridge block, Raydium CLMM FLWS/USDC pool 62sC…Z9if:
program, USDC mint (block quote mint), memo, pool, amm
config, observation, bitmap extension, FLWS vault,
USDC vault, 3 tick arrays
[44-54] direct block, DLMM Tulip/SOL pair GFVj…Gz9B: program,
WSOL mint (block quote mint), event authority, memo,
pair, Tulip vault, SOL vault, oracle, 3 bin arrays
[55-65] direct block, DLMM Tulip/SOL pair 6DkV…rhhR (same
shape)
[66-76] direct block, DLMM Tulip/SOL pair Tj1o…H4WG (same
shape)The intermediate block's quote mint is B (FLWS), the bridge block's quote mint is Q (USDC), and the direct blocks' quote mint is S (SOL). The DLMM blocks carry the memo program because Tulip is a Token-2022 mint.
Fee collector
The fee collector must be a token account of the settlement mint:
- WSOL settlement:
GPpkDpzCDmYJY5qNhYmM14c7rct1zmkjWc2CjR5g7RZ1,J6c7noBHvWju4mMA3wXt3igbBSp2m9ATbA6cjMtAUgedorBjsfwxDu7GX7RRW6oSRTpMkASdXAgCcHnXEcatqSfuuY - WSOL settlement with flashloan:
6AGB9kqgSp2mQXwYpdrV4QVV8urvCaDS35U1wsLssy6H - USDC settlement, with or without flashloan:
GzVRuLF349u78FHpr8KbqMhrZ1aDxnhSF59JWiZ6tbgt
Additional fee account
Insert your additional fee collector right after the fee collector (index 2) and set additional_fee_bp at offset 13. The account must be a token account of the settlement mint.
Both the program fee and your additional fee are calculated on the profit above minimum_profit, not on the whole profit. Without an additional fee account the split is 10% to the program and 90% to you. With additional_fee_bp = 500 you take 5%, the program takes 10%, and the user keeps 85%.
let additional_fee_collector = Pubkey::from_str("XXX").unwrap();
let mut accounts = vec![
AccountMeta::new(*wallet, true), // 0. Wallet (signer)
AccountMeta::new_readonly(settlement.mint, false), // 1. S mint
AccountMeta::new(fee_collector, false), // 2. Fee collector
AccountMeta::new(additional_fee_collector, false), // 2.5 Additional fee account
AccountMeta::new(settlement.wallet_account, false), // 3. Wallet S account
AccountMeta::new_readonly(settlement.token_program, false), // 4. Token program
AccountMeta::new_readonly(system_program::id(), false), // 5. System program
AccountMeta::new_readonly(ASSOCIATED_TOKEN, false), // 6. Associated Token program
];
...
...
// Create instruction data
let mut data = vec![61u8];
data.extend_from_slice(&minimum_profit.to_le_bytes());
data.extend_from_slice(&compute_unit_limit.to_le_bytes());
data.extend_from_slice(if no_failure_mode { &[1] } else { &[0] });
// Example of 5% additional fee
let additional_fee_bp = 500u16;
data.extend_from_slice(&additional_fee_bp.to_le_bytes());
data.extend_from_slice(if use_flashloan { &[1] } else { &[0] });
data.extend_from_slice(&trade_size.to_le_bytes());
// ... V10 tail as aboveUsing fixed trade size
Same field as V9 at offset 16, but in V10 it is always present rather than optional. 0 enables automatic sizing. A nonzero value trades exactly that amount of the settlement mint, in base units (lamports for WSOL, 1e-6 for USDC).
Differences from V9 at a glance
| V9 | V10 | |
|---|---|---|
| Opcode | 28 | 61 |
| Mints | X + base | S + T + B per group, optional Q |
| Pool roles | one flat list per DEX | intermediate / bridge / direct, or direct-only |
| Routes | S → X → S | S → T → S, S → T → B → S, S → B → T → S (+ Q variants) |
| SOL/USDC mixing | auto-detected, fixed Raydium pool appended | explicit conversion pool via flags bit 0 |
| Pool block header | program id, base mint | program id, block quote mint |
| Account counts | implied by DEX | explicit u16 per pool in instruction data |
| Compute unit limit | any | required, 1..=1_400_000 |
| Trade size | optional trailing u64 | always present at offset 16, 0 = automatic sizing |
| Pump cashback tail | user volume accumulator WSOL ATA | user volume accumulator quote ATA, then user volume accumulator |
| Pump tail | pool-v2 | pool-v2 (if coin creator), then buyback recipient + its quote ATA |
| DAMM V2 block | ends with instructions sysvar | no sysvar |