> ## Documentation Index
> Fetch the complete documentation index at: https://www.zkcompression.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Light PDA

> Create Solana PDA accounts with sponsored rent-exemption and minimal code changes. Use like any Solana PDA, e.g. for DeFi pools, vaults, pool accounts, or other shared state.

| Creation Cost        |  Regular PDA |      Light-PDA |
| :------------------- | -----------: | -------------: |
| **100-byte account** | \~0.0016 SOL | \~0.000012 SOL |

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.

| Area                                  | Change                                                                     |
| ------------------------------------- | -------------------------------------------------------------------------- |
| State struct                          | Derive `LightAccount`, add `compression_info: CompressionInfo`             |
| Accounts struct                       | Derive `LightAccounts`, add `#[light_account]` on init accounts            |
| Program module                        | Add `#[light_program]` above `#[program]`                                  |
| Instructions (reads, updates, closes) | No program changes. Client prepends a load instruction if account is cold. |

***

<Accordion title="Agent skill">
  Install or view [dedicated agent skills](/ai-tools/overview#agent-skills).

  ```
  npx skills add Lightprotocol/skills
  ```

  Install orchestrator agent skill or view [skill.md](https://www.zkcompression.com/skill.md):

  ```bash theme={null}
  npx skills add https://zkcompression.com
  ```
</Accordion>

<Tabs>
  <Tab title="Guide">
    ## Step 1: Dependencies

    ```toml theme={null}
    [dependencies]
    light-account = { version = "0.23.0", features = ["anchor"] }
    light-sdk = { version = "0.23.0", features = ["anchor", "v2", "cpi-context"] }
    anchor-lang = "0.31"
    ```

    ## Step 2: State struct

    Add `compression_info` field and derive `LightAccount`:

    ```rust theme={null}
    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:

    ```rust theme={null}
    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:

    ```rust theme={null}
    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

    <Tabs>
      <Tab title="Program">
        ```rust lib.rs expandable theme={null}
        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>,
        }
        ```
      </Tab>

      <Tab title="Client">
        ```rust counter.rs expandable theme={null}
        //! Light-PDA lifecycle test: create → increment → close.

        use anchor_lang::{InstructionData, ToAccountMetas};
        use light_client::interface::{
            get_create_accounts_proof, CreateAccountsProofInput, InitializeRentFreeConfig,
        };
        use light_program_test::{
            program_test::{setup_mock_program_data, LightProgramTest},
            ProgramTestConfig, Rpc,
        };
        use light_token::instruction::RENT_SPONSOR;
        use solana_instruction::Instruction;
        use solana_keypair::Keypair;
        use solana_pubkey::Pubkey;
        use solana_signer::Signer;

        const PROGRAM_ID: Pubkey = counter::ID;

        /// Setup: create test RPC and initialize rent-free config for the counter program.
        async fn setup() -> (LightProgramTest, Keypair, Pubkey) {
            let config = ProgramTestConfig::new_v2(true, Some(vec![("counter", PROGRAM_ID)]));
            let mut rpc = LightProgramTest::new(config).await.unwrap();
            let payer = rpc.get_payer().insecure_clone();

            let program_data_pda = setup_mock_program_data(&mut rpc, &payer, &PROGRAM_ID);

            // Register this program for rent-free accounts. One-time setup per program.
            let (init_config_ix, compression_config) = InitializeRentFreeConfig::new(
                &PROGRAM_ID,
                &payer.pubkey(),
                &program_data_pda,
                RENT_SPONSOR,
                payer.pubkey(),
            )
            .build();

            rpc.create_and_send_transaction(&[init_config_ix], &payer.pubkey(), &[&payer])
                .await
                .expect("initialize rent-free config");

            (rpc, payer, compression_config)
        }

        #[tokio::test]
        async fn test_counter_lifecycle() {
            let (mut rpc, payer, compression_config) = setup().await;

            // ── Create ───────────────────────────────────────────────────────────
            let (counter_pda, _) = Pubkey::find_program_address(
                &[counter::COUNTER_SEED, payer.pubkey().as_ref()],
                &PROGRAM_ID,
            );

            let proof_result = get_create_accounts_proof(
                &rpc,
                &PROGRAM_ID,
                vec![CreateAccountsProofInput::pda(counter_pda)],
            )
            .await
            .unwrap();

            let create_accounts = counter::accounts::CreateCounter {
                fee_payer: payer.pubkey(),
                owner: payer.pubkey(),
                compression_config,
                counter: counter_pda,
                system_program: solana_sdk::system_program::ID,
            };

            let create_data = counter::instruction::CreateCounter {
                params: counter::CreateCounterParams {
                    create_accounts_proof: proof_result.create_accounts_proof,
                    count: 0,
                },
            };

            let create_ix = Instruction {
                program_id: PROGRAM_ID,
                accounts: [
                    create_accounts.to_account_metas(None),
                    proof_result.remaining_accounts,
                ]
                .concat(),
                data: create_data.data(),
            };

            rpc.create_and_send_transaction(&[create_ix], &payer.pubkey(), &[&payer])
                .await
                .expect("create_counter");

            // Verify initial state.
            let account = rpc.get_account(counter_pda).await.unwrap().unwrap();
            let ctr: counter::Counter =
                anchor_lang::AccountDeserialize::try_deserialize(&mut account.data.as_slice()).unwrap();
            assert_eq!(ctr.count, 0);
            assert_eq!(ctr.owner, payer.pubkey());

            // ── Increment (standard Anchor) ────────────────────────────
            let inc_accounts = counter::accounts::Increment {
                owner: payer.pubkey(),
                counter: counter_pda,
            };
            let inc_data = counter::instruction::Increment {};
            let inc_ix = Instruction {
                program_id: PROGRAM_ID,
                accounts: inc_accounts.to_account_metas(None),
                data: inc_data.data(),
            };

            rpc.create_and_send_transaction(&[inc_ix], &payer.pubkey(), &[&payer])
                .await
                .expect("increment");

            let account = rpc.get_account(counter_pda).await.unwrap().unwrap();
            let ctr: counter::Counter =
                anchor_lang::AccountDeserialize::try_deserialize(&mut account.data.as_slice()).unwrap();
            assert_eq!(ctr.count, 1);

            // ── Close (standard Anchor) ────────────────────────────────
            let close_accounts = counter::accounts::CloseCounter {
                fee_payer: payer.pubkey(),
                owner: payer.pubkey(),
                counter: counter_pda,
            };
            let close_data = counter::instruction::CloseCounter {};
            let close_ix = Instruction {
                program_id: PROGRAM_ID,
                accounts: close_accounts.to_account_metas(None),
                data: close_data.data(),
            };

            rpc.create_and_send_transaction(&[close_ix], &payer.pubkey(), &[&payer])
                .await
                .expect("close_counter");

            // Account should no longer exist.
            let account = rpc.get_account(counter_pda).await.unwrap();
            assert!(account.is_none(), "counter should be closed");
        }
        ```
      </Tab>
    </Tabs>

    <Callout type="info">
      View counter example on GitHub: [counter](https://github.com/Lightprotocol/examples-light-token/tree/main/programs/anchor/basic-macros/counter)
    </Callout>
  </Tab>

  <Tab title="AI Prompt">
    <Prompt description="Add rent-free PDAs to an Anchor program" actions={["copy", "cursor"]}>
      {`---
            description: Add rent-free PDAs to an Anchor program
            allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
            ---

            ## Add rent-free PDAs to an Anchor program

            Context:
            - Guide: https://zkcompression.com/pda/light-pda/overview
            - Skills and resources index: https://zkcompression.com/skill.md
            - SPL to Light reference: https://zkcompression.com/api-reference/solana-to-light-comparison
            - Crates: light-account (features: anchor), light-sdk (features: anchor, v2, cpi-context)
            - Counter example: https://github.com/Lightprotocol/examples-light-token/tree/main/programs/anchor/basic-macros/counter

            Key macros/APIs: #[light_program], LightAccount, LightAccounts, #[light_account(init)], CompressionInfo, CreateAccountsProof

            ### 1. Index project
            - Grep \`#\[program\]|anchor_lang|Account<|Accounts|InitSpace|seeds|init|payer\` across src/
            - Glob \`**/*.rs\` and \`**/Cargo.toml\` for project structure
            - Identify: existing program module, account structs, PDA seeds, token accounts, init instructions
            - Read Cargo.toml — note existing dependencies and framework version
            - Task subagent (Grep/Read/WebFetch) if project has multiple crates to scan in parallel

            ### 2. Read references
            - WebFetch the guide above
            - WebFetch skill.md — check for a dedicated skill and resources matching this task
            - TaskCreate one todo per phase below to track progress

            ### 3. Clarify intention
            - AskUserQuestion: what is the goal? (new program from scratch, migrate existing program to rent-free, add rent-free accounts to specific instructions)
            - Summarize findings and wait for user confirmation before implementing

            ### 4. Create plan
            - Based on steps 1–3, draft an implementation plan
            - Follow the guide's step order: Dependencies → State Struct → Program Module → Accounts Struct
            - Identify which existing structs need changes (CompressionInfo field, LightAccount derive, etc.)
            - If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
            - Present the plan to the user for approval before proceeding

            ### 5. Implement
            - Add deps if missing: Bash \`cargo add light-account@0.23 --features anchor\` and \`cargo add light-sdk@0.23 --features anchor,v2,cpi-context\`
            - Follow the guide and the approved plan
            - Write/Edit to create or modify files
            - TaskUpdate to mark each step done

            ### 6. Verify
            - Bash \`anchor build\`
            - Bash \`anchor test\` if tests exist
            - TaskUpdate to mark complete

            ### Tools
            - mcp__zkcompression__SearchLightProtocol("<query>") for API details
            - mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "<q>") for architecture
            - Task subagent with Grep/Read/WebFetch for parallel lookups
            - TaskList to check remaining work`}
    </Prompt>

    ```text theme={null}
    ---
    description: Add rent-free PDAs to an Anchor program
    allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
    ---

    ## Add rent-free PDAs to an Anchor program

    Context:
    - Guide: https://zkcompression.com/pda/light-pda/overview
    - Skills and resources index: https://zkcompression.com/skill.md
    - SPL to Light reference: https://zkcompression.com/api-reference/solana-to-light-comparison
    - Crates: light-account (features: anchor), light-sdk (features: anchor, v2, cpi-context)
    - Counter example: https://github.com/Lightprotocol/examples-light-token/tree/main/programs/anchor/basic-macros/counter

    Key macros/APIs: #[light_program], LightAccount, LightAccounts, #[light_account(init)], CompressionInfo, CreateAccountsProof

    ### 1. Index project
    - Grep `#\[program\]|anchor_lang|Account<|Accounts|InitSpace|seeds|init|payer` across src/
    - Glob `**/*.rs` and `**/Cargo.toml` for project structure
    - Identify: existing program module, account structs, PDA seeds, token accounts, init instructions
    - Read Cargo.toml — note existing dependencies and framework version
    - Task subagent (Grep/Read/WebFetch) if project has multiple crates to scan in parallel

    ### 2. Read references
    - WebFetch the guide above
    - WebFetch skill.md — check for a dedicated skill and resources matching this task
    - TaskCreate one todo per phase below to track progress

    ### 3. Clarify intention
    - AskUserQuestion: what is the goal? (new program from scratch, migrate existing program to rent-free, add rent-free accounts to specific instructions)
    - Summarize findings and wait for user confirmation before implementing

    ### 4. Create plan
    - Based on steps 1–3, draft an implementation plan
    - Follow the guide's step order: Dependencies → State Struct → Program Module → Accounts Struct
    - Identify which existing structs need changes (CompressionInfo field, LightAccount derive, etc.)
    - If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
    - Present the plan to the user for approval before proceeding

    ### 5. Implement
    - Add deps if missing: Bash `cargo add light-account@0.23 --features anchor` and `cargo add light-sdk@0.23 --features anchor,v2,cpi-context`
    - Follow the guide and the approved plan
    - Write/Edit to create or modify files
    - TaskUpdate to mark each step done

    ### 6. Verify
    - Bash `anchor build`
    - Bash `anchor test` if tests exist
    - TaskUpdate to mark complete

    ### Tools
    - mcp__zkcompression__SearchLightProtocol("<query>") for API details
    - mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "<q>") for architecture
    - Task subagent with Grep/Read/WebFetch for parallel lookups
    - TaskList to check remaining work
    ```
  </Tab>
</Tabs>

***

## 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)   |
| ----------------- | ------------ | ----------------- |
| Storage           | On-chain     | Compressed        |
| Latency/CU        | No change    | +load instruction |
| Your program code | No change    | No change         |

## FAQ

<Accordion title="How does it prevent re-init attacks?"> 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. </Accordion>

<Accordion title="Who triggers compression?">
  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.
</Accordion>

<Accordion title="How is the SDK able to sponsor rent exemption?">
  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.
</Accordion>

<Accordion title="Do rent-free accounts increase CU?">
  **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.
</Accordion>

***

<Callout type="info">
  Questions or need hands-on support? [Telegram](https://t.me/swen_light) | [email](mailto:support@lightprotocol.com) | [Discord](https://discord.com/invite/7cJ8BhAXhu)
</Callout>
