first commit
Made-with: Cursor
This commit is contained in:
267
context/Frontend_overview.md
Normal file
267
context/Frontend_overview.md
Normal 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 30–60 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
359
context/backend_overview.md
Normal 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
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