Skip to main content

Airdrop System Overview

The TokenOps airdrop system enables efficient, secure, and gas-optimized token distributions to large numbers of recipients. Using Merkle tree technology, the system can handle distributions to thousands of addresses while keeping gas costs minimal.

What are Airdrops?

Airdrops are distributions of tokens or cryptocurrency to multiple wallet addresses, typically used for:

  • Community Building: Reward early adopters and community members
  • Marketing Campaigns: Create awareness and attract new users
  • Product Launches: Distribute governance or utility tokens
  • Retroactive Rewards: Compensate users for past participation

Key Benefits

  • Gas Efficiency: Merkle trees minimize on-chain storage and gas costs
  • Scalability: Handle distributions to unlimited recipients
  • Security: Cryptographic proofs ensure only eligible users can claim
  • Flexibility: Support for ERC20 tokens and native ETH distributions

Airdrop System Architecture

Airdrop Contract Types

1. ERC20 Merkle Distributor

Standard token airdrops

// Perfect for: Token distributions, governance token launches
const distributor = new MerkleDistributorERC20(contractAddress, provider);

Features:

  • ERC20 token distribution
  • Merkle proof verification
  • Efficient gas usage
  • Unclaimed token recovery
  • Time-based claim windows

2. Native Token Merkle Distributor

ETH and native token airdrops

// Perfect for: ETH rewards, native token distributions
const nativeDistributor = new MerkleDistributorNative(contractAddress, provider);

Features:

  • ETH/native token distribution
  • No token approval required
  • Direct value transfers
  • Gas refund mechanisms
  • Emergency withdrawal functions

Factory Deployment

Deploy airdrop contracts using the factory pattern:

import { MerkleDistributorFactory } from 'tokenops-sdk';

const factory = new MerkleDistributorFactory(factoryAddress, provider);

// Deploy ERC20 airdrop
const erc20AirdropResult = await factory.newMerkleDistributor(
tokenAddress, // ERC20 token to distribute
merkleRoot, // Merkle tree root
BigInt(Math.floor(Date.now() / 1000) + 3600), // Start in 1 hour
BigInt(Math.floor(Date.now() / 1000) + 7 * 24 * 3600), // End in 1 week
stakingContractAddress, // Optional staking contract for bonuses
rewardOwnerAddress, // Who receives unclaimed tokens
BigInt(20), // 20% bonus for immediate staking
{ account: deployerAddress }
);

console.log('ERC20 Airdrop deployed:', erc20AirdropResult.merkleDistributor);

Claiming Process

Basic Token Claiming

// Recipients claim their tokens using Merkle proofs
async function claimAirdrop(
distributorAddress: string,
userAddress: string,
amount: bigint,
merkleProof: string[]
) {
const distributor = new MerkleDistributorERC20(distributorAddress, provider);

// Check if user has already claimed
const hasClaimed = await distributor.isClaimed(userAddress);
if (hasClaimed) {
throw new Error('User has already claimed their airdrop');
}

// Claim tokens
const claimResult = await distributor.claim(
0, // Claim index (from Merkle tree)
userAddress, // Recipient address
amount, // Amount to claim
merkleProof, // Merkle proof array
{ account: userAddress }
);

console.log('Claimed amount:', claimResult.amount);
return claimResult;
}

Claim and Stake

// Claim tokens and immediately stake them for bonus rewards
async function claimAndStake(
distributorAddress: string,
userAddress: string,
amount: bigint,
merkleProof: string[]
) {
const claimStakeDistributor = new MerkleDistributorNative(
distributorAddress,
provider
);

// Claim and stake in one transaction
const result = await claimStakeDistributor.claimAndStake(
0, // Claim index
userAddress, // Recipient
amount, // Base amount
merkleProof, // Proof
{ account: userAddress }
);

console.log('Base claimed:', result.baseAmount);
console.log('Bonus received:', result.bonusAmount);
console.log('Total staked:', result.totalStaked);

return result;
}

Analytics and Monitoring

Claim Analytics

Track and analyze airdrop performance with comprehensive analytics:

import { MerkleDistributorERC20, TokenOpsApi } from 'tokenops-sdk';

interface AirdropAnalytics {
totalRecipients: number;
claimedCount: number;
unclaimedCount: number;
totalDistributed: bigint;
claimRate: number;
averageClaimAmount: bigint;
claimsByDay: Array<{ date: string; claims: number }>;
topClaims: Array<{ address: string; amount: bigint }>;
timeToExpiration: number;
unclaimedValue: bigint;
}

