zoo/ blog
Back to all articles
blockchainluxinfrastructuredevtools

Building a Blockchain Faucet That Doesn't Get Drained

How we built the Lux testnet faucet to resist abuse while remaining accessible to developers.

Every blockchain testnet needs a faucet - a way for developers to get test tokens. Every faucet eventually gets abused. Here's how we built one that stays running.

The Faucet Problem

Testnets need tokens for:

  • Deploying contracts
  • Testing transactions
  • Running integration tests
  • Development workflows

But faucets attract abuse:

  • Drainers: Scripts that empty the faucet
  • Hoarders: Requesting far more than needed
  • Bots: Automated requests at scale
  • Resellers: Selling "rare" testnet tokens

Anti-Abuse Layers

Our faucet has multiple defense layers:

Layer 1: Rate Limiting

// Per-IP rate limiting
limiter := ratelimit.New(
    ratelimit.PerIP(1, time.Hour),     // 1 request per hour per IP
    ratelimit.PerWallet(1, time.Hour), // 1 request per hour per wallet
    ratelimit.Global(100, time.Hour),  // 100 total per hour
)

Basic, but catches casual abuse.

Layer 2: Proof of Humanity

Before requesting tokens, users must prove they're human:

  • CAPTCHA: Traditional image challenges
  • PoW: Compute a proof-of-work (hashcash style)
  • Social: Verify via GitHub/Twitter account
  • Farcaster/Lens: Web3 social verification
const proof = await faucet.proveHumanity({
    method: "github",
    minAge: 30,  // Account must be 30+ days old
    minRepos: 1  // Must have at least 1 repo
});

Layer 3: Smart Rationing

Not everyone gets the same amount:

Developer TypeAmountFrequency
New address0.1 LUXDaily
Verified GitHub1 LUXDaily
Active developer10 LUXDaily
Ecosystem project100 LUXWeekly

Activity determines allocation.

Layer 4: Behavioral Analysis

Machine learning flags suspicious patterns:

  • Address clustering: Same entity, multiple wallets
  • Request timing: Bot-like regularity
  • Usage patterns: Tokens transferred immediately
  • Network analysis: Connected to known drainers
risk_score = analyzer.score(request)
if risk_score > 0.7:
    require_additional_verification()
elif risk_score > 0.9:
    block_request()

Layer 5: Economic Friction

Make abuse unprofitable:

  • Drip distribution: 10% now, 90% over 24 hours
  • Activity bonding: Tokens locked until used
  • Reputation stake: Abuse loses future access

Architecture

Request → Rate Limit → PoH Challenge → Risk Score → Allocation → Drip
    ↓          ↓              ↓             ↓            ↓         ↓
  Block    Challenge       Verify        Score       Approve    Distribute

                         Record

Every request is logged for pattern analysis.

Results

Since launch:

  • 100K+ developers served
  • Zero complete drains
  • 99.9% uptime
  • <5s average request time

Compared to other testnets where faucets are regularly emptied.

Open Source

The faucet is open source. Use it for your testnet:

git clone https://github.com/luxfi/faucet
cd faucet
cp .env.example .env
# Configure your chain
docker compose up

Lessons Learned

  1. Defense in depth: No single layer is enough
  2. Assume abuse: Design for attackers from day one
  3. Reward good actors: Make legitimate use easy
  4. Monitor constantly: Patterns emerge over time
  5. Iterate quickly: Attackers adapt, so must you

The best faucet is one nobody notices - it just works for developers while silently blocking abuse.


This post is part of our retrospective series exploring the technical foundations of Hanzo and Lux.