Skip to content

Abuse Prevention Layers

The Nimiq Simple Faucet uses 10 pluggable abuse-prevention layers arranged in a defense-in-depth pipeline. Rate limiting is on by default; all other layers are opt-in via environment variables.

Integrating an app against a faucet that has layers enabled? The per-layer pages below are operator-facing (how to configure each layer). For the client side — how your app reads /v1/config, renders the captcha widget the operator chose, solves hashcash, and forwards a signed hostContext — see integration-guide.md.

How the pipeline works

Every claim request passes through all enabled layers sequentially. Each layer returns:

  • A score between 0 (clean) and 1 (certain abuse)
  • Signals — structured evidence (country, ASN, fingerprint match, etc.)
  • An optional hard decision: allow, challenge, review, or deny

A deny decision short-circuits the pipeline — remaining layers don't run. A challenge or review decision is recorded but does NOT short-circuit, so later layers can still escalate to deny. After all layers run, the hardest decision wins. If no hard decision, the aggregate weighted score is compared against configurable thresholds.

DecisionHTTP statusWhat happens
allow200Transaction is broadcast
challenge202Client must solve an additional challenge and retry
review202Held for manual admin review
deny403Rejected with reason

Execution order

Layers run in this fixed order:

  1. Blocklist (weight 5) — always on
  2. Rate Limiting (weight 3) — always on
  3. Cloudflare Turnstile (weight 2) — if FAUCET_TURNSTILE_SITE_KEY set
  4. hCaptcha (weight 2) — if FAUCET_HCAPTCHA_SITE_KEY set
  5. FCaptcha (weight 2) — if FAUCET_FCAPTCHA_URL + site key + secret set
  6. Hashcash (weight 1) — if FAUCET_HASHCASH_SECRET set
  7. GeoIP / ASN (weight 1) — if FAUCET_GEOIP_BACKEND is not none
  8. Device Fingerprint (weight 1) — if FAUCET_FINGERPRINT_ENABLED
  9. On-Chain Heuristics (weight 1) — if FAUCET_ONCHAIN_ENABLED
  10. AI Anomaly Scoring (weight 1) — if FAUCET_AI_ENABLED

The order is not currently configurable. Each layer's weight affects its contribution to the aggregate score (higher weight = more influence).

Combining captcha with hashcash

You can enable a captcha provider (Turnstile, hCaptcha, or FCaptcha) and hashcash simultaneously. The ClaimUI will render both widgets — the captcha widget and the hashcash progress bar. The claim button only activates when both challenges are satisfied.

This is the recommended production setup: captcha verifies the user is human, hashcash adds CPU cost to prevent rapid automated claiming even if the captcha is bypassed.

Note: Turnstile, hCaptcha, and FCaptcha are mutually exclusive (pick one). Any of them can be combined with hashcash.

Layer summary

#LayerWhat it catchesCostEnabled by
1BlocklistKnown bad actorsFreeAlways on
2Rate LimitingVolume abuseFreeAlways on
3Cloudflare TurnstileBotsFree tier availableFAUCET_TURNSTILE_SITE_KEY
4hCaptchaBotsFree tier availableFAUCET_HCAPTCHA_SITE_KEY
5FCaptchaBots (behavioural + PoW)Free (self-hosted)FAUCET_FCAPTCHA_URL
6HashcashScripted floodingFree (self-hosted)FAUCET_HASHCASH_SECRET
7GeoIP / ASNGeo-evasion, VPN, datacenterFree (DB-IP) or paidFAUCET_GEOIP_BACKEND
8Device FingerprintMulti-account farmingFree (client-side)FAUCET_FINGERPRINT_ENABLED
9On-Chain HeuristicsSweeper wallets, faucet clustersFree (RPC queries)FAUCET_ONCHAIN_ENABLED
10AI Anomaly ScoringNovel attack patternsFree (local CPU)FAUCET_AI_ENABLED

Browser-only mode

Set FAUCET_REQUIRE_BROWSER=true to reject claim and challenge requests that don't come from a real browser. This checks for the Sec-Fetch-Site header (sent by all modern browsers but not by scripts like curl or Python requests).

Integrators with HMAC auth (X-Faucet-Api-Key) bypass this check, so SDK-to-server integrations continue to work.

Limitation: Determined attackers can spoof Sec-Fetch-* headers, but this stops the majority of naive scripts and raises the bar significantly.

At minimum, enable:

  1. Rate limiting (always on) — caps claims per IP per day
  2. Browser-only mode (FAUCET_REQUIRE_BROWSER=true) — blocks non-browser scripts
  3. Turnstile, hCaptcha, or FCaptcha — blocks automated scripts with human verification
  4. Hashcash — adds CPU cost to every claim (use alongside captcha, not as sole defense)
  5. GeoIP with country allowlist — restricts to your target regions

For higher-value faucets, add fingerprint correlation, on-chain heuristics, and AI scoring.

Warning: Hashcash alone is NOT sufficient for public-facing faucets. A parallel Python script can solve difficulty 20 in ~0.17 seconds. Always combine hashcash with at least one human-verification layer (Turnstile, hCaptcha, FCaptcha, or FAUCET_REQUIRE_BROWSER).

Adding your own abuse layer

The abuse pipeline is designed for extensibility. Any module that implements the AbuseCheck interface can be plugged in.

Step 1: Create the package

bash
mkdir packages/abuse-mycheck
cd packages/abuse-mycheck
pnpm init

Step 2: Implement the AbuseCheck interface

typescript
import type { AbuseCheck, CheckResult, ClaimRequest } from '@faucet/core';

export interface MyCheckConfig {
  threshold: number;
}

export function myCheck(config: MyCheckConfig): AbuseCheck {
  return {
    id: 'my-check',
    description: 'My custom abuse check',
    weight: 1,
    async check(req: ClaimRequest): Promise<CheckResult> {
      // Your logic here — inspect req.ip, req.address,
      // req.hostContext, req.fingerprint, etc.
      const suspicious = /* ... */ false;
      return {
        score: suspicious ? 0.8 : 0,
        signals: { reason: 'example' },
        decision: suspicious ? 'deny' : undefined,
        reason: suspicious ? 'failed my check' : undefined,
      };
    },
  };
}

Step 3: Register in the pipeline

In apps/server/src/abuse/pipeline.ts, import your check and add it conditionally:

typescript
import { myCheck } from '@faucet/abuse-mycheck';

// Inside buildPipeline():
if (config.myCheckEnabled) {
  checks.push(myCheck({ threshold: config.myCheckThreshold }));
}

Step 4: Add config + env vars

In apps/server/src/config.ts, add your config fields and env var mappings:

typescript
// In ServerConfigSchema:
myCheckEnabled: z.coerce.boolean().default(false),
myCheckThreshold: z.coerce.number().default(0.5),

// In ENV_KEYS:
myCheckEnabled: 'FAUCET_MY_CHECK_ENABLED',
myCheckThreshold: 'FAUCET_MY_CHECK_THRESHOLD',

Step 5: Test

Add tests in your package and in apps/server/test/ following the pattern of existing abuse layer tests (e.g., geoip.e2e.test.ts, hashcash.e2e.test.ts).

See CONTRIBUTING.md for the full development workflow.

Built with ❤️ by Richy.