- Add ADMIN_API_KEY to config; accept via X-Admin-Key or Bearer (constant-time compare) - Never serve cached null for nostr_first_seen_at; admin clear-cache and override-age - Add clearAllUsersCache() and POST /admin/users/clear-cache for all users - Update .env.example with admin API key and pubkeys comments Made-with: Cursor
Sats Faucet
A Lightning faucet for the Nostr ecosystem. Nostr users can claim a small, randomized sats payout on a cooldown schedule. The app uses NIP-98 for authenticated requests, LNbits for Lightning payouts, and enforces anti-abuse rules (account age, activity score, per-pubkey and per-IP cooldowns).
Features
- Nostr auth — Connect via NIP-07 (browser extension) or external signer; all claim requests are signed with NIP-98.
- Entropy payouts — Weighted random payout buckets (e.g. 10, 25, 50, 100 sats); quote is locked so users can’t re-roll by refreshing.
- Anti-abuse — Account age (e.g. 14 days), activity score, per-pubkey cooldown (e.g. 7 days), per-IP cooldown and limits, daily budget cap.
- Transparency — Public stats: pool balance, total paid, claim counts, recent payouts (anonymized).
- Deposits — Lightning address and LNURLp QR for community funding.
- Operational controls —
FAUCET_ENABLED,EMERGENCY_STOP, minimum wallet balance guard.
Tech stack
| Layer | Stack |
|---|---|
| Frontend | React, TypeScript, Vite, Framer Motion |
| Backend | Node.js, Express, TypeScript |
| Database | SQLite (default) or PostgreSQL |
| Payments | LNbits (Lightning) |
| Auth | NIP-98 (Nostr HTTP Auth) |
Prerequisites
- Node.js 18+
- LNbits instance with a wallet and admin key
- Nostr signer (e.g. browser extension with NIP-07) for testing claims
Quick start
1. Clone and install
git clone https://git.azzamo.net/Michilis/SatsFaucet.git
cd SatsFaucet
npm install
cd backend && npm install
cd ../frontend && npm install
2. Backend configuration
cd backend
cp .env.example .env
# Edit .env: set HMAC_IP_SECRET, JWT_SECRET, LNbits keys, deposit address, etc.
Required at minimum:
HMAC_IP_SECRET— min 32 chars (for IP hashing)JWT_SECRET— min 32 charsLNBITS_BASE_URL,LNBITS_ADMIN_KEY,LNBITS_WALLET_IDDEPOSIT_LIGHTNING_ADDRESS,DEPOSIT_LNURLP(for the deposit section)
See backend/.env.example for all options (payout weights, cooldowns, eligibility thresholds, Nostr relays).
3. Database
SQLite (default): ensure backend/data exists; migrations create the DB.
cd backend
npm run migrate
PostgreSQL: set DATABASE_URL in .env and run:
cd backend
npm run migrate
4. Frontend configuration
cd frontend
cp .env.example .env
# For local dev, VITE_API_URL=http://localhost:3001 is usually correct
5. Run in development
Terminal 1 — backend:
npm run dev:backend
# or: cd backend && npm run dev
Terminal 2 — frontend:
npm run dev:frontend
# or: cd frontend && npm run dev
Open the frontend URL (e.g. http://localhost:5173), connect Nostr, enter a Lightning address, and use “Check” then “Confirm” to claim.
Production build
npm run build
# Builds backend (TypeScript → dist/) and frontend (Vite → frontend/dist/)
Run the backend (serves API; optionally serve frontend from same host or use a static host):
npm start
# or: cd backend && npm start
Set TRUST_PROXY=true when behind a reverse proxy (nginx, Caddy, etc.) so client IPs are read from X-Forwarded-For.
Project structure
├── backend/ # Express API
│ ├── src/
│ │ ├── db/ # SQLite/Pg, migrations, schema
│ │ ├── middleware/ # NIP-98, auth, IP, rate limit
│ │ ├── routes/ # claim, auth, public, user
│ │ └── services/ # eligibility, LNbits, Nostr, quote
│ ├── .env.example
│ └── package.json
├── frontend/ # React SPA
│ ├── src/
│ │ ├── components/
│ │ ├── contexts/
│ │ ├── hooks/
│ │ ├── pages/
│ │ └── styles/
│ ├── .env.example
│ └── package.json
├── context/ # Design/overview docs
├── package.json # Root scripts
└── README.md
API overview
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /health |
— | Health check |
| GET | /config |
— | Public config |
| GET | /stats |
— | Balance, counts |
| GET | /deposit |
— | Deposit address/QR |
| POST | /claim/quote |
NIP-98 | Get payout quote |
| POST | /claim/confirm |
NIP-98 | Confirm and pay |
Claim flow: frontend calls POST /claim/quote with Lightning address; if eligible, backend returns quote_id and payout_sats. Frontend then calls POST /claim/confirm with quote_id to execute the payout. Quotes are short-lived to prevent re-rolling.
Environment variables (summary)
- Security:
HMAC_IP_SECRET,JWT_SECRET,NIP98_MAX_SKEW_SECONDS,NONCE_TTL_SECONDS,TRUST_PROXY,ALLOWED_ORIGINS - Faucet:
FAUCET_ENABLED,EMERGENCY_STOP, payout weights and bucket sats,DAILY_BUDGET_SATS,MIN_WALLET_BALANCE_SATS - Eligibility:
MIN_ACCOUNT_AGE_DAYS,MIN_ACTIVITY_SCORE,MIN_NOTES_COUNT,MIN_FOLLOWING_COUNT, etc. - Cooldowns:
COOLDOWN_DAYS,IP_COOLDOWN_DAYS,MAX_CLAIMS_PER_IP_PER_PERIOD - Nostr:
NOSTR_RELAYS,RELAY_TIMEOUT_MS,METADATA_CACHE_HOURS - LNbits:
LNBITS_BASE_URL,LNBITS_ADMIN_KEY,LNBITS_WALLET_ID,DEPOSIT_LIGHTNING_ADDRESS,DEPOSIT_LNURLP
See backend/.env.example for full list and defaults.
Security notes
- LNbits keys and secrets stay on the backend; the frontend never sees them.
- IPs are stored only as HMAC hashes (using
HMAC_IP_SECRET). - NIP-98 enforces method, URL, timestamp, and nonce; nonces are checked for replay.
- Use HTTPS and a reverse proxy in production; set
TRUST_PROXYand restrictALLOWED_ORIGINS.
License
MIT (or as specified in the repository).