387 lines
10 KiB
Markdown
387 lines
10 KiB
Markdown
# 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.
|