Skip to main content

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.

This page describes the layout and role of each account. Seeds are canonical and listed in reference/program-addresses. A CLMM pool is more account-heavy than a CPMM pool because liquidity is stored sparsely across the tick range; understanding that sparsity is the bulk of this page.

Account inventory

A live CLMM pool is described by the following account families. All are owned by the CLMM program except the two mints and their vaults.
AccountPurposeCount per pool
AmmConfigFee tier: trade-fee rate, protocol share, fund share, default tick-spacing. Shared across all pools in this tier.1 (shared)
PoolStateCurrent sqrt_price_x64, current tick, total liquidity, fee growth globals, reward info, observation pointer.1
TickArrayStateA block of TICK_ARRAY_SIZE adjacent ticks. Only initialized on demand.0 ≤ N ≤ range
TickArrayBitmapExtensionOverflow bitmap tracking which tick arrays exist past the inline bitmap in PoolState.0 or 1
PersonalPositionStateOne per LP position. Stores the range, liquidity, and last-seen fee/reward growth. Authority = NFT owner.1 per position
Position NFT mintMint with supply 1, associated with PersonalPositionState. Transferring transfers the position.1 per position
ObservationStateRing buffer of price observations for the TWAP.1
token_0_vault, token_1_vaultToken accounts holding the pool’s balances. Owned by pool authority.2
DynamicFeeConfigReusable parameter set for the dynamic-fee mechanism. Pools created via create_customizable_pool can opt in. Admin-managed.shared (per index)
LimitOrderStateOne per open limit order. Records owner, tick, side, total amount, settled-output snapshot.1 per order
LimitOrderNoncePer-(wallet, nonce_index) counter that derives unique order PDAs.1 per (wallet, index)

PoolState

The pool’s live state, read on every swap and every position change.
// programs/amm/src/states/pool.rs
pub struct PoolState {
    pub bump:           [u8; 1],
    pub amm_config:     Pubkey,            // fee tier binding
    pub owner:          Pubkey,            // admin (multisig)
    pub token_mint_0:   Pubkey,
    pub token_mint_1:   Pubkey,
    pub token_vault_0:  Pubkey,
    pub token_vault_1:  Pubkey,
    pub observation_key: Pubkey,

    pub mint_decimals_0: u8,
    pub mint_decimals_1: u8,
    pub tick_spacing:   u16,               // inherited from amm_config at init

    pub liquidity:      u128,              // total active (in-range) liquidity
    pub sqrt_price_x64: u128,              // Q64.64 of sqrt(price)
    pub tick_current:   i32,               // current tick index

    pub padding3:       u16,
    pub padding4:       u16,

    // Global fee growth per unit of liquidity, Q64.64.
    pub fee_growth_global_0_x64: u128,
    pub fee_growth_global_1_x64: u128,

    // Accrued-but-not-swept protocol fees (per mint).
    pub protocol_fees_token_0: u64,
    pub protocol_fees_token_1: u64,

    // Reserved padding for future upgrades.
    pub padding5: [u128; 4],

    // Status bitmask. Bits 0-5: open-position, decrease-liquidity,
    // collect-fee, collect-reward, swap, limit-order. A set bit disables
    // the corresponding operation.
    pub status:  u8,

    // Fee-collection mode (CollectFeeOn).
    //   0 = FromInput (deduct fee from the swap input — Uniswap-V3 default)
    //   1 = Token0Only (always deduct fee from token0 vault)
    //   2 = Token1Only (always deduct fee from token1 vault)
    pub fee_on: u8,
    pub padding: [u8; 6],

    // Live reward streams (up to REWARD_NUM = 3).
    pub reward_infos: [RewardInfo; 3],

    // Inline bitmap tracking initialized tick-arrays in the primary range.
    pub tick_array_bitmap: [u64; 16],

    // Reserved padding for future upgrades.
    pub padding6: [u64; 4],

    pub fund_fees_token_0: u64,
    pub fund_fees_token_1: u64,

    pub open_time:    u64,                 // currently disabled by the program
    pub recent_epoch: u64,

    // Per-pool dynamic-fee state. Zero-valued unless the pool was
    // created with `enable_dynamic_fee = true` via create_customizable_pool.
    pub dynamic_fee_info: DynamicFeeInfo,

    // Reserved for future upgrades.
    pub padding1: [u64; 14],
    pub padding2: [u64; 32],
}
Fields you will actually touch:
  • sqrt_price_x64 and tick_current are the pool’s price state. They are updated together on every swap. tick_current is the floor of log_{1.0001}(price).
  • liquidity is the active liquidity — the sum of L values for all positions whose range contains tick_current. It changes every time a swap crosses a tick and every time a position is opened/closed/resized.
  • fee_growth_global_{0,1}_x64 are the cumulative fees earned per unit of liquidity across the entire pool history. Positions read this to compute what’s owed to them.
  • tick_spacing is locked to the AmmConfig at initialization and never changes. It determines which tick indices are even allowed to be position endpoints.
  • tick_array_bitmap is an inline bitmap covering the commonly used tick range around spot price. For pools whose positions reach far out, overflow tracking lives in the separate TickArrayBitmapExtension.
  • fee_on is fixed at pool creation. 0 (FromInput) reproduces classic Uniswap-V3 behavior. 1 and 2 route the swap fee to a single side of the book — see products/clmm/fees for trade-offs.
  • dynamic_fee_info carries volatility state for the dynamic-fee surcharge. When enabled, every swap recomputes a dynamic_fee_component on top of AmmConfig.trade_fee_rate. Layout is documented under DynamicFeeInfo below; pools without dynamic fee leave the entire struct zero.

AmmConfig

pub struct AmmConfig {
    pub bump: u8,
    pub index: u16,                       // uses "amm_config"+u16 seed

    pub owner:             Pubkey,        // admin
    pub protocol_fee_rate: u32,           // fraction of trade fee to protocol, denom 1e6
    pub trade_fee_rate:    u32,           // trade fee in 1e6ths of volume
    pub tick_spacing:      u16,           // default spacing for pools using this config
    pub fund_fee_rate:     u32,           // fraction of trade fee to fund, denom 1e6
    pub padding_u32: u32,

