๐ก๏ธ Security review ~5 min โ
One-pager for stakeholders and integrators asking "what prevents abuse?"
Short answer: defense in depth. Nine independent abuse-detection layers compose into a single allow/challenge/review/deny decision per claim, plus operator controls, plus an honest admission that any faucet can be drained if the payout exceeds the attacker's cost.
What we have today โ
Every claim runs through a pipeline. Any layer can flag, score, or deny.
| Layer | What it catches | Cost | Enabled by |
|---|---|---|---|
| Per-IP rate limit | Same IP hammering (per minute) | ~0 | Always on |
| Per-IP daily cap | Over-claiming from a single IP in 24h | ~0 | Always on |
| Blocklist | Known-bad IP, address, UID, ASN, or country | ~0 | Admin-curated |
| Captcha (Turnstile / hCaptcha) | Trivial bots | 1 verify call | Env var + site key |
| Hashcash (client puzzle) | Raises attacker CPU cost per claim | ~1 s client CPU | FAUCET_HASHCASH_SECRET |
| GeoIP / ASN / VPN / Tor / datacenter | Regional policy + datacenter IPs | <5 ms (MaxMind), ~150 ms (IPinfo) | FAUCET_GEOIP_BACKEND |
| Fingerprint correlation | Multi-account / multi-browser farming | DB lookup | FAUCET_FINGERPRINT_ENABLED |
| On-chain heuristics | Sweeper wallets, sibling faucet cross-funding | 1 RPC call | FAUCET_ONCHAIN_ENABLED |
| AI scoring (rules + optional ONNX) | Composite signal fusion โ velocity, entropy, timing | <10 ms | FAUCET_AI_ENABLED |
Final decision is one of: allow, challenge (client must complete extra work and retry), review (claim routed to admin for manual allow/deny), or deny.
Every decision is logged with the full signal bundle, viewable in the admin dashboard and via the MCP tool faucet.explain_decision(claimId).
Trust connectors โ the extra layer worth adding โ
The problem. IP rotation is cheap. Even captcha farms cost <$0.01 per solve. Bot operators can fake everything except a real human identity.
The lever. The faucet already accepts hostContext from integrators: a hash of the user's stable identity plus claims about what the integrator already verified (email, phone, account age, etc.). If the integrator signs hostContext with their HMAC secret, the faucet can trust it and weight those claims heavily in the scoring.
The proposal. Extend hostContext with verifiedIdentities โ a list of third-party identity providers the integrator has successfully authenticated the user against:
hostContext: {
uid: "<hash of your app's stable user id>",
kycLevel: "email" | "phone" | "id" | "none",
verifiedIdentities: ["apple", "google", "github", "email", "phone"],
accountAgeDays: 42,
emailDomainHash: "<hash of the user's email domain>",
tags: ["beta-tester", "premium"],
signature: "<HMAC(integratorSecret, canonical(context))>",
}Why this moves the needle. Apple, Google, and GitHub SSO accounts aren't free to farm:
- Apple: phone-verified Apple ID, Family Sharing friction, strong anti-abuse inside Apple.
- Google: phone + recovery email + behavioral signals.
- GitHub: older accounts with real commit history cost real time to cultivate.
A captcha-farm bot can buy 10,000 IPs for $5. Buying 10,000 aged Apple IDs is orders of magnitude more expensive. Shifting rate-limiting from per-IP (cheap to rotate) to per-UID (hard to rotate when backed by SSO) flips the math.
Where it fits in the architecture.
- Add
verifiedIdentities: string[]toHostContextSchemain packages/core/src/hostContext.ts and tocanonicalizeHostContextso it's included in the integrator signature. - Add a scoring bonus in packages/abuse-ai (or a new
abuse-identityplugin) that down-weights risk when a signedhostContexthas โฅ1 verified identity. - Admin dashboard signal drawer surfaces the list of verified identities.
This is a roadmap item โ see ROADMAP.md ยง1.4 "Integrator-signed host context".
The honest limits โ abuse at scale โ
Faucets leak. Always have. The goal is to raise attacker ROI above what they're willing to pay, and to cap total damage when abuse ramps up.
Reality check.
- MTurk-style captcha farms solve for $0.001โ0.003 per challenge.
- Hashcash at 20 bits adds ~$0.0001 of CPU cost.
- Low-end VPN IPs rent at $0.001/day/IP.
- Datacenter IPs are free from cloud trial accounts.
So the per-claim attacker cost floor is roughly $0.005. If the faucet pays out more value per claim than that, someone will drain it โ no matter how many layers you stack. The math is that simple.
What actually works at scale.
- Lower the per-claim payout โ often dramatically. Reach > claim size > attacker floor cost.
- Cap daily outflow โ have the server pause claims (or decline) once the wallet's sent more than a target amount today. Configurable via admin config.
- Trust connectors (above) โ raises the attacker floor from $0.005 to dollars per identity, not per claim.
- Manual review queue โ the
reviewdecision routes suspicious claims to an admin drawer; many false positives are better than leaking. - Monitor the balance + claim rate โ alert when the balance drops faster than expected. See docs/health-observability.md.
- Blocklist pattern matching โ when an attack is in progress, block the ASN, country, or address prefix; don't play whack-a-mole per-IP.
Claim amount, daily cap, and rate limits are all editable from the admin dashboard without restarting the server. Tune them under attack, tune them back afterwards.
Operator playbook โ "we're being abused" โ
- See it early. Admin dashboard โ Overview โ watch
claims/hour,success rate,top rejection reasons, andwallet balance. Sudden rate spike or balance drop = signal. - Identify the vector. Open Claims โ filter by status=rejected or decision=review. Click into a row. The signal bundle shows which abuse layer scored highest. Check if the abusers share an ASN, country, or address prefix.
- Pattern-block. Abuse page โ add a blocklist entry for the shared feature (ASN, country, address prefix). Set
expiresAta day or two out so you don't permanently block a regular country. - Tighten knobs. Config page โ turn up
rateLimitPerIpPerDay(fewer claims per IP), turn up hashcash difficulty, toggle on any abuse layer that was off. - Pause if needed. Set
claimAmountLuna=0โ the faucet still responds but sends nothing. No financial loss while you investigate. - Post-mortem. Open Logs โ audit trail of every admin action. Review what stopped the bleeding, adjust defaults, document in the incident log.
Integrator playbook โ add a trust connector โ
You're building an app, you integrate the faucet, your users get NIM. You can raise your users' trust score by integrating a real-identity provider before sending the claim.
Example with Next.js + Sign in with Apple:
import { useFaucetClaim } from '@nimiq-faucet/react';
import { signInWithApple } from './your-auth';
import { signHostContext } from './your-backend-api'; // calls your backend
async function claimForUser(address: string) {
// 1. Your own Apple sign-in (returns the Apple `sub` identifier).
const apple = await signInWithApple();
const uidHash = sha256(`my-app:${apple.sub}`);
// 2. Build the hostContext on your backend + sign it.
// (See docs/integrator-hmac.md for the HMAC scheme.)
const hostContext = await signHostContext({
uid: uidHash,
kycLevel: 'email',
verifiedIdentities: ['apple'],
accountAgeDays: user.ageDays,
});
// 3. Faucet claim โ signed context gets a trust bonus.
const { claim } = useFaucetClaim({
url: process.env.NEXT_PUBLIC_FAUCET_URL,
address,
hostContext,
});
await claim();
}Same pattern for Google One Tap, GitHub OAuth, or any other identity provider you already use. See docs/integrator-hmac.md for the signature scheme details.
FAQ โ
"Can you guarantee no abuse?" No. Nobody can. We guarantee defense in depth, full observability of every claim decision, and operator controls to respond in real time.
"Is this as good as what $BIG_CAPTCHA_VENDOR offers?" We compose the same inputs they use (behavioral, reputation, proof-of-work) plus on-chain heuristics they can't see plus integrator-signed identity they don't have access to. Different threat model; complementary, not a substitute.
"What about Sybil attacks via compromised SSO accounts?" Still possible. The defense is per-UID rate limiting + account-age signal + the review queue. Every layer has an escape; we stack them so no single escape drains the faucet.
"What's the recommended minimum setup for production?" Always-on layers + Turnstile + hashcash + GeoIP allowlist aligned with your legal footprint + a reasonable daily cap. Start there. See docs/deployment-production.md ยง6 "Hardening checklist".
Detailed layer documentation โ
For configuration, provider options, decision logic, and trade-offs for each layer, see the per-layer docs:
- Blocklist โ admin-managed deny-list
- Rate Limiting โ per-IP daily cap
- Cloudflare Turnstile โ invisible human-presence challenge
- hCaptcha โ visual/invisible CAPTCHA
- Hashcash โ self-hosted client puzzle
- GeoIP / ASN โ country, VPN, datacenter detection
- Device Fingerprint โ multi-account farming detection
- On-Chain Heuristics โ sweeper and cluster detection
- AI Anomaly Scoring โ rules + optional ML classifier
- Adding your own layer โ how to create a custom layer
See also โ
- docs/integrator-hmac.md โ how to sign
hostContextfrom your backend - docs/deployment-production.md โ production hardening
- docs/health-observability.md โ alerting on abuse waves
- docs/admin-first-run.md โ operator onboarding
- ROADMAP.md โ scheduled work including verified-identity scoring
- docs/security/threat-model.md โ STRIDE analysis