class AirdropAnalytics {
private distributor: MerkleDistributorERC20;
private api: TokenOpsApi;

constructor(
distributorAddress: string,
provider: any,
apiKey: string
) {
this.distributor = new MerkleDistributorERC20(distributorAddress, provider);
this.api = new TokenOpsApi(apiKey);
}

async getComprehensiveAnalytics(): Promise<AirdropAnalytics> {
// Get contract state
const merkleRoot = await this.distributor.getMerkleRoot();
const endTime = await this.distributor.getEndTime();
const token = await this.distributor.getToken();

// Get all claim events
const claimEvents = await this.getClaimEvents();

// Calculate basic metrics
const totalRecipients = await this.getTotalRecipients();
const claimedCount = claimEvents.length;
const unclaimedCount = totalRecipients - claimedCount;

const totalDistributed = claimEvents.reduce(
(sum, event) => sum + event.amount,
BigInt(0)
);

const claimRate = (claimedCount / totalRecipients) * 100;
const averageClaimAmount = claimedCount > 0 ?
totalDistributed / BigInt(claimedCount) : BigInt(0);

// Time-based analytics
const claimsByDay = this.groupClaimsByDay(claimEvents);
const topClaims = this.getTopClaims(claimEvents, 10);

// Expiration analytics
const timeToExpiration = Number(endTime) - Math.floor(Date.now() / 1000);
const unclaimedValue = await this.calculateUnclaimedValue(totalRecipients - claimedCount);

return {
totalRecipients,
claimedCount,
unclaimedCount,
totalDistributed,
claimRate,
averageClaimAmount,
claimsByDay,
topClaims,
timeToExpiration,
unclaimedValue
};
}

private async getClaimEvents(): Promise<Array<{
account: string;
amount: bigint;
timestamp: number;
blockNumber: number;
transactionHash: string;
}>> {
// Get events from contract deployment block to current
const deploymentBlock = await this.distributor.getDeploymentBlockNumber();
const currentBlock = await this.distributor.provider.viemClient.getBlockNumber();

const events = await this.distributor.provider.viemClient.getLogs({
address: this.distributor.distributorAddress,
event: {
type: 'event',
name: 'Claimed',
inputs: [
{ name: 'index', type: 'uint256', indexed: true },
{ name: 'account', type: 'address', indexed: true },
{ name: 'amount', type: 'uint256', indexed: false }
]
},
fromBlock: deploymentBlock,
toBlock: currentBlock
});

// Enrich events with timestamp data
const enrichedEvents = await Promise.all(
events.map(async (event) => {
const block = await this.distributor.provider.viemClient.getBlock({
blockNumber: event.blockNumber
});

return {
account: event.args.account,
amount: event.args.amount,
timestamp: Number(block.timestamp),
blockNumber: Number(event.blockNumber),
transactionHash: event.transactionHash
};
})
);

return enrichedEvents;
}

private groupClaimsByDay(events: any[]): Array<{ date: string; claims: number }> {
const groupedByDay = events.reduce((acc, event) => {
const date = new Date(event.timestamp * 1000).toISOString().split('T')[0];
acc[date] = (acc[date] || 0) + 1;
return acc;
}, {} as Record<string, number>);

return Object.entries(groupedByDay)
.map(([date, claims]) => ({ date, claims }))
.sort((a, b) => a.date.localeCompare(b.date));
}

private getTopClaims(events: any[], limit: number): Array<{ address: string; amount: bigint }> {
return events
.sort((a, b) => b.amount > a.amount ? 1 : -1)
.slice(0, limit)
.map(event => ({
address: event.account,
amount: event.amount
}));
}

private async getTotalRecipients(): Promise<number> {
// This would need to be calculated from the original merkle tree data
// For now, we'll estimate based on contract state or metadata
const totalAmount = await this.distributor.getTotalAmount();
return 1000; // Placeholder - implement based on your merkle tree data
}

private async calculateUnclaimedValue(unclaimedCount: number): Promise<bigint> {
const totalAmount = await this.distributor.getTotalAmount();
const claimedAmount = await this.getClaimedAmount();
return totalAmount - claimedAmount;
}

private async getClaimedAmount(): Promise<bigint> {
const events = await this.getClaimEvents();
return events.reduce((sum, event) => sum + event.amount, BigInt(0));
}
}

Real-time Monitoring

Set up real-time monitoring for airdrop claims:

