Skip to main content
sdk-api/rust-cpi covers the low-level mechanics of invoking each Raydium program. This page is the higher-level companion: why you would compose Raydium into your own program, which pattern fits your use case, and the full glue you need end-to-end.

When CPI is the right tool

A custom program makes sense when the trade needs to happen atomically with other on-chain state changes that only your program can make. Common cases:
  • Escrow / limit-order programs — user deposits a mint into your escrow, your program watches for a price condition, and when it triggers, your program atomically swaps through Raydium and credits the user’s account.
  • Aggregator proxies — a single instruction that routes a swap through Raydium + one or more other DEXes, with all hops under a single slippage check owned by your program.
  • Auto-compounding vaults — deposit LP or farm stake into your vault, vault harvests rewards on a schedule, re-supplies liquidity, issues share tokens.
  • Strategy vaults — leveraged LP positions that rebalance by swapping through CLMM; liquidators that close positions and swap collateral in one transaction.
  • Token-launch platforms with custom vesting — your program holds vesting tokens and releases into a Raydium pool on a schedule.
If you just want to send a swap from off-chain code, CPI is overkill — use the SDK. CPI earns its complexity only when atomicity with your own state is the requirement.

Composition patterns

Pattern 1: Thin proxy

Your program exposes a single instruction that validates some policy (e.g. whitelisted mint pairs, fee discount for verified users) and then forwards to Raydium.
State lives in the user’s ATAs. Your program owns no tokens. Minimal trust footprint.

Pattern 2: Escrow

Your program owns a PDA that holds the user’s input mint. On trigger, the PDA signs a CPI to Raydium to swap its own balance.
Critical detail: the PDA signs via CpiContext::new_with_signer. See Signer seeds.

Pattern 3: Composed multi-hop

Your program issues multiple CPIs in one instruction, enforcing a single slippage bound across all of them. The Raydium swap instructions each have their own minimum_amount_out, but you set those to 0 (or a very loose floor) and enforce a strict final minimum yourself after the last hop.
This gives you a single reverting gate for the whole route. Only use this pattern when you trust every hop to be slippage-safe; otherwise, let each hop enforce its own min.

Pattern 4: Vault / strategy

Your program holds LP tokens or farm stake in a PDA. A keeper (or the user) calls compound(), which:
  1. Harvests rewards from the farm.
  2. Swaps rewards for pool tokens (CPI into CPMM or CLMM).
  3. Deposits the proceeds back into the LP (another CPI).
  4. Stakes the new LP (another CPI).
All in one transaction so the vault’s NAV moves atomically. Compute budget is typically 600k–1M CU; address lookup tables are mandatory.

Account list construction

The calling program’s Accounts struct mirrors the Raydium program’s account order, but most Raydium-side accounts are UncheckedAccount because Raydium validates them itself. You only add constraints on accounts you own:
The asymmetry — strict validation on your accounts, UncheckedAccount on Raydium’s — is not laziness. The receiver validates its own; double-validating at the caller just burns CU and risks going out of sync when Raydium ships a new struct layout field.

The CPI call itself

PDA signer seeds

The CPI succeeds only if the PDA passed as authority matches the derivation the caller claims. The two must agree on:
  1. The seed byte sequence (here [b"escrow", user.key().as_ref()]).
  2. The bump.
  3. The calling program ID (your program, not Raydium’s).
Note what the PDA has to match. CPMM’s authority slot is its own vault PDA — a fixed, program-wide account it derives and signs with itself, and which your program neither controls nor substitutes. The account your PDA seeds have to line up with is payer: the check happens inside CPMM’s own transfer_from_user_to_pool_vault helper, which requires the account passed as payer to be the owner of input_token_account. Common bug: passing user as payer while escrow_input_ata is owned by the escrow PDA. The SPL Token program rejects with owner mismatch. Always make payer the ATA’s owner — and sign for it with new_with_signer when that owner is a PDA.

Remaining accounts

Several Raydium instructions take a variable-length list of accounts appended after the fixed ones — remaining accounts.
  • CLMM SwapV2: 1–8 TickArrayState accounts for the tick arrays the swap may traverse, in swap direction.
  • Farm v6 Deposit / Harvest / Withdraw: (reward_vault, user_reward_ata) pairs, one pair per live reward slot.
  • Token-2022 transfer-hook mints: the transfer-hook program plus any accounts the hook needs.
