Create an Airdrop
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.
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
Guides
There are two ways to use ZK Compression to distribute your SPL tokens.
Programmatic Airdrop
What you will do
By the end of this guide you will have a fully functioning programmatic airdrop.
The high-level overview is this:
Mint and send the to-be-airdropped SPL tokens to a wallet you control.
Create batches of instructions based on a list of recipients and amounts.
Build transactions from these instruction batches, then sign, send, and confirm them.
Tokens will appear in the recipients wallets automatically, or you can implement a claim function.
Get started
Preqrequisites to Initialize Airdrop Project
Make sure you have dependencies and developer environment set up!
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;
})();
Next Steps
You're ready to deploy an airdrop on devnet or mainnet.
You can use an existing mint or create a new one with createMint
.
Configure your environment variables
RPC_ENDPOINT=https://devnet.helius-rpc.com?api-key=YOUR_API_KEY
PAYER_KEYPAIR=YOUR_BASE58_ENCODED_PRIVATE_KEY
MINT_ADDRESS=YOUR_MINT_ADDRESS
Mint SPL tokens to your wallet, as shown in the guide above. See the source code here.
Choose below between the
Simple Airdrop Script for <10,000 recipients, and
Script for large-scale Airdrops with batched operations
Add the variables to your airdrop script & execute the airdrop!
A. Simple Airdrop
For small airdrops (<10,000 recipients). View the source code here.
B. Large-scale Airdrop with Batched Operations
For large-scale airdrops (10,000+ recipients) we recommend to batch operations efficiently. View the source code here.
create-instructions.ts - Process recipients in chunks, create batched CompressedTokenProgram.compress() instructions with optimized compute limits
update-blockhash.ts - Maintain fresh blockhashes with background refresh loop using getLatestBlockhash() every 30 seconds
sign-and-send.ts - Execute batched transactions with VersionedTransaction, retry logic, and sendAndConfirmTx() confirmation
airdrop.ts - Finally, put it all together in your main file:
Advanced Features
Decompress / Claim
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 section.
CookbookLast updated