The dashboard now speaks seven languages but every failure path still showed
the API's raw English text ("insufficient balance", "current password is
incorrect"), which is the most frequent and least forgiving part of the UI to
leave untranslated.
Rather than teach the API about locales, it keeps answering in one language
and hands the client something to translate: `detail` becomes
{code, message, params}, where message stays English for non-dashboard
consumers (curl, tests) and code maps onto `error.<code>` in i18n.js. An
unknown code falls back to message, so a client older or newer than the server
degrades to English instead of a blank toast.
Domain exceptions (BetError, WithdrawalError) subclass the new ApiError and
carry the code from where the failure actually happens; str(exc) is still the
English message, so existing tests keep matching on it. Interpolated values
travel in params rather than baked into the English sentence — amounts as
*_sats, from which the frontend derives a *_plm sibling, so each language can
place them wherever its grammar wants.
admin.js reads detail.message defensively: the admin endpoints still return a
bare string, but the shared auth dependencies now return the structured form.
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").