The payout has to spend one pool UTXO per bet, so reusing MAX_TX_INPUTS (50) for it made any round past ~50 players unpayable: select_utxos raised too_many_inputs, the round stayed "paying_out" retrying every 60s forever, and since no new round may open while one is active, the whole lottery stopped with the pool stuck. The cap was being enforced on the payout side, i.e. discovered once the money was already committed and there was no way back. Two halves: - select_utxos takes the cap as a parameter. Bets and withdrawals keep MAX_TX_INPUTS = 50, which protects a user from a fee that eats into the amount they are moving; the payout uses MAX_PAYOUT_TX_INPUTS = 500, where that argument doesn't apply — 400 inputs at 1 sat/vB cost ~0.00027 PLM out of the winner's 70% share. What actually bounds it is relay policy: 500 inputs is ~34 kvB against the 100 kvB standardness limit, and signing that many measures ~0.4s, once per round, inside a background task. - place_bet refuses the 401st bet with a new round_full error (translated into all 7 languages), so "a round can always be paid out" is an invariant checked before any money moves. MAX_PARTICIPANTS_PER_ROUND sits below the input cap to leave the payout headroom for pool change from earlier rounds, and counts every participant row rather than only confirmed ones, since a failed bet frees a slot. A round already wedged past the old cap now pays out on the next retry tick. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PLM Lottery
A periodic-round lottery system built on PLM, a Bitcoin-like coin (mainnet). Each user gets a dedicated server-derived P2WPKH address; they deposit PLM to that address, place a fixed-cost bet to enter the current round, and when the round closes a winner is drawn who receives 70% of the prize pool (the remaining 30% goes to fees).
This is a custodial system: private keys are generated and held server-side, encrypted at rest. See CLAUDE.md for the full architecture, domain decisions, and known gaps before treating this as production-ready.
Quick start
The server always runs via Docker (app + Caddy reverse proxy with automatic
TLS) — in dev and production alike, with only SITE_ADDRESS differing
between the two. There's no supported way to run uvicorn directly; the
venv is only for local tooling (tests, Alembic migrations, the one-time key
scripts) — see CLAUDE.md.
cp .env.example .env # then fill in the generated secrets, see docs/setup.md
mkdir -p data/db data/keys data/logs
docker compose run --rm app python scripts/generate_master_key.py
docker compose up -d --build
Open https://localhost/ for the test UI, https://localhost/admin for the
admin dashboard (a self-signed-certificate warning on first visit is
expected in dev — accept it, or use curl -k). The interactive API docs at
/docs are disabled by default (they'd otherwise expose the whole API
surface, admin endpoints included) — set ENABLE_API_DOCS=true in .env to
enable them.
See docs/setup.md and docs/running-the-server.md for the full walkthrough (secrets, master key generation, production TLS with a real domain).
Documentation
- CLAUDE.md — architecture, commands, domain decisions, known gaps (for anyone/anything working on the code)
- flowchart.mmd — the source-of-truth flow diagram the implementation follows node-by-node
- docs/setup.md — one-time setup (secrets, master key, migrations)
- docs/running-the-server.md — how to launch it (local venv vs. Docker+Caddy, dev vs. production TLS)
- docs/guida-utente.md — end-user guide to the test UI (Italian)
- docs/guida-admin.md — admin dashboard guide (Italian)
Tech stack
Python (FastAPI, SQLAlchemy async + Alembic, Argon2 + JWT auth), Electrum protocol for PLM network access (no full node), Docker + Caddy for deployment. See CLAUDE.md for the complete list and the reasoning behind each choice.
Testing
python -m pytest # all tests
python -m pytest tests/unit/test_hd.py # one file
232 unit tests cover HD derivation, PSBT building, the Electrum client, bets, deposits, withdrawals, the round/draw engine, RBF fee-bumping, admin config, the pending-inclusive balance calculation, and the SSE push channel. No automated integration tests against a live Electrum connection — mainnet verification so far has been manual (see CLAUDE.md's "Project status").