This document captures the non-negotiable constraints the MVP must respect today and the boundaries that protect us when we eventually go live. It is intentionally short — every rule here is something that, if broken, would cost us in either trust, money, or both.
Status: living document. Update with every PR that materially changes what tokens, payments, or user data do in the system.
- "Today" means the sandbox-first MVP described in
AGENTS.md. - "Live" means any future state where real money or real user funds enter or exit the system.
- Tokens are internal test units. They are not crypto, not redeemable for cash, not exchangeable for goods or services. They have no monetary value outside this MVP.
- No real-money withdrawals exist — no flow that converts internal tokens back into fiat or any other store of value.
- Deposits use the M8 mock provider by default (
NUVEI_MODE=mock). No external PSP credentials are required for MVP. Real provider HTTP is deferred (ADR-0011). Mock flow +grant_tokensare supported funding paths. - Tokens only move via ledger entries. No code path may mutate
wallet.balancedirectly — there is nobalancecolumn onWallet, by design. Balance is alwaysSUM(LedgerEntry.amount)for the wallet, computed at query time. The only sanctioned writer to the ledger iswallets.services.record_entry. Reviewers should reject any PR that callsLedgerEntry.objects.create()directly. Two DB-levelCheckConstraints back this up (amount != 0andreference_type != "") so even a misbehaving service cannot produce a meaningless or untraceable entry. - Payment-to-ledger traceability is mandatory. Every ledger entry
caused by a payment links back to the originating
PaymentTransaction. Every ledger entry caused by a bet must link back to the originatingBetvia(reference_type, reference_id). As of M6 this is enforced for stake debits: everybet_stakeLedgerEntrycarriesreference_type="bet",reference_id=Bet.id, and a derivedidempotency_key=f"bet-stake-{Bet.id}".betting.services.place_betis the only sanctioned creator ofBetrows and writes the bet and the ledger debit in one DB transaction. As of M7, winning payouts (bet_payout) and void refunds (bet_void) use the same reference pair with derived keysf"bet-payout-{Bet.id}"/f"bet-void-{Bet.id}". The per-matchSettlementEventrow (uniqueidempotency_key=f"settle-{match.id}") is the durable anchor that makes the entire settlement run safe to retry — seebetting.services.settle_match. As of M8, deposit credits (deposit) follow the same shape:reference_type="payment_transaction",reference_id=PaymentTransaction.id, derivedidempotency_key=f"deposit-{PaymentTransaction.id}". The per-event(PaymentTransaction, provider_event_id)unique index onPaymentEventis the durable webhook-replay anchor — seepayments.services.record_provider_eventand ADR-0007. - Server is the source of truth for balances, odds snapshots,
match results, and settlement outcomes. The client is never trusted
for any of those values. As of M6, odds are frozen onto
Bet.odds_snapshotat placement; M7 settlement credits payouts fromBet.potential_payout(computed at placement from the snapshot), never liveMatch.odds_*. - Idempotency on every retriable write — bet placement (M6),
settlement runs (M7), deposits (M8), payment-provider callbacks (M8),
GRID sync HTTP (M5b —
ExternalSyncLogaudit; settlement idempotent per match viaSettlementEvent). Duplicate input must not produce duplicate effect.POST /api/bets/enforces this: theIdempotency-Keyheader is required (400 if missing); replays return the original bet with200 OKand write no second ledger entry.settle_matchis idempotent per match: a second call returns the existingSettlementEventand writes no additional ledger rows.POST /api/payments/deposits/requires the sameIdempotency-Keyheader (400 if missing); replays return the originalPaymentTransactionwith200 OKand never call the provider a second time. Provider webhook delivery (or, in M8 mock, thesimulate-callbackendpoint) is idempotent on(PaymentTransaction, provider_event_id); the walletdepositcredit is itself idempotent on its derived ledger key. Three layers of belt-and-braces — even a wildly broken caller cannot double-credit.
Minimal by design:
- Email (login identifier). Users may also be created via
POST /api/auth/register/(self-serve) — there is no email verification, age gate, or KYC in MVP; duplicate emails return409(code: EmailAlreadyRegistered). See ADR-0008. - Password (Django-hashed, never logged, never returned in any API
response — see
accounts.serializers.UserSerializer). - Session records produced by Django sessions (
django_session).
Out of scope for the MVP: real names, addresses, payment instruments, phone numbers, government IDs, geolocation, behavioral analytics.
- No real-money flows. Live-mode payment credentials must not be configured in any deployed environment.
- No KYC / AML automation. Real launches will require this; the MVP doesn't pretend to do it.
- No responsible-gambling tooling (deposit limits, self-exclusion, cool-down periods). To be designed before any non-sandbox release.
- No bonuses, promotions, referral credits, or marketing token giveaways. Each of these would require its own ledger entry type and audit story; we keep the surface clean.
- No raw provider payloads in API responses. Webhook bodies and provider-side metadata stay backend-internal.
Before any environment is flipped to a non-sandbox payment mode, this document must be expanded with:
- Jurisdictional review — where can the product legally operate.
- KYC/AML strategy — provider, retention, escalation.
- Responsible-gambling controls — limits, self-exclusion, audit log.
- Data-retention policy — for ledger, payment, and session records.
- Incident response — for payment disputes, refund handling, and webhook spoofing attempts.
These are deliberately not in MVP scope and should not be added piecemeal.
AGENTS.md— non-negotiable engineering rules.docs/architecture.md— domain boundaries and idempotency strategy.docs/api-contracts.md— endpoint shapes.docs/decisions/0001-use-monorepo.mddocs/decisions/0002-python-code-style.mddocs/decisions/0003-database-conventions.mddocs/decisions/0004-m7-bet-settlement.mddocs/decisions/0007-m8-nuvei-deposits.mddocs/decisions/0008-self-serve-registration.mddocs/decisions/0009-m5b-grid-ingestion.md— GRID CS2 sync.docs/decisions/0010-multi-game-catalog-target.md— planned multi-game shape.docs/decisions/0011-psp-deferred-mvp-completion.md— mock-only MVP; PSP deferred.