Skip to content

Latest commit

 

History

63 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

token_e-sports_betting

Sandbox e-sports betting MVP that proves the core loop (wallet + betting + settlement) on internal test tokens — no real money, no external payment provider required to run or demo.

Public repo: safe to clone and run locally. Never commit .env or API keys. GRID ingestion is optional (GRID_API_KEY in .env). Tokens have no cash value — see docs/compliance-notes.md.

Payment flows use a provider-shaped architecture (M8: PaymentTransaction, events, idempotency, mock checkout) so a future PSP can plug in without rewriting wallet or betting. Live PSP integration is deferred (Nuvei onboarding blocked commercially — see ADR-0011).

MVP status (Jun 2026)

Feature-complete for portfolio demo on CS2 winner bets (ADR-0011):

Shipped Planned (later)
Session auth + registration Dota 2 ingestion (ADR-0010)
Ledger wallet + mock deposits Parlays / stat markets (new ADR)
GRID CS2 sync + auto-settle + undecided void Real PSP HTTP (ADR-0011 deferred)
Bet placement + settlement + refunds Scheduled GRID cron (infra)
Frontend: catalog, detail, results, bets, wallet Deeper UI polish (bet slip, skeletons)
  • Sandbox-only: test balances; fund via mock deposit or grant_tokens.
  • Tokens are internal test units (not real money, not crypto, not withdrawable cash).
  • No external PSP credentials needed for local dev, CI, or portfolio demo.
  • Games: CS2 ingested via GRID (M5b + stale cleanup + score fallback ADR-0012). Dota 2 is documented target scope — not ingested yet.

Tech stack (current decisions)

  • Frontend: Next.js + TypeScript
  • Backend: Django + Django REST Framework
  • Auth: Django sessions
  • Database: PostgreSQL
  • Match data: GRID Open Access API (CS2 ingested; Dota 2 planned)
  • Payments: mock provider (NUVEI_MODE=mock); architecture swappable (ADR-0011)
  • Repo: Monorepo (frontend/ + django-backend/)

Repo structure

  • frontend/ — Next.js app
  • django-backend/ — Django app + API
  • docs/ — product + architecture + API contracts + compliance notes
  • infra/ — deployment/ops planning notes (infra/README.md)

Local development

Prereqs

  • Git
  • Node.js (LTS)
  • Python 3.12
  • PostgreSQL 14+ (the backend connects to a local Postgres on :5432; setup steps below)

Environment variables

  • Use .env.example as the template (never commit real secrets).
  • Backend will use environment variables for DB connection and Django settings.
  • Frontend will use environment variables for API base URL.

Backend setup

By convention the Python virtual environment lives at the repository root (./.venv) and is shared by the backend tooling.

1. Clone, create venv, install dependencies

git clone git@github.com:KekoFigueroa-dev/token_e-sports_betting.git
cd token_e-sports_betting
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r django-backend/requirements.txt

2. Create your local .env at the repo root

cp .env.example .env

Then edit .env — at minimum, replace these placeholders:

  • DJANGO_SECRET_KEY — generate a real value:

    python -c "from secrets import token_urlsafe; print(token_urlsafe(50))"
  • POSTGRES_PASSWORD — pick anything; whatever you set here must match the password you give your local Postgres role in step 3.

.env is gitignored — never commit it.

3. Install and start PostgreSQL locally

The backend talks to a local Postgres database. Pick the path for your OS:

Ubuntu / Debian:

sudo apt update
sudo apt install -y postgresql
sudo systemctl start postgresql

macOS (Homebrew):

brew install postgresql@16
brew services start postgresql@16

Then create the project's database and user. Replace changeme with whatever you set for POSTGRES_PASSWORD in .env:

sudo -u postgres psql <<SQL
CREATE USER token_esports WITH PASSWORD 'changeme';
CREATE DATABASE token_esports OWNER token_esports;
ALTER USER token_esports CREATEDB;       -- needed for `python manage.py test`
SQL

The ALTER USER … CREATEDB line lets the user create temporary test databases. Skip if you don't plan to run the test suite locally.

