Skip to main content
The Light-SDK pays rent-exemption for your PDAs, token accounts, and mints (98% cost savings). Your program logic stays the same.

What Changes

AreaChange
State structDerive LightAccount and add a compression_info: Option<CompressionInfo> field
AccountsDerive LightAccounts and add #[light_account] on init accounts
Program moduleAdd #[light_program] on top of #[program]
Instructions (swap, deposit, withdraw, …)No changes
Audit overhead is minimal as your program logic is mostly untouched. The rest is macro-generated. If you don’t use Anchor, let us know. References for native solana-program integration coming soon.
You can find a complete rent-free AMM reference implementation here.

Step 1: Dependencies

[dependencies]

light-sdk = { version = "0.18.0", features = ["anchor", "v2", "cpi-context"] }
light-sdk-macros = { version = "0.18.0" }
light-token = { version = "0.3.0", features = ["anchor"] }

light-anchor-spl = { version = "0.31" }    # TokenInterface uses light_token::ID
anchor-lang = "0.31"

Step 2: State Struct

Add compression_info field and derive LightAccount:
use light_sdk::{compressible::CompressionInfo, LightDiscriminator};
use light_sdk_macros::LightAccount;

#[derive(Default, Debug, InitSpace, LightAccount)]
#[account]
pub struct PoolState {
    /// Add this:
    pub compression_info: Option<CompressionInfo>,
    
    /// Your existing fields
    /// ...
}

Step 3: Program

Add #[light_program] above #[program]:
use light_sdk_macros::light_program;

#[light_program]
#[program]
pub mod my_amm {
    use super::*;

    pub fn initialize_pool(ctx: Context<InitializePool>, params: InitializeParams) -> Result<()> {
        process_initialize_pool(ctx, params)
    }

    // These don't change
    pub fn swap(ctx: Context<Swap>, amount_in: u64, min_out: u64) -> Result<()> {
        process_swap(ctx, amount_in, min_out)
    }
}

Step 4: Accounts Struct

Derive LightAccounts on your Accounts struct and add #[light_account(...)] next to #[account(...)].
#[account(
    init, 
    seeds = [...], 
    bump, 
    payer = creator, 
    space = 8 + PoolState::INIT_SPACE
)]
#[light_account(init)]
pub pool_state: Box<Account<'info, PoolState>>,
We also need to add light_token_interface_config, rent_sponsor, and light_token_cpi_authority.
use light_sdk::interface::CreateAccountsProof;
use light_sdk_macros::LightAccounts;
use light_token::instruction::{COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR as LIGHT_TOKEN_RENT_SPONSOR};

#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct InitializeParams {
    pub create_accounts_proof: CreateAccountsProof,
    pub lp_mint_signer_bump: u8,
    pub creator_lp_token_bump: u8,
    pub authority_bump: u8,
}

#[derive(Accounts, LightAccounts)]
#[instruction(params: InitializeParams)]
pub struct InitializePool<'info> {
    #[account(mut)]
    pub creator: Signer<'info>,

    #[account(mut, seeds = [AUTH_SEED.as_bytes()], bump)]
    pub authority: UncheckedAccount<'info>,

    #[account(
        init,
        seeds = [POOL_SEED.as_bytes(), token_0_mint.key().as_ref(), token_1_mint.key().as_ref()],
        bump,
        payer = creator,
        space = 8 + PoolState::INIT_SPACE
    )]
    #[light_account(init)]
    pub pool_state: Box<Account<'info, PoolState>>,

    pub token_0_mint: Box<InterfaceAccount<'info, Mint>>,
    pub token_1_mint: Box<InterfaceAccount<'info, Mint>>,

    #[account(seeds = [POOL_LP_MINT_SIGNER_SEED, pool_state.key().as_ref()], bump)]
    pub lp_mint_signer: UncheckedAccount<'info>,

    #[account(mut)]
    #[light_account(init, mint,
        mint_signer = lp_mint_signer,
        authority = authority,
        decimals = 9,
        mint_seeds = &[POOL_LP_MINT_SIGNER_SEED, self.pool_state.to_account_info().key.as_ref(), &[params.lp_mint_signer_bump]],
        authority_seeds = &[AUTH_SEED.as_bytes(), &[params.authority_bump]]
    )]
    pub lp_mint: UncheckedAccount<'info>,

    #[account(mut, seeds = [POOL_VAULT_SEED.as_bytes(), pool_state.key().as_ref(), token_0_mint.key().as_ref()], bump)]
    #[light_account(token, authority = [AUTH_SEED.as_bytes()])]
    pub token_0_vault: UncheckedAccount<'info>,

    #[account(mut, seeds = [POOL_VAULT_SEED.as_bytes(), pool_state.key().as_ref(), token_1_mint.key().as_ref()], bump)]
    #[light_account(token, authority = [AUTH_SEED.as_bytes()])]
    pub token_1_vault: UncheckedAccount<'info>,

    #[account(mut)]
    pub creator_lp_token: UncheckedAccount<'info>,


    pub light_interface_config: AccountInfo<'info>,
    #[account(address = COMPRESSIBLE_CONFIG_V1)]
    pub light_token_interface_config: AccountInfo<'info>,
    #[account(mut, address = LIGHT_TOKEN_RENT_SPONSOR)]
    pub rent_sponsor: AccountInfo<'info>,
    pub light_token_program: AccountInfo<'info>,
    pub light_token_cpi_authority: AccountInfo<'info>,
    pub system_program: Program<'info, System>,
}

