Agent skill
Agent skill
Use the airdrop agent skill for token distribution and airdrops:For orchestration, install the general skill:
- Claude Code
- Cursor
- Any Agent
Add the marketplace and install:
Report incorrect code
Copy
Ask AI
/plugin marketplace add Lightprotocol/skills
/plugin install solana-rent-free-dev
- Open Settings (Cmd+Shift+J / Ctrl+Shift+J)
- Navigate to Rules & Commands → Project Rules → Add Rule → Remote Rule (GitHub)
- Enter:
https://github.com/Lightprotocol/skills.git
Report incorrect code
Copy
Ask AI
npx skills add Lightprotocol/skills
Report incorrect code
Copy
Ask AI
npx skills add https://zkcompression.com
- Client Side Distribution
- Claim Reference Implementations
Choose your implementation based on your needs:
Add decompression of SPL Tokens with this script.
| 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 |
Via Webapp you can use Airship by Helius Labs to airdrop to up to 200,000 recipients.
- Localnet Guide
- Simple Airdrop
- Airdrop with Batched Instructions
- AI Prompt
What you’ll build: A test airdrop sending compressed tokens to 3 recipients on your local validator.
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
Prerequisites & Setup
DependenciesDeveloper Environment
- npm
- yarn
- pnpm
Report incorrect code
Copy
Ask AI
npm install @lightprotocol/stateless.js@beta \
@lightprotocol/compressed-token@beta
Report incorrect code
Copy
Ask AI
yarn add @lightprotocol/stateless.js@beta \
@lightprotocol/compressed-token@beta
Report incorrect code
Copy
Ask AI
pnpm add @lightprotocol/stateless.js@beta \
@lightprotocol/compressed-token@beta
- Localnet
- Devnet
By default, all guides use Localnet.
- npm
- yarn
- pnpm
Report incorrect code
Copy
Ask AI
npm install -g @lightprotocol/zk-compression-cli@beta
Report incorrect code
Copy
Ask AI
yarn global add @lightprotocol/zk-compression-cli@beta
Report incorrect code
Copy
Ask AI
pnpm add -g @lightprotocol/zk-compression-cli@beta
Report incorrect code
Copy
Ask AI
# Start a local test validator
light test-validator
## ensure you have the Solana CLI accessible in your system PATH
Report incorrect code
Copy
Ask AI
// 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();
Replace
<your-api-key> with your actual API key. Get your API key here, if you don’t have one yet.Report incorrect code
Copy
Ask AI
import { createRpc } from "@lightprotocol/stateless.js";
const RPC_ENDPOINT = "https://devnet.helius-rpc.com?api-key=<your_api_key>";
const connection = createRpc(RPC_ENDPOINT);
async function main() {
let slot = await connection.getSlot();
console.log(slot);
let health = await connection.getIndexerHealth(slot);
console.log(health);
// "Ok"
}
main();
Mint SPL tokens to your wallet
Run thismint-spl-tokens.ts to mint SPL tokens to your wallet.mint-spl-tokens.ts
Report incorrect code
Copy
Ask AI
// 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()}`);
})();
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
Report incorrect code
Copy
Ask AI
// 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;
})();
For small airdrops (<10,000 recipients). View the source code here.
Environment Setup
Configure your environment variables:.env
Report incorrect code
Copy
Ask AI
PAYER_KEYPAIR=YOUR_BASE58_ENCODED_PRIVATE_KEY
MINT_ADDRESS=YOUR_MINT_ADDRESS
Mint SPL Tokens
Use your existing mint or mint SPL tokens to your wallet for distribution.Report incorrect code
Copy
Ask AI
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}`);
})();
Execute Airdrop
Run the airdrop script with your configured environment:Report incorrect code
Copy
Ask AI
// 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}`);
})();
For large-scale airdrops (10,000+ recipients) we recommend batched instructions.
View the source code here.
View the source code here.
This implementation creates the mint and executes the airdrop in a single operation. You only need to configure your RPC endpoint and payer keypair.
Create Instructions
Process recipients in chunks and create batched instructions with optimized compute limits.create-instructions.ts
Report incorrect code
Copy
Ask AI
// 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;
}
Update Blockhash
Maintain fresh blockhashes with background refresh loop usinggetLatestBlockhash() every 30 seconds.update-blockhash.ts
Report incorrect code
Copy
Ask AI
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));
})();
}
Sign and Send
Execute batched transactions withVersionedTransaction, retry logic, and sendAndConfirmTx() confirmation.sign-and-send.ts
Report incorrect code
Copy
Ask AI
// 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();
}
Main Airdrop File
Put it all together in the main airdrop file.airdrop.ts
Report incorrect code
Copy
Ask AI
// 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.");
})();
Distribute compressed tokens via airdrop
Report incorrect code
Copy
Ask AI
---
description: Distribute compressed tokens via airdrop
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
---
## Distribute compressed tokens via airdrop
Context:
- Guide: https://zkcompression.com/compressed-tokens/airdrop
- Skills and resources index: https://zkcompression.com/skill.md
- Dedicated skill: https://github.com/Lightprotocol/skills/tree/main/skills/airdrop
- Packages: @lightprotocol/compressed-token, @lightprotocol/stateless.js, @solana/spl-token
- Example repo: https://github.com/Lightprotocol/example-token-distribution
- Webapp alternative: https://airship.helius.dev/ (Airship by Helius Labs, up to 200k recipients)
Key APIs: LightTokenProgram.compress(), getTokenPoolInfos(), selectTokenPoolInfo(), getStateTreeInfos(), selectStateTreeInfo(), buildAndSignTx(), sendAndConfirmTx()
### 1. Index project
- Grep `LightTokenProgram|compress|getTokenPoolInfos|selectTokenPoolInfo|getStateTreeInfos|@lightprotocol|airdrop|distribution` across src/
- Glob `**/*.ts` for project structure
- Identify: existing airdrop/distribution logic, token minting setup, recipient list format
- Check package.json for existing @lightprotocol/* or @solana/spl-token dependencies
- Task subagent (Grep/Read/WebFetch) if project has multiple packages to scan in parallel
### 2. Read references
- WebFetch the guide above — review all three tabs (Localnet Guide, Simple Airdrop, Batched)
- WebFetch skill.md — check for a dedicated skill and resources matching this task
- TaskCreate one todo per phase below to track progress
### 3. Clarify intention
- AskUserQuestion: what scale? (<10k recipients = simple airdrop, 10k+ = batched)
- AskUserQuestion: localnet testing first, or production deploy?
- AskUserQuestion: do you have an existing SPL mint, or need to create one?
- AskUserQuestion: do you need decompression/claim functionality, or just direct distribution?
- Summarize findings and wait for user confirmation before implementing
### 4. Create plan
- Based on steps 1–3, draft an implementation plan
- For simple airdrop: create mint → mint SPL tokens → LightTokenProgram.compress() with recipients array
- For batched: create instruction batches → manage blockhash refresh → sign and send with retry logic
- Address lookup table needed for production (mainnet: 9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ)
- If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
- Present the plan to the user for approval before proceeding
### 5. Implement
- Add deps if missing: Bash `npm install @lightprotocol/compressed-token @lightprotocol/stateless.js @solana/spl-token`
- Set up RPC: `createRpc(RPC_ENDPOINT)` with a ZK Compression endpoint (Helius, Triton)
- Follow the guide and the approved plan
- Write/Edit to create or modify files
- TaskUpdate to mark each step done
### 6. Verify
- Bash `tsc --noEmit`
- Bash run existing test suite or execute localnet test airdrop
- TaskUpdate to mark complete
### Tools
- mcp__zkcompression__SearchLightProtocol("<query>") for API details
- mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "<q>") for architecture
- Task subagent with Grep/Read/WebFetch for parallel lookups
- TaskList to check remaining work
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.
Report incorrect code
Copy
Ask AI
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);
})();
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.
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 |
The programs are reference implementations and should be audited before production use.