4. Run Django

From inside django-backend/:

cd django-backend
python manage.py check         # should report "0 issues"
python manage.py migrate       # built-in + accounts + wallets + matches + betting + payments
python manage.py createsuperuser   # prompts for email + password (no username)
python manage.py runserver     # serves on http://localhost:8000

The following surfaces should be live:

  • http://localhost:8000/api/health/ → JSON {"status":"ok","service":"token-esports-backend"} (M1).
  • http://localhost:8000/admin → Django admin login form (use the email + password from createsuperuser).
  • /api/auth/csrf/, /api/auth/register/, /api/auth/login/, /api/auth/logout/, /api/auth/me/ (M2 + self-serve registration).
  • /api/wallet/ — auth-required, returns the calling user's wallet, derived balance, and the last 50 ledger entries (M4).
  • /api/matches/ and /api/matches/<uuid>/ — public read-only match catalog (M5a + M6 odds; M5b GRID rows alongside fixtures). Supports pagination and catalog filters (known_teams_only, bettable_only). After migrate, seed sample rows with python manage.py loaddata sample_matches, or ingest live CS2 data: python manage.py sync_grid_matches --past-days 2 --future-days 3 (requires GRID_API_KEY in .env — see .env.example).
  • /api/bets/ — auth-required. POST places a bet (requires an Idempotency-Key header; 201 on first creation, 200 on replay) and GET returns the caller's own bets (M6). See docs/api-contracts.md for the full contract (including M7: settlement is CLI-only; no public settle HTTP — see that section's note).
  • /api/payments/deposits/ — auth-required. POST starts a sandbox Nuvei deposit (Idempotency-Key required; 201 on first creation, 200 on replay), GET /api/payments/deposits/<id>/ returns the caller's own deposit, and the dev-only POST .../simulate-callback/ is the M8 mock stand-in for Nuvei's webhook (M8). See docs/api-contracts.md.

Quick wallet smoke test:

# Seed your user's wallet via the dev-only management command.
# Goes through the same record_entry service every production code
# path uses; the wallet is auto-created on first access.
python manage.py grant_tokens you@example.com 100

# Re-running with the same --idempotency-key is a no-op (returns the
# same ledger entry instead of duplicating it).
python manage.py grant_tokens you@example.com 50 --idempotency-key smoke-1
python manage.py grant_tokens you@example.com 50 --idempotency-key smoke-1

# Inspect via the admin: /admin/wallets/wallet/ and /admin/wallets/ledgerentry/
# (both registered read-only — append-only invariant).

Quick GRID catalog smoke test (M5b): with GRID_API_KEY set in .env:

cd django-backend
python manage.py sync_grid_matches --past-days 2 --future-days 3
curl -s 'http://localhost:8000/api/matches/?status=scheduled&known_teams_only=true&bettable_only=true&ordering=start_time&page=1' | python -m json.tool | head -20

Open http://localhost:3000/matches — bettable matches only (25 per page, real team names, soonest kickoff first). /matches/results shows completed winners and undecided voids (GRID finished with no winner — stakes refunded). See django-backend/matches/README.md, ADR-0009, and ADR-0012.

Quick bet smoke test (M6): after granting yourself tokens above, seed matches with python manage.py loaddata sample_matches, log in via /api/auth/login/, then POST /api/bets/ with an Idempotency-Key header and a body like {"match_id":"<scheduled match uuid>","selection":"team_a","stake":"50"}. Re-posting with the same Idempotency-Key returns the original bet with 200 OK and does NOT double-debit the wallet — that's the M6 guarantee. Full curl recipe and status-code matrix live in docs/api-contracts.md.

Quick deposit smoke test (M8 — mock Nuvei): sign in, go to http://localhost:3000/wallet/deposit, enter an amount, click "Continue to checkout →". The pseudo Hosted Payment Page at /wallet/deposit/<id>/checkout lets you click Pay / Decline / Cancel. Pay flips the PaymentTransaction to settled, writes one deposit ledger entry of +amount, and the wallet badge auto-refreshes via the wallet:refresh event. Re-clicking the same button is an idempotent no-op (no second credit). Clicking a different button after a terminal status returns 409 PaymentTransactionAlreadyTerminal.

