Skip to main content
Creation CostRegular PDALight-PDA
100-byte account~1,600,000 lamports~11,500 lamports
A Light-PDA is a standard Solana PDA. Seeds, bump derivation, and invoke_signed work the same way. Your instruction handlers for reads, updates, and closes don’t change.

What changes

Audit overhead is minimal as your program logic is mostly untouched. The rest is macro-generated.
AreaChange
State structDerive LightAccount, add compression_info: CompressionInfo
Accounts structDerive LightAccounts, add #[light_account] on init accounts
Program moduleAdd #[light_program] above #[program]
Instructions (reads, updates, closes)No program changes. Client prepends a load instruction if account is cold.

Install the agent skill:
npx skills add https://zkcompression.com
See the AI tools guide for dedicated skills.

Step 1: Dependencies

[dependencies]
light-account = { version = "0.20.0", features = ["anchor"] }
light-sdk = { version = "0.20.0", features = ["anchor", "v2", "cpi-context"] }
anchor-lang = "0.31"

Step 2: State struct

Add compression_info field and derive LightAccount:
use light_account::{CompressionInfo, LightAccount};

#[derive(Default, Debug, InitSpace, LightAccount)]
#[account]
pub struct Counter {
    /// Add this:
    pub compression_info: CompressionInfo,

    pub owner: Pubkey,
    pub count: u64,
}

Step 3: Program module

Add #[light_program] above #[program]. Define the CPI signer constant with your program ID:
use light_account::{derive_light_cpi_signer, light_program, CpiSigner};

pub const LIGHT_CPI_SIGNER: CpiSigner =
    derive_light_cpi_signer!("YourProgramId11111111111111111111111111111111");

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

    pub fn create_counter<'info>(
        ctx: Context<'_, '_, '_, 'info, CreateCounter<'info>>,
        params: CreateCounterParams,
    ) -> Result<()> {
        ctx.accounts.counter.owner = ctx.accounts.owner.key();
        ctx.accounts.counter.count = params.count;
        Ok(())
    }

    /// Standard Anchor — no Light-specific changes.
    pub fn increment(ctx: Context<Increment>) -> Result<()> {
        ctx.accounts.counter.count = ctx.accounts.counter.count.checked_add(1).unwrap();
        Ok(())
    }

    /// Standard Anchor — no Light-specific changes.
    pub fn close_counter(_ctx: Context<CloseCounter>) -> Result<()> {
        Ok(())
    }
}

Step 4: Accounts struct

Derive LightAccounts on your Accounts struct and add #[light_account(...)] next to #[account(...)].Only the init struct derives LightAccounts. The increment and close structs are standard Anchor:
use light_account::{CreateAccountsProof, LightAccounts};

#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct CreateCounterParams {
    pub create_accounts_proof: CreateAccountsProof,
    pub count: u64,
}

#[derive(Accounts, LightAccounts)]
#[instruction(params: CreateCounterParams)]
pub struct CreateCounter<'info> {
    #[account(mut)]
    pub fee_payer: Signer<'info>,

    /// CHECK: Read-only, used for PDA derivation.
    pub owner: AccountInfo<'info>,

    /// CHECK: Validated by Light Protocol CPI.
    pub compression_config: AccountInfo<'info>,

    /// CHECK: PDA rent sponsor for compression rent reimbursement.
    #[account(mut)]
    pub pda_rent_sponsor: AccountInfo<'info>,

    #[account(
        init,
        payer = fee_payer,
        space = 8 + <Counter as anchor_lang::Space>::INIT_SPACE,
        seeds = [COUNTER_SEED, owner.key().as_ref()],
        bump,
    )]
    #[light_account(init)]
    pub counter: Account<'info, Counter>,

    pub system_program: Program<'info, System>,
}

Full Example

lib.rs
use anchor_lang::prelude::*;
use light_account::{
    CompressionInfo, LightAccount, LightAccounts, CreateAccountsProof,
    derive_light_cpi_signer, light_program, CpiSigner,
};

declare_id!("YourProgramId11111111111111111111111111111111");

pub const LIGHT_CPI_SIGNER: CpiSigner =
    derive_light_cpi_signer!("YourProgramId11111111111111111111111111111111");

pub const COUNTER_SEED: &[u8] = b"counter";

