6.1 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
H2H (Head to Head) is a peer-to-peer betting platform MVP where users can create, accept, and settle wagers directly with other users. The platform demonstrates core betting workflows with escrow, real-time updates, and a marketplace.
Tech Stack
- Backend: FastAPI (Python 3.11+) with async SQLAlchemy ORM
- Database: SQLite (via aiosqlite) - designed for easy migration to PostgreSQL
- Frontend: React 18+ with Vite, TypeScript, TailwindCSS
- Authentication: JWT tokens with refresh mechanism
- Real-time: WebSockets for live updates
- State Management: Zustand for client state, React Query for server state
Commands
Backend Development
# Setup
cd backend
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
# Initialize database
python seed_data.py
# Run development server
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# Run with Docker
docker-compose up backend
Frontend Development
# Setup
cd frontend
npm install
# Run development server
npm run dev
# Build for production
npm run build
# Type checking
npm run type-check # If configured
# Run with Docker
docker-compose up frontend
Full Stack
# Start both services
docker-compose up --build
# Stop all services
docker-compose down
# Reset database (when needed)
docker-compose down -v
Architecture
Core Data Flow
-
Bet Creation Flow:
- User creates bet → Validates funds → Bet enters marketplace (status: OPEN)
- No funds locked until bet is accepted
-
Bet Matching Flow:
- User accepts bet → Validates opponent funds → Locks funds in escrow for BOTH parties → Updates bet status to MATCHED
- Both creator and acceptor have funds locked in wallet.escrow
-
Settlement Flow:
- Winner submits claim → Loser confirms OR disputes → Funds distributed or flagged for review
- On confirm: escrow released, winner receives stake from both sides
- On dispute: bet marked DISPUTED for manual resolution
Database Architecture
Critical relationship pattern: The Bet model uses multiple foreign keys to User:
creator_id→ User who created the betopponent_id→ User who accepted the bet (nullable until matched)winner_id→ User who won (nullable until settled)
When querying bets with relationships, use selectinload() or joinedload() to prevent N+1 queries:
query = select(Bet).options(
selectinload(Bet.creator),
selectinload(Bet.opponent)
)
Async SQLAlchemy Patterns
All database operations are async. Key patterns:
Transaction safety for critical operations:
async with db.begin_nested(): # Creates savepoint
bet = await db.get(Bet, bet_id, with_for_update=True) # Row lock
wallet.balance -= amount
wallet.escrow += amount
# All changes committed together or rolled back
CRUD operations must use async methods:
await db.execute(query)notdb.execute(query)await db.commit()notdb.commit()await db.refresh(obj)notdb.refresh(obj)
Wallet & Escrow System
The wallet has two balance fields:
balance: Available funds the user can useescrow: Locked funds in active bets
Critical invariant: balance + escrow should always equal total funds. When accepting a bet:
- Deduct from
balance - Add to
escrow - Create ESCROW_LOCK transaction record
When settling:
- Deduct from loser's
escrow - Add to winner's
balance - Deduct from winner's
escrow(their original stake) - Add to winner's
balance(their original stake returned)
WebSocket Events
WebSocket connections authenticated via query param: ws://host/api/v1/ws?token={jwt}
Event types broadcast to relevant users:
bet_created: New bet in marketplacebet_accepted: Bet matched with opponentbet_settled: Bet result confirmedwallet_updated: Balance or escrow changednotification: General user notifications
Key Constraints
Bet State Transitions
Valid state transitions:
- OPEN → MATCHED (via accept)
- OPEN → CANCELLED (creator cancels before match)
- MATCHED → IN_PROGRESS (event starts)
- IN_PROGRESS → PENDING_RESULT (event ends, awaiting settlement)
- PENDING_RESULT → COMPLETED (settlement confirmed)
- PENDING_RESULT → DISPUTED (settlement disputed)
Invalid: Cannot cancel MATCHED bets, cannot settle OPEN bets
Validation Rules
- Stake amount: Must be > 0, ≤ $10,000
- User cannot accept their own bet
- User must have sufficient balance to accept bet
- Only creator can modify/cancel OPEN bets
- Only bet participants can settle
- Odds must be > 0
Environment Configuration
Backend requires (see backend/.env.example):
DATABASE_URL: SQLite path (default:sqlite+aiosqlite:///./data/h2h.db)JWT_SECRET: Secret for signing tokensJWT_ALGORITHM: HS256ACCESS_TOKEN_EXPIRE_MINUTES: Token TTL
Frontend requires (see frontend/.env.example):
VITE_API_URL: Backend API URL (default:http://localhost:8000)VITE_WS_URL: WebSocket URL (default:ws://localhost:8000)
Migration to Production Database
The codebase is designed for easy migration from SQLite to PostgreSQL:
-
Update
DATABASE_URLto PostgreSQL connection string:DATABASE_URL = "postgresql+asyncpg://user:pass@host:5432/h2h" -
Update
requirements.txt:- Remove:
aiosqlite - Add:
asyncpg
- Remove:
-
Run Alembic migrations against new database
-
No code changes needed - SQLAlchemy ORM abstracts database differences
Testing Data
Run python seed_data.py to create test users:
- alice@example.com / password123
- bob@example.com / password123
- charlie@example.com / password123
Each starts with $1000 balance and sample bets in marketplace.
MVP Scope
In scope: User auth, virtual wallet, bet creation/acceptance, basic settlement, WebSocket updates, responsive UI
Out of scope (future): Real payments, KYC/AML, blockchain, odds feeds, admin panel, email notifications, social features