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

View File

@@ -0,0 +1,267 @@
# frontend_overview.md
## 1. Purpose
This document defines the complete frontend architecture, user experience, and interaction model for the Sats Faucet application.
The frontend must:
* Provide a simple, clean, and trustworthy interface.
* Integrate Nostr login (NIP-07 or external signer).
* Guide users through eligibility check and claim confirmation.
* Display transparent faucet statistics and funding information.
* Clearly communicate rules, cooldowns, and denial reasons.
The frontend must never expose secrets or LNbits keys.
---
## 2. Tech Stack Requirements
Recommended:
* Framework: React (Next.js preferred) or similar SPA framework
* Styling: TailwindCSS or clean minimal CSS system
* QR generation library for Lightning deposit QR
* Nostr integration via NIP-07 browser extension (window.nostr)
Frontend must be deployable as static site or server-rendered app.
---
## 3. Global App Structure
### 3.1 Pages
1. Home Page (/)
2. Claim Modal or Claim Section
3. Transparency / Stats Section
4. Deposit Section
5. Optional: About / Rules page
Navigation should be minimal and focused.
---
## 4. Nostr Authentication
### 4.1 Connect Flow
* User clicks "Connect Nostr".
* Frontend requests pubkey via NIP-07.
* Pubkey stored in memory (not localStorage unless necessary).
* Show truncated pubkey (npub format preferred).
### 4.2 NIP-98 Signing
For protected API calls:
* Build request payload (method + URL + timestamp + nonce).
* Request signature via NIP-07.
* Attach NIP-98 header to backend request.
Frontend must:
* Generate secure nonce.
* Handle signature errors gracefully.
---
## 5. Home Page Layout
### 5.1 Hero Section
* Title: Sats Faucet
* Short description of rules
* Claim button
### 5.2 Live Stats Section
Display:
* Current pool balance
* Total sats paid
* Total claims
* Claims in last 24h
* Daily budget progress bar
Stats pulled from GET /stats.
Must auto-refresh every 3060 seconds.
---
## 6. Claim Flow UI
### 6.1 Step 1: Connect
If not connected:
* Disable claim button.
* Prompt user to connect Nostr.
### 6.2 Step 2: Enter Lightning Address
Input field:
* Validate basic format (user@domain).
* Do not over-validate client-side.
### 6.3 Step 3: Quote
On "Check Eligibility":
* Send POST /claim/quote with NIP-98.
Possible responses:
Eligible:
* Show payout amount.
* Show Confirm button.
Not eligible:
* Show clear message from backend.
* If cooldown, show next eligible date.
* If account too new, show required age.
### 6.4 Step 4: Confirm
On confirm:
* Call POST /claim/confirm.
* Show loading state.
* On success:
* Show payout amount.
* Show next eligible time.
* On failure:
* Show clear error.
Must prevent double-click submission.
---
## 7. Deposit Section
Display:
* Lightning Address (copyable).
* QR code of LNURL.
* Optional: "Create invoice" button.
Copy buttons required for:
* Lightning address
* LNURL
Deposit section must feel transparent and trustworthy.
---
## 8. Transparency Section
Display:
* Recent payouts (anonymized pubkey prefix).
* Payout amount.
* Timestamp.
Optional:
* Distribution breakdown (small/medium/large payouts).
---
## 9. Error Handling UX
Frontend must handle:
* Network failures.
* Signature rejection.
* Expired quote.
* Payout failure.
All errors must be displayed in a clean alert component.
Do not expose internal error stack traces.
---
## 10. State Management
Minimal state required:
* pubkey
* lightning_address
* eligibility result
* quote_id
* payout_sats
* next_eligible_at
Use React state or lightweight state manager.
No need for complex global store.
---
## 11. Security Requirements
Frontend must:
* Never store secrets.
* Never expose LNbits keys.
* Never trust client-side eligibility logic.
* Rely entirely on backend for final validation.
If Turnstile or CAPTCHA enabled:
* Include widget only when backend indicates required.
---
## 12. Design Principles
* Minimal, modern UI.
* Clear visual feedback.
* No clutter.
* Show transparency clearly.
* Make it feel fair and legitimate.
Recommended style:
* Dark mode default.
* Bitcoin-inspired accent color.
* Clean typography.
---
## 13. Performance Requirements
* Fast initial load.
* Avoid blocking Nostr relay calls on frontend.
* All relay interaction handled by backend.
---
## 14. Accessibility
* Buttons must have disabled states.
* Inputs labeled clearly.
* QR accessible with copy option.
---
## 15. Completion Criteria
Frontend is complete when:
* User can connect via NIP-07.
* Eligibility check works and displays correct messages.
* Confirm claim triggers payout.
* Deposit Lightning address and QR visible.
* Live stats update correctly.
* Cooldown and denial reasons clearly displayed.
* No sensitive data exposed in frontend code.

359
context/backend_overview.md Normal file
View File

@@ -0,0 +1,359 @@
# backend_overview.md
## 1. Purpose
This document defines the complete backend architecture and behavior for the Sats Faucet application.
The backend is responsible for:
* Verifying NIP-98 signed authentication.
* Enforcing all eligibility rules.
* Fetching and caching Nostr account data.
* Enforcing cooldowns and anti-abuse constraints.
* Generating and locking entropy payout quotes.
* Executing Lightning payouts via LNbits.
* Maintaining transparency statistics.
* Providing operational safety controls.
This backend must be production-safe, abuse-resistant, and configurable entirely via environment variables.
---
## 2. Tech Stack Requirements
Recommended stack:
* Language: Go, TypeScript (Node/Express), or Python (FastAPI)
* Database: PostgreSQL
* Optional: Redis (rate limiting + nonce replay protection)
* LNbits for Lightning payments
* Reverse proxy with TLS (nginx or Caddy)
The backend must be stateless except for database persistence.
---
## 3. Environment Variables
All logic must be driven via .env configuration.
Key categories:
### 3.1 Security
* JWT_SECRET
* HMAC_IP_SECRET
* NIP98_MAX_SKEW_SECONDS
* NONCE_TTL_SECONDS
* TRUST_PROXY
* ALLOWED_ORIGINS
### 3.2 Faucet Economics
* FAUCET_ENABLED
* EMERGENCY_STOP
* FAUCET_MIN_SATS
* FAUCET_MAX_SATS
* PAYOUT_WEIGHT_SMALL
* PAYOUT_WEIGHT_MEDIUM
* PAYOUT_WEIGHT_LARGE
* PAYOUT_WEIGHT_JACKPOT
* PAYOUT_SMALL_SATS
* PAYOUT_MEDIUM_SATS
* PAYOUT_LARGE_SATS
* PAYOUT_JACKPOT_SATS
* DAILY_BUDGET_SATS
* MAX_CLAIMS_PER_DAY
* MIN_WALLET_BALANCE_SATS
### 3.3 Eligibility
* MIN_ACCOUNT_AGE_DAYS
* MIN_ACTIVITY_SCORE
* MIN_NOTES_COUNT
* MIN_FOLLOWING_COUNT
* MIN_FOLLOWERS_COUNT
* ACTIVITY_LOOKBACK_DAYS
### 3.4 Cooldowns
* COOLDOWN_DAYS
* IP_COOLDOWN_DAYS
* MAX_CLAIMS_PER_IP_PER_PERIOD
### 3.5 Nostr
* NOSTR_RELAYS
* RELAY_TIMEOUT_MS
* MAX_EVENTS_FETCH
* METADATA_CACHE_HOURS
### 3.6 LNbits
* LNBITS_BASE_URL
* LNBITS_ADMIN_KEY
* LNBITS_WALLET_ID
* DEPOSIT_LIGHTNING_ADDRESS
* DEPOSIT_LNURLP
---
## 4. Database Schema
### 4.1 users
* pubkey (PK)
* nostr_first_seen_at
* notes_count
* followers_count
* following_count
* activity_score
* last_metadata_fetch_at
* created_at
* updated_at
### 4.2 claims
* id (PK)
* pubkey (FK users.pubkey)
* claimed_at
* payout_sats
* ip_hash
* payout_destination_hash
* status (pending, paid, failed)
* lnbits_payment_hash
* error_message
### 4.3 ip_limits
* ip_hash (PK)
* last_claimed_at
* claim_count_period
### 4.4 quotes
* quote_id (PK)
* pubkey
* payout_sats
* created_at
* expires_at
* status (active, consumed, expired)
### 4.5 daily_stats (optional)
* date (PK)
* total_paid_sats
* total_claims
* unique_pubkeys
All IP addresses must be stored as HMAC(IP, HMAC_IP_SECRET).
---
## 5. NIP-98 Authentication
Every protected endpoint must:
1. Extract NIP-98 header or payload.
2. Verify signature against pubkey.
3. Verify HTTP method and URL match signed payload.
4. Verify timestamp within allowed skew.
5. Verify nonce not previously used.
6. Reject if invalid.
Nonces must be stored in Redis or DB for NONCE_TTL_SECONDS.
---
## 6. IP Resolution
If TRUST_PROXY=true:
* Read first valid IP from X-Forwarded-For.
Else:
* Use request remote address.
Then:
* Hash IP using HMAC_IP_SECRET.
* Never store raw IP.
---
## 7. Eligibility Engine
Eligibility flow:
1. Check FAUCET_ENABLED.
2. Check EMERGENCY_STOP.
3. Check LNbits wallet balance >= MIN_WALLET_BALANCE_SATS.
4. Check pubkey cooldown.
5. Check IP cooldown.
6. Fetch or load cached Nostr profile.
7. Compute account age.
8. Compute activity score.
9. Compare with thresholds.
Return structured result with denial code if failed.
---
## 8. Nostr Data Fetching
For a given pubkey:
1. Query relays in parallel.
2. Fetch:
* kind 0 (metadata)
* kind 1 (notes)
* kind 3 (contacts)
3. Compute:
* earliest created_at
* notes count in ACTIVITY_LOOKBACK_DAYS
* following count
Cache results for METADATA_CACHE_HOURS.
If no events found:
* Deny as account_too_new.
---
## 9. Activity Scoring
Example scoring logic:
* Has metadata: +10
* Notes >= MIN_NOTES_COUNT: +20
* Following >= MIN_FOLLOWING_COUNT: +10
* Followers >= MIN_FOLLOWERS_COUNT: +10
Score must be deterministic and logged.
---
## 10. Entropy Payout System
Weighted random selection:
1. Build array based on configured weights.
2. Generate secure random number.
3. Select payout bucket.
Before finalizing quote:
* Check daily spend.
* Check MAX_CLAIMS_PER_DAY.
If budget exceeded:
* Either deny or downgrade payout to FAUCET_MIN_SATS.
---
## 11. Claim Flow
### 11.1 POST /claim/quote
Input:
* lightning_address
Steps:
* Run eligibility engine.
* If eligible, generate payout.
* Insert quote record with expiry (e.g., 60 seconds).
* Return quote_id and payout_sats.
### 11.2 POST /claim/confirm
Input:
* quote_id
Steps:
* Verify quote exists and active.
* Re-check cooldown and budget.
* Execute LNbits payment.
* Update claim record.
* Mark quote consumed.
Must be idempotent.
---
## 12. LNbits Integration
Payout flow:
1. Resolve Lightning address to LNURL.
2. Fetch LNURL payRequest.
3. Call callback with amount in millisats.
4. Handle success/failure response.
5. Store payment hash.
On failure:
* Mark claim failed.
* Do not lock cooldown unless configured.
---
## 13. Public Endpoints
GET /health
GET /config
GET /stats
GET /deposit
No authentication required.
---
## 14. Logging and Monitoring
Each claim attempt must log:
* pubkey
* ip_hash
* eligibility result
* payout amount
* payment status
Metrics to track:
* denial reasons count
* payout distribution
* daily spend
---
## 15. Security Hard Requirements
* Strict CORS
* Rate limit /claim endpoints
* Nonce replay protection
* HMAC IP hashing
* Admin keys never exposed
* All secrets loaded from env
---
## 16. Production Readiness Checklist
Backend is complete when:
* NIP-98 auth fully verified.
* Pubkey and IP cooldown enforced.
* Account age check enforced.
* Activity score enforced.
* Entropy payout cannot be rerolled.
* Daily budget cannot be exceeded.
* LNbits payout works and errors handled safely.
* Emergency stop disables claims instantly.
* Logs clearly show denial reasons.

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.