# H2H Betting Platform - MVP Prototype Generation Prompt

## Project Overview

Build a prototype MVP for **Head to Head Bet (H2H)** — a peer-to-peer betting platform where users can create, accept, and settle wagers directly with other users. This is a functional prototype to demonstrate core betting workflows.

## Tech Stack

- **Backend:** FastAPI (Python 3.11+)
- **Frontend:** React 18+ with Vite, TypeScript, TailwindCSS
- **Database:** SQLite with SQLAlchemy ORM (async via aiosqlite)
- **Authentication:** JWT tokens
- **Real-time:** WebSockets for live updates

> **Note:** SQLite is used for rapid prototyping. The SQLAlchemy ORM allows easy migration to PostgreSQL or MySQL later by simply changing the connection string.

## Project Structure

```
h2h-mvp/
├── backend/
│   ├── app/
│   │   ├── __init__.py
│   │   ├── main.py                 # FastAPI app entry point
│   │   ├── config.py               # Settings and environment config
│   │   ├── database.py             # SQLAlchemy engine, session, Base
│   │   ├── models/
│   │   │   ├── __init__.py         # Export all models
│   │   │   ├── user.py
│   │   │   ├── bet.py
│   │   │   ├── wallet.py
│   │   │   └── transaction.py
│   │   ├── schemas/
│   │   │   ├── __init__.py
│   │   │   ├── user.py
│   │   │   ├── bet.py
│   │   │   └── wallet.py
│   │   ├── routers/
│   │   │   ├── __init__.py
│   │   │   ├── auth.py
│   │   │   ├── users.py
│   │   │   ├── bets.py
│   │   │   ├── wallet.py
│   │   │   └── websocket.py
│   │   ├── services/
│   │   │   ├── __init__.py
│   │   │   ├── auth_service.py
│   │   │   ├── bet_service.py      # Bet matching and settlement
│   │   │   ├── wallet_service.py
│   │   │   └── notification.py
│   │   ├── crud/
│   │   │   ├── __init__.py
│   │   │   ├── user.py
│   │   │   ├── bet.py
│   │   │   └── wallet.py
│   │   └── utils/
│   │       ├── __init__.py
│   │       ├── security.py         # Password hashing, JWT
│   │       └── validators.py
│   ├── data/
│   │   └── .gitkeep                # SQLite db file goes here
│   ├── alembic/                    # Database migrations
│   │   ├── versions/
│   │   ├── env.py
│   │   └── alembic.ini
│   ├── requirements.txt
│   ├── Dockerfile
│   ├── seed_data.py                # Script to populate test data
│   └── .env.example
├── frontend/
│   ├── src/
│   │   ├── main.tsx
│   │   ├── App.tsx
│   │   ├── api/
│   │   │   ├── client.ts           # Axios instance
│   │   │   ├── auth.ts
│   │   │   ├── bets.ts
│   │   │   └── wallet.ts
│   │   ├── components/
│   │   │   ├── layout/
│   │   │   │   ├── Header.tsx
│   │   │   │   ├── Sidebar.tsx
│   │   │   │   └── Layout.tsx
│   │   │   ├── auth/
│   │   │   │   ├── LoginForm.tsx
│   │   │   │   └── RegisterForm.tsx
│   │   │   ├── bets/
│   │   │   │   ├── BetCard.tsx
│   │   │   │   ├── BetList.tsx
│   │   │   │   ├── CreateBetModal.tsx
│   │   │   │   ├── BetMatchingQueue.tsx
│   │   │   │   └── MyBets.tsx
│   │   │   ├── wallet/
│   │   │   │   ├── WalletBalance.tsx
│   │   │   │   ├── DepositModal.tsx
│   │   │   │   └── TransactionHistory.tsx
│   │   │   └── common/
│   │   │       ├── Button.tsx
│   │   │       ├── Modal.tsx
│   │   │       ├── Card.tsx
│   │   │       └── Loading.tsx
│   │   ├── pages/
│   │   │   ├── Home.tsx
│   │   │   ├── Login.tsx
│   │   │   ├── Register.tsx
│   │   │   ├── Dashboard.tsx
│   │   │   ├── BetMarketplace.tsx
│   │   │   ├── CreateBet.tsx
│   │   │   ├── BetDetails.tsx
│   │   │   ├── MyBets.tsx
│   │   │   └── Wallet.tsx
│   │   ├── hooks/
│   │   │   ├── useAuth.ts
│   │   │   ├── useBets.ts
│   │   │   ├── useWallet.ts
│   │   │   └── useWebSocket.ts
│   │   ├── store/
│   │   │   ├── index.ts            # Zustand store
│   │   │   ├── authSlice.ts
│   │   │   └── betSlice.ts
│   │   ├── types/
│   │   │   └── index.ts
│   │   └── utils/
│   │       ├── formatters.ts
│   │       └── constants.ts
│   ├── index.html
│   ├── package.json
│   ├── vite.config.ts
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   └── .env.example
├── docker-compose.yml
└── README.md
```

