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.

Guides

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

Cost Comparison

You can use the Airship Calculator to anticipate the cost of your airdrop.

Airdrop Size
Regular Airdrop
ZK Compression Airdrop

10,000

20.4 SOL (*$4,080)

0.0065 SOL (*$1.3)

100,000

203.96 SOL (*$40,080)

0.065 SOL (*$13)

1,000,000

2039.28 SOL (*$400,080)

0.65 SOL (*$130)

** assuming $200 per SOL

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

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

Prerequisites & Setup

Dependencies

npm install --save-dev typescript tsx @types/node && \
npm install --save \
    @lightprotocol/stateless.js \
    @lightprotocol/compressed-token \
    @solana/web3.js \
    @solana/spl-token

Alternatives:

yarn add --dev typescript tsx @types/node && \
yarn add \
    @lightprotocol/stateless.js \
    @lightprotocol/compressed-token \
    @solana/web3.js \
    @solana/spl-token
pnpm add --save-dev typescript tsx @types/node && \
pnpm add \
    @lightprotocol/stateless.js \
    @lightprotocol/compressed-token \
    @solana/web3.js \
    @solana/spl-token

Developer Environment

By default, this guide uses Localnet.

# Install the development CLI
npm install @lightprotocol/zk-compression-cli
# Start a local test validator
light test-validator
// 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();

Alternative: Using Devnet

Follow these steps to create an RPC Connection. Replace <your_api_key> with your API key before running.

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);
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.

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 < 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.

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.

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

Explore more guides in our cookbook.

Guides

Last updated

Was this helpful?