Step 5: Instructions

Replace spl_token with light_token instructions as you need. The API is a superset of SPL-token so switching is straightforward. Examples include: MintToCpi, TransferCpi, TransferInterfaceCpi, CreateTokenAccountCpi, and CreateTokenAtaCpi.
use light_token::instruction::{CreateTokenAccountCpi, CreateTokenAtaCpi, MintToCpi};

pub fn process_initialize_pool(ctx: Context<InitializePool>, params: InitializeParams) -> Result<()> {
    let pool_key = ctx.accounts.pool_state.key();
    
    // Create rent-free token vault
    CreateTokenAccountCpi {
        payer: ctx.accounts.creator.to_account_info(),
        account: ctx.accounts.token_0_vault.to_account_info(),
        mint: ctx.accounts.token_0_mint.to_account_info(),
        owner: ctx.accounts.authority.key(),
    }
    .rent_free(
        ctx.accounts.light_token_interface_config.to_account_info(),
        ctx.accounts.rent_sponsor.to_account_info(),
        ctx.accounts.system_program.to_account_info(),
        &crate::ID,
    )
    .invoke_signed(&[
        POOL_VAULT_SEED.as_bytes(),
        pool_key.as_ref(),
        ctx.accounts.token_0_mint.key().as_ref(),
        &[ctx.bumps.token_0_vault],
    ])?;

    // Create rent-free ATA for LP tokens
    CreateTokenAtaCpi {
        payer: ctx.accounts.creator.to_account_info(),
        owner: ctx.accounts.creator.to_account_info(),
        mint: ctx.accounts.lp_mint.to_account_info(),
        ata: ctx.accounts.creator_lp_token.to_account_info(),
        bump: params.creator_lp_token_bump,
    }
    .idempotent()
    .rent_free(
        ctx.accounts.light_token_interface_config.to_account_info(),
        ctx.accounts.rent_sponsor.to_account_info(),
        ctx.accounts.system_program.to_account_info(),
    )
    .invoke()?;

    // Mint LP tokens (standard CPI, no changes)
    MintToCpi {
        mint: ctx.accounts.lp_mint.to_account_info(),
        destination: ctx.accounts.creator_lp_token.to_account_info(),
        amount: 1000,
        authority: ctx.accounts.authority.to_account_info(),
        system_program: ctx.accounts.system_program.to_account_info(),
        max_top_up: None,
    }
    .invoke_signed(&[&[AUTH_SEED.as_bytes(), &[ctx.bumps.authority]]])?;

    // Populate pool state (unchanged)
    let pool = &mut ctx.accounts.pool_state;
    pool.token_0_vault = ctx.accounts.token_0_vault.key();
    pool.lp_mint = ctx.accounts.lp_mint.key();
    // ...

    Ok(())
}

Client SDK

To make it easy for clients to integrate with your program, ship an SDK crate implementing the LightProgramInterface trait. For a detailed example of how clients use this trait, check out the Router Integration page.
pub struct AmmSdk {
    pool_state_pubkey: Option<Pubkey>,
    token_0_vault: Option<Pubkey>,
    token_1_vault: Option<Pubkey>,
    // ... other fields
    program_owned_specs: HashMap<Pubkey, PdaSpec<LightAccountVariant>>,
}

pub enum AmmInstruction {
    Swap,
    Deposit,
    Withdraw,
}

impl LightProgramInterface for AmmSdk {
    type Variant = LightAccountVariant;
    type Instruction = AmmInstruction;
    type Error = AmmSdkError;

