Payment Gateway
Security & Compliance
Key management, custody controls and regulatory posture.
1. Secret inventory
| Secret | Storage | Rotation |
|---|---|---|
| LNbits user wallet keys | AES-256-CBC in Postgres (existing pattern), key from ENCRYPTION_KEY env | on compromise; document procedure |
| Platform LNbits superuser | env / secrets manager (never DB) | quarterly |
| Webhook signing secrets | per-project, hashed display, full value shown once at creation | user-initiated |
API keys (zap_live_) | store SHA-256 hash only (like Stripe), prefix for lookup | user-initiated |
| Platform Nostr zap-signing nsec | KMS/HSM in prod, env in dev | quarterly |
| User Nostr nsec | NEVER stored — client-side only (NIP-07/46) | n/a |
| DB backups | encrypted at rest, 30-day retention | n/a |
Rules:
- No secret ever in logs (existing sanitization covers passwords/tokens — extend to
X-Api-Key,preimageis fine to show to invoice owner only) ENCRYPTION_KEYrotation = re-encrypt script, tested in staging
2. Custody model & risk controls
We hold user funds (LNbits sub-wallets) and act as a responsible custodian:
- Withdrawals from day 1 — no lock-in, no minimum (minus LN fee reserve)
- Auto-sweep (Phase 4): user sets threshold auto-withdraw to their address
- Custody ceiling alert: total platform liability > X BTC page on-call
- Withdrawal safety:
- TOTP 2FA required to enable/disable withdrawals address book
- New withdrawal address 24h hold (opt-out) + email confirmation
- Velocity limits (e.g. ≤ 3 withdrawals/hour, configurable)
- Hot wallet minimization: keep only operating float on LNbits node; treasury in cold storage; documented top-up procedure
- Segregation: one LNbits wallet per user (done) — a breach of one wallet key ≠ all funds
3. Application security
Existing: JWT+refresh, RBAC, rate limiting, XSS/SQLi sanitization, Winston logs, Sentry.
Add for payment surface:
- LNURL endpoints: aggressive per-IP rate limit + Cloudflare/edge cache of params
- Invoice creation: per-key daily volume caps (kill compromised keys' blast radius)
- Webhook endpoints: replay protection (timestamp window) + signature (done in spec)
- NIP-57 validation strictness (sig, amount match, time window) — zap spam vector
- Admin actions (fee change, manual credit) audit log table w/ actor + diff
- Subdomain take-over care on custom domains feature
- Dependency scanning (existing CI) + quarterly penetration test before fiat-scale
4. Regulatory Posture (NOT legal advice — engage counsel)
| Question | Pragmatic stance |
|---|---|
| Are we a money transmitter? | Custodial likely MSB obligations in US/EU depending on volume/jurisdiction. Options: (a) register/get licenses, (b) partner with licensed custodian, (c) pivot custody to user-owned NWC wallets for large balances (hybrid). Decide before public launch with counsel. |
| KYC/AML | Tiered: no-KYC under low limits (tips/zaps typical), KYC above thresholds & for fiat conversions. Keep per-user lifetime volume tracking ready. |
| Sanctions | Blocklist screening on withdrawal destinations is impractical on LN; mitigate via limits + monitoring + ToS |
| Taxes | We're a service provider; creators get CSV exports for their own reporting (1099-K equivalents N/A for BTC in most places — document) |
| GDPR | Customer CRM = personal data (npub/email). Provide export + delete endpoints (feature 7.8). Data processing agreement for EU creators. |
| Terms of Service | Must cover: custody risk, no chargebacks, prohibited use, fee schedule, termination. Template from BTCPay/OpenNode ToS as reference. |
5. Incident Response
- Compromised LNbits freeze withdrawals endpoint (feature flag), rotate all keys via script, notify users (email + status page)
- Compromised API key user revokes in dashboard; we offer forced-rotate per project
- Zap receipt relay failure non-critical (funds settled); retry queue 24h
- Double-credit bug ledger is append-only; corrections as reversing entries, never UPDATE balance fields directly
- Status page (status.zapapi.com) + postmortem template in repo
6. Data Integrity (money math)
- All amounts: integer msats in DB (
bigint), never float - Ledger pattern:
wallet_transactionsappend-only withtype,amount,ref_id; cachedbalancecolumn updated in same DB transaction - Nightly reconciliation job: sum(ledger) vs wallet.balance vs LNbits balance alert on drift > 1000 sats