The Anchor CPI helpers don’t type-check remaining accounts. Pass them through:
Ordering matters. For CLMM:
For farm v6 harvest:
Your calling program must pass the remaining accounts it receives from the client through unchanged. Don’t try to filter or reorder them.

Compute budget for composed calls

A CPI costs ~1,500 CU for the call frame itself; the callee’s own CU use stacks on top. The callee figures below are measured from live mainnet transactions on high-volume pools on 2026-09-09, read from the Program <id> consumed N of M compute units log line for the Raydium program’s own invocation (so they include its inner token-program CPIs): Add ~1,500 for each CPI frame and your own program’s overhead on top. CLMM swap cost scales with tick crossings, so treat its figure as a floor. Token-2022 mints add the extension-handling cost of the transfer itself; measure it for your own mints rather than applying a flat multiplier.
Earlier revisions of this page carried estimates 5–7× higher (a ~150,000 CU CPMM swap, ~180,000 for CLMM). Those were never measured. Budget from your own computeUnitsConsumed reading, not from a documented number — and note that a full transaction costs more than the Raydium instruction alone once ATA creation, wSOL wrapping and compute-budget instructions are counted.
Always set an explicit ComputeBudgetProgram::set_compute_unit_limit:
The default 200k CU ceiling will silently exhaust long before a composed call completes.

Error propagation

Raydium’s programs return Anchor errors with stable error codes. Your calling program sees them as Err(ProgramError::Custom(code)). Bubble through by default:
Or intercept for specific codes:
Note the ERROR_CODE_OFFSET term: #[error_code] variants are emitted starting at 6000, so comparing against the bare enum discriminant never matches. (There is no is_err helper in anchor-lang or in raydium_cp_swap — earlier revisions of this page used one that does not exist.) The error code-to-meaning mapping is stable per the IDL policy (sdk-api/anchor-idl); new codes append at the end, existing codes never change meaning.

Full worked example: limit-order escrow

Flow:
  1. open_order — user deposits amount_in of input_mint into escrow PDA; record target min_amount_out and expiry.
  2. execute_order — anyone (keeper) calls with the current pool accounts. Program checks the current quote ≥ min_amount_out, then CPIs Raydium swap and keeps the output in escrow.
  3. claim — user withdraws the output mint from escrow.
The keeper pays the transaction fee (they get a keeper fee elsewhere — not shown). The order PDA signs the CPI as payer, because it owns the escrow’s input ATA; ExecuteOrder therefore also needs a pool_authority: UncheckedAccount<'info> field for CPMM’s own vault PDA. Both the Raydium-side slippage check and the escrow’s own delta check enforce the floor — belt and braces.

Testing

Pulling Raydium programs into a local validator for integration tests (from Anchor.toml):
Clone the pool state accounts too so your tests can actually execute swaps; anchor test fetches them from mainnet at startup. See sdk-api/rust-cpi.

Pitfalls specific to composition

Reentrancy

Solana has no true reentrancy — a CPI can’t call back into the originating program in the same invocation. But you can still build yourself into a logical reentrancy: a CPI that reads your state, then your code reads it again assuming the CPI didn’t change it. For Raydium, the CPIs don’t touch your state, so this is less a concern than e.g. flash-loan contexts. But if you compose Raydium with a lending protocol, be aware.

Account mutability drift

If your program passes an account as mut but Raydium expects it read-only (or vice versa), the runtime rejects the invocation with InvalidAccountData. Always check Raydium’s instruction’s expected mutability in the IDL; raydium_cp_swap::cpi::accounts::Swap sets each account’s mutability for you, from the #[account(mut)] markers on CPMM’s own Swap struct — the generated fields are all plain AccountInfo<'info>, so it is the derived ToAccountMetas impl, not the field types, that carries the flags.

Token-2022 program field

Input and output mints may be under different token programs — one SPL Token, one Token-2022. The CPI has separate input_token_program and output_token_program fields for this reason. Always check each mint’s owner field and route the correct program into each slot.

Versioned transactions

A composed tx that does 2+ Raydium CPIs plus an ATA creation rarely fits in a legacy (v0-without-LUT) transaction. Use V0 with address lookup tables; pull Raydium’s public LUTs via raydium.getRaydiumLutAddresses().

Pointers

Sources: