Building Sybil-Resistant Airdrops: A Step-by-Step Tutorial
You’ve built a protocol, grown a community, and now it’s time to distribute tokens. The problem: up to 50% of eligible addresses in a typical airdrop are sybil wallets — bots, farmers, and multi-account abusers who dilute your real community.
This tutorial walks you through building a sybil-resistant airdrop pipeline from scratch using WalletIQ’s wallet intelligence API.
Architecture Overview
The pipeline has four stages:
- Collect — gather eligible addresses from on-chain activity
- Profile — fetch wallet intelligence for each address
- Filter — apply sybil detection rules to score and rank wallets
- Distribute — allocate tokens based on eligibility tiers
We’ll focus on stages 2-3, where WalletIQ does the heavy lifting.
Prerequisites
Install the WalletIQ SDK:
npm install walletiq Get an API key from walletiq.dev/register. The free tier gives you 100 lookups/month — enough to prototype. Scale to Developer ($49/mo, 10K lookups) for production.
Step 1: Define Eligibility Criteria
Before filtering, decide what a “real user” looks like for your protocol. Common criteria:
const CRITERIA = {
// Minimum wallet age (days)
minAge: 60,
// Minimum unique contracts interacted with
minContracts: 3,
// Minimum transaction count
minTransactions: 10,
// Maximum risk score (0-100, lower = safer)
maxRiskScore: 50,
// Must have DeFi protocol interactions
requireDeFi: true,
// Bonus: multi-chain activity
multiChainBonus: true,
}; Step 2: Fetch Wallet Profiles
Use the WalletIQ SDK to profile each address:
import { WalletIQ } from "walletiq";
const wiq = new WalletIQ({ apiKey: process.env.WALLETIQ_API_KEY });
async function profileAddresses(addresses) {
const profiles = [];
for (const address of addresses) {
try {
const profile = await wiq.getProfile(address);
profiles.push({ address, profile });
} catch (err) {
// Handle rate limits gracefully
if (err.name === "RateLimitError") {
const waitMs = (err.retryAfter ?? 60) * 1000;
await new Promise(r => setTimeout(r, waitMs));
// Retry once
const profile = await wiq.getProfile(address);
profiles.push({ address, profile });
} else {
console.error(`Failed to profile ${address}:`, err.message);
profiles.push({ address, profile: null });
}
}
}
return profiles;
} For large lists (10K+ addresses), batch with concurrency control and respect the rate limit headers.
Step 3: Score and Filter
Apply your criteria to each profile and compute an eligibility score:
function scoreWallet(profile, criteria) {
if (!profile) return { score: 0, eligible: false, reason: "profile_failed" };
const checks = [];
let score = 0;
const maxScore = 100;
// Age check (30 points)
if (profile.age.days >= criteria.minAge) {
score += 30;
checks.push("age_ok");
} else {
checks.push("too_young");
}
// Contract diversity (25 points)
if (profile.stats.uniqueContractsInteracted >= criteria.minContracts) {
score += 25;
checks.push("diversity_ok");
} else {
checks.push("low_diversity");
}
// Transaction volume (15 points)
if (profile.stats.totalTransactions >= criteria.minTransactions) {
score += 15;
checks.push("activity_ok");
} else {
checks.push("low_activity");
}
// Risk score (20 points)
if (profile.risk.score <= criteria.maxRiskScore) {
score += 20;
checks.push("risk_ok");
} else {
checks.push("high_risk");
}
// DeFi activity (10 points)
if (!criteria.requireDeFi || profile.defi.protocols.length > 0) {
score += 10;
checks.push("defi_ok");
} else {
checks.push("no_defi");
}
// Bonus: multi-chain
if (criteria.multiChainBonus && profile.chains.length >= 3) {
score = Math.min(score + 10, maxScore);
checks.push("multichain_bonus");
}
return {
score,
eligible: score >= 60,
checks,
tier: score >= 90 ? "gold" : score >= 75 ? "silver" : score >= 60 ? "bronze" : "ineligible",
};
} Step 4: Generate Distribution Tiers
Group eligible wallets into tiers for differentiated token allocation:
function buildDistribution(profiles, criteria) {
const distribution = { gold: [], silver: [], bronze: [], filtered: [] };
for (const { address, profile } of profiles) {
const result = scoreWallet(profile, criteria);
if (result.tier === "ineligible") {
distribution.filtered.push({ address, score: result.score, checks: result.checks });
} else {
distribution[result.tier].push({ address, score: result.score });
}
}
return distribution;
} Example allocation multipliers:
| Tier | Score | Multiplier | Rationale |
|---|---|---|---|
| Gold | 90+ | 3x | Power users, long history, multi-chain |
| Silver | 75-89 | 2x | Active users with good history |
| Bronze | 60-74 | 1x | Meets minimum criteria |
| Filtered | Under 60 | 0x | Likely sybil or inactive |
Step 5: Putting It All Together
Here’s the complete pipeline:
import { WalletIQ } from "walletiq";
const wiq = new WalletIQ({ apiKey: process.env.WALLETIQ_API_KEY });
const CRITERIA = {
minAge: 60,
minContracts: 3,
minTransactions: 10,
maxRiskScore: 50,
requireDeFi: true,
multiChainBonus: true,
};
async function runAirdropFilter(addresses) {
console.log(`Processing ${addresses.length} addresses...`);
// Profile all addresses
const profiles = await profileAddresses(addresses);
// Score and distribute
const distribution = buildDistribution(profiles, CRITERIA);
console.log(`Results:`);
console.log(` Gold: ${distribution.gold.length} wallets (3x allocation)`);
console.log(` Silver: ${distribution.silver.length} wallets (2x allocation)`);
console.log(` Bronze: ${distribution.bronze.length} wallets (1x allocation)`);
console.log(` Filtered: ${distribution.filtered.length} wallets (sybil/inactive)`);
return distribution;
} Using Labels for Extra Precision
WalletIQ assigns behavioral labels like whale, defi-user, og, new-wallet, and repetitive-pattern. Use these as additional signals:
// Boost score for power users
if (profile.labels.includes("defi-user")) score += 5;
if (profile.labels.includes("og")) score += 5;
// Penalize suspicious patterns
if (profile.labels.includes("repetitive-pattern")) score -= 20;
if (profile.labels.includes("new-wallet")) score -= 10; Production Considerations
Rate limits: The free tier allows 10 requests/min. For 10K+ addresses, upgrade to Developer (60 req/min) or Growth (300 req/min).
Caching: WalletIQ caches profiles for 5 minutes server-side. If you need fresher data, space your requests accordingly.
Transparency: Publish your filtering criteria so your community understands the rules. This builds trust and reduces appeals.
Appeals process: Always allow users to contest their score. Some legitimate users have unusual wallet patterns.
Snapshot timing: Profile wallets at snapshot time, not claim time. This prevents post-announcement gaming.
Python Alternative
The same pipeline works with the Python SDK:
from walletiq import WalletIQ
wiq = WalletIQ(api_key="wiq_...")
profile = wiq.get_profile("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
print(f"Age: {profile.age.days} days")
print(f"Risk: {profile.risk.level} ({profile.risk.score}/100)")
print(f"Labels: {profile.labels}") Ready to build your sybil-resistant airdrop? Get a free API key and start filtering today.
Ready to integrate wallet intelligence?
Get Free API Key