Architecture & limitations
BetProof is a technical proof of concept showing frontend/backend boundaries, transactional ledger logic, idempotent wagering and settlement, and wallet connectivity. It is a simulation — not a betting product.
Request flow
01
Browser
Zustand (UI state) · TanStack Query (server cache) · ethers v6 (read-only)
02
Route handlers
Next.js on Node · HttpOnly session cookie · Zod · origin check · rate limits
03
BettingStore
Business rules · BEGIN IMMEDIATE tx
04
SQLite
WAL · CHECK + UNIQUE constraints
GET|POST /api/session · GET /api/events · account · ledger · settlementsGET|POST /api/wagersPOST /api/demo/settle · close · reset · sample-historyInvariants enforced by the backend
- Credits and odds are integers (minor units / basis points). Payout = floor(stake × oddsBp ÷ 10000) in BigInt.
- Client never supplies odds, payout or balance — unknown request fields are rejected with 400.
- Wager insert, debit ledger entry and balance update commit in one transaction, or not at all.
- Idempotency key unique per account: replay returns the same wager; different payload returns 409.
- Ledger reference keys are UNIQUE (initial:, debit:, win:) so no entry can be written twice.
- Balance has a CHECK (>= 0) constraint and a guarded UPDATE — overdraft is impossible even under races.
- Settlement is idempotent; a conflicting outcome returns 409 and changes nothing.
- Every financial read and write is resolved from the session cookie; no endpoint accepts an account id.
- Each session has its own copy of event state: one visitor's settlement or reset never touches another's records.
- Server state lives only in the TanStack Query cache and is invalidated after every mutation; Zustand holds UI state only.
Prototype vs. what production would require
| Area | This demo | Production would need |
|---|---|---|
| Funds | Fictional integer credits seeded at 1,000.00 | Custody model, licensed payment rails or audited contracts; reconciled against external balances |
| Identity | Anonymous per-browser demo session (random HttpOnly cookie); wallet connection is not authentication | KYC/AML, Sign-In with Ethereum (EIP-4361) or equivalent, session management |
| Authorization | Operator controls gated only by DEMO_MODE=true and scoped to the caller's own session | Role-based operator access, 4-eyes approval for settlement, immutable audit log |
| Outcomes | Clicked by the visitor on the demo Operator page (per-session event state) | Verified data feed or oracle with dispute and correction process |
| Odds | Fixed, illustrative, labelled simulated | Trading/risk engine, price-change handling, limits and liability management |
| Ledger | Single SQLite file, serialized writes, versioned schema (PRAGMA user_version) | Double-entry ledger on a managed RDBMS, reconciliation jobs, backups, PITR |
| On-chain | Nothing is signed or sent | Audited contracts, key management (HSM/MPC), chain reorg & gas handling |
| Compliance | Out of scope | Jurisdictional gambling licences, responsible-gambling tooling, legal review |
| Operations | Local process, console logs, in-memory per-IP / per-session rate limits | Secrets management, monitoring/alerting, rate limiting, fraud controls, incident response |
Wallet connection is not identity
The wallet integration uses an injected EIP-1193 provider via ethers.BrowserProvider, requests accounts only on click, and reacts to account/chain changes. It never requests signatures, approvals or transactions. A connected address is public information the wallet chooses to share; without a signed challenge it does not prove ownership, which is why it is deliberately not linked to the demo credits.