class AirdropMonitor {
private distributor: MerkleDistributorERC20;
private webhookUrl?: string;
private isMonitoring = false;

constructor(distributorAddress: string, provider: any, webhookUrl?: string) {
this.distributor = new MerkleDistributorERC20(distributorAddress, provider);
this.webhookUrl = webhookUrl;
}

startMonitoring() {
if (this.isMonitoring) return;

this.isMonitoring = true;
console.log('Starting airdrop monitoring...');

// Listen for claim events
this.distributor.provider.viemClient.watchContractEvent({
address: this.distributor.distributorAddress,
abi: [{
type: 'event',
name: 'Claimed',
inputs: [
{ name: 'index', type: 'uint256', indexed: true },
{ name: 'account', type: 'address', indexed: true },
{ name: 'amount', type: 'uint256', indexed: false }
]
}],
eventName: 'Claimed',
onLogs: async (logs) => {
for (const log of logs) {
await this.handleClaimEvent(log);
}
}
});

// Monitor deadlines
this.monitorDeadlines();
}

private async handleClaimEvent(event: any) {
const claimData = {
account: event.args.account,
amount: event.args.amount,
timestamp: new Date(),
blockNumber: event.blockNumber,
transactionHash: event.transactionHash
};

console.log('New claim detected:', claimData);

// Send webhook notification
if (this.webhookUrl) {
await this.sendWebhook({
type: 'claim',
data: claimData
});
}

// Update analytics dashboard
await this.updateAnalyticsDashboard(claimData);

// Check for milestones
await this.checkMilestones(claimData);
}

private async sendWebhook(data: any) {
try {
const response = await fetch(this.webhookUrl!, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});

if (!response.ok) {
throw new Error(`Webhook failed: ${response.status}`);
}
} catch (error) {
console.error('Webhook error:', error);
}
}

private async updateAnalyticsDashboard(claimData: any) {
// Update your analytics dashboard with new claim data
// This could update a database, send to analytics service, etc.
console.log('Updating analytics dashboard...');
}

private async checkMilestones(claimData: any) {
const analytics = await new AirdropAnalytics(
this.distributor.distributorAddress,
this.distributor.provider,
process.env.TOKENOPS_API_KEY!
).getComprehensiveAnalytics();

// Check for claim milestones
const milestones = [
{ threshold: 0.25, message: '25% of tokens claimed!' },
{ threshold: 0.5, message: '50% of tokens claimed!' },
{ threshold: 0.75, message: '75% of tokens claimed!' },
{ threshold: 0.9, message: '90% of tokens claimed!' }
];

const claimPercentage = analytics.claimRate / 100;

for (const milestone of milestones) {
if (claimPercentage >= milestone.threshold) {
console.log(`🎉 Milestone reached: ${milestone.message}`);

if (this.webhookUrl) {
await this.sendWebhook({
type: 'milestone',
data: {
message: milestone.message,
claimRate: analytics.claimRate,
totalClaimed: analytics.claimedCount
}
});
}
}
}
}

private async monitorDeadlines() {
const checkInterval = 60 * 60 * 1000; // Check every hour

setInterval(async () => {
const endTime = await this.distributor.getEndTime();
const timeLeft = Number(endTime) - Math.floor(Date.now() / 1000);

// Alert when approaching deadline
const alertThresholds = [
{ hours: 24, message: '24 hours remaining' },
{ hours: 12, message: '12 hours remaining' },
{ hours: 6, message: '6 hours remaining' },
{ hours: 1, message: '1 hour remaining' }
];

for (const threshold of alertThresholds) {
const thresholdSeconds = threshold.hours * 3600;

if (timeLeft <= thresholdSeconds && timeLeft > (thresholdSeconds - 3600)) {
console.warn(`⚠️ Airdrop deadline warning: ${threshold.message}`);

if (this.webhookUrl) {
await this.sendWebhook({
type: 'deadline_warning',
data: {
message: threshold.message,
timeLeft: timeLeft,
endTime: Number(endTime)
}
});
}
}
}
}, checkInterval);
}

stopMonitoring() {
this.isMonitoring = false;
console.log('Stopped airdrop monitoring');
}
}

Usage Example

// Initialize analytics and monitoring
const analytics = new AirdropAnalytics(
airdropAddress,
provider,
process.env.TOKENOPS_API_KEY!
);

// Get comprehensive analytics
const data = await analytics.getComprehensiveAnalytics();

console.log('Airdrop Analytics:', {
claimRate: `${data.claimRate.toFixed(2)}%`,
totalClaimed: data.claimedCount,
totalValue: data.totalDistributed.toString(),
timeToExpiration: `${Math.floor(data.timeToExpiration / 3600)} hours`,
unclaimedValue: data.unclaimedValue.toString()
});

// Start real-time monitoring
const monitor = new AirdropMonitor(
airdropAddress,
provider,
'https://your-webhook.com/airdrop-events'
);

monitor.startMonitoring();

// Monitor for specific events
process.on('SIGINT', () => {
monitor.stopMonitoring();
process.exit(0);
});

Next Steps

Now that you understand the airdrop system:

  1. ERC20 Airdrops - Standard token distributions using Merkle proofs
  2. Native Airdrops - ETH and native token airdrops without approvals
  3. Merkle Trees - Technical implementation details covered in this documentation
  4. Integration Examples - Real-world implementation patterns demonstrated above

Resources