---

## Database Schema (SQLAlchemy Models)

### database.py - Connection Setup
```python
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase

DATABASE_URL = "sqlite+aiosqlite:///./data/h2h.db"

engine = create_async_engine(DATABASE_URL, echo=True)
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

class Base(DeclarativeBase):
    pass

async def get_db():
    async with async_session() as session:
        try:
            yield session
        finally:
            await session.close()

async def init_db():
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
```

### User Model
```python
from sqlalchemy import String, DateTime, Enum, Float, Integer
from sqlalchemy.orm import Mapped, mapped_column, relationship
from datetime import datetime
import enum

class UserStatus(enum.Enum):
    ACTIVE = "active"
    SUSPENDED = "suspended"
    PENDING_VERIFICATION = "pending_verification"

class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
    username: Mapped[str] = mapped_column(String(50), unique=True, index=True)
    password_hash: Mapped[str] = mapped_column(String(255))
    
    # Profile fields
    display_name: Mapped[str] = mapped_column(String(100), nullable=True)
    avatar_url: Mapped[str] = mapped_column(String(500), nullable=True)
    bio: Mapped[str] = mapped_column(String(500), nullable=True)
    
    # Stats
    total_bets: Mapped[int] = mapped_column(Integer, default=0)
    wins: Mapped[int] = mapped_column(Integer, default=0)
    losses: Mapped[int] = mapped_column(Integer, default=0)
    win_rate: Mapped[float] = mapped_column(Float, default=0.0)
    
    status: Mapped[UserStatus] = mapped_column(Enum(UserStatus), default=UserStatus.ACTIVE)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
    updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    # Relationships
    wallet: Mapped["Wallet"] = relationship(back_populates="user", uselist=False)
    created_bets: Mapped[list["Bet"]] = relationship(back_populates="creator", foreign_keys="Bet.creator_id")
    accepted_bets: Mapped[list["Bet"]] = relationship(back_populates="opponent", foreign_keys="Bet.opponent_id")
    transactions: Mapped[list["Transaction"]] = relationship(back_populates="user")
```

### Wallet Model
```python
from sqlalchemy import ForeignKey, Numeric
from decimal import Decimal

class Wallet(Base):
    __tablename__ = "wallets"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), unique=True)
    balance: Mapped[Decimal] = mapped_column(Numeric(12, 2), default=Decimal("0.00"))
    escrow: Mapped[Decimal] = mapped_column(Numeric(12, 2), default=Decimal("0.00"))
    currency: Mapped[str] = mapped_column(String(3), default="USD")
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
    updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    # Relationships
    user: Mapped["User"] = relationship(back_populates="wallet")
    transactions: Mapped[list["Transaction"]] = relationship(back_populates="wallet")
```