#[derive(Default, Debug, InitSpace, LightAccount)]
#[account]
pub struct Counter {
    pub compression_info: CompressionInfo,
    pub owner: Pubkey,
    pub count: u64,
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct CreateCounterParams {
    pub create_accounts_proof: CreateAccountsProof,
    pub count: u64,
}

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

    pub fn create_counter<'info>(
        ctx: Context<'_, '_, '_, 'info, CreateCounter<'info>>,
        params: CreateCounterParams,
    ) -> Result<()> {
        ctx.accounts.counter.owner = ctx.accounts.owner.key();
        ctx.accounts.counter.count = params.count;
        Ok(())
    }

    /// Standard Anchor — no Light-specific changes.
    pub fn increment(ctx: Context<Increment>) -> Result<()> {
        ctx.accounts.counter.count = ctx.accounts.counter.count.checked_add(1).unwrap();
        Ok(())
    }

    /// Standard Anchor — no Light-specific changes.
    pub fn close_counter(_ctx: Context<CloseCounter>) -> Result<()> {
        Ok(())
    }
}

#[derive(Accounts, LightAccounts)]
#[instruction(params: CreateCounterParams)]
pub struct CreateCounter<'info> {
    #[account(mut)]
    pub fee_payer: Signer<'info>,

    /// CHECK: Read-only, used for PDA derivation.
    pub owner: AccountInfo<'info>,

    /// CHECK: Validated by Light Protocol CPI.
    pub compression_config: AccountInfo<'info>,

    /// CHECK: PDA rent sponsor for compression rent reimbursement.
    #[account(mut)]
    pub pda_rent_sponsor: AccountInfo<'info>,

    #[account(
        init,
        payer = fee_payer,
        space = 8 + <Counter as anchor_lang::Space>::INIT_SPACE,
        seeds = [COUNTER_SEED, owner.key().as_ref()],
        bump,
    )]
    #[light_account(init)]
    pub counter: Account<'info, Counter>,

    pub system_program: Program<'info, System>,
}

/// Standard Anchor
#[derive(Accounts)]
pub struct Increment<'info> {
    pub owner: Signer<'info>,

    #[account(
        mut,
        seeds = [COUNTER_SEED, owner.key().as_ref()],
        bump,
        has_one = owner,
    )]
    pub counter: Account<'info, Counter>,
}

/// Standard Anchor close
#[derive(Accounts)]
pub struct CloseCounter<'info> {
    #[account(mut)]
    pub fee_payer: Signer<'info>,

    pub owner: Signer<'info>,

    #[account(
        mut,
        close = fee_payer,
        seeds = [COUNTER_SEED, owner.key().as_ref()],
        bump,
        has_one = owner,
    )]
    pub counter: Account<'info, Counter>,
}
View counter example on Github: counter

How it works

The SDK sponsors rent-exemption so you don’t pay the full rent-exempt balance upfront. After extended inactivity, the account compresses to cold state and returns the rent-exempt balance to the rent sponsor. Clients call create_load_instructions to load a cold account back on-chain when it’s needed again. Your program only ever interacts with hot accounts.
Hot (active)Cold (inactive)
StorageOn-chainCompressed
Latency/CUNo change+load instruction
Your program codeNo changeNo change

FAQ

When creating an account for the first time, the SDK provides a proof that the account doesn’t exist in the cold address space. The SVM already verifies this for the onchain space. Both address spaces are checked before creation, preventing re-init attacks, even if the account is currently cold.
Miners (Forester nodes) compress accounts that have been inactive for an extended period of time (when their virtual rent balance drops below threshold). In practice, having to load cold accounts should be rare. The common path (hot) has no extra overhead and does not increase CU or txn size.
When accounts compress after extended inactivity, the on-chain rent-exemption is released back to the rent sponsor. This creates a revolving lifecycle: active “hot” accounts hold a rent-exempt lamports balance, inactive “cold” accounts release it back. The rent sponsor must be derived from the program owner. For all mint, ATA, and token accounts, the Light Token Program is the rent sponsor. For your own program-owned PDAs, the SDK derives a rent sponsor address automatically.
Hot path (e.g. reads, updates, closes): No. Active accounts do not add CU overhead to your instructions.First time init + loading cold accounts: Yes, adds up to 15k-400k CU, depending on number and type of accounts being initialized or loaded.

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