The sandbox deposit also runs entirely from the API:

# 1) Sign in via the frontend or POST /api/auth/login/, then:
curl -b cookies.txt -c cookies.txt -X POST \
  http://localhost:8000/api/payments/deposits/ \
  -H "Content-Type: application/json" \
  -H "X-CSRFToken: $(grep csrftoken cookies.txt | awk '{print $7}')" \
  -H "Idempotency-Key: smoke-1" \
  -d '{"amount": "100.0000", "currency": "TOKENS"}'
# Returns 201 with hosted_page_url + status=pending_provider.

# 2) Simulate the (mock) Nuvei webhook for the returned <id>:
curl -b cookies.txt -X POST \
  "http://localhost:8000/api/payments/deposits/<id>/simulate-callback/" \
  -H "Content-Type: application/json" \
  -H "X-CSRFToken: $(grep csrftoken cookies.txt | awk '{print $7}')" \
  -d '{"result": "success"}'
# Returns 200 with status=settled. /api/wallet/ now shows +100 TOKENS.

Full status-code matrix lives in docs/api-contracts.md. ADR for the design (state machine, three layers of idempotency, mock-only rationale): docs/decisions/0007-m8-nuvei-deposits.md.

Quick settlement smoke test (M7): after the M6 bet smoke above, complete a scheduled match and settle it from django-backend/:

export MATCH_UUID=22222222-2222-4222-8222-222222222201   # sample scheduled match
export WINNER_TEAM_UUID=11111111-1111-4111-8111-111111111101  # team_a of that fixture

python manage.py complete_match "$MATCH_UUID" --winner "$WINNER_TEAM_UUID"
python manage.py settle_match "$MATCH_UUID"

Re-running settle_match with the same UUID is a no-op (same SettlementEvent; no duplicate payouts). Re-running complete_match with the same winner is also a no-op; a different winner raises CommandError (history is not rewritten). Then GET /api/bets/ and /api/wallet/ should show won/lost/void and an updated derived balance; the Next.js /bets page reflects that after a refresh.

To exercise the full HTTP API end-to-end (CSRF + login + wallet + bets) from the command line, see the auth + wallet + bets endpoint contracts in docs/api-contracts.md.

If manage.py check and migrate both succeed and you can hit /api/health/, the backend is set up correctly.

5. Common issues

  • connection refused on manage.py migrate: Postgres isn't running. Re-run sudo systemctl start postgresql (Linux) or brew services start postgresql@16 (macOS).
  • password authentication failed: the password in .env doesn't match the one you set in step 3. Either change POSTGRES_PASSWORD in .env or re-run the ALTER USER token_esports WITH PASSWORD '…' command in psql.
  • ImproperlyConfigured: Set the DJANGO_SECRET_KEY environment variable: .env is missing or doesn't have DJANGO_SECRET_KEY. Re-do step 2.

Code style

Python source under django-backend/ uses double-quoted strings throughout. See docs/decisions/0002-python-code-style.md.

Frontend setup

The Next.js app lives in frontend/. It assumes the backend is running on http://localhost:8000 (see "Backend setup" above).

1. Install dependencies and create your local env

cd frontend
npm install
cp .env.local.example .env.local

