Production-ready Catalyst testnet-only faucet backend.
- Chain: Catalyst testnet (EVM JSON-RPC)
- Native token: KAT (native transfer; not ERC-20 for MVP)
- Explorer:
https://explorer.catalystnet.org/tx/<txHash> - ChainId allowlist: only
0xbf8457c(service refuses to start otherwise)
- Node.js + TypeScript
- Fastify (HTTP + structured JSON logging)
- ethers (native value transfers)
- Redis (cooldowns + global request rate limiting + pause state)
- Postgres + Prisma (claim log + migrations)
- Cloudflare Turnstile (captcha)
Checks RPC + Redis + DB health.
Returns faucet settings for the UI.
{
"networkName": "Catalyst Testnet",
"chainId": "0xbf8457c",
"symbol": "KAT",
"amount": "0.1",
"cooldownSeconds": 86400
}Body:
{ "address": "0x...", "turnstileToken": "token-from-turnstile" }Returns:
{ "txHash": "0x...", "nextEligibleAt": "2026-02-23T14:06:22.806Z" }Notes:
- Enforces per-address and per-IP 24h cooldown (configurable via
COOLDOWN_HOURS). - Enforces global requests/min (
GLOBAL_RPM, default 60). - When
ENABLE_COUNTRY_LIMIT=true, also enforces cooldown per country (requirescf-ipcountryheader). - When
ENABLE_ASN_LIMIT=true, also enforces cooldown per ASN (requirescf-asnheader).
Accepts either { address, captchaToken } or { address, turnstileToken } and returns the same response shape as /v1/request.
All admin endpoints require an admin token via:
Authorization: Bearer <ADMIN_TOKEN>orx-admin-token: <ADMIN_TOKEN>
Recent claims with optional filters:
address=0x...ipHash=<sha256>sinceMs=<epochMs>untilMs=<epochMs>limit=<1..200>
Pauses claims (health/info still work).
Unpauses claims.
Required:
RPC_URL: Catalyst testnet JSON-RPC URLCHAIN_ID: must be0xbf8457cFAUCET_PRIVATE_KEY: private key used to sign transfersFAUCET_AMOUNT: amount in whole tokens (decimal string), e.g.0.1COOLDOWN_HOURS: integer cooldown (hours), e.g.24REDIS_URL: e.g.redis://redis:6379DATABASE_URL: Postgres connection stringTURNSTILE_SECRET_KEY: Cloudflare Turnstile secretADMIN_TOKEN: shared secret for admin endpointsIP_HASH_SALT: long random string used to hash IPs before storing
Optional:
PORT(default 8080)HOST(default0.0.0.0)GLOBAL_RPM(default 60)ENABLE_COUNTRY_LIMIT(defaultfalse)ENABLE_ASN_LIMIT(defaultfalse)
You need Redis and Postgres running, then:
cp .env.example .env
# edit .env
npm install
npm run prisma:generate
npm run prisma:migrate:dev
npm run devcp .env.example .env
# edit .env (use the compose hostnames: redis, postgres)
docker compose up -d --buildThe container runs prisma migrate deploy on startup.
- High-volume draining: bots requesting repeatedly to drain faucet balance
- Sybil attacks: many addresses controlled by one attacker
- IP rotation / proxy abuse: multiple claims via changing IPs
- Service overload: request floods affecting availability
- Misconfiguration: accidentally running against non-testnet
- Captcha (Turnstile): blocks basic automation before any on-chain action.
- Per-address cooldown: Redis cooldown key prevents repeat claims from the same address for
COOLDOWN_HOURS. - Per-IP cooldown (hashed IP only): only a salted SHA-256 hash of the client IP is stored in Redis/DB.
- Optional per-country / per-ASN cooldown: when enabled, the service also rate-limits per
cf-ipcountry/cf-asn.- Best used behind Cloudflare (or an edge that provides equivalent headers).
- Global RPM limit: caps total
POST /v1/claimvolume per minute. - DB fallback checks: even if Redis keys are lost (restart/flush), the service checks recent claims in Postgres before sending.
- Pause switch: admin can pause claims quickly during abuse or wallet issues.
- ChainId allowlist: service refuses to start unless
CHAIN_IDmatches Catalyst testnet. - Structured logs with request id: every request returns
x-request-idand logs are JSON for correlation and incident response.
- Put the faucet behind a reverse proxy/WAF (Cloudflare recommended) for additional bot filtering.
- Keep the faucet key in a secret manager; rotate if exposed.
- Consider adding allow/deny lists (countries/ASNs) and monitoring/alerting as you scale.