Documentation Index
Fetch the complete documentation index at: https://docs.raydium.io/llms.txt
Use this file to discover all available pages before exploring further.
Version banner. All demos target @raydium-io/raydium-sdk-v2@0.2.42-alpha against Solana mainnet-beta, verified 2026-04. Program IDs come from reference/program-addresses via the SDK.
Setup
npm install @raydium-io/raydium-sdk-v2 @solana/web3.js @solana/spl-token bn.js decimal.js
Every demo on this page mirrors a file in raydium-sdk-V2-demo/src/clmm; the GitHub link sits next to each section. Bootstrap follows the demo repo’s config.ts.template (source) — disableFeatureCheck: true is the recommended setting for any non-trivial integration:
import { Connection, Keypair, clusterApiUrl } from "@solana/web3.js";
import { Raydium, TxVersion } from "@raydium-io/raydium-sdk-v2";
import fs from "node:fs";
const connection = new Connection(process.env.RPC_URL ?? clusterApiUrl("mainnet-beta"));
const owner = Keypair.fromSecretKey(
new Uint8Array(JSON.parse(fs.readFileSync(process.env.KEYPAIR!, "utf8"))),
);
const raydium = await Raydium.load({
owner,
connection,
cluster: "mainnet",
disableFeatureCheck: true,
blockhashCommitment: "finalized",
});
export const txVersion = TxVersion.V0;
Create a CLMM pool
Source: src/clmm/createPool.ts
import { PublicKey } from "@solana/web3.js";
import { CLMM_PROGRAM_ID } from "@raydium-io/raydium-sdk-v2";
import BN from "bn.js";
import Decimal from "decimal.js";
const mintA = await raydium.token.getTokenInfo(
new PublicKey("So11111111111111111111111111111111111111112")); // wSOL
const mintB = await raydium.token.getTokenInfo(
new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")); // USDC
const ammConfigs = await raydium.api.getClmmConfigs();
const ammConfig = ammConfigs.find((c) => c.index === 1)!; // 0.05% tier
const initialPrice = new Decimal(160); // 160 USDC per SOL
const { execute, extInfo } = await raydium.clmm.createPool({
programId: CLMM_PROGRAM_ID,
mint1: mintA,
mint2: mintB,
ammConfig,
initialPrice,
startTime: new BN(0),
txVersion: TxVersion.V0,
});
const { txId } = await execute({ sendAndConfirm: true });
console.log("Pool:", extInfo.address.id.toBase58(), "tx:", txId);
The SDK:
- Sorts
mint1/mint2 by byte order before derivation.
- Computes
sqrt_price_x64 = floor(sqrt(initialPrice × 10^(dB−dA)) × 2^64).
- Creates the
observation and tick_array_bitmap_extension accounts.
- Pays the pool-creation fee defined by
ammConfig.
Open a position in a chosen range
Source: src/clmm/createPosition.ts
import { PoolUtils, TickUtils } from "@raydium-io/raydium-sdk-v2";
const poolId = new PublicKey("<POOL_ID>");
const { poolInfo, poolKeys, rpcData } = await raydium.clmm.getPoolInfoFromRpc(poolId);
// Choose a price range. Here: ±10% of current.
const currentPrice = new Decimal(poolInfo.price);
const lowerPrice = currentPrice.mul(0.9);
const upperPrice = currentPrice.mul(1.1);
// Snap to valid ticks for this pool's tick_spacing.
const { tick: tickLower } = TickUtils.getPriceAndTick({
poolInfo, price: lowerPrice, baseIn: true,
});
const { tick: tickUpper } = TickUtils.getPriceAndTick({
poolInfo, price: upperPrice, baseIn: true,
});
// How much of each token to deposit.
const inputAmount = new BN(10_000_000); // 0.01 SOL
const inputMint = poolInfo.mintA.address;
const res = PoolUtils.getLiquidityAmountOutFromAmountIn({
poolInfo,
slippage: 0.01,
inputA: true,
tickUpper,
tickLower,
amount: inputAmount,
add: true,
amountHasFee: true,
epochInfo: await raydium.fetchEpochInfo(),
});
const { execute } = await raydium.clmm.openPositionFromBase({
poolInfo,
poolKeys,
tickUpper,
tickLower,
base: "MintA",
ownerInfo: { useSOLBalance: true },
baseAmount: inputAmount,
otherAmountMax: res.amountSlippageB.amount,
txVersion: TxVersion.V0,
});
const { txId } = await execute({ sendAndConfirm: true });
console.log("Position opened, tx:", txId);
The SDK automatically computes which tick arrays the range touches and bundles InitTickArray instructions if any are uninitialized.
Increase liquidity on an existing position
Source: src/clmm/increaseLiquidity.ts
const positionNftMint = new PublicKey("<POSITION_NFT_MINT>");
const positionAccount = await raydium.clmm.getPositionInfo({
nftMint: positionNftMint,
});
const { execute } = await raydium.clmm.increasePositionFromBase({
poolInfo,
poolKeys,
ownerPosition: positionAccount,
base: "MintA",
baseAmount: new BN(5_000_000),
otherAmountMax: new BN(1_000_000_000),
txVersion: TxVersion.V0,
});
await execute({ sendAndConfirm: true });
Decrease liquidity (and collect fees at the same time)
Source: src/clmm/decreaseLiquidity.ts and src/clmm/closePosition.ts
const { execute } = await raydium.clmm.decreaseLiquidity({
poolInfo,
poolKeys,
ownerPosition: positionAccount,
liquidity: positionAccount.liquidity.divn(2), // halve
amountMinA: new BN(0),
amountMinB: new BN(0),
closePosition: false,
txVersion: TxVersion.V0,
});
await execute({ sendAndConfirm: true });
To collect fees only, call decreaseLiquidity with liquidity = new BN(0). The instruction’s side-effect is settling tokens_fees_owed_{0,1} and transferring them out.
To close the position entirely after zeroing liquidity and fees, pass closePosition: true on the final decreaseLiquidity call. The SDK appends ClosePosition and burns the NFT.
Collect reward(s)
Source: src/clmm/harvestAllRewards.ts
const { execute } = await raydium.clmm.harvestAllRewards({
ownerInfo: { useSOLBalance: true },
allPoolInfo: { [poolInfo.id]: poolInfo },
allPositions: { [poolInfo.id]: [positionAccount] },
txVersion: TxVersion.V0,
});
await execute({ sendAndConfirm: true });
harvestAllRewards walks every position on every pool passed in, batches CollectReward (and any UpdateRewardInfos) instructions, and splits them across transactions if needed.
Swap
Source: src/clmm/swap.ts
import { PoolUtils } from "@raydium-io/raydium-sdk-v2";
const amountIn = new BN(10_000_000);
const baseIn = true; // swap A (SOL) → B (USDC)
const slippage = 0.005;
const { minAmountOut, remainingAccounts, priceImpact } =
PoolUtils.computeAmountOutFormat({
poolInfo,
tickArrayCache: await raydium.clmm.fetchTickArrays({ poolInfo }),
amountIn,
tokenOut: poolInfo.mintB,
slippage,
epochInfo: await raydium.fetchEpochInfo(),
});
const { execute } = await raydium.clmm.swap({
poolInfo,
poolKeys,
inputMint: new PublicKey(poolInfo.mintA.address),
amountIn,
amountOutMin: minAmountOut.amount,
observationId: poolKeys.observationId,
remainingAccounts,
txVersion: TxVersion.V0,
});
await execute({ sendAndConfirm: true });
computeAmountOutFormat walks the tick map off-chain using the same logic as the on-chain program and returns:
- the expected amount out,
- the minimum amount out after slippage,
- the list of tick-array accounts the actual swap will touch (
remainingAccounts).
Always pass remainingAccounts returned by the simulation: if you pass too few, the swap reverts mid-walk with TickArrayNotFound; if you pass stale ones, wasted compute.
Create a customizable CLMM pool
createCustomizablePool is the new entry point that exposes the dynamic-fee and single-sided-fee toggles at pool-creation time. It takes the same shape as createPool plus three additions:
import { CLMM_PROGRAM_ID, CollectFeeOn } from "@raydium-io/raydium-sdk-v2";
const dynamicFeeConfigs = await raydium.api.getClmmDynamicConfigs(); // GET /main/clmm-dynamic-config
const dynamicFeeConfig = dynamicFeeConfigs.find((c) => c.index === 0); // pick a calibration tier
const { execute, extInfo } = await raydium.clmm.createCustomizablePool({
programId: CLMM_PROGRAM_ID,
mint1: mintA,
mint2: mintB,
ammConfig,
initialPrice,
startTime: new BN(0),
// New fields:
collectFeeOn: CollectFeeOn.Token1Only, // 0 = FromInput, 1 = Token0Only, 2 = Token1Only
enableDynamicFee: true,
dynamicFeeConfigId: dynamicFeeConfig?.id, // omit when enableDynamicFee is false
txVersion: TxVersion.V0,
});
await execute({ sendAndConfirm: true });
console.log("Customizable pool:", extInfo.address.id.toBase58());
createPool continues to work for the default-fee, no-limit-order, no-dynamic-fee path. Use createCustomizablePool whenever you need any of the three new knobs. See products/clmm/instructions for the on-chain account list.
Limit orders
A limit order parks user input at a single tick and is filled FIFO when a swap crosses that tick. Outputs are pushed to the owner’s ATA at settle time; the owner does not need to be online to be filled.
Open a limit order
import { TickUtils } from "@raydium-io/raydium-sdk-v2";
const limitConfigs = await raydium.api.getClmmLimitOrderConfigs(); // GET /main/clmm-limit-order-config
const limitConfig = limitConfigs.find((c) => c.poolId === poolInfo.id);
// Limit price MUST be quantized to tick_spacing.
const targetPrice = new Decimal(180); // sell SOL at 180 USDC
const { tick: limitTick } = TickUtils.getPriceAndTick({
poolInfo, price: targetPrice, baseIn: true,
});
const { execute } = await raydium.clmm.openLimitOrder({
poolInfo,
poolKeys,
limitOrderConfig: limitConfig,
inputMint: poolInfo.mintA.address, // selling SOL
inputAmount: new BN(50_000_000), // 0.05 SOL
tick: limitTick,
txVersion: TxVersion.V0,
});
const { txId } = await execute({ sendAndConfirm: true });
console.log("Limit order opened, tx:", txId);
The SDK derives the LimitOrderState PDA from (pool, owner, tick, nonce), bumps the per-(pool, owner) LimitOrderNonce, and inserts the order into the FIFO cohort at that tick.
Increase / decrease an open order
await raydium.clmm.increaseLimitOrder({
poolInfo,
poolKeys,
limitOrderId: <LIMIT_ORDER_PUBKEY>,
addAmount: new BN(20_000_000),
txVersion: TxVersion.V0,
}).then((b) => b.execute({ sendAndConfirm: true }));
await raydium.clmm.decreaseLimitOrder({
poolInfo,
poolKeys,
limitOrderId: <LIMIT_ORDER_PUBKEY>,
removeAmount: new BN(10_000_000),
txVersion: TxVersion.V0,
}).then((b) => b.execute({ sendAndConfirm: true }));
decreaseLimitOrder can only remove from the unfilled portion of the order; the filled portion is locked until settlement. Both instructions revert with InvalidOrderPhase if the order has already been fully filled.
Settle a filled order
await raydium.clmm.settleLimitOrder({
poolInfo,
poolKeys,
limitOrderId: <LIMIT_ORDER_PUBKEY>,
txVersion: TxVersion.V0,
}).then((b) => b.execute({ sendAndConfirm: true }));
settleLimitOrder reads the order’s unfilled_ratio_x64 against the cohort tracker, computes the filled output, and transfers it to the owner’s ATA. The owner can call this themselves; limit_order_admin (an off-chain operational keeper) can also call it on the owner’s behalf — the output still goes to the owner.
For closing fully-settled orders to recover rent, use closeLimitOrder (single) or closeAllLimitOrder (batch). For settling many at once, settleAllLimitOrder packs as many SettleLimitOrder calls as fit into a v0 tx.
List a wallet’s parked orders (off-chain)
// API helper. See api-reference/temp-api-v1.
const active = await fetch(
`https://temp-api-v1.raydium.io/limit-order/order/list?wallet=<your-wallet-pubkey>`,
).then((r) => r.json());
The active-orders endpoint returns both unfilled and partially-filled orders in one payload (totalAmount / filledAmount / pendingSettle distinguish the phases). For closed-order history use /limit-order/history/order/list-by-user?wallet=… (per-wallet, paginated by nextPageId); for the full event log of a specific order use /limit-order/history/event/list-by-pda?pda=….
Rust CPI skeleton
use anchor_lang::prelude::*;
use raydium_amm_v3::cpi;
use raydium_amm_v3::program::AmmV3;
use raydium_amm_v3::cpi::accounts::SwapV2;
#[derive(Accounts)]
pub struct ProxyClmmSwap<'info> {
#[account(mut)]
pub payer: Signer<'info>,
/// CHECK:
pub amm_config: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK:
pub pool_state: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK:
pub input_token_account: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK:
pub output_token_account: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK:
pub input_vault: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK:
pub output_vault: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK:
pub observation_state: UncheckedAccount<'info>,
/// CHECK:
pub token_program: UncheckedAccount<'info>,
/// CHECK:
pub token_program_2022: UncheckedAccount<'info>,
/// CHECK:
pub memo_program: UncheckedAccount<'info>,
/// CHECK:
pub input_vault_mint: UncheckedAccount<'info>,
/// CHECK:
pub output_vault_mint: UncheckedAccount<'info>,
pub clmm_program: Program<'info, AmmV3>,
// `remaining_accounts` carries the tick_array and bitmap_extension accounts.
}
pub fn proxy_swap(
ctx: Context<ProxyClmmSwap>,
amount: u64,
other_amount_threshold: u64,
sqrt_price_limit_x64: u128,
is_base_input: bool,
) -> Result<()> {
let cpi_accounts = SwapV2 {
payer: ctx.accounts.payer.to_account_info(),
amm_config: ctx.accounts.amm_config.to_account_info(),
pool_state: ctx.accounts.pool_state.to_account_info(),
input_token_account: ctx.accounts.input_token_account.to_account_info(),
output_token_account: ctx.accounts.output_token_account.to_account_info(),
input_vault: ctx.accounts.input_vault.to_account_info(),
output_vault: ctx.accounts.output_vault.to_account_info(),
observation_state: ctx.accounts.observation_state.to_account_info(),
token_program: ctx.accounts.token_program.to_account_info(),
token_program_2022: ctx.accounts.token_program_2022.to_account_info(),
memo_program: ctx.accounts.memo_program.to_account_info(),
input_vault_mint: ctx.accounts.input_vault_mint.to_account_info(),
output_vault_mint: ctx.accounts.output_vault_mint.to_account_info(),
};
let cpi_ctx = CpiContext::new(ctx.accounts.clmm_program.to_account_info(), cpi_accounts)
.with_remaining_accounts(ctx.remaining_accounts.to_vec());
cpi::swap_v2(cpi_ctx, amount, other_amount_threshold, sqrt_price_limit_x64, is_base_input)
}
Remaining-account order for SwapV2:
[tick_array_bitmap_extension?, tick_array_0, tick_array_1, …]
If the swap never needs the extension, omit it; otherwise it is the first remaining account.
Common pitfalls
- Off-spacing tick endpoints →
InvalidTickIndex. Always snap via TickUtils.getPriceAndTick.
- Not enough tick arrays supplied in
SwapV2 → TickArrayNotFound. Use computeAmountOutFormat to get the full list.
- Full-range position without the bitmap extension → the extension PDA must be writable; the SDK handles this automatically.
- Mistaking
sqrt_price_x64 for price → a factor-of-2 confusion here is particularly painful. When in doubt, let the SDK compute it from a human-readable price.
- Collecting rewards too eagerly → each collect costs one transaction. Batch via
harvestAllRewards across many positions.
- Burning the NFT while rent is still owed to the mint →
ClosePosition also closes the NFT mint and ATA; do not close them separately or the program will revert.
- Opening a limit order at a non-spaced tick →
InvalidTickIndex. Always quantize via TickUtils.getPriceAndTick.
- Calling
decreaseLimitOrder on a fully-filled order → InvalidOrderPhase. Use settleLimitOrder then closeLimitOrder instead.
- Forgetting
dynamicFeeConfigId while passing enableDynamicFee: true → the CreateCustomizablePool revert is InvalidDynamicFeeConfigParams. Either turn dynamic fee off, or pick a config from /main/clmm-dynamic-config.
Where to go next
Sources: