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
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:
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
What you'll build: A test airdrop sending compressed tokens to 3 recipients on your local validator.
Mint SPL tokens to your wallet
Run this mint-spl-tokens.ts to mint SPL tokens to your wallet.
// 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()}`);
})();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 - 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;
})();For small airdrops (<10,000 recipients). View the source code here.
Mint SPL Tokens
Mint SPL tokens to your wallet for distribution. See the mint script example.
Execute Airdrop
Run the airdrop script with your configured environment:
// 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}`);
})();For large-scale airdrops (10,000+ recipients) we recommend batched instructions. View the source code here.
Advanced Features
Full Example on Github to create an Airdrop with Claim.
Add decompression of SPL Tokens with this script.
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.
GuidesLast updated
Was this helpful?



