Two schema changes the fixes in the following commits build on (BUGS.md B-09, B-04): ix_rounds_single_active is a unique index over the constant expression (1), restricted to the active statuses, so the table holds any number of closed rounds and only ever one live one. Rounds never overlapping was previously enforced only by a read-then-insert in open_new_round_if_needed, which two concurrent callers can both pass — and a second stuck "open" row blocks every future round forever, since get_active_round matches on status. The migration doesn't create that index blind: an instance that already has two active rounds (the very bug) would fail mid-migration with an opaque IntegrityError, so it first closes the stale duplicates and keeps the newest — which is what get_active_round was already doing silently. Verified against a DB seeded with an 'open' plus a 'closing' round. pending_transactions.failure_reason is for the reconciler added next: when it gives up on a transaction, an operator needs to see whether it was dropped or rejected. The PendingTransaction docstring now also documents the full status lifecycle (building -> pending -> confirmed | failed), since "building" is new and load-bearing. Co-Authored-By: Claude Opus 5 <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
cp .env.example .env # then fill in the generated secrets, see docs/setup.md
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
PYTHONPATH=. python scripts/generate_master_key.py
alembic upgrade head
uvicorn app.main:app --reload --port 8123
Open http://127.0.0.1:8123/ for the test UI, http://127.0.0.1:8123/admin
for the admin dashboard, http://127.0.0.1:8123/docs for the interactive API
docs.
Or run the whole stack (app + Caddy reverse proxy with automatic TLS) via Docker:
mkdir -p data/db data/keys data/logs
docker compose run --rm app python scripts/generate_master_key.py
docker compose up -d --build
See docs/setup.md and docs/running-the-server.md for the full walkthrough (both workflows, dev vs. production TLS).
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
76 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").