    fn program_id(&self) -> Pubkey {
        PROGRAM_ID
    }

    fn from_keyed_accounts(accounts: &[AccountInterface]) -> Result<Self, Self::Error> {
        let mut sdk = Self::new();
        for account in accounts {
            sdk.parse_account(account)?;
        }
        Ok(sdk)
    }

    fn get_accounts_to_update(&self, ix: &Self::Instruction) -> Vec<AccountToFetch> {
        match ix {
            AmmInstruction::Swap => vec![
                AccountToFetch::pda(self.pool_state_pubkey.unwrap(), PROGRAM_ID),
                AccountToFetch::token(self.token_0_vault.unwrap()),
                AccountToFetch::token(self.token_1_vault.unwrap()),
            ],
            // ...
        }
    }

    fn update(&mut self, accounts: &[AccountInterface]) -> Result<(), Self::Error> {
        for account in accounts {
            self.parse_account(account)?;
        }
        Ok(())
    }

    fn get_specs_for_instruction(&self, ix: &Self::Instruction) -> Vec<AccountSpec<Self::Variant>> {
        // Return specs for accounts needed by this instruction
        // Specs include the variant (seeds) needed for loading cold accounts back onchain.
        self.program_owned_specs
            .values()
            .cloned()
            .map(AccountSpec::Pda)
            .collect()
    }
}
ResourceLink
Trait Implementation ExampleCpSwapSdk

Testing

use light_program_test::{LightProgramTest, ProgramTestConfig, Rpc};
use light_sdk::interface::rent::SLOTS_PER_EPOCH;
use light_client::interface::{create_load_instructions, LightProgramInterface, AccountSpec};

#[tokio::test]
async fn test_pool_lifecycle() {
    let config = ProgramTestConfig::new_v2(true, Some(vec![("my_amm", MY_AMM_ID)]));
    let mut rpc = LightProgramTest::new(config).await.unwrap();

    // 1. Init pool (rent-free)
    // ... build and send init instruction ...
    assert!(rpc.get_account_interface(&pool_address, &program_id).await.unwrap().is_some());

    // 2. Swap (hot path - works normally)
    // ... build and send swap instruction ...

    // 3. Trigger compression (advance time)
    rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap();

    let pool_interface = rpc
        .get_account_interface(&pool_address, &program_id)
        .await
        .unwrap();
    assert!(pool_interface.is_cold()); // get_account would return None

    // 5. get load instructions
    let mut sdk = AmmSdk::from_keyed_accounts(&[pool_interface]).unwrap();
    let accounts_to_fetch = sdk.get_accounts_to_update(&AmmInstruction::Deposit);
    let keyed_accounts = rpc.get_multiple_account_interfaces(&accounts_to_fetch).await.unwrap();
    sdk.update(&keyed_accounts).unwrap();
    
    let specs = sdk.get_specs_for_instruction(&AmmInstruction::Deposit);
    let load_ixs = create_load_instructions(
        &specs,
        payer.pubkey(),
        config_pda,
        payer.pubkey(),
        &rpc,
    ).await.unwrap();

    // 6. send transaction
    rpc.create_and_send_transaction(&load_ixs, &payer.pubkey(), &[&payer]).await.unwrap();
    assert!(rpc.get_account_interface(&pool_address, &program_id).await.unwrap().is_hot());
}
ResourceLink
Test exampleprogram.rs

How it works

The SDK pays the rent-exemption cost. Inactive (cold) accounts auto-compress. Your program only ever interacts with hot accounts. Clients can load cold accounts back when needed via create_load_instructions. Under the hood, clients use AccountInterface - a superset of Solana’s Account that unifies hot and cold state. See Router Integration for details.
Hot (active)Cold (inactive)
StorageOn-chainCompressed
Latency/CUNo change+load instruction
Your program codeNo changeNo change

Existing programs

If you have an existing program that you would like to migrate to rent-free accounts, join our tech Discord for migration support.

FAQ

When you create an account, under the hood, the SDK auto-provides a proof that verifies that the account does not yet exist in the compressed address space. The SVM takes care of uniqueness in the onchain space. This way both account spaces are covered, preventing re-init attacks.
Miners automatically compress when virtual rent is below threshold (eg 24h without write bump).
No. Any write bumps the virtual rent balance. Active pools never compress.
No. Helius and Triton run the Interface RPC endpoints, self-hosting optional.
Hot pools work normally. Cold pools can’t load until recovery. No data or safety loss.

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