Create Airdrop

Step-by-Step: Create Airdrop

This guide walks you through the full process of creating an airdrop using the Dropsy SDK. Make sure you’ve already initialized a Controller and have a funded wallet.

Before creating an airdrop, you need to prepare the complete list of eligible wallets and their token allocations. Each entry must include:

index

Unique position in the list (0-based)

address

Recipient's wallet address

amount

Tokens to allocate (in base units)

Type Definition
export type ClaimEntry = {
  index: number;          // Position in the list (0-based)
  address: string;       // Recipient wallet address  (string)
  amount: number;         // Token amount in base units 
};

export type ClaimList = ClaimEntry[];
Example Implementation

// Example claim list for 3 recipients
const claimList: ClaimList = [
  {
    index: 0,
    address: "6TMm1JHo6yBvQzJLu9jo8L1rD1XkqX8Z7Q5J7Xy6oE1r",
    amount: 500, // 500 tokens (in base units)
  },
  {
    index: 1,
    address: "CeN4TqZaxMc9adCeCtE6VqfpwRbmt6DRYMnH41vv9V5F",
    amount: 1000, // 1000 tokens
  },
  {
    index: 2,
    address: "BtjQcFXEA3qQ51JcmKCgHDKBooKW7pCM7iukfAr5cuW9",
    amount: 250, // 250 tokens
  },
]; 

💡 Best Practice: For airdrops with many recipients, generate this list programmatically from a CSV file or database, ensuring each address is properly validated.

2. Generate Merkle Root

The Merkle root serves as the cryptographic fingerprint of your entire claim list. It enables efficient verification of individual claims without storing all addresses on-chain.

Helper Functions
import { createHash } from "crypto";
import { MerkleTree } from "merkletreejs";
import type { ClaimList } from "./types";

// Hash a single claim entry
export function hashClaimEntry(entry: ClaimEntry): Buffer {
  const data = `${entry.index}:${entry.address}:${entry.amount}`;
  return createHash("sha256").update(data).digest();
}

// Generate complete Merkle tree helper
export const createMerkleTree = (
  claimList: { index: number; address: string; amount: number }[]
) => {
  const leaves = claimList.map(({ index, address, amount }) =>
    hashClaimEntry(index, address, amount)
  );

  const merkleTree = new MerkleTree(
    leaves,
    (data: any) => createHash("sha256").update(data).digest(),
    { sortPairs: true }
  );

  return merkleTree;
};

// Get root as Uint8Array for on-chain use
export function getMerkleRootArray(tree: MerkleTree): Uint8Array {
  return new Uint8Array(tree.getRoot());
}

// helper to get proof for eligible wallets
export const getMerkleProof = (
  merkleTree: MerkleTree,
  address: string,
  amount: number,
  index: number
): number[][] => {
  const leaf = hashClaimEntry(index, address, amount);
  const proof = merkleTree.getProof(leaf);

  return proof.map((p) => Array.from(p.data));
};

Note: The sortPairs option ensures deterministic tree generation across different runs.

Example Usage
import { createMerkleTree, getMerkleRootArray, getMerkleProof } from "./merkleUtils"; // from utils we created in top 

// define claimList like it shown in top 
const claimList = [{....}, {....}]

// Generate the Merkle tree
const merkleTree = createMerkleTree(claimList);

// Get the root hash as Uint8Array
const merkleRootArray = getMerkleRootArray(merkleTree);

// For proof generation later while claiming airdrop :
const claimProof = getMerkleProof(merkleTree, claimer.address, 500, 1);
Performance Considerations
  • For large claim lists (more than 10k entries), consider processing in batches
  • Store the generated tree for future proof generation
  • Validate all addresses before tree generation
4. Get Create Airdrop Instruction

This creates the transaction instruction with your claim list's Merkle root, mint, and controller:

const instruction = await getInitializeAirdropInstructionAsync({
  authority: client.wallet,   // the wallet signer 
  mint,           // mint address (support both token standard or token 2022)
  controller,  // find an Already created controller or create your own 
  merkleRoot: merkleRootArray,
  startsTime: null, // or BigInt(Date.now() / 1000)
  endTime: null,  // or BigInt(Date.now() / 1000 + 60*60*24)
});
5. Transaction Building and Signing
const transactionMessage = await createTransactionMessageFromInstructions(
  client.rpc,
  client.wallet,
  [instruction]
);
const signedTransaction = await signTransactionMessageWithSigners(
  transactionMessage
);

Prepares the transaction for network transmission and signs it with the wallet.

6. Send and Confirm Transaction
await client.sendAndConfirmTransaction(signedTransaction, { commitment: "confirmed" });
const signature = getSignatureFromTransaction(signedTransaction);

console.log("signature:", signature);

Submits the signed transaction to the network and waits for confirmation.

✅ Almost there !! one more step to initialize the required claimMap to verify claim process

Initialize the ClaimMap

A ClaimMap is a bitmap account that tracks claims for an airdrop. Each ClaimMap can store up to 50,000 entries. If your airdrop has fewer users, you only need one ClaimMap.

📌 How it Works
  • The ClaimMap is a PDA derived using the Airdrop PDA and the map id (starts from 0).
  • The total field defines how many bits to allocate. This reduces rent for smaller airdrops.
🔧 Initialize the ClaimMap
const instruction = await getInitializeBitmapInstructionAsync({
  airdrop: airdropPda,
  authority: client.wallet,
  id: 0, // First map, for up to 50,000 users
  total: 50000, // Number of eligible users to track (smaller = cheaper rent)
});

🧠 If you expect more than 50,000 users, call this again with id: 1, id: 2, etc.

✅ Once the ClaimMap is initialized, you can safely deposit tokens and let users begin claiming.

✅ You now have an on-chain airdrop account ready for deposits and claims!