Skip to main content

Choose your implementation based on your needs:
TabBest ForWhat You’ll GetTime
Localnet GuideFirst-time users, learningStep-by-step Localnet tutorial20 min
Simple Airdrop<10,000 recipientsProduction-ready single script10 min
Airdrop with Batched Instructions10,000+ recipientsBatched system with retry logic15 min
Via Webapp you can use Airship by Helius Labs to airdrop to up to 200,000 recipients.
What you’ll build: A test airdrop sending compressed tokens to 3 recipients on your local validator.
1

Prerequisites

Make sure you have dependencies and developer environment set up!
System Requirements
  • Node.js >= 20.18.0 (required by latest Solana packages)
  • npm or yarn package manager
Dependencies
npm install @lightprotocol/stateless.js@alpha \
            @lightprotocol/compressed-token@alpha
Developer Environment
By default, all guides use Localnet.
npm install -g @lightprotocol/zk-compression-cli@alpha
# Start a local test validator
light test-validator

## ensure you have the Solana CLI accessible in your system PATH
// 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();
2

Mint SPL tokens to your wallet

Run this mint-spl-tokens.ts to mint SPL tokens to your wallet.
mint-spl-tokens.ts
// 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()}`);
})();
3

Execute the Airdrop

Next, distribute the SPL tokens to all recipients.
Ensure you have the latest @lightprotocol/stateless.js and @lightprotocol/compressed-token versions ≥ 0.21.0!
simple-airdrop.ts
// 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;
})();

Advanced Features

Solana Wallets like Phantom and Backpack already support compressed tokens. Still, you can let users decompress to SPL via your Frontend to customize claims.
Add decompression of SPL Tokens with this script.
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);
})();

Set priority fees dynamically for decompression. Learn more here.

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.

Next Steps

Create an airdrop with claim functionality.