Edit .env.local only if your backend is on a different host/port. The defaults (NEXT_PUBLIC_API_BASE_URL=http://localhost:8000, PORT=3000) match the rest of the docs.

2. Run the dev server

npm run dev

Open http://localhost:3000. With the backend running side-by-side you should see:

  • A green "Backend reachable" banner (M1 health handshake).
  • A session widget showing "Not signed in" with links to Create account and Sign in.
  • Use Create account → (/register) to self-register with email + password (you land signed in on /), or Sign in → (/login) with the email + password from createsuperuser (or any registered user). Refresh to confirm the session persists; click Sign out to end it.

Once signed in, the following surfaces are live:

  • Site header — Matches · Results · My bets · Wallet · Deposit; balance badge links to /wallet.
  • /matches — public bettable catalog (25/page): confirmed teams, future kickoffs only. Match titles link to /matches/<uuid> detail. Click an odds button to expand a stake form (Idempotency-Key per open).
  • /matches/<uuid> — match detail: odds, status, inline betting (scheduled), or scores + winner/undecided (terminal).
  • /matches/results — completed and undecided void matches with series scores; undecided rows explain stake refunds.
  • /bets — bet history with team names; void bets on undecided matches show a refund message.
  • /wallet — balance + last 50 ledger entries (stakes, payouts, refunds, deposits).
  • /wallet/deposit — sandbox mock deposit → pseudo checkout at /wallet/deposit/<id>/checkout (NUVEI_MODE=mock).

Real Nuvei sandbox HTTP is deferred (ADR-0011); mock mode is the supported path.

End-to-end demo checklist

Use this flow before a portfolio walkthrough or after cloning the repo:

  1. Start stack — Postgres, python manage.py runserver (backend), npm run dev (frontend). Home page shows green Backend reachable.
  2. Account/register or /login (or createsuperuser + /login).
  3. Fund/wallet/deposit (mock Pay) or python manage.py grant_tokens you@example.com 500.
  4. Matches — optional: python manage.py sync_grid_matches --past-days 2 --future-days 3 (needs GRID_API_KEY). Or loaddata sample_matches for fixtures only.
  5. Bet/matches → pick odds → confirm stake. Balance drops via bet_stake.
  6. Settle — re-run sync_grid_matches after real matches finish (GRID auto-settles), or dev path: complete_match + settle_match on a fixture.
  7. Verify/bets (won/lost/void), /wallet (ledger), /matches/results (winner or undecided + refund copy).

Idempotency smoke: replay the same bet Idempotency-Key200 and no double debit; replay sync_grid_matches on a settled match → no duplicate payouts.

3. Common scripts

npm run dev        # local dev server with hot reload
npm run build      # production build (also catches TypeScript errors)
npm run start      # serve the production build locally
npm run lint       # ESLint

4. Conventions

  • All backend calls go through frontend/src/lib/api.ts. It sets credentials: "include" so the browser attaches Django session cookies on cross-origin requests.
  • .env.local is gitignored; .env.local.example is the committed template.
  • Tailwind CSS is wired up; styles live in frontend/src/app/globals.css.

Continuous integration

Pull requests and pushes to main run GitHub Actions (.github/workflows/ci.yml):

  • Backend: Python 3.12, PostgreSQL 16 (service container), full suite python manage.py test from django-backend/.
  • Frontend: Node 22, npm ci, npm run lint, npx tsc --noEmit. next build is intentionally not in CI yet (optional follow-up). npm audit is not part of CI; if it flags nested PostCSS inside next, do not run npm audit fix --force — see frontend/README.md.

Match CI locally before you push: docs/ci.md. After the first green run on main, you can require these checks under GitHub → Settings → Branches → Branch protection rules (optional).

Docs

Branching

Format:

<type>/<owner>/<short-description>

Examples:

  • feature/keko/auth-sessions
  • feature/emi/match-list-ui
  • docs/keko/readme-setup

Near-term roadmap

See docs/architecture.md for detail. Shipped on main: CS2 GRID sync (stale pass, score fallback, undecided void), core frontend nav + match detail + wallet ledger view.

Planned (not started — pick up when resuming):

  1. Dota 2 ingestion — verify GRID Open Access scope, then mirror CS2 (ADR-0010)
  2. Betting depth — parlays / stat markets (needs new ADR)
  3. Frontend polish — bet slip UX, loading skeletons, mobile pass
  4. GRID ops--stale-only flag, wider lookback, optional cron (infra)
  5. Real PSP HTTPdeferred until provider access exists (ADR-0011)

Notes

We intentionally scaffolded structure + docs first to keep implementation clean and repeatable before feature coding.

About

Sandbox e-sports betting MVP — Django API + Next.js UI, ledger-based tokens, Nuvei test-mode deposits (mock), winner-only bets.

Topics

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages