Files
SatsFaucet/context/overview.md
Michaël 3734365463 first commit
Made-with: Cursor
2026-02-26 18:33:00 -03:00

10 KiB
Raw Permalink Blame History

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 users 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.