first commit

Made-with: Cursor
This commit is contained in:
Michaël
2026-02-26 18:33:00 -03:00
commit 3734365463
76 changed files with 14133 additions and 0 deletions

386
context/overview.md Normal file
View File

@@ -0,0 +1,386 @@
# 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.