176 lines
5.9 KiB
Markdown
176 lines
5.9 KiB
Markdown
# Sats Faucet
|
||
|
||
A Lightning faucet for the Nostr ecosystem. Nostr users can claim a small, randomized sats payout on a cooldown schedule. The app uses **NIP-98** for authenticated requests, **LNbits** for Lightning payouts, and enforces anti-abuse rules (account age, activity score, per-pubkey and per-IP cooldowns).
|
||
|
||
## Features
|
||
|
||
- **Nostr auth** — Connect via NIP-07 (browser extension) or external signer; all claim requests are signed with NIP-98.
|
||
- **Entropy payouts** — Weighted random payout buckets (e.g. 10, 25, 50, 100 sats); quote is locked so users can’t re-roll by refreshing.
|
||
- **Anti-abuse** — Account age (e.g. 14 days), activity score, per-pubkey cooldown (e.g. 7 days), per-IP cooldown and limits, daily budget cap.
|
||
- **Transparency** — Public stats: pool balance, total paid, claim counts, recent payouts (anonymized).
|
||
- **Deposits** — Lightning address and LNURLp QR for community funding.
|
||
- **Operational controls** — `FAUCET_ENABLED`, `EMERGENCY_STOP`, minimum wallet balance guard.
|
||
|
||
## Tech stack
|
||
|
||
| Layer | Stack |
|
||
|----------|--------|
|
||
| Frontend | React, TypeScript, Vite, Framer Motion |
|
||
| Backend | Node.js, Express, TypeScript |
|
||
| Database | SQLite (default) or PostgreSQL |
|
||
| Payments | LNbits (Lightning) |
|
||
| Auth | NIP-98 (Nostr HTTP Auth) |
|
||
|
||
## Prerequisites
|
||
|
||
- **Node.js** 18+
|
||
- **LNbits** instance with a wallet and admin key
|
||
- **Nostr** signer (e.g. browser extension with NIP-07) for testing claims
|
||
|
||
## Quick start
|
||
|
||
### 1. Clone and install
|
||
|
||
```bash
|
||
git clone https://git.azzamo.net/Michilis/SatsFaucet.git
|
||
cd SatsFaucet
|
||
npm install
|
||
cd backend && npm install
|
||
cd ../frontend && npm install
|
||
```
|
||
|
||
### 2. Backend configuration
|
||
|
||
```bash
|
||
cd backend
|
||
cp .env.example .env
|
||
# Edit .env: set HMAC_IP_SECRET, JWT_SECRET, LNbits keys, deposit address, etc.
|
||
```
|
||
|
||
Required at minimum:
|
||
|
||
- `HMAC_IP_SECRET` — min 32 chars (for IP hashing)
|
||
- `JWT_SECRET` — min 32 chars
|
||
- `LNBITS_BASE_URL`, `LNBITS_ADMIN_KEY`, `LNBITS_WALLET_ID`
|
||
- `DEPOSIT_LIGHTNING_ADDRESS`, `DEPOSIT_LNURLP` (for the deposit section)
|
||
|
||
See `backend/.env.example` for all options (payout weights, cooldowns, eligibility thresholds, Nostr relays).
|
||
|
||
### 3. Database
|
||
|
||
**SQLite (default):** ensure `backend/data` exists; migrations create the DB.
|
||
|
||
```bash
|
||
cd backend
|
||
npm run migrate
|
||
```
|
||
|
||
**PostgreSQL:** set `DATABASE_URL` in `.env` and run:
|
||
|
||
```bash
|
||
cd backend
|
||
npm run migrate
|
||
```
|
||
|
||
### 4. Frontend configuration
|
||
|
||
```bash
|
||
cd frontend
|
||
cp .env.example .env
|
||
# For local dev, VITE_API_URL=http://localhost:3001 is usually correct
|
||
```
|
||
|
||
### 5. Run in development
|
||
|
||
**Terminal 1 — backend:**
|
||
|
||
```bash
|
||
npm run dev:backend
|
||
# or: cd backend && npm run dev
|
||
```
|
||
|
||
**Terminal 2 — frontend:**
|
||
|
||
```bash
|
||
npm run dev:frontend
|
||
# or: cd frontend && npm run dev
|
||
```
|
||
|
||
Open the frontend URL (e.g. `http://localhost:5173`), connect Nostr, enter a Lightning address, and use “Check” then “Confirm” to claim.
|
||
|
||
## Production build
|
||
|
||
```bash
|
||
npm run build
|
||
# Builds backend (TypeScript → dist/) and frontend (Vite → frontend/dist/)
|
||
```
|
||
|
||
Run the backend (serves API; optionally serve frontend from same host or use a static host):
|
||
|
||
```bash
|
||
npm start
|
||
# or: cd backend && npm start
|
||
```
|
||
|
||
Set `TRUST_PROXY=true` when behind a reverse proxy (nginx, Caddy, etc.) so client IPs are read from `X-Forwarded-For`.
|
||
|
||
## Project structure
|
||
|
||
```
|
||
├── backend/ # Express API
|
||
│ ├── src/
|
||
│ │ ├── db/ # SQLite/Pg, migrations, schema
|
||
│ │ ├── middleware/ # NIP-98, auth, IP, rate limit
|
||
│ │ ├── routes/ # claim, auth, public, user
|
||
│ │ └── services/ # eligibility, LNbits, Nostr, quote
|
||
│ ├── .env.example
|
||
│ └── package.json
|
||
├── frontend/ # React SPA
|
||
│ ├── src/
|
||
│ │ ├── components/
|
||
│ │ ├── contexts/
|
||
│ │ ├── hooks/
|
||
│ │ ├── pages/
|
||
│ │ └── styles/
|
||
│ ├── .env.example
|
||
│ └── package.json
|
||
├── context/ # Design/overview docs
|
||
├── package.json # Root scripts
|
||
└── README.md
|
||
```
|
||
|
||
## API overview
|
||
|
||
| Method | Endpoint | Auth | Description |
|
||
|--------|-------------------|--------|--------------------|
|
||
| GET | `/health` | — | Health check |
|
||
| GET | `/config` | — | Public config |
|
||
| GET | `/stats` | — | Balance, counts |
|
||
| GET | `/deposit` | — | Deposit address/QR |
|
||
| POST | `/claim/quote` | NIP-98 | Get payout quote |
|
||
| POST | `/claim/confirm` | NIP-98 | Confirm and pay |
|
||
|
||
Claim flow: frontend calls `POST /claim/quote` with Lightning address; if eligible, backend returns `quote_id` and `payout_sats`. Frontend then calls `POST /claim/confirm` with `quote_id` to execute the payout. Quotes are short-lived to prevent re-rolling.
|
||
|
||
## Environment variables (summary)
|
||
|
||
- **Security:** `HMAC_IP_SECRET`, `JWT_SECRET`, `NIP98_MAX_SKEW_SECONDS`, `NONCE_TTL_SECONDS`, `TRUST_PROXY`, `ALLOWED_ORIGINS`
|
||
- **Faucet:** `FAUCET_ENABLED`, `EMERGENCY_STOP`, payout weights and bucket sats, `DAILY_BUDGET_SATS`, `MIN_WALLET_BALANCE_SATS`
|
||
- **Eligibility:** `MIN_ACCOUNT_AGE_DAYS`, `MIN_ACTIVITY_SCORE`, `MIN_NOTES_COUNT`, `MIN_FOLLOWING_COUNT`, etc.
|
||
- **Cooldowns:** `COOLDOWN_DAYS`, `IP_COOLDOWN_DAYS`, `MAX_CLAIMS_PER_IP_PER_PERIOD`
|
||
- **Nostr:** `NOSTR_RELAYS`, `RELAY_TIMEOUT_MS`, `METADATA_CACHE_HOURS`
|
||
- **LNbits:** `LNBITS_BASE_URL`, `LNBITS_ADMIN_KEY`, `LNBITS_WALLET_ID`, `DEPOSIT_LIGHTNING_ADDRESS`, `DEPOSIT_LNURLP`
|
||
|
||
See `backend/.env.example` for full list and defaults.
|
||
|
||
## Security notes
|
||
|
||
- LNbits keys and secrets stay on the backend; the frontend never sees them.
|
||
- IPs are stored only as HMAC hashes (using `HMAC_IP_SECRET`).
|
||
- NIP-98 enforces method, URL, timestamp, and nonce; nonces are checked for replay.
|
||
- Use HTTPS and a reverse proxy in production; set `TRUST_PROXY` and restrict `ALLOWED_ORIGINS`.
|
||
|
||
## License
|
||
|
||
MIT (or as specified in the repository).
|