| title | Token Distribution | ||||||
|---|---|---|---|---|---|---|---|
| sidebarTitle | Token Distribution | ||||||
| description | Complete client and program guides to create an airdrop – with or without code. ZK compression is the most efficient way to distribute SPL tokens. | ||||||
| keywords |
|
import { TokenAccountCompressedVsSpl } from '/snippets/jsx/token-account-compressed-vs-spl.jsx'; import InstallDependencies from '/snippets/setup/install-dependencies-codegroup.mdx'; import SetupEnvironment from '/snippets/setup/setup-environment-tabs.mdx'; import AgentSkillAirdrop from "/snippets/setup/agent-skill-airdrop.mdx"; import AirdropAiPrompt from "/snippets/ai-prompts/toolkits/airdrop.mdx";
Choose your implementation based on your needs:
| Tab | Best For | What You'll Get | Time |
|---|---|---|---|
| Localnet Guide | 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 |
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
Developer Environment
### Mint SPL tokens to your walletRun 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 SPL interface 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 { createSplInterface } 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 SPL interface
const mint = await createMint(connection, payer, payer.publicKey, null, 9);
const poolTxId = await createSplInterface(connection, payer, mint);
console.log(`Mint address: ${mint.toBase58()}`);
console.log(`SPL interface 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 SPL interface 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 SPL interface 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()}`);
})();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 LightTokenProgram.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 {
LightTokenProgram,
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 SPL interface
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 SPL interface 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 LightTokenProgram.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 SPL interface 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;
})();Configure your environment variables:
PAYER_KEYPAIR=YOUR_BASE58_ENCODED_PRIVATE_KEY
MINT_ADDRESS=YOUR_MINT_ADDRESSUse your existing mint or mint SPL tokens to your wallet for distribution.
import { Rpc, createRpc } from '@lightprotocol/stateless.js';
import { createMint } from '@lightprotocol/compressed-token';
import {
getOrCreateAssociatedTokenAccount,
mintTo as mintToSpl,
} from '@solana/spl-token';
import { PAYER_KEYPAIR, RPC_ENDPOINT } from '../constants';
const payer = PAYER_KEYPAIR;
const connection: Rpc = createRpc(RPC_ENDPOINT);
const decimals = 9;
const mintAmount = 100;
(async () => {
// airdrop lamports to pay tx fees
// await confirmTx(
// connection,
// await connection.requestAirdrop(payer.publicKey, 1e7)
// );
const { mint, transactionSignature } = await createMint(
connection,
payer,
payer.publicKey,
decimals,
);
console.log(
`create-mint success! txId: ${transactionSignature}, mint: ${mint.toBase58()}`,
);
const ata = await getOrCreateAssociatedTokenAccount(
connection,
payer,
mint,
payer.publicKey,
);
console.log(`ata: ${ata.address}`);
const mintTxId = await mintToSpl(
connection,
payer,
mint,
ata.address,
payer.publicKey,
mintAmount,
);
console.log(`mint-spl success! txId: ${mintTxId}`);
})();Run the airdrop script with your configured environment:
// 1. Load environment and select compression infrastructure with getStateTreeInfos() and getTokenPoolInfos()
// 2. Build LightTokenProgram.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 {
LightTokenProgram,
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 SPL interface
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 LightTokenProgram.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}`);
})();Process recipients in chunks and create batched instructions with optimized compute limits.
// 1. Process recipients in chunks with selectStateTreeInfo() and selectTokenPoolInfo() per batch
// 2. Create LightTokenProgram.compress() instructions with ComputeBudgetProgram limits for multiple recipients
// 3. Return batched instructions for optimized large-scale airdrop execution
import {
LightTokenProgram,
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 LightTokenProgram.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;
}Maintain fresh blockhashes with background refresh loop using getLatestBlockhash() every 30 seconds.
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));
})();
}Execute batched transactions with VersionedTransaction, retry logic, and sendAndConfirmTx() confirmation.
// 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();
}Put it all together in the main airdrop file.
// 1. Create compressed mint with createMint(), mint supply with mintTo(), get infrastructure with getStateTreeInfos() and getTokenPoolInfos() (SPL interface infos)
// 2. Generate batched compression instructions with createAirdropInstructions() - create LightTokenProgram.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 SPL interface infos for compression
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,
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.");
})();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 {
LightTokenProgram,
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 SPL interface infos
const tokenPoolInfos = await getTokenPoolInfos(connection, mint);
// 6. Select
const selectedTokenPoolInfos = selectTokenPoolInfosForDecompression(
tokenPoolInfos,
amount
);
// 7. Build instruction
const ix = await LightTokenProgram.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);
})();If you have a custom FE, you can let users swap compressed tokens using the Jup-API. A reference implementation is available here.
Customize token distribution and let users claim.
| distributor | simple-claim | |
|---|---|---|
| Vesting | Linear Vesting | Cliff at Slot X |
| Partial claims | Yes | No |
| Clawback | Yes | No |
| Frontend | REST API + CLI | None |
<Card title="Simple Airdrop with Claim" icon="hand-holding" color="#0066ff" href="https://github.com/Lightprotocol/program-examples/tree/main/airdrop-implementations/simple-claim" horizontal
Simple SPL token airdrop with claim.