> ## Documentation Index
> Fetch the complete documentation index at: https://luminouslabs-cc5545c6-swen-add-code-runner.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Create an Airdrop without Claim

> Complete guide to create an airdrop – with or without code. Access to cost calculation and best practices. ZK compression is the most efficient way to distribute SPL tokens.

export const TokenAccountCompressedVsSpl = () => {
  const [numAccounts, setNumAccounts] = useState(100000);
  const [showCustomAccounts, setShowCustomAccounts] = useState(false);
  const ACCOUNT_STORAGE_OVERHEAD = 128;
  const LAMPORTS_PER_BYTE = 6960;
  const DATA_LEN = 165;
  const COMPRESSED_COST_PER_ACCOUNT = 10300;
  const LAMPORTS_PER_SOL = 1_000_000_000;
  const ACCOUNTS_MAX = 1000000;
  const solanaCost = numAccounts * (ACCOUNT_STORAGE_OVERHEAD + DATA_LEN) * LAMPORTS_PER_BYTE;
  const compressedCost = numAccounts * COMPRESSED_COST_PER_ACCOUNT;
  const savings = solanaCost - compressedCost;
  const savingsPercent = (savings / solanaCost * 100).toFixed(1);
  const handleAccountsChange = value => {
    const num = Math.max(1, Math.min(1000000, Number.parseInt(value) || 1));
    setNumAccounts(num);
  };
  const formatSOL = lamports => {
    const sol = lamports / LAMPORTS_PER_SOL;
    if (sol >= 1000) {
      return sol.toLocaleString(undefined, {
        maximumFractionDigits: 2
      });
    } else if (sol >= 1) {
      return sol.toFixed(4);
    }
    return sol.toFixed(6);
  };
  const accountsPresets = [10000, 100000, 500000];
  const SliderMarkers = ({max, step}) => {
    const marks = [];
    for (let i = step; i < max; i += step) {
      const percent = i / max * 100;
      marks.push(<div key={i} className="absolute top-1/2 -translate-y-1/2 w-px h-2 bg-zinc-300 dark:bg-white/30" style={{
        left: `${percent}%`
      }} />);
    }
    return <>{marks}</>;
  };
  return <div className="p-5 rounded-3xl not-prose mt-4 dark:bg-white/5 backdrop-blur-xl border border-black/[0.04] dark:border-white/10 shadow-lg" style={{
    fontFamily: 'Inter, sans-serif'
  }}>
      <div className="space-y-5">
        {}
        <div className="space-y-2 px-3">
          <div className="flex justify-between items-center">
            <span className="text-sm text-zinc-700 dark:text-white/80">Number of Token Accounts</span>
            <div className="flex items-center gap-1.5">
              {accountsPresets.map(a => <button key={a} onClick={() => {
    setNumAccounts(a);
    setShowCustomAccounts(false);
  }} className={`px-2.5 py-1 text-xs font-medium rounded-lg border backdrop-blur-sm transition-all ${numAccounts === a && !showCustomAccounts ? 'bg-blue-500/20 border-blue-500/50 text-blue-600 dark:text-blue-400' : 'bg-black/[0.015] dark:bg-white/5 border-black/[0.04] dark:border-white/20 text-zinc-600 dark:text-white/70 hover:bg-black/[0.03]'}`}>
                  {a.toLocaleString()}
                </button>)}
              {showCustomAccounts ? <input type="number" min="1" max="1000000" value={numAccounts} onChange={e => handleAccountsChange(e.target.value)} className="w-24 px-2 py-1 text-right text-xs font-mono font-medium bg-blue-500/10 dark:bg-blue-500/20 border border-blue-500/50 rounded-lg backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50" autoFocus /> : <button onClick={() => setShowCustomAccounts(true)} className="px-2.5 py-1 text-xs font-medium rounded-lg border backdrop-blur-sm transition-all bg-black/[0.015] dark:bg-white/5 border-black/[0.04] dark:border-white/20 text-zinc-600 dark:text-white/70 hover:bg-black/[0.03]">
                  Custom
                </button>}
            </div>
          </div>
          <div className="flex items-center">
            <span className="w-1/3 text-xs text-zinc-500 dark:text-white/50 whitespace-nowrap">
              {numAccounts.toLocaleString()} accounts
            </span>
            <div className="w-2/3 relative">
              <SliderMarkers max={ACCOUNTS_MAX} step={200000} />
              <input type="range" min="5000" max={ACCOUNTS_MAX} step="5000" value={Math.min(numAccounts, ACCOUNTS_MAX)} onChange={e => {
    setNumAccounts(Number.parseInt(e.target.value));
    setShowCustomAccounts(false);
  }} className="relative w-full h-1.5 bg-black/[0.03] dark:bg-white/20 rounded-full appearance-none cursor-pointer backdrop-blur-sm z-10" />
            </div>
          </div>
        </div>

        {}
        <div className="grid grid-cols-3 gap-3">
          <div className="p-4 bg-black/[0.015] dark:bg-white/5 backdrop-blur-md rounded-2xl text-center border border-black/[0.04] dark:border-white/10 shadow-sm">
            <div className="text-xs text-zinc-500 dark:text-white/50 mb-1 uppercase tracking-wide">SPL Token</div>
            <div className="text-xl font-mono font-semibold text-zinc-900 dark:text-white">
              {formatSOL(solanaCost)}
            </div>
            <div className="text-xs text-zinc-400 dark:text-white/40">SOL</div>
          </div>

          <div className="p-4 bg-black/[0.015] dark:bg-white/5 backdrop-blur-md rounded-2xl text-center border border-black/[0.04] dark:border-white/10 shadow-sm">
            <div className="text-xs text-zinc-500 dark:text-white/50 mb-1 uppercase tracking-wide">Compressed</div>
            <div className="text-xl font-mono font-semibold text-zinc-900 dark:text-white">
              {formatSOL(compressedCost)}
            </div>
            <div className="text-xs text-zinc-400 dark:text-white/40">SOL</div>
          </div>

          <div className="p-4 bg-black/[0.015] dark:bg-white/5 backdrop-blur-md rounded-2xl text-center border border-black/[0.04] dark:border-white/10 shadow-sm">
            <div className="text-xs text-zinc-500 dark:text-white/50 mb-1 uppercase tracking-wide">Savings</div>
            <div className="text-xl font-mono font-semibold text-zinc-900 dark:text-white">
              {savingsPercent}%
            </div>
            <div className="text-xs text-zinc-400 dark:text-white/40">{formatSOL(savings)} SOL</div>
          </div>
        </div>
      </div>
    </div>;
};

