# Sats Faucet App ## 1. Purpose Build a sats faucet web app that lets Nostr users claim a small, randomized sats payout (entropy payout) on a cooldown schedule, with strong anti-abuse protections. Key goals: * Fun community experiment that can evolve into a long-term public faucet. * Simple UX: connect Nostr, enter Lightning address, claim. * Abuse-resistant: NIP-98 signed requests, account age checks, activity scoring, per-pubkey cooldown, per-IP cooldown, and budget guards. * Transparent: public stats and recent payouts (anonymized). * Sustainable: daily budget cap, emergency stop switch, and minimum wallet balance guard. Non-goals: * Perfect Sybil resistance. The system raises the cost of abuse rather than claiming to fully prevent it. ## 2. Core Requirements ### 2.1 Claim rules A user may claim only if all conditions are true: * Authenticated via Nostr using NIP-98 signature on the claim request. * Account age is at least MIN_ACCOUNT_AGE_DAYS (default 14 days), based on earliest observed event timestamp on configured relays. * Activity score meets or exceeds MIN_ACTIVITY_SCORE. * Pubkey has not successfully claimed within the last COOLDOWN_DAYS (default 7 days). * IP has not successfully claimed within the last IP_COOLDOWN_DAYS (default 7 days) and under MAX_CLAIMS_PER_IP_PER_PERIOD. * Faucet is enabled and not in emergency stop. * LNbits wallet balance is at or above MIN_WALLET_BALANCE_SATS. * Daily budget not exceeded. ### 2.2 Payout * Payout uses weighted randomness (entropy) configured by environment variables. * Reward amount is fixed per bucket (example buckets: 10, 25, 50, 100 sats), selected by weight. * The payout selected for a claim is locked to a short-lived quote to prevent re-rolling by refreshing. * Payout is sent via LNbits to a Lightning address provided by the user. ### 2.3 Deposits * The app must display a deposit Lightning address and QR for community funding. * Preferred: LNURLp/Lightning Address for deposits (static address and QR). * Optional: allow users to create an invoice for a chosen deposit amount. ### 2.4 Transparency Publicly visible: * Current faucet pool balance (from LNbits). * Total sats paid. * Total claim count. * Claims in last 24h. * Daily budget and daily spend. * Recent claims list (anonymized, limited number). ## 3. System Overview ### 3.1 Components 1. Frontend Web App * Connect Nostr (NIP-07 or external signer). * Collect Lightning address. * Calls backend to get a quote and confirm claim. * Shows eligibility failures with friendly reasons and next eligible time. * Shows deposit info and public transparency stats. 2. Faucet Backend API * Validates NIP-98 signed requests. * Resolves client IP safely (with proxy support). * Enforces cooldown locks and rate limiting. * Fetches Nostr metadata + activity from relays and caches it. * Computes account age and activity score. * Computes payout quote, enforces daily budget, executes LNbits payout. * Records claims, payout results, and stats. 3. LNbits * Holds the faucet wallet. * Executes outgoing payments. * Provides deposit address (Lightning Address/LNURLp). ### 3.2 Trust boundaries * LNbits keys live only on the backend. * Frontend never sees LNbits keys. * Backend stores only HMAC-hashed IPs, not raw IPs. * Lightning address should not be stored in plaintext unless explicitly desired; store a hash for privacy. ## 4. User Experience ### 4.1 Home page * Primary call-to-action: Claim sats. * Deposit section: * Show DEPOSIT_LIGHTNING_ADDRESS. * Show QR code for DEPOSIT_LNURLP (or encoded LNURL). * Optional button: Create deposit invoice. * Live stats: * Balance, total paid, total claims, last 24h. * Recent payouts list. * Rules summary: * Cooldown days, min age, min score, per-IP limit. ### 4.2 Claim flow 1. User clicks Connect Nostr. 2. User enters Lightning address. 3. User clicks Check / Quote. 4. App shows: * If ineligible: exact reason and next eligible time. * If eligible: payout amount and Confirm button. 5. User confirms. 6. App shows success state: * payout amount * status * next eligible date UI must clearly separate: * Eligibility check * Payment attempt ## 5. Backend Behavior ### 5.1 NIP-98 authentication The backend must verify each protected request: * Signature is valid for the provided pubkey. * Signed payload includes correct method and URL. * Timestamp is within NIP98_MAX_SKEW_SECONDS. * Nonce is present and not reused (store for NONCE_TTL_SECONDS). ### 5.2 IP handling * Backend must support TRUST_PROXY mode. * When TRUST_PROXY=true, derive IP from X-Forwarded-For correctly. * Hash IP using HMAC_IP_SECRET and store only the hash. ### 5.3 Eligibility engine For a given pubkey and ip_hash: * Check emergency stop and faucet enabled flags. * Check wallet balance guard. * Check per-pubkey cooldown: latest successful claim timestamp. * Check per-IP cooldown: latest successful claim timestamp and quota. * Fetch or load cached Nostr profile and activity metrics. * Verify account age and activity score. Return structured result: * eligible: boolean * denial_reason: enum * denial_message: user-friendly text * next_eligible_at: timestamp if applicable ### 5.4 Quote then confirm Use a two-step claim to prevent payout rerolling. POST /claim/quote * Auth required (NIP-98) * Input: lightning_address * Performs full eligibility check. * Selects payout using weighted randomness. * Enforces daily budget guard: * If today_spent + payout > DAILY_BUDGET_SATS, either deny or reduce to FAUCET_MIN_SATS based on config. * Creates a short-lived quote record (quote_id, pubkey, payout_sats, expires_at). * Response includes payout_sats and quote_id. POST /claim/confirm * Auth required (NIP-98) * Input: quote_id * Re-check critical guards (pubkey lock, ip lock, budget, balance, quote expiry). * Executes LNbits payment. * Records claim row with status. * Returns success/failure payload. Notes: * Confirm must be idempotent: multiple confirms for same quote_id should not pay twice. * If confirm is called after quote expiry, return a clean error. ### 5.5 LNbits payout Backend sends payment to the user’s Lightning address. Must handle: * Lightning address validation (basic format and domain resolve). * LNURLp resolution: fetch payRequest, then call callback with amount. * Handle failures: * Mark claim as failed, store error. * Do not consume cooldown if payout did not actually send (configurable, but recommended). Retries: * For MVP, 0 retries. * For long-term, add a background worker to retry transient failures. ## 6. Nostr data sourcing ### 6.1 Relay list Backend reads NOSTR_RELAYS from env. Must query multiple relays in parallel. ### 6.2 Account age Compute nostr_first_seen_at as earliest observed event timestamp for the pubkey. Practical strategy: * Query kinds: 0, 1, 3. * Use earliest created_at found. * Cache result. If no events found: * Treat as new or unknown and deny with a friendly message. ### 6.3 Activity score Compute a simple score (0 to 100) using cached metrics. Suggested inputs: * Has kind 0 metadata. * Notes count in last ACTIVITY_LOOKBACK_DAYS. * Following count. * Followers count (optional if expensive). * Optional zap receipts. The scoring formula must be controlled via env thresholds so it can be tuned as the faucet grows. ## 7. Persistence and Storage ### 7.1 Database Use Postgres. Required tables: * users: pubkey, nostr_first_seen_at, cached metrics, last fetch timestamps. * claims: pubkey, claimed_at, payout, ip_hash, status, lnbits identifiers, errors. * ip_limits: ip_hash, last_claimed_at, rolling counters. * quotes: quote_id, pubkey, payout_sats, expires_at, status. * stats_daily (optional): daily totals. ### 7.2 Privacy * Store ip_hash only (HMAC). * Store payout destination as hash. * Never log raw Lightning address in plaintext logs. ## 8. Observability Logging must include: * Request id * pubkey * eligibility denial reason * payout amount (if eligible) * LNbits payment result (success/failure) Metrics dashboard (optional): * total claims * total paid * denial counts by reason * payout distribution ## 9. Operational Controls Environment variables must support: * FAUCET_ENABLED * EMERGENCY_STOP * DAILY_BUDGET_SATS * MAX_CLAIMS_PER_DAY * MIN_WALLET_BALANCE_SATS Behavior: * If EMERGENCY_STOP=true, deny all claims with a maintenance message. * If wallet balance below MIN_WALLET_BALANCE_SATS, deny claims and encourage deposits. ## 10. API Surface Public endpoints * GET /health * GET /config * GET /stats * GET /deposit Claim endpoints (auth: NIP-98) * POST /claim/quote * POST /claim/confirm Optional admin endpoints (later) * GET /admin/claims * GET /admin/users * POST /admin/ban * POST /admin/allow ## 11. Error Handling All errors must return: * code (stable string) * message (user-friendly) * details (optional for debugging) Common denial codes: * faucet_disabled * emergency_stop * insufficient_balance * daily_budget_exceeded * cooldown_pubkey * cooldown_ip * account_too_new * low_activity * invalid_nip98 * invalid_lightning_address * quote_expired * payout_failed ## 12. Security Checklist Minimum required: * Strict NIP-98 verification (method, url, timestamp, nonce). * Nonce replay prevention. * IP hashing. * Cloudflare or similar rate limiting on claim endpoints. * CORS restricted to FRONTEND_URL. * No secret keys in frontend. Nice to have: * Bot protection (Turnstile) behind a feature flag. * VPN/proxy detection behind a feature flag. ## 13. Deployment * Backend behind a reverse proxy with TLS. * TRUST_PROXY=true when behind proxy. * Use database migrations. Recommended: * Docker compose for backend + Postgres + optional Redis. * Separate LNbits deployment. ## 14. Acceptance Criteria A developer is done when: * A Nostr user can connect and claim sats successfully to a Lightning address. * The faucet enforces cooldowns per pubkey and per IP. * The faucet rejects accounts younger than 14 days. * The faucet rejects accounts below activity threshold. * Payouts are randomized and cannot be rerolled by refreshing. * Public page shows deposit address and QR. * Public stats show balance and transparency counters. * Admin can stop the faucet instantly via env. * No raw IPs or LNbits keys leak to the client.