Most airdrop bots fail not because of bad code, but because of architectural decisions made when the project had 100 users — not 100,000. Wallet validation, referral chains, and anti-fraud protection all require database patterns that collapse under real load. This guide covers exactly what we use in production.
The Stack We Use
- ▸Python 3.11 + aiogram 3.x — async, battle-tested for high-throughput bots
- ▸PostgreSQL + asyncpg with PgBouncer for connection pooling
- ▸Redis for rate limiting, session state, and referral chain lookups
- ▸Celery + Redis broker for background wallet verification tasks
- ▸Docker + Railway or Render for deployment
Core Bot Setup with aiogram 3
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.redis import RedisStorage
storage = RedisStorage.from_url("redis://localhost:6379/0")
dp = Dispatcher(storage=storage)
bot = Bot(token="BOT_TOKEN", parse_mode="HTML")
@dp.message(Command("start"))
async def cmd_start(message: Message):
ref_code = message.text.split()[-1] if len(message.text.split()) > 1 else None
await register_user(message.from_user.id, ref_code)
await message.answer("Welcome! Use /verify to link your wallet.")
async def main():
await dp.start_polling(bot)Wallet Verification Flow
Never trust a wallet address the user submits without cryptographic proof of ownership. Always verify via signed message.
- 1.User submits wallet address via /verify command
- 2.Bot generates a unique nonce (random 8-char hex string, stored in Redis with 10-minute TTL)
- 3.User signs the nonce with their wallet (MetaMask 'Sign Message' or equivalent)
- 4.User pastes the signature back into the bot
- 5.Bot verifies signature using eth_account — recovers address and compares
- 6.Wallet is linked to Telegram user ID in PostgreSQL with a unique constraint
from eth_account.messages import encode_defunct
from web3 import Web3
def verify_eth_signature(address: str, nonce: str, signature: str) -> bool:
w3 = Web3()
message = encode_defunct(text=f"WeBuildCrew Airdrop Verification: {nonce}")
recovered = w3.eth.account.recover_message(message, signature=signature)
return recovered.lower() == address.lower()Referral Tracking Database Schema
CREATE TABLE users (
telegram_id BIGINT PRIMARY KEY,
wallet_address VARCHAR(64) UNIQUE,
referred_by BIGINT REFERENCES users(telegram_id),
referral_count INTEGER DEFAULT 0,
points INTEGER DEFAULT 0,
joined_at TIMESTAMP DEFAULT NOW()
);
-- Critical for performance at 100K+ users
CREATE INDEX idx_referred_by ON users(referred_by);
CREATE INDEX idx_joined_at ON users(joined_at DESC);Anti-Sybil Protection
Without anti-sybil measures, a single actor can create thousands of fake Telegram accounts and drain your entire airdrop allocation in minutes.
- ▸Require Telegram account age > 30 days (check via getChat API — accounts < 30 days are auto-rejected)
- ▸Enforce one wallet per Telegram ID with a unique database constraint
- ▸IP rate limiting via Redis: max 3 registrations per IP per hour
- ▸Wallet activity check: reject wallets with zero on-chain transactions
- ▸Phone number uniqueness: Telegram enforces one account per phone number in most regions
Scaling Infrastructure
At 10K users/day, enable PgBouncer with transaction pooling mode (not session mode). At 50K+ users, partition the users table by joined_at date and run multiple aiogram bot instances behind a webhook load balancer — polling does not scale past ~3 bot instances.
Production Launch Checklist
- 1.Switch from polling to webhook (setWebhook API call) — 10x more reliable under load
- 2.Enable PgBouncer in transaction pooling mode, pool_size=20 per bot instance
- 3.Store all secrets in environment variables — never in code
- 4.Implement SIGTERM handler for graceful bot shutdown without message loss
- 5.Set up Sentry for real-time error tracking with Telegram user ID in context
- 6.Add /status command for ops visibility (user count, pending verifications, queue depth)
- 7.Load test with 10,000 simulated concurrent users before public launch