## Guides

There are two ways to use ZK Compression to distribute your SPL tokens.

<CardGroup>
  <Card title="No-Code Airdrop Tool" href="https://www.helius.dev/docs/airship/getting-started" icon="laptop-code">
    Use **Airship by Helius Labs** to airdrop to up to 200,000 recipients via Webapp.
  </Card>

  <Card title="Custom Programmatic Airdrop" href="#programmatic-airdrop" icon="terminal">
    Create a programmatic airdrop with this guide for more control.
  </Card>
</CardGroup>

### Cost Comparison

<TokenAccountCompressedVsSpl />

## Programmatic Airdrop

Choose your implementation based on your needs:

| Tab                                   | Best For                   | What You'll Get                 | Time   |
| :------------------------------------ | :------------------------- | :------------------------------ | :----- |
| **Implementation Steps**              | First-time users, learning | Step-by-step Localnet tutorial  | 20 min |
| **Simple Airdrop**                    | \<10,000 recipients        | Production-ready single script  | 10 min |
| **Airdrop with Batched Instructions** | 10,000+ recipients         | Batched system with retry logic | 15 min |

<Tabs>
  <Tab title="Implementation Steps">
    <Check>
      **What you'll build:** A test airdrop sending compressed tokens to 3 recipients on your local validator.
    </Check>

    <Steps>
      <Step>
        ### Prerequisites

        Make sure you have dependencies and developer environment set up!

        <Info>
          System Requirements

          * **Node.js >= 20.18.0** (required by latest Solana packages)
          * npm or yarn package manager
        </Info>

        <Accordion title="Prerequisites & Setup">
          **Dependencies**

          <Tabs>
            <Tab title="npm">
              ```bash theme={null}
              npm install @lightprotocol/stateless.js@alpha \
                          @lightprotocol/compressed-token@alpha
              ```
            </Tab>

            <Tab title="yarn">
              ```bash theme={null}
              yarn add @lightprotocol/stateless.js@alpha \
                       @lightprotocol/compressed-token@alpha
              ```
            </Tab>

            <Tab title="pnpm">
              ```bash theme={null}
              pnpm add @lightprotocol/stateless.js@alpha \
                       @lightprotocol/compressed-token@alpha
              ```
            </Tab>
          </Tabs>

          **Developer Environment**

          <Tabs>
            <Tab title="Localnet">
              By default, all guides use Localnet.

              <Tabs>
                <Tab title="npm">
                  ```bash theme={null}
                  npm install -g @lightprotocol/zk-compression-cli@alpha
                  ```
                </Tab>

                <Tab title="yarn">
                  ```bash theme={null}
                  yarn global add @lightprotocol/zk-compression-cli@alpha
                  ```
                </Tab>

                <Tab title="pnpm">
                  ```bash theme={null}
                  pnpm add -g @lightprotocol/zk-compression-cli@alpha
                  ```
                </Tab>
              </Tabs>

              ```bash theme={null}
              # Start a local test validator
              light test-validator

              ## ensure you have the Solana CLI accessible in your system PATH
              ```

              ```typescript theme={null}
              // createRpc() defaults to local test validator endpoints
              import {
                Rpc,
                createRpc,
              } from "@lightprotocol/stateless.js";

              const connection: Rpc = createRpc();

              async function main() {
                let slot = await connection.getSlot();
                console.log(slot);

                let health = await connection.getIndexerHealth(slot);
                console.log(health);
                // "Ok"
              }

              main();
              ```
            </Tab>

            <Tab title="Devnet">
              Replace `<your-api-key>` with your actual API key. [Get your API key here](https://www.helius.dev/zk-compression), if you don't have one yet.

              ```typescript theme={null}
              import { createRpc } from "@lightprotocol/stateless.js";

              // Helius exposes Solana and Photon RPC endpoints through a single URL
              const RPC_ENDPOINT = "https://devnet.helius-rpc.com?api-key=<your_api_key>";
              const connection = createRpc(RPC_ENDPOINT, RPC_ENDPOINT, RPC_ENDPOINT);

              console.log("Connection created!");
              console.log("RPC Endpoint:", RPC_ENDPOINT);
              ```
            </Tab>
          </Tabs>
        </Accordion>
      </Step>

      <Step>
        ### Mint SPL tokens to your wallet

        Run this `mint-spl-tokens.ts` to mint SPL tokens to your wallet.

        ```typescript mint-spl-tokens.ts expandable theme={null}
        // Mint SPL Tokens for Airdrop - LocalNet
        // 1. Load wallet and connect to local validator
        // 2. Create SPL mint with token pool for compression via createMint()
        // 3. Create ATA and mint SPL tokens to sender for airdrop preparation
        // 4. Output mint address for use in simple-airdrop.ts

        import { Keypair } from "@solana/web3.js";
        import { createRpc } from "@lightprotocol/stateless.js";
        import {
          createMint,
          getOrCreateAssociatedTokenAccount,
          mintTo,
        } from "@solana/spl-token";
        import { createTokenPool } from "@lightprotocol/compressed-token";
        import * as fs from 'fs';
        import * as os from 'os';

        // Step 1: Setup local connection and load wallet
        const connection = createRpc(); // defaults to localhost:8899

        // Load wallet from filesystem
        const walletPath = `${os.homedir()}/.config/solana/id.json`;
        const secretKey = JSON.parse(fs.readFileSync(walletPath, 'utf8'));
        const payer = Keypair.fromSecretKey(Buffer.from(secretKey));

        (async () => {
          // Step 2: Create SPL mint with token pool for compression
          const mint = await createMint(connection, payer, payer.publicKey, null, 9);
          const poolTxId = await createTokenPool(connection, payer, mint);
          console.log(`Mint address: ${mint.toBase58()}`);
          console.log(`TokenPool created: ${poolTxId}`);

          // Step 3: Create associated token account for sender
          // The sender will send tokens from this account to the recipients as compressed tokens.
          const ata = await getOrCreateAssociatedTokenAccount(
            connection,
            payer,
            mint, // SPL mint with token pool for compression
            payer.publicKey
          );
          console.log(`ATA address: ${ata.address.toBase58()}`);

          // Step 4: Mint SPL tokens to ATA.
          // The sender will send tokens from this account to the recipients as compressed tokens.
          const mintToTxId = await mintTo(
            connection,
            payer,
            mint, // SPL mint with token pool for compression
            ata.address, // distributor ATA
            payer.publicKey,
            100_000_000_000 // amount: 100 tokens with 9 decimals
          );
          console.log(`\nSPL tokens minted and ready for distribution!`);
          console.log(`Transaction: ${mintToTxId}`);

          console.log(`\nCopy mint address to your airdrop script: ${mint.toBase58()}`);
        })();
        ```
      </Step>

      <Step>
        ### Execute the Airdrop

        Next, distribute the SPL tokens to all recipients.

        <Warning>
          Ensure you have the latest `@lightprotocol/stateless.js` and `@lightprotocol/compressed-token` versions `≥ 0.21.0`!
        </Warning>

        ```typescript simple-airdrop.ts expandable highlight={29-30,45,74,100} theme={null}
        // Simple Airdrop - LocalNet
        // 1. Load wallet and select compression infrastructure with getStateTreeInfos() and getTokenPoolInfos()
        // 2. Build CompressedTokenProgram.compress() instruction for multiple recipients in one transaction
        // 3. Execute transaction with compute budget and confirm compression operation with sendAndConfirmTx()
        // 4. Verify distribution via getCompressedTokenAccountsByOwner

        import { Keypair, PublicKey, ComputeBudgetProgram } from "@solana/web3.js";
        import {
          CompressedTokenProgram,
          getTokenPoolInfos,
          selectTokenPoolInfo,
        } from "@lightprotocol/compressed-token";
        import {
          bn,
          buildAndSignTx,
          calculateComputeUnitPrice,
          createRpc,
          dedupeSigner,
          Rpc,
          selectStateTreeInfo,
          sendAndConfirmTx,
        } from "@lightprotocol/stateless.js";
        import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token";
        import * as fs from 'fs';
        import * as os from 'os';

        // Step 1: Setup local connection and load wallet
        const connection: Rpc = createRpc(); // defaults to localhost:8899
        const mint = new PublicKey("MINTADDRESS"); // Replace with mint address from mint-spl-tokens.ts
        // Local uses file wallet. Use constants from .env file in production
        const walletPath = `${os.homedir()}/.config/solana/id.json`;
        const secretKey = JSON.parse(fs.readFileSync(walletPath, 'utf8'));
        const payer = Keypair.fromSecretKey(Buffer.from(secretKey));
        const owner = payer;

        (async () => {
          // Step 2: Select state tree and token pool
          const activeStateTrees = await connection.getStateTreeInfos();
          const treeInfo = selectStateTreeInfo(activeStateTrees);

          const infos = await getTokenPoolInfos(connection, mint);
          const info = selectTokenPoolInfo(infos);

          // Step 3: Get or create source token account for distribution
          // The sender will send tokens from this account to the recipients as compressed tokens.
          const sourceTokenAccount = await getOrCreateAssociatedTokenAccount(
            connection,
            payer,
            mint, // SPL mint with token pool for compression
            payer.publicKey
          );

          // Step 4: Define airdrop recipients and amounts
          const airDropAddresses = [
            Keypair.generate().publicKey,
            Keypair.generate().publicKey,
            Keypair.generate().publicKey,
          ];

          const amounts = [
            bn(20_000_000_000), // 20 tokens
            bn(30_000_000_000), // 30 tokens
            bn(40_000_000_000), // 40 tokens
          ];

          const totalAmount = amounts.reduce((sum, amt) => sum + amt.toNumber(), 0);
          console.log(`Distributing ${totalAmount / 1e9} compressed tokens to ${airDropAddresses.length} recipients`);

          const initialSplBalance = await connection.getTokenAccountBalance(sourceTokenAccount.address);
          console.log(`Sender initial balance: ${initialSplBalance.value.uiAmount} tokens`);

          // Step 5: Build transaction with compute budget and compression instruction
          const instructions = [];
          // Set compute unit limits based on recipient count (estimated 120k CU per recipient)
          instructions.push(
            ComputeBudgetProgram.setComputeUnitLimit({ units: 120_000 * airDropAddresses.length }),
            ComputeBudgetProgram.setComputeUnitPrice({
              microLamports: calculateComputeUnitPrice(20_000, 120_000 * airDropAddresses.length), // dynamic priority fee
            })
          );

          // Create compression instruction for multiple recipients in one transaction
          const compressInstruction = await CompressedTokenProgram.compress({
            payer: payer.publicKey,
            owner: owner.publicKey,
            source: sourceTokenAccount.address, // source ATA holding SPL tokens
            toAddress: airDropAddresses, // recipient addresses for compressed tokens
            amount: amounts, // different amounts for each recipient
            mint, // SPL mint with token pool for compression
            tokenPoolInfo: info,
            outputStateTreeInfo: treeInfo, // destination state tree
          });
          instructions.push(compressInstruction);

          // Step 6: Sign and send transaction
          const additionalSigners = dedupeSigner(payer, [owner]);
          const { blockhash } = await connection.getLatestBlockhash();
          const tx = buildAndSignTx(instructions, payer, blockhash, additionalSigners);

          // For production: Add address lookup table to reduce transaction size and fees
          // const lookupTableAddress = new PublicKey("9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ"); // mainnet // or "qAJZMgnQJ8G6vA3WRcjD9Jan1wtKkaCFWLWskxJrR5V" for devnet
          // const lookupTableAccount = (await connection.getAddressLookupTable(lookupTableAddress)).value!;
          // const tx = buildAndSignTx(instructions, payer, blockhash, additionalSigners, [lookupTableAccount]);
          const txId = await sendAndConfirmTx(connection, tx);

          console.log(`\nAirdrop completed!`);
          console.log(`Transaction: ${txId}`);

          // Step 7: Verify distribution via getCompressedTokenAccountsByOwner
          for (let i = 0; i &#x3C; airDropAddresses.length; i++) {
            const recipientAccounts = await connection.getCompressedTokenAccountsByOwner(airDropAddresses[i], { mint });
            const balance = recipientAccounts.items.reduce((sum, account) => sum + Number(account.parsed.amount), 0);
            console.log(`Recipient ${i + 1} (${airDropAddresses[i].toString()}): ${balance / 1e9} compressed tokens`);
          }

          const finalSplBalance = await connection.getTokenAccountBalance(sourceTokenAccount.address);
          console.log(`\nSender balance after airdrop: ${finalSplBalance.value.uiAmount} SPL tokens`);

          return txId;
        })();
        ```
      </Step>
    </Steps>
  </Tab>

  <Tab title="Simple Airdrop">
    <Check>
      For small airdrops (\<10,000 recipients). [View the source code here](https://github.com/Lightprotocol/example-token-distribution/blob/main/src/simple-airdrop/simple-airdrop.ts).
    </Check>

    <Steps>
      <Step>
        ### Environment Setup

        Configure your environment variables:

        ```bash .env theme={null}
        PAYER_KEYPAIR=YOUR_BASE58_ENCODED_PRIVATE_KEY
        MINT_ADDRESS=YOUR_MINT_ADDRESS
        ```
      </Step>

      <Step>
        ### Mint SPL Tokens

        Mint SPL tokens to your wallet for distribution. See the [mint script example](https://github.com/Lightprotocol/example-token-distribution/blob/main/src/simple-airdrop/mint.ts).

        <Info>
          You can also use an existing mint if you already have one.
        </Info>
      </Step>

      <Step>
        ### Execute Airdrop

        Run the airdrop script with your configured environment:

        ```typescript expandable theme={null}
        // 1. Load environment and select compression infrastructure with getStateTreeInfos() and getTokenPoolInfos()
        // 2. Build CompressedTokenProgram.compress() instruction for multiple recipients in one transaction
        // 3. Execute transaction with compute budget, address lookup table, and confirm with sendAndConfirmTx()
        // 4. Verify distribution via getCompressedTokenAccountsByOwner

        import {
            PublicKey,
            TransactionInstruction,
            ComputeBudgetProgram,
        } from '@solana/web3.js';
        import {
            CompressedTokenProgram,
            getTokenPoolInfos,
            selectTokenPoolInfo,
        } from '@lightprotocol/compressed-token';
        import {
            bn,
            buildAndSignTx,
            calculateComputeUnitPrice,
            createRpc,
            dedupeSigner,
            Rpc,
            selectStateTreeInfo,
            sendAndConfirmTx,
        } from '@lightprotocol/stateless.js';
        import { getOrCreateAssociatedTokenAccount } from '@solana/spl-token';
        import { MINT_ADDRESS, PAYER_KEYPAIR, RPC_ENDPOINT } from '../constants';

        (async () => {
            const connection: Rpc = createRpc(RPC_ENDPOINT);
            const mintAddress = MINT_ADDRESS;
            const payer = PAYER_KEYPAIR;
            const owner = payer;
            const recipients = [
                PublicKey.default,
                // ...
            ];

            // 1. Select a state tree
            const treeInfos = await connection.getStateTreeInfos(); // Fixed: removed deprecated getCachedActiveStateTreeInfos
            const treeInfo = selectStateTreeInfo(treeInfos);

            // 2. Select a token pool
            const tokenPoolInfos = await getTokenPoolInfos(connection, mintAddress);
            const tokenPoolInfo = selectTokenPoolInfo(tokenPoolInfos);

            // Create an SPL token account for the sender.
            // The sender will send tokens from this account to the recipients as compressed tokens.
            const sourceTokenAccount = await getOrCreateAssociatedTokenAccount(
                connection,
                payer,
                mintAddress,
                payer.publicKey,
            );

            // 1 recipient = 120_000 CU
            // 5 recipients = 170_000 CU
            const units = 120_000;
            const amount = bn(333);
            // To land faster, replace this with a dynamic fee based on network
            // conditions.
            const microLamports = calculateComputeUnitPrice(20_000, units);

            const instructions: TransactionInstruction[] = [
                ComputeBudgetProgram.setComputeUnitLimit({ units }),
                ComputeBudgetProgram.setComputeUnitPrice({
                    microLamports,
                }),
            ];

            const compressInstruction = await CompressedTokenProgram.compress({
                payer: payer.publicKey,
                owner: owner.publicKey,
                source: sourceTokenAccount.address,
                toAddress: recipients,
                amount: recipients.map(() => amount),
                mint: mintAddress,
                outputStateTreeInfo: treeInfo,
                tokenPoolInfo,
            });
            instructions.push(compressInstruction);

            // https://www.zkcompression.com/developers/protocol-addresses-and-urls#lookup-tables
            const lookupTableAddress = new PublicKey(
                '9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ', // mainnet
                // "qAJZMgnQJ8G6vA3WRcjD9Jan1wtKkaCFWLWskxJrR5V" // devnet
            );

            // Get the lookup table account state
            const lookupTableAccount = (
                await connection.getAddressLookupTable(lookupTableAddress)
            ).value!;

            const additionalSigners = dedupeSigner(payer, [owner]);

            const { blockhash } = await connection.getLatestBlockhash();

            const tx = buildAndSignTx(
                instructions,
                payer,
                blockhash,
                additionalSigners,
                [lookupTableAccount],
            );

            const txId = await sendAndConfirmTx(connection, tx);
            console.log(`txId: ${txId}`);
        })();
        ```
      </Step>
    </Steps>
  </Tab>

  <Tab title="Airdrop with Batched Instructions">
    <Check>
      For large-scale airdrops (10,000+ recipients) we recommend batched instructions.\
      [View the source code here.](https://github.com/Lightprotocol/example-token-distribution/tree/main/src/optimized-airdrop)
    </Check>

    <Info>
      This implementation creates the mint and executes the airdrop in a single operation. You only need to configure your RPC endpoint and payer keypair.
    </Info>

    <Steps>
      <Step>
        ### Create Instructions

        Process recipients in chunks and create batched instructions with optimized compute limits.

        ```typescript create-instructions.ts expandable theme={null}

        // 1. Process recipients in chunks with selectStateTreeInfo() and selectTokenPoolInfo() for each batch
        // 2. Create CompressedTokenProgram.compress() instructions with ComputeBudgetProgram limits for multiple recipients
        // 3. Return batched instructions for optimized large-scale airdrop execution

        import {
          CompressedTokenProgram,
          TokenPoolInfo,
        } from "@lightprotocol/compressed-token";
        import {
          bn,
          selectStateTreeInfo,
          StateTreeInfo,
        } from "@lightprotocol/stateless.js";
        import {
          ComputeBudgetProgram,
          TransactionInstruction,
          PublicKey,
        } from "@solana/web3.js";

        interface CreateAirdropInstructionsParams {
          amount: number | bigint;
          recipients: PublicKey[];
          payer: PublicKey;
          sourceTokenAccount: PublicKey;
          mint: PublicKey;
          stateTreeInfos: StateTreeInfo[];
          tokenPoolInfos: TokenPoolInfo[];
          maxRecipientsPerInstruction?: number;
          maxInstructionsPerTransaction?: number;
          computeUnitLimit?: number;
          computeUnitPrice?: number | undefined;
        }

        export type InstructionBatch = TransactionInstruction[];

        export async function createAirdropInstructions({
          amount,
          recipients,
          payer,
          sourceTokenAccount,
          mint,
          stateTreeInfos,
          tokenPoolInfos,
          maxRecipientsPerInstruction = 5,
          maxInstructionsPerTransaction = 3,
          computeUnitLimit = 500_000,
          computeUnitPrice = undefined,
        }: CreateAirdropInstructionsParams): Promise<InstructionBatch[]> {
          const instructionBatches: InstructionBatch[] = [];
          const amountBn = bn(amount.toString());

          // Process recipients in chunks
          for (
            let i = 0;
            i < recipients.length;
            i += maxRecipientsPerInstruction * maxInstructionsPerTransaction
          ) {
            const instructions: TransactionInstruction[] = [];

            instructions.push(
              ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnitLimit })
            );
            if (computeUnitPrice) {
              instructions.push(
                ComputeBudgetProgram.setComputeUnitPrice({
                  microLamports: computeUnitPrice,
                })
              );
            }

            const treeInfo = selectStateTreeInfo(stateTreeInfos);
            const tokenPoolInfo = selectTokenPoolInfo(tokenPoolInfos);

            for (let j = 0; j < maxInstructionsPerTransaction; j++) {
              const startIdx = i + j * maxRecipientsPerInstruction;
              const recipientBatch = recipients.slice(
                startIdx,
                startIdx + maxRecipientsPerInstruction
              );

              if (recipientBatch.length === 0) break;

              const compressIx = await CompressedTokenProgram.compress({
                payer,
                owner: payer,
                source: sourceTokenAccount,
                toAddress: recipientBatch,
                amount: recipientBatch.map(() => amountBn),
                mint,
                tokenPoolInfo,
                outputStateTreeInfo: treeInfo,
              });

              instructions.push(compressIx);
            }

            if (
              (computeUnitPrice && instructions.length > 2) ||
              (!computeUnitPrice && instructions.length > 1)
            ) {
              instructionBatches.push(instructions);
            }
          }

          return instructionBatches;
        }
        ```
      </Step>

      <Step>
        ### Update Blockhash

        Maintain fresh blockhashes with background refresh loop using `getLatestBlockhash()` every 30 seconds.

        ```typescript update-blockhash.ts expandable theme={null}
        import { Rpc } from "@lightprotocol/stateless.js";

        // 1. Fetch initial blockhash with getLatestBlockhash() and store in exported variable
        // 2. Set up background refresh loop with setTimeout() to update blockhash every 30 seconds
        // 3. Provide AbortSignal support to stop background updates when airdrop completes

        export let currentBlockhash: string;

        export async function updateBlockhash(
          connection: Rpc,
          signal: AbortSignal
        ): Promise<void> {
          try {
            const { blockhash } = await connection.getLatestBlockhash();
            currentBlockhash = blockhash;
            console.log(`Initial blockhash: ${currentBlockhash}`);
          } catch (error) {
            console.error("Failed to fetch initial blockhash:", error);
            return;
          }

          // Update blockhash in the background
          (function updateInBackground() {
            if (signal.aborted) return;
            const timeoutId = setTimeout(async () => {
              if (signal.aborted) return;
              try {
                const { blockhash } = await connection.getLatestBlockhash();
                currentBlockhash = blockhash;
                console.log(`Updated blockhash: ${currentBlockhash}`);
              } catch (error) {
                console.error("Failed to update blockhash:", error);
              }
              updateInBackground();
            }, 30_000);

            signal.addEventListener("abort", () => clearTimeout(timeoutId));
          })();
        }
        ```
      </Step>

      <Step>
        ### Sign and Send

        Execute batched transactions with `VersionedTransaction`, retry logic, and `sendAndConfirmTx()` confirmation.

        ```typescript sign-and-send.ts expandable theme={null}

        // 1. Initialize blockhash updates with updateBlockhash() and get address lookup table with getAddressLookupTable()
        // 2. Process instruction batches with VersionedTransaction and retry logic for failed transactions
        // 3. Yield batch results with sendAndConfirmTx() confirmation and comprehensive error handling

        import { Rpc, sendAndConfirmTx } from "@lightprotocol/stateless.js";
        import {
          Keypair,
          PublicKey,
          TransactionMessage,
          VersionedTransaction,
        } from "@solana/web3.js";
        import { InstructionBatch } from "./create-instructions";
        import { currentBlockhash, updateBlockhash } from "./update-blockhash";
        import bs58 from "bs58";

        export enum BatchResultType {
          Success = "success",
          Error = "error",
        }

        export type BatchResult =
          | { type: BatchResultType.Success; index: number; signature: string }
          | { type: BatchResultType.Error; index: number; error: string };

        export async function* signAndSendAirdropBatches(
          batches: InstructionBatch[],
          payer: Keypair,
          connection: Rpc,
          maxRetries = 3
        ): AsyncGenerator<BatchResult> {
          const abortController = new AbortController();
          const { signal } = abortController;

          await updateBlockhash(connection, signal);

          const statusMap = new Array(batches.length).fill(0); // Initialize all as pending (0)

          // Use zk-compression look up table for your network
          // https://www.zkcompression.com/developers/protocol-addresses-and-urls#lookup-tables
          const lookupTableAddress = new PublicKey(
            "9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ"
          );

          // Get the lookup table account
          const lookupTableAccount = (
            await connection.getAddressLookupTable(lookupTableAddress)
          ).value!;

          while (statusMap.includes(0)) {
            // Continue until all are confirmed or errored
            const pendingBatches = statusMap.filter((status) => status === 0).length;
            console.log(`Sending ${pendingBatches} transactions`);

            const sends = statusMap.map(async (status, index) => {
              if (status !== 0) return; // Skip non-pending batches

              let retries = 0;
              while (retries < maxRetries && statusMap[index] === 0) {
                if (!currentBlockhash) {
                  console.warn("Waiting for blockhash to be set...");
                  await new Promise((resolve) => setTimeout(resolve, 1000));
                  continue;
                }

                try {
                  const tx = new VersionedTransaction(
                    new TransactionMessage({
                      payerKey: payer.publicKey,
                      recentBlockhash: currentBlockhash,
                      instructions: batches[index],
                    }).compileToV0Message([lookupTableAccount])
                  );
                  tx.sign([payer]);

                  const sig = bs58.encode(tx.signatures[0]);
                  console.log(`Batch ${index} signature: ${sig}`);

                  const confirmedSig = await sendAndConfirmTx(connection, tx, {
                    skipPreflight: true,
                    commitment: "confirmed",
                  });

                  if (confirmedSig) {
                    statusMap[index] = 1; // Mark as confirmed
                    return {
                      type: BatchResultType.Success,
                      index,
                      signature: confirmedSig,
                    };
                  }
                } catch (e) {
                  retries++;
                  console.warn(`Retrying batch ${index}, attempt ${retries + 1}`);
                  if (retries >= maxRetries) {
                    statusMap[index] = `err: ${(e as Error).message}`; // Mark as error
                    return {
                      type: BatchResultType.Error,
                      index,
                      error: (e as Error).message,
                    };
                  }
                }
              }
            });

            const results = await Promise.all(sends);
            for (const result of results) {
              if (result) yield result as BatchResult;
            }
          }

          // Stop the blockhash update loop
          abortController.abort();
        }
        ```
      </Step>

      <Step>
        ### Main Airdrop File

        Put it all together in the main airdrop file.

        ```typescript airdrop.ts expandable theme={null}
        // 1. Create compressed mint with createMint(), mint supply with mintTo(), get infrastructure with getStateTreeInfos() and getTokenPoolInfos()
        // 2. Generate batched compression instructions with createAirdropInstructions() - create CompressedTokenProgram.compress() calls
        // 3. Execute batched airdrop with signAndSendAirdropBatches() - sign transactions and confirm with sendAndConfirmTx() for large-scale distribution

        import { Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
        import {
          calculateComputeUnitPrice,
          createRpc,
          Rpc,
        } from "@lightprotocol/stateless.js";
        import { createMint, getTokenPoolInfos } from "@lightprotocol/compressed-token";
        import { getOrCreateAssociatedTokenAccount, mintTo } from "@solana/spl-token";
        import { createAirdropInstructions } from "./create-instructions";
        import { BatchResultType, signAndSendAirdropBatches } from "./sign-and-send";
        import dotenv from "dotenv";
        import bs58 from "bs58";
        dotenv.config();

        // Step 1: Setup environment and RPC connection
        const RPC_ENDPOINT = `https://mainnet.helius-rpc.com?api-key=${process.env.HELIUS_API_KEY}`;
        const connection: Rpc = createRpc(RPC_ENDPOINT);
        const PAYER = Keypair.fromSecretKey(bs58.decode(process.env.PAYER_KEYPAIR!));

        // Step 2: Define airdrop recipient list (20 example addresses)
        const recipients = [
          "GMPWaPPrCeZPse5kwSR3WUrqYAPrVZBSVwymqh7auNW7",
          "GySGrTgPtPfMtYoYTmwUdUDFwVJbFMfip7QZdhgXp8dy",
          "Bk1r2vcgX2uTzwV3AUyfRbSfGKktoQrQufBSrHzere74",
          "8BvkadZ6ycFNmQF7S1MHRvEVNb1wvDBFdjkAUnxjK9Ug",
          "EmxcvFKXsWLzUho8AhV9LCKeKRFHg5gAs4sKNJwhe5PF",
          "6mqdHkSpcvNexmECjp5XLt9V9KnSQre9TvbMLGr6sEPM",
          "3k4MViTWXBjFvoUZiJcNGPvzrqnTa41gcrbWCMMnV6ys",
          "2k6BfYRUZQHquPtpkyJpUx3DzM7W3K6H95igtJk8ztpd",
          "89jPyNNLCcqWn1RZThSS4jSqU5VCJkR5mAaSaVzuuqH4",
          "3MzSRLf9jSt6d1MFFMMtPfUcDY6XziRxTB8C5mfvgxXG",
          "9A1H6f3N8mpAPSdfqvYRD4cM1NwDZoMe3yF5DwibL2R2",
          "PtUAhLvUsVcoesDacw198SsnMoFNVskR5pT3QvsBSQw",
          "6C6W6WpgFK8TzTTMNCPMz2t9RaMs4XnkfB6jotrWWzYJ",
          "8sLy9Jy8WSh6boq9xgDeBaTznn1wb1uFpyXphG3oNjL5",
          "GTsQu2XCgkUczigdBFTWKrdDgNKLs885jKguyhkqdPgV",
          "85UK4bjC71Jwpyn8mPSaW3oYyEAiHPbESByq9s5wLcke",
          "9aEJT4CYHEUWwwSQwueZc9EUjhWSLD6AAbpVmmKDeP7H",
          "CY8QjRio1zd9bYWMKiVRrDbwVenf3JzsGf5km5zLgY9n",
          "CeHbdxgYifYhpB6sXGonKzmaejqEfq2ym5utTmB6XMVv",
          "4z1qss12DjUzGUkK1fFesqrUwrEVJJvzPMNkwqYnbAR5",
        ].map((address) => new PublicKey(address));

        (async () => {
          // Step 3: Create compressed mint and register for compression
          // 3a: Call createMint() to initialize mint with compression pool
          const { mint, transactionSignature } = await createMint(
            connection,
            PAYER, // fee payer
            PAYER.publicKey, // mint authority
            9 // decimals
          );
          console.log(
            `create-mint success! txId: ${transactionSignature}, mint: ${mint.toBase58()}`
          );

          // Step 4: Create associated token account for distributor
          // 4a: Ensure PAYER has ATA for holding tokens before compression
          const ata = await getOrCreateAssociatedTokenAccount(
            connection,
            PAYER, // fee payer
            mint, // token mint
            PAYER.publicKey // token owner
          );
          console.log(`ATA: ${ata.address.toBase58()}`);

          // Step 5: Mint initial token supply to distributor
          // 5a: Create 10 billion tokens in the ATA for airdrop distribution
          const mintToTxId = await mintTo(
            connection,
            PAYER, // fee payer and mint authority
            mint, // token mint
            ata.address, // destination ATA
            PAYER.publicKey, // mint authority
            10e9 * LAMPORTS_PER_SOL // amount: 10 billion tokens with decimals
          );
          console.log(`mint-to success! txId: ${mintToTxId}`);

          // Step 6: Get compression infrastructure for batch operations
          // 6a: Fetch available state trees for compressed account storage
          const stateTreeInfos = await connection.getStateTreeInfos();

          // 6b: Get token pool infos for compression operations
          const tokenPoolInfos = await getTokenPoolInfos(connection, mint);

          // Step 7: Create instruction batches for large-scale airdrop
          // 7a: Generate batched compression instructions with compute optimization
          const instructionBatches = await createAirdropInstructions({
            amount: 1e6, // 1 million tokens per recipient
            recipients, // array of recipient addresses
            payer: PAYER.publicKey, // transaction fee payer
            sourceTokenAccount: ata.address, // source ATA holding SPL tokens
            mint, // token mint
            stateTreeInfos, // state trees for compressed accounts
            tokenPoolInfos, // token pools for compression
            computeUnitPrice: calculateComputeUnitPrice(10_000, 500_000), // dynamic priority fee
          });

          // Step 8: Execute batched airdrop with error handling
          // 8a: Process instruction batches with retry logic and confirmation
          for await (const result of signAndSendAirdropBatches(
            instructionBatches,
            PAYER,
            connection
          )) {
            if (result.type === BatchResultType.Success) {
              console.log(`Batch ${result.index} confirmed: ${result.signature}`);
            } else if (result.type === BatchResultType.Error) {
              console.log(`Batch ${result.index} failed: ${result.error}`);
              // Use result.index to access the specific batch in instructionBatches
              const failedBatch = instructionBatches[result.index];
              console.log(`Failed batch instructions:`, failedBatch);
              // Additional logic to handle failed instructions
            }
          }

          console.log("Airdrop process complete.");
        })();
        ```
      </Step>
    </Steps>
  </Tab>
</Tabs>

# Advanced Features

<Info>
  Solana Wallets like Phantom and Backpack already support compressed tokens.
  Still, you can let users decompress to SPL via your Frontend to customize claims.
</Info>

* Full Example on Github to [create an Airdrop with Claim](https://github.com/Lightprotocol/example-compressed-claim).

* Add decompression of SPL Tokens with this script.

```typescript expandable theme={null}
import {
  bn,
  buildAndSignTx,
  sendAndConfirmTx,
  dedupeSigner,
  Rpc,
  createRpc,
} from "@lightprotocol/stateless.js";
import { ComputeBudgetProgram, Keypair, PublicKey } from "@solana/web3.js";
import {
  CompressedTokenProgram,
  getTokenPoolInfos,
  selectMinCompressedTokenAccountsForTransfer,
  selectTokenPoolInfosForDecompression,
} from "@lightprotocol/compressed-token";
import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token";
import bs58 from "bs58";
import dotenv from "dotenv";
dotenv.config();

// Set these values in your .env file
const RPC_ENDPOINT = process.env.RPC_ENDPOINT;
const mint = new PublicKey(process.env.MINT_ADDRESS!);
const payer = Keypair.fromSecretKey(bs58.decode(process.env.PAYER_KEYPAIR!));

const owner = payer;
const amount = 1e5;
const connection: Rpc = createRpc(RPC_ENDPOINT);

(async () => {
  // 1. Create an associated token account for the user if it doesn't exist
  const ata = await getOrCreateAssociatedTokenAccount(
    connection,
    payer,
    mint,
    payer.publicKey
  );

  // 2. Fetch compressed token accounts
  const compressedTokenAccounts =
    await connection.getCompressedTokenAccountsByOwner(owner.publicKey, {
      mint,
    });

  // 3. Select
  const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer(
    compressedTokenAccounts.items,
    bn(amount)
  );

  // 4. Fetch validity proof
  const proof = await connection.getValidityProof(
    inputAccounts.map((account) => account.compressedAccount.hash)
  );

  // 5. Fetch token pool infos
  const tokenPoolInfos = await getTokenPoolInfos(connection, mint);

  // 6. Select
  const selectedTokenPoolInfos = selectTokenPoolInfosForDecompression(
    tokenPoolInfos,
    amount
  );

  // 7. Build instruction
  const ix = await CompressedTokenProgram.decompress({
    payer: payer.publicKey,
    inputCompressedTokenAccounts: inputAccounts,
    toAddress: ata.address,
    amount,
    tokenPoolInfos: selectedTokenPoolInfos,
    recentInputStateRootIndices: proof.rootIndices,
    recentValidityProof: proof.compressedProof,
  });

  // 8. Sign, send, and confirm
  const { blockhash } = await connection.getLatestBlockhash();
  const additionalSigners = dedupeSigner(payer, [owner]);
  const signedTx = buildAndSignTx(
    [ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }), ix],
    payer,
    blockhash,
    additionalSigners
  );
  return await sendAndConfirmTx(connection, signedTx);
})();

```

<Tip>
  Set priority fees dynamically for decompression. Learn more [here](https://docs.helius.dev/guides/sending-transactions-on-solana#summary).
</Tip>

### Native Swap via Jup-API

If you have a custom FE, you can let users swap compressed tokens using the Jup-API. A reference implementation is available [here](https://github.com/Lightprotocol/example-jupiter-swap-node).

# Next Steps

<Card title="Create an airdrop with claim functionality." icon="chevron-right" color="#0066ff" href="https://github.com/Lightprotocol/example-compressed-claim/blob/c9e4d049ab6c6c36fe06cb7e7770b79b072271ef/README.md" horizontal />
