first commit
Made-with: Cursor
This commit is contained in:
386
context/overview.md
Normal file
386
context/overview.md
Normal 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 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.
|
||||
Reference in New Issue
Block a user