← Back to Blog

Building Sybil-Resistant Airdrops: A Step-by-Step Tutorial

by WalletIQ Team

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:

  1. Collect — gather eligible addresses from on-chain activity
  2. Profile — fetch wallet intelligence for each address
  3. Filter — apply sybil detection rules to score and rank wallets
  4. 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:

TierScoreMultiplierRationale
Gold90+3xPower users, long history, multi-chain
Silver75-892xActive users with good history
Bronze60-741xMeets minimum criteria
FilteredUnder 600xLikely 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