### Transaction Model
```python
class TransactionType(enum.Enum):
    DEPOSIT = "deposit"
    WITHDRAWAL = "withdrawal"
    BET_PLACED = "bet_placed"
    BET_WON = "bet_won"
    BET_LOST = "bet_lost"
    BET_CANCELLED = "bet_cancelled"
    ESCROW_LOCK = "escrow_lock"
    ESCROW_RELEASE = "escrow_release"

class TransactionStatus(enum.Enum):
    PENDING = "pending"
    COMPLETED = "completed"
    FAILED = "failed"

class Transaction(Base):
    __tablename__ = "transactions"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    wallet_id: Mapped[int] = mapped_column(ForeignKey("wallets.id"))
    type: Mapped[TransactionType] = mapped_column(Enum(TransactionType))
    amount: Mapped[Decimal] = mapped_column(Numeric(12, 2))
    balance_after: Mapped[Decimal] = mapped_column(Numeric(12, 2))
    reference_id: Mapped[int] = mapped_column(Integer, nullable=True)  # bet_id if bet-related
    description: Mapped[str] = mapped_column(String(500))
    status: Mapped[TransactionStatus] = mapped_column(Enum(TransactionStatus), default=TransactionStatus.COMPLETED)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)

    # Relationships
    user: Mapped["User"] = relationship(back_populates="transactions")
    wallet: Mapped["Wallet"] = relationship(back_populates="transactions")
```

### Bet Model
```python
class BetCategory(enum.Enum):
    SPORTS = "sports"
    ESPORTS = "esports"
    POLITICS = "politics"
    ENTERTAINMENT = "entertainment"
    CUSTOM = "custom"

class BetStatus(enum.Enum):
    OPEN = "open"
    MATCHED = "matched"
    IN_PROGRESS = "in_progress"
    PENDING_RESULT = "pending_result"
    COMPLETED = "completed"
    CANCELLED = "cancelled"
    DISPUTED = "disputed"

class BetVisibility(enum.Enum):
    PUBLIC = "public"
    PRIVATE = "private"
    FRIENDS_ONLY = "friends_only"

class Bet(Base):
    __tablename__ = "bets"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    creator_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    opponent_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=True)
    
    title: Mapped[str] = mapped_column(String(200))
    description: Mapped[str] = mapped_column(String(2000))
    category: Mapped[BetCategory] = mapped_column(Enum(BetCategory))
    
    # Event info
    event_name: Mapped[str] = mapped_column(String(200))
    event_date: Mapped[datetime] = mapped_column(DateTime, nullable=True)
    
    # Terms
    creator_position: Mapped[str] = mapped_column(String(500))
    opponent_position: Mapped[str] = mapped_column(String(500))
    creator_odds: Mapped[float] = mapped_column(Float, default=1.0)
    opponent_odds: Mapped[float] = mapped_column(Float, default=1.0)
    
    # Stake
    stake_amount: Mapped[Decimal] = mapped_column(Numeric(12, 2))
    currency: Mapped[str] = mapped_column(String(3), default="USD")
    
    status: Mapped[BetStatus] = mapped_column(Enum(BetStatus), default=BetStatus.OPEN)
    visibility: Mapped[BetVisibility] = mapped_column(Enum(BetVisibility), default=BetVisibility.PUBLIC)
    
    # Result
    winner_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=True)
    settled_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
    settled_by: Mapped[str] = mapped_column(String(20), nullable=True)  # creator, opponent, admin, oracle
    
    expires_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
    updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    # Relationships
    creator: Mapped["User"] = relationship(back_populates="created_bets", foreign_keys=[creator_id])
    opponent: Mapped["User"] = relationship(back_populates="accepted_bets", foreign_keys=[opponent_id])
    winner: Mapped["User"] = relationship(foreign_keys=[winner_id])
    proposals: Mapped[list["BetProposal"]] = relationship(back_populates="bet")
```

### BetProposal Model
```python
class ProposalStatus(enum.Enum):
    PENDING = "pending"
    ACCEPTED = "accepted"
    REJECTED = "rejected"
    EXPIRED = "expired"

class BetProposal(Base):
    __tablename__ = "bet_proposals"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    bet_id: Mapped[int] = mapped_column(ForeignKey("bets.id"))
    proposer_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    proposed_stake: Mapped[Decimal] = mapped_column(Numeric(12, 2))
    proposed_creator_odds: Mapped[float] = mapped_column(Float)
    proposed_opponent_odds: Mapped[float] = mapped_column(Float)
    message: Mapped[str] = mapped_column(String(500), nullable=True)
    status: Mapped[ProposalStatus] = mapped_column(Enum(ProposalStatus), default=ProposalStatus.PENDING)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
    expires_at: Mapped[datetime] = mapped_column(DateTime)

    # Relationships
    bet: Mapped["Bet"] = relationship(back_populates="proposals")
    proposer: Mapped["User"] = relationship()
```

---

## API Endpoints

### Authentication
```
POST   /api/v1/auth/register        # Register new user
POST   /api/v1/auth/login           # Login, returns JWT
POST   /api/v1/auth/refresh         # Refresh JWT token
POST   /api/v1/auth/logout          # Invalidate token
GET    /api/v1/auth/me              # Get current user
```

### Users
```
GET    /api/v1/users/{user_id}      # Get user profile
PUT    /api/v1/users/me             # Update own profile
GET    /api/v1/users/{user_id}/stats # Get user betting stats
GET    /api/v1/users/{user_id}/bets # Get user's public bets
```

### Wallet
```
GET    /api/v1/wallet               # Get wallet balance
POST   /api/v1/wallet/deposit       # Simulate deposit (MVP)
POST   /api/v1/wallet/withdraw      # Simulate withdrawal (MVP)
GET    /api/v1/wallet/transactions  # Get transaction history
```

### Bets
```
GET    /api/v1/bets                 # List open bets (marketplace)
POST   /api/v1/bets                 # Create new bet
GET    /api/v1/bets/{bet_id}        # Get bet details
PUT    /api/v1/bets/{bet_id}        # Update bet (creator only, if open)
DELETE /api/v1/bets/{bet_id}        # Cancel bet (creator only, if open)

POST   /api/v1/bets/{bet_id}/accept # Accept a bet
POST   /api/v1/bets/{bet_id}/propose # Counter-propose terms
POST   /api/v1/bets/{bet_id}/settle # Submit result (winner claims)
POST   /api/v1/bets/{bet_id}/confirm # Confirm result (loser confirms)
POST   /api/v1/bets/{bet_id}/dispute # Dispute result

GET    /api/v1/bets/my/created      # My created bets
GET    /api/v1/bets/my/accepted     # Bets I've accepted
GET    /api/v1/bets/my/active       # All my active bets
GET    /api/v1/bets/my/history      # Completed bet history
```

### WebSocket
```
WS     /api/v1/ws                   # Real-time updates
       - bet_created
       - bet_accepted
       - bet_settled
       - wallet_updated
       - notification
```

---

## Core Features for MVP

### 1. User Authentication
- Email/password registration with validation
- JWT-based authentication with refresh tokens
- Basic profile management

### 2. Wallet System
- Virtual wallet with USD balance
- Simulated deposits (for testing - just add funds)
- Escrow mechanism (lock funds when bet is placed/accepted)
- Transaction history

### 3. Bet Creation
- Create bets with title, description, terms
- Set stake amount and simple odds
- Choose category and visibility
- Set expiration for bet offers

### 4. Bet Marketplace
- Browse open bets with filters (category, stake range, etc.)
- Search functionality
- Sort by newest, stake amount, expiring soon

### 5. Bet Matching
- Accept existing bets
- Funds automatically escrowed from both parties
- Real-time notifications via WebSocket

### 6. Bet Settlement
- Winner submits result claim
- Loser confirms OR disputes
- If confirmed: funds distributed automatically
- If disputed: marked for admin review (MVP just flags it)

### 7. Dashboard
- Overview of active bets
- Recent activity
- Win/loss stats
- Wallet balance

---

## Implementation Notes

### Backend (FastAPI + SQLAlchemy)

1. **Async SQLAlchemy setup:**
```python
# database.py
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase

DATABASE_URL = "sqlite+aiosqlite:///./data/h2h.db"

engine = create_async_engine(
    DATABASE_URL,
    echo=True,  # SQL logging, disable in production
    connect_args={"check_same_thread": False}  # SQLite specific
)

async_session = async_sessionmaker(
    engine, 
    class_=AsyncSession, 
    expire_on_commit=False
)
```

2. **Dependency injection:**
```python
from sqlalchemy.ext.asyncio import AsyncSession

async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with async_session() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db)
) -> User:
    # Validate JWT and return user from database
    pass
```

3. **CRUD operations:**
```python
# crud/bet.py
from sqlalchemy import select
from sqlalchemy.orm import selectinload

async def get_open_bets(
    db: AsyncSession,
    skip: int = 0,
    limit: int = 20,
    category: BetCategory | None = None
) -> list[Bet]:
    query = select(Bet).where(Bet.status == BetStatus.OPEN)
    
    if category:
        query = query.where(Bet.category == category)
    
    query = query.options(
        selectinload(Bet.creator)
    ).offset(skip).limit(limit).order_by(Bet.created_at.desc())
    
    result = await db.execute(query)
    return result.scalars().all()

async def create_bet(db: AsyncSession, bet_data: BetCreate, user_id: int) -> Bet:
    bet = Bet(**bet_data.model_dump(), creator_id=user_id)
    db.add(bet)
    await db.flush()
    await db.refresh(bet)
    return bet
```

4. **Bet service with transaction:**
```python
# services/bet_service.py
async def accept_bet(db: AsyncSession, bet_id: int, user_id: int) -> Bet:
    # Use database transaction for atomic operations
    async with db.begin_nested():  # Savepoint
        # 1. Get and lock the bet
        bet = await db.get(Bet, bet_id, with_for_update=True)
        if not bet or bet.status != BetStatus.OPEN:
            raise BetNotAvailableError()
        
        if bet.creator_id == user_id:
            raise CannotAcceptOwnBetError()
        
        # 2. Get user's wallet and verify funds
        wallet = await get_user_wallet(db, user_id)
        if wallet.balance < bet.stake_amount:
            raise InsufficientFundsError()
        
        # 3. Lock funds in escrow
        wallet.balance -= bet.stake_amount
        wallet.escrow += bet.stake_amount
        
        # 4. Update bet
        bet.opponent_id = user_id
        bet.status = BetStatus.MATCHED
        
        # 5. Create transaction record
        transaction = Transaction(
            user_id=user_id,
            wallet_id=wallet.id,
            type=TransactionType.ESCROW_LOCK,
            amount=-bet.stake_amount,
            balance_after=wallet.balance,
            reference_id=bet.id,
            description=f"Escrow for bet: {bet.title}"
        )
        db.add(transaction)
        
    await db.commit()
    await db.refresh(bet)
    return bet
```

5. **Pydantic schemas:**
```python
# schemas/bet.py
from pydantic import BaseModel, Field
from decimal import Decimal
from datetime import datetime

class BetCreate(BaseModel):
    title: str = Field(..., min_length=5, max_length=200)
    description: str = Field(..., max_length=2000)
    category: BetCategory
    event_name: str = Field(..., max_length=200)
    event_date: datetime | None = None
    creator_position: str = Field(..., max_length=500)
    opponent_position: str = Field(..., max_length=500)
    stake_amount: Decimal = Field(..., gt=0, le=10000)
    creator_odds: float = Field(default=1.0, gt=0)
    opponent_odds: float = Field(default=1.0, gt=0)
    visibility: BetVisibility = BetVisibility.PUBLIC
    expires_at: datetime | None = None

class BetResponse(BaseModel):
    id: int
    title: str
    description: str
    category: BetCategory
    status: BetStatus
    stake_amount: Decimal
    creator: UserSummary
    opponent: UserSummary | None
    created_at: datetime
    
    model_config = ConfigDict(from_attributes=True)
```

6. **Error handling:**
```python
# utils/exceptions.py
from fastapi import HTTPException

class BetNotFoundError(HTTPException):
    def __init__(self):
        super().__init__(status_code=404, detail="Bet not found")

class InsufficientFundsError(HTTPException):
    def __init__(self):
        super().__init__(status_code=400, detail="Insufficient funds")

class BetNotAvailableError(HTTPException):
    def __init__(self):
        super().__init__(status_code=400, detail="Bet is no longer available")
```

### Frontend (React + Vite)

1. **State management with Zustand:**
```typescript
interface AuthState {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}
```

2. **React Query for server state:**
```typescript
const { data: bets, isLoading } = useQuery({
  queryKey: ['bets', filters],
  queryFn: () => fetchBets(filters),
});
```

3. **WebSocket hook:**
```typescript
const useWebSocket = () => {
  useEffect(() => {
    const ws = new WebSocket(`${WS_URL}?token=${token}`);
    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      // Handle different event types
    };
    return () => ws.close();
  }, [token]);
};
```

4. **Tailwind styling** - Use consistent design system:
   - Primary: Blue (#3B82F6)
   - Success: Green (#10B981)
   - Warning: Yellow (#F59E0B)
   - Error: Red (#EF4444)
   - Dark theme support

---

## Requirements Files

### backend/requirements.txt
```
fastapi==0.109.0
uvicorn[standard]==0.27.0
sqlalchemy[asyncio]==2.0.25
aiosqlite==0.19.0
pydantic==2.5.3
pydantic-settings==2.1.0
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-multipart==0.0.6
websockets==12.0
alembic==1.13.1
httpx==0.26.0
```

### frontend/package.json (dependencies)
```json
{
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.21.0",
    "@tanstack/react-query": "^5.17.0",
    "zustand": "^4.4.7",
    "axios": "^1.6.5",
    "date-fns": "^3.2.0",
    "lucide-react": "^0.303.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.48",
    "@types/react-dom": "^18.2.18",
    "@vitejs/plugin-react": "^4.2.1",
    "autoprefixer": "^10.4.17",
    "postcss": "^8.4.33",
    "tailwindcss": "^3.4.1",
    "typescript": "^5.3.3",
    "vite": "^5.0.12"
  }
}
```

---

## Docker Compose Setup

```yaml
version: '3.8'

services:
  backend:
    build: ./backend
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=sqlite+aiosqlite:///./data/h2h.db
      - JWT_SECRET=${JWT_SECRET:-your-secret-key-change-in-production}
      - JWT_ALGORITHM=HS256
      - ACCESS_TOKEN_EXPIRE_MINUTES=30
    volumes:
      - ./backend:/app
      - sqlite_data:/app/data
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

  frontend:
    build: ./frontend
    ports:
      - "5173:5173"
    environment:
      - VITE_API_URL=http://localhost:8000
      - VITE_WS_URL=ws://localhost:8000
    volumes:
      - ./frontend:/app
      - /app/node_modules
    command: npm run dev -- --host

volumes:
  sqlite_data:
```

---

## Seed Data Script

```python
# backend/seed_data.py
import asyncio
from decimal import Decimal
from datetime import datetime, timedelta
from app.database import async_session, init_db
from app.models import User, Wallet, Bet, BetCategory, BetStatus, BetVisibility
from app.utils.security import get_password_hash

async def seed():
    await init_db()
    
    async with async_session() as db:
        # Create test users
        users = [
            User(
                email="alice@example.com",
                username="alice",
                password_hash=get_password_hash("password123"),
                display_name="Alice Smith"
            ),
            User(
                email="bob@example.com",
                username="bob",
                password_hash=get_password_hash("password123"),
                display_name="Bob Jones"
            ),
            User(
                email="charlie@example.com",
                username="charlie",
                password_hash=get_password_hash("password123"),
                display_name="Charlie Brown"
            ),
        ]
        
        for user in users:
            db.add(user)
        await db.flush()
        
        # Create wallets with starting balance
        for user in users:
            wallet = Wallet(
                user_id=user.id,
                balance=Decimal("1000.00"),
                escrow=Decimal("0.00")
            )
            db.add(wallet)
        
        # Create sample bets
        bets = [
            Bet(
                creator_id=users[0].id,
                title="Super Bowl LVIII Winner",
                description="Bet on who will win Super Bowl LVIII",
                category=BetCategory.SPORTS,
                event_name="Super Bowl LVIII",
                event_date=datetime(2024, 2, 11),
                creator_position="Kansas City Chiefs win",
                opponent_position="San Francisco 49ers win",
                stake_amount=Decimal("100.00"),
                creator_odds=1.5,
                opponent_odds=2.0,
                visibility=BetVisibility.PUBLIC,
                expires_at=datetime.utcnow() + timedelta(days=7)
            ),
            Bet(
                creator_id=users[1].id,
                title="League of Legends Worlds 2024",
                description="Who will win the LoL World Championship?",
                category=BetCategory.ESPORTS,
                event_name="LoL Worlds 2024",
                creator_position="T1 wins",
                opponent_position="Any other team wins",
                stake_amount=Decimal("50.00"),
                creator_odds=1.8,
                opponent_odds=1.8,
                visibility=BetVisibility.PUBLIC,
                expires_at=datetime.utcnow() + timedelta(days=14)
            ),
            Bet(
                creator_id=users[2].id,
                title="Oscar Best Picture 2024",
                description="Which film will win Best Picture at the 2024 Oscars?",
                category=BetCategory.ENTERTAINMENT,
                event_name="96th Academy Awards",
                event_date=datetime(2024, 3, 10),
                creator_position="Oppenheimer wins",
                opponent_position="Any other film wins",
                stake_amount=Decimal("25.00"),
                creator_odds=1.3,
                opponent_odds=3.0,
                visibility=BetVisibility.PUBLIC,
                expires_at=datetime.utcnow() + timedelta(days=30)
            ),
        ]
        
        for bet in bets:
            db.add(bet)
        
        await db.commit()
        print("Seed data created successfully!")

if __name__ == "__main__":
    asyncio.run(seed())
```

---

## MVP Scope Boundaries

### In Scope (Build This)
- User registration/login with JWT
- Virtual wallet with simulated deposits
- Create, browse, accept bets
- Basic bet settlement (winner claims, loser confirms)
- Real-time updates via WebSocket
- Responsive UI (mobile-friendly)

### Out of Scope (Future)
- Real payment processing
- KYC/AML verification
- Blockchain integration
- Odds feed integration
- Complex matching algorithms
- Admin panel
- Email notifications
- Social features (friends, chat)

---

## Migration Path to Production Database

When ready to move from SQLite to PostgreSQL:

1. **Update connection string:**
```python
# Change from:
DATABASE_URL = "sqlite+aiosqlite:///./data/h2h.db"

# To:
DATABASE_URL = "postgresql+asyncpg://user:pass@host:5432/h2h"
```

2. **Update requirements:**
```
# Remove: aiosqlite
# Add: asyncpg
```

3. **Run Alembic migrations** against new database

4. **Update Docker Compose** to include PostgreSQL service

That's it! SQLAlchemy ORM handles the rest.

---

## Getting Started Commands

```bash
# Clone and setup
git clone <repo>
cd h2h-mvp

# Start with Docker
docker-compose up --build

# Or run separately:

# Backend
cd backend
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate
pip install -r requirements.txt
python seed_data.py       # Create test data
uvicorn app.main:app --reload

# Frontend
cd frontend
npm install
npm run dev
```

---

## Success Criteria

The MVP is complete when:

1. ✅ User can register, login, and manage profile
2. ✅ User can deposit virtual funds to wallet
3. ✅ User can create a bet with custom terms
4. ✅ User can browse and search open bets
5. ✅ User can accept another user's bet
6. ✅ Funds are properly escrowed on bet acceptance
7. ✅ Winner can claim victory, loser can confirm
8. ✅ Funds are distributed correctly on settlement
9. ✅ Real-time updates work via WebSocket
10. ✅ UI is clean, responsive, and intuitive

---

## Generate This Now

Please generate the complete MVP codebase following this specification. Start with the backend (FastAPI + SQLAlchemy + SQLite), then the frontend (React + Vite + TypeScript + Tailwind). Include:

1. All files in the project structure
2. Working Docker Compose setup
3. Environment variable examples
4. README with setup instructions
5. Seed data script for testing
6. Alembic migration setup

Focus on clean, production-quality code with proper error handling, validation, and TypeScript types. Use async/await throughout the backend.