    pub fund_owner: Pubkey,
    pub padding: [u64; 3],
}
A typical published set of CLMM fee tiers (confirm against GET https://api-v3.raydium.io/main/clmm-config):
Indextrade_fee_rateTick spacingTypical use
0100 (0.01%)1Stable pairs, USDC/USDT
1500 (0.05%)10Correlated blue-chips
22_500 (0.25%)60Standard pairs
310_000 (1.00%)120Volatile or long-tail
protocol_fee_rate and fund_fee_rate are fractions of the trade fee; same convention as CPMM. See products/clmm/fees.

TickArrayState

CLMM does not store a single record per tick. That would be billions of accounts. Instead it groups TICK_ARRAY_SIZE adjacent initialized-or-not ticks (typically 60 or 88 depending on program version) into a TickArrayState that is lazily created on first use.
pub const TICK_ARRAY_SIZE: usize = 60;
pub const TICK_ARRAY_SIZE_USIZE: usize = 60;

pub struct TickArrayState {
    pub pool_id:                Pubkey,
    pub start_tick_index:       i32,                            // lowest tick in this array
    pub ticks:                  [TickState; TICK_ARRAY_SIZE],   // 60 entries
    pub initialized_tick_count: u8,
    pub recent_epoch:           u64,
    pub padding:                [u8; 107],
}

pub struct TickState {
    pub tick:                       i32,
    pub liquidity_net:              i128,                       // ΔL when crossing this tick upward
    pub liquidity_gross:            u128,                       // total L referencing this tick
    pub fee_growth_outside_0_x64:   u128,                       // see math.mdx
    pub fee_growth_outside_1_x64:   u128,
    pub reward_growths_outside_x64: [u128; 3],

    // Limit-order bookkeeping. All zero for ticks that have never carried
    // a limit order. See products/clmm/math for the matching algorithm.
    pub order_phase:                  u64,                      // monotonic FIFO cohort id
    pub orders_amount:                u64,                      // unfilled tokens in current cohort
    pub part_filled_orders_remaining: u64,                      // remaining tokens of partially-filled cohort
    pub unfilled_ratio_x64:           u128,                     // Q64.64; starts at 1.0 and shrinks as fills occur

    pub padding:                    [u32; 3],
}
The four limit-order fields are zero on any tick that has never been used for a limit order. When orders are opened on a tick, the program tracks them as a sequence of cohorts:
  • order_phase is the cohort id. It increments every time a cohort transitions from “all unfilled” to “partially filled.”
  • orders_amount is the input-token total of the current (newest) cohort.
  • part_filled_orders_remaining tracks the previous cohort that is currently being filled by ongoing swaps.
  • unfilled_ratio_x64 is a Q64.64 multiplier carried on the cohort: when a swap fills X% of the cohort, the ratio is multiplied by (1 − X). Each open order stores its own (order_phase, unfilled_ratio_x64) snapshot at open time, so settle math reduces to comparing snapshots.
Rules:
  • A position endpoint tick t must satisfy t % tick_spacing == 0. The program rejects off-spacing positions.
  • The tick’s array is located at floor(t / (TICK_ARRAY_SIZE * tick_spacing)) * (TICK_ARRAY_SIZE * tick_spacing).
  • A tick array is initialized lazily: the first position or swap that touches an uninitialized array creates it, paying the rent.
  • A tick array is never closed by the program. Once allocated it persists for the life of the pool, even after every tick inside it returns to liquidity_gross == 0. Subsequent positions and swaps reuse the existing account at no extra rent. There is no ClosePosition-driven cleanup path for tick arrays.

TickArrayBitmapExtension

PoolState.tick_array_bitmap (inline) covers the “close to spot” range — ±1,024 tick arrays. Outside that range (for extreme tick values), the program maintains an extension account:
pub struct TickArrayBitmapExtension {
    pub pool_id: Pubkey,
    pub positive_tick_array_bitmap: [[u64; 8]; 14],
    pub negative_tick_array_bitmap: [[u64; 8]; 14],
}
If your position’s range is “normal”, you never think about the extension account. Full-range positions (e.g., (MIN_TICK, MAX_TICK)) require it; the SDK resolves it for you.

Positions

A CLMM position is a bundle of three accounts plus a mint:

Position NFT mint

An SPL Token mint with supply 1. The mint’s address is a deterministic PDA; the position NFT in the owner’s wallet is just an ATA holding that single token. Transferring the NFT is how a position changes hands — the program keys authorization to the current holder of the NFT’s ATA balance, not to a Pubkey stored in state.

PersonalPositionState

One per open position. Keyed off the NFT mint.
pub struct PersonalPositionState {
    pub bump: [u8; 1],
    pub nft_mint: Pubkey,                 // this position's NFT mint
    pub pool_id:  Pubkey,

    pub tick_lower_index: i32,
    pub tick_upper_index: i32,

    pub liquidity: u128,                  // this position's L

    // Fee-growth snapshots at last time the position was touched.
    pub fee_growth_inside_0_last_x64: u128,
    pub fee_growth_inside_1_last_x64: u128,
    pub token_fees_owed_0: u64,           // accrued since last collect
    pub token_fees_owed_1: u64,

    pub reward_infos: [PositionRewardInfo; 3],
    pub recent_epoch: u64,
    pub padding: [u64; 7],
}

pub struct PositionRewardInfo {
    pub growth_inside_last_x64: u128,
    pub reward_amount_owed: u64,
}

ProtocolPositionState (deprecated)

Older CLMM releases stored aggregate per-(pool, tick_lower, tick_upper) bookkeeping in a ProtocolPositionState PDA. Newer releases no longer create or read this account. The slot still appears on the OpenPosition / IncreaseLiquidity / DecreaseLiquidity account lists as an UncheckedAccount for ABI compatibility, but the program does not write to it. Existing accounts on-chain are vestigial; the admin can call CloseProtocolPosition to reclaim rent for them.Aggregate range bookkeeping is now derived directly from the two endpoint ticks (liquidity_gross, liquidity_net, and the per-tick fee_growth_outside_* / reward_growths_outside_x64) in TickArrayState. The fee-growth-inside formula fee_growth_inside = global − outside_lower − outside_upper continues to work without an aggregate position account.

Observation

pub const OBSERVATION_NUM: usize = 100;

pub struct Observation {
    pub block_timestamp: u32,
    pub tick_cumulative: i64,                            // Σ tick_current × Δt
    pub padding:         [u64; 4],
}

pub struct ObservationState {
    pub initialized:       bool,
    pub recent_epoch:      u64,
    pub observation_index: u16,
    pub pool_id:           Pubkey,
    pub observations:      [Observation; OBSERVATION_NUM], // 100 entries
    pub padding:           [u64; 4],
}
CLMM’s observation buffer stores a cumulative tick, not a cumulative price. External consumers compute the geometric-mean price over an interval from (tick_cumulative[t1] − tick_cumulative[t0]) / (t1 − t0) and then price = 1.0001 ** tick. See algorithms/clmm-math.

DynamicFeeConfig and DynamicFeeInfo

Dynamic fee parameters live in two places. The reusable template — DynamicFeeConfig — is admin-managed and shared across pools that opt in. The per-pool runtime state — DynamicFeeInfo — is embedded in PoolState and updated by every swap.

DynamicFeeConfig

// programs/amm/src/states/pool_fee.rs
pub struct DynamicFeeConfig {
    pub index:                       u16,    // identifier; PDA seed component
    pub filter_period:               u16,    // seconds — within this window the volatility reference is held
    pub decay_period:                u16,    // seconds — beyond this window the reference fully decays
    pub reduction_factor:            u16,    // fixed-point in [1, 10_000); applied at decay
    pub dynamic_fee_control:         u32,    // fixed-point in (0, 100_000); fee-rate gain
    pub max_volatility_accumulator:  u32,    // ceiling on the volatility accumulator
    pub padding:                     [u64; 8],
}
PDA seed: ["dynamic_fee_config", index.to_be_bytes()]. Created via create_dynamic_fee_config (admin-gated) and modified via update_dynamic_fee_config. A pool created with enable_dynamic_fee = true snapshots the config’s five calibration parameters (filter_period, decay_period, reduction_factor, dynamic_fee_control, max_volatility_accumulator) into its own DynamicFeeInfo at creation time; later edits to the DynamicFeeConfig do not retroactively affect existing pools.

DynamicFeeInfo (embedded in PoolState)

pub struct DynamicFeeInfo {
    pub filter_period:                u16,
    pub decay_period:                 u16,
    pub reduction_factor:             u16,
    pub dynamic_fee_control:          u32,
    pub max_volatility_accumulator:   u32,
    pub tick_spacing_index_reference: i32,    // tick-spacing-units; reference for next swap
    pub volatility_reference:         u32,    // running floor for the accumulator
    pub volatility_accumulator:       u32,    // current cumulative volatility (capped)
    pub last_update_timestamp:        u64,
    pub padding:                      [u8; 46],
}
The bottom four fields are state; the top five are calibration copied from DynamicFeeConfig. The fee math and the decay rules are documented under products/clmm/math and products/clmm/fees. Constants used by the formula:
ConstantValueMeaning
VOLATILITY_ACCUMULATOR_SCALE10_000Granularity of the volatility accumulator
REDUCTION_FACTOR_DENOMINATOR10_000Denominator for reduction_factor
DYNAMIC_FEE_CONTROL_DENOMINATOR100_000Denominator for dynamic_fee_control
MAX_FEE_RATE_NUMERATOR100_000Hard cap of 10% on the resulting fee rate

LimitOrderState

One account per open limit order.
// programs/amm/src/states/limit_order.rs
pub struct LimitOrderState {
    pub pool_id:            Pubkey,
    pub owner:              Pubkey,
    pub tick_index:         i32,
    pub zero_for_one:       bool,    // direction: true sells token0 for token1
    pub order_phase:        u64,     // snapshot of TickState.order_phase at open time
    pub total_amount:       u64,     // input-token amount placed
    pub filled_amount:      u64,     // informational; computed precisely on settle
    pub settle_base:        u64,     // unfilled remainder at last settle/decrease
    pub settled_output:     u64,     // cumulative output-token paid to owner
    pub open_time:          u64,
    pub unfilled_ratio_x64: u128,    // Q64.64 snapshot of TickState.unfilled_ratio_x64 at open
    pub padding:            [u64; 4],
}
Lifecycle:
  1. Open — user calls open_limit_order, deposits total_amount of the input token, the order is bound to a TickState cohort.
  2. (optional) Increase / Decreaseincrease_limit_order adds to total_amount; decrease_limit_order returns unfilled tokens (and any settled output up to that point).
  3. Settle — when the cohort is fully or partially filled, the owner or the operational keeper calls settle_limit_order to push output tokens to the owner’s ATA.
  4. Close — once unfilled_amount == 0, the account is closeable. Rent always returns to owner.
PDA seed: [owner.as_ref(), limit_order_nonce.key().as_ref(), limit_order_nonce.order_nonce.to_be_bytes().as_ref()]. The order PDA is therefore unique per (owner, nonce_index, order_nonce).

LimitOrderNonce

Per-(wallet, nonce_index) counter that lets a single user run multiple parallel pipelines of limit orders without colliding on PDAs.
pub struct LimitOrderNonce {
    pub user_wallet: Pubkey,
    pub nonce_index: u8,             // user-chosen, 0..255
    pub order_nonce: u64,            // monotonic, incremented every time a new order is opened
    pub padding:     [u64; 4],
}
PDA seed: [user_wallet.as_ref(), &[nonce_index]]. Most clients use nonce_index = 0 and let order_nonce carry the cardinality.

Deriving the key accounts

import { PublicKey } from "@solana/web3.js";

const CLMM_PROGRAM_ID = new PublicKey(
  "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"
); // see reference/program-addresses

function i32ToBytes(n: number): Buffer {
  const b = Buffer.alloc(4);
  b.writeInt32BE(n);
  return b;
}

export function deriveClmmAccounts(
  ammConfig: PublicKey,
  token0Mint: PublicKey,           // must already be sorted
  token1Mint: PublicKey,
) {
  const [poolState] = PublicKey.findProgramAddressSync(
    [Buffer.from("pool"), ammConfig.toBuffer(),
     token0Mint.toBuffer(), token1Mint.toBuffer()],
    CLMM_PROGRAM_ID,
  );
  const [observation] = PublicKey.findProgramAddressSync(
    [Buffer.from("observation"), poolState.toBuffer()],
    CLMM_PROGRAM_ID,
  );
  const [tickArrayBitmapExtension] = PublicKey.findProgramAddressSync(
    [Buffer.from("pool_tick_array_bitmap_extension"), poolState.toBuffer()],
    CLMM_PROGRAM_ID,
  );
  return { poolState, observation, tickArrayBitmapExtension };
}

export function deriveTickArray(
  pool: PublicKey,
  startTickIndex: number,
) {
  const [tickArray] = PublicKey.findProgramAddressSync(
    [Buffer.from("tick_array"), pool.toBuffer(), i32ToBytes(startTickIndex)],
    CLMM_PROGRAM_ID,
  );
  return tickArray;
}

export function deriveDynamicFeeConfig(index: number) {
  const idx = Buffer.alloc(2);
  idx.writeUInt16BE(index);
  const [pda] = PublicKey.findProgramAddressSync(
    [Buffer.from("dynamic_fee_config"), idx],
    CLMM_PROGRAM_ID,
  );
  return pda;
}

export function deriveLimitOrderNonce(
  wallet: PublicKey,
  nonceIndex: number,
) {
  const [pda] = PublicKey.findProgramAddressSync(
    [wallet.toBuffer(), Buffer.from([nonceIndex & 0xff])],
    CLMM_PROGRAM_ID,
  );
  return pda;
}

export function deriveLimitOrder(
  wallet: PublicKey,
  nonceAccount: PublicKey,
  orderNonce: bigint,
) {
  const nonceBytes = Buffer.alloc(8);
  nonceBytes.writeBigUInt64BE(orderNonce);
  const [pda] = PublicKey.findProgramAddressSync(
    [wallet.toBuffer(), nonceAccount.toBuffer(), nonceBytes],
    CLMM_PROGRAM_ID,
  );
  return pda;
}

export function derivePersonalPosition(nftMint: PublicKey) {
  const [personalPosition] = PublicKey.findProgramAddressSync(
    [Buffer.from("position"), nftMint.toBuffer()],
    CLMM_PROGRAM_ID,
  );
  return personalPosition;
}
The exact seed strings should always be double-checked against the on-chain IDL and reference/program-addresses.

Lifecycle quick reference

EventAccounts createdAccounts destroyed
CreatePoolpoolState, observation, token_0_vault, token_1_vault
OpenPosition[WithToken22Nft]NFT mint + ATA, personalPosition, possibly new tickArrayState(s), tickArrayBitmapExtension if not yet existing
IncreaseLiquidityPossibly new tickArrayState(s)
DecreaseLiquidityPossibly clears tick entries (but the tickArrayState itself is not closed)
ClosePositionNFT mint, personalPosition
SwapV2Possibly new tickArrayState
OpenLimitOrderlimitOrderState, possibly limitOrderNonce (init-if-needed), possibly new tickArrayState
IncreaseLimitOrder
DecreaseLimitOrderCloses limitOrderState if order is fully consumed
SettleLimitOrder
CloseLimitOrderlimitOrderState (rent → owner)
CreateDynamicFeeConfigdynamicFeeConfig
CreateCustomizablePoolpoolState, observation, vaults — same as CreatePool. Snapshots dynamicFeeConfig if enable_dynamic_fee = true.
CollectRewards
UpdateRewardInfos
CloseProtocolPosition (admin)Vestigial protocolPositionState (rent → admin)
TickArrayState accounts are never closed by the program — they persist for the life of the pool. Once a tick array has been initialised it remains on-chain even when every tick inside it returns to liquidity_gross == 0. Re-using an existing tick array is free; only the first position to touch a never-initialised array pays its rent.

What to read where

Sources: