Skip to main content
Your existing quoting, routing, and swap-building logic stays the same. The only addition: when a market has cold accounts, detect them, and prepend load instructions before the swap.

What changes

Hot market (99%+)Cold market
QuotingNo changeNo change
Swap instructionNo changeNo change
TransactionNo changePrepend create_load_instructions

Detecting cold accounts

Add a cache for cold accounts. This can be separate from your regular account cache.
cold_cache: HashMap<[u8; 32], AccountInterface>, // Accounts with `ColdContext` (used for loading)
If you stream, subscribe to accounts and transactions with the Light Token Program.
SubscriptionDetects
Account sub (owner: cToken...)Hot state + cold-to-hot
Transaction sub (account_include: cToken...)Hot-to-cold
Hot-to-cold — in your transaction handler, listen to accounts whose balance dropped to zero. Async-fetch the AccountInterface (which includes the ColdContext needed for load instructions):
Some(UpdateOneof::Transaction(tx_update)) => {
    if let Some(ref tx_info) = tx_update.transaction {
        for pubkey in find_closed_accounts(tx_info) {
            if cache.remove(&pubkey).is_some() {
                let rpc = rpc.clone();
                let cold_cache = cold_cache.clone();
                tokio::spawn(async move {
                    if let Ok(Some(iface)) = rpc.get_account_interface(&pubkey, None).await {
                        cold_cache.insert(pubkey, iface);
                    }
                });
            }
        }
    }
}
find_closed_accounts checks pre_balances[i] > 0 && post_balances[i] == 0 across all transaction keys (including ALT-loaded addresses). See full implementation. Cold-to-hot — your existing account subscription picks up the hot account again. No cache changes needed because account state does not change while cold.
Some(UpdateOneof::Account(account_update)) => {
    if let Some(account) = account_update.account {
        let pubkey: [u8; 32] = account.pubkey.as_slice().try_into().unwrap();
        // remove from cold cache.
        cold_cache.remove(&pubkey);
    }
}
For the full streaming guide, see Streaming Token Accounts and Streaming Mints. If you don’t stream, call get_multiple_account_interfaces at swap time and check is_cold() to detect cold accounts.

Building swap transactions with cold accounts

When you detect cold accounts in a market (via your cold_set or via is_cold() on fetched accounts), fetch their ColdContext via get_account_interface and build load instructions.
use light_client::interface::{create_load_instructions, LightProgramInterface};

// 1. Identify which accounts the swap touches
let pubkeys = sdk.instruction_accounts(&AmmInstruction::Swap);

// 2. Check which are cold (from your streaming cache, or fetch)
let cold_pubkeys: Vec<_> = pubkeys.iter().filter(|p| cold_set.contains(p)).collect();

// 3. If any are cold, fetch their ColdContext and build load instructions
let mut ixs = vec![];
if !cold_pubkeys.is_empty() {
    let interfaces = rpc
        .get_multiple_account_interfaces(cold_pubkeys, None)
        .await?
        .value;
    let cold: Vec<_> = interfaces.into_iter().flatten().collect();
    let specs = sdk.load_specs(&cold)?;
    ixs.extend(create_load_instructions(&specs, payer, config_pda, &rpc).await?);
}

// 4. Swap instruction is unchanged
ixs.push(sdk.swap_ix(&swap_params)?);

The LightProgramInterface trait

Each rent-free AMM SDK exposes this trait. It tells you which accounts an instruction touches and how to build load specs for cold ones.
pub trait LightProgramInterface {
    type Variant: Pack<AccountMeta> + Clone + Debug;
    type Instruction;

    fn program_id() -> Pubkey;
    fn instruction_accounts(&self, ix: &Self::Instruction) -> Vec<Pubkey>;
    fn load_specs(
        &self,
        cold_accounts: &[AccountInterface],
    ) -> Result<Vec<AccountSpec<Self::Variant>>, Box<dyn Error>>;
}
  • instruction_accounts — returns the pubkeys the instruction reads/writes.
  • load_specs — given cold AccountInterfaces (with ColdContext), returns the AccountSpecs that create_load_instructions needs to bring them back on-chain.

Full example

Dependencies

[dependencies]
light-client = { version = "0.19.0", features = ["v2"] }

# AMM SDK that implements LightProgramInterface (provided by the AMM team)
example-amm-sdk = "0.1"

Code

use light_client::interface::{create_load_instructions, LightProgramInterface};
use example_amm_sdk::{ExampleAmmSdk, AmmInstruction};

// Construct SDK from pool data (same as before -- pool data is always available,
// hot or cold, via get_account_interface or your cache).
let sdk = ExampleAmmSdk::new(pool_address, pool_data)?;

// Quote works the same regardless of hot/cold.
let quote = sdk.quote(amount_in, min_out)?;

// Build transaction.
let mut ixs = vec![];

// Check if any swap accounts are cold.
let pubkeys = sdk.instruction_accounts(&AmmInstruction::Swap);
let cold_pubkeys: Vec<_> = pubkeys.iter().filter(|p| cold_set.contains(p)).collect();

if !cold_pubkeys.is_empty() {
    // Fetch ColdContext for cold accounts.
    let interfaces = rpc
        .get_multiple_account_interfaces(cold_pubkeys, None)
        .await?
        .value;
    let cold: Vec<_> = interfaces.into_iter().flatten().collect();
    let specs = sdk.load_specs(&cold)?;
    ixs.extend(create_load_instructions(&specs, payer.pubkey(), config_pda, &rpc).await?);
}

// Swap instruction is the same as without rent-free accounts.
ixs.push(sdk.swap_ix(&swap_params)?);

rpc.send_transaction(&ixs, &payer).await?;

Key types

TypeSourcePurpose
AccountInterfacelight-clientAccount data with optional ColdContext
LightProgramInterfacelight-clientTrait that AMM SDKs implement
AccountSpeclight-clientInput to create_load_instructions

Reference implementation

ResourceLink
AMM Programcp-swap-reference
LightProgramInterface Trait ImplCpSwapSdk

Hot vs Cold

HotCold
On-chainYesLedger (compressed)
QuoteWorksWorks
SwapDirectLoad first / Bundle
LatencyNormal+0-200ms*
Tx sizeNormal+100-2400 bytes*
CUNormal+15k-400k CU*
Depends on the number and type of cold accounts.

When does a market go cold?

Accounts go cold after extended inactivity. Their virtual rent balance drops below a threshold and miners compress them onto the Solana ledger. They stay cold until any client loads them back in-flight via create_load_instructions. Touching cold markets is rare. The hot path has zero overhead.

FAQ

No. Swap instructions are identical. If the market is hot, the transaction is the same as today. If cold, you prepend create_load_instructions.
Yes. get_account_interface returns full account data regardless of hot/cold. Quoting works the same.
Hot (common path): No.Cold: Loading accounts adds 1-200ms depending on whether a validity proof is needed. If load + swap exceed Solana’s 1232 byte limit, use Jito bundles.
Until they go inactive again. Each write resets the timer. The inactivity threshold is configurable by the program owner (e.g. 24h of no writes).
Send as a Jito bundle:
use solana_sdk::{instruction::Instruction, pubkey::Pubkey, system_instruction};

const JITO_TIP_ACCOUNTS: &[&str] = &[
    "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5",
    "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe",
    "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY",
    "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49",
    "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh",
    "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt",
    "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL",
    "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT",
];

fn jito_tip_ix(payer: &Pubkey, tip_lamports: u64) -> Instruction {
    let tip_account = JITO_TIP_ACCOUNTS[rand::random::<usize>() % JITO_TIP_ACCOUNTS.len()]
        .parse::<Pubkey>().unwrap();
    system_instruction::transfer(payer, &tip_account, tip_lamports)
}

let tip_ix = jito_tip_ix(&payer.pubkey(), 10_000);
swap_ixs.push(tip_ix);

let bundle = vec![load_tx_base64, swap_tx_base64];
let resp = client
    .post("https://mainnet.block-engine.jito.wtf/api/v1/bundles")
    .json(&serde_json::json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "sendBundle",
        "params": [bundle, {"encoding": "base64"}]
    }))
    .send().await?;
Yes. Supported by Helius and Triton. Can also be self-hosted via the open-source Photon indexer.
Hot markets work as long as Solana is up. Cold accounts can’t be loaded until the indexer recovers. Compression is cryptographically verifiable — integrity doesn’t depend on the indexer.
Yes. At swap time, call get_multiple_account_interfaces for the instruction’s accounts and check is_cold(). This adds a round-trip per swap but requires no streaming setup.

API is in Beta and subject to change.Questions or need hands-on support? Telegram | email | Discord