Three submission methods: - POST /api/bets/quickslip — structured bet entry - POST /api/bets/screenshot — stub OCR with confirm flow - POST /api/bets/sync — coming soon stub Full bet lifecycle: - PATCH /api/bets/:id/settle — settle with outcome, recalculates performance - GET /api/bets — list with status/book/pagination filters - GET /api/bets/performance — ROI, win rate, profit (weekly/monthly/all_time) Payout calculator handles straight bets (American odds) and parlays (multiplied leg payouts). Performance service recalculates on each settlement and upserts into performance table. 33 new tests, 221 total (194 Node.js + 27 Python), all passing. All backend features for Phase 1 + Phase 2 now complete. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
10 KiB
Feature 1.5 — Bet Submission
Overview
Three ways for users to log their bets: quick slip (structured form), screenshot upload (OCR extraction), and sportsbook sync (stub for future OAuth integration). Every bet writes to the bets table, optionally links to a scan_session, and can be settled later with an outcome. Settled bets feed the performance table for ROI/win-rate tracking.
Dependencies
- Feature 1.4 — Database Schema (
bets,outcomes,performancetables) - Feature 2.1 — Parlay Scan (
scan_sessionsfor linking)
Auth
All endpoints require Supabase JWT (same requireAuth middleware from Feature 2.1). All tiers can submit bets — no tier gating.
Endpoints
POST /api/bets/quickslip
Structured bet submission. User fills a form or sends structured JSON.
Request body:
{
"legs": [
{
"player": "Nikola Jokic",
"stat_type": "points",
"line": 26.5,
"direction": "over",
"odds": -110
}
],
"amount": 20.00,
"book": "draftkings",
"bet_type": "straight",
"scan_session_id": "uuid-or-null",
"placed_at": "2026-03-21T18:00:00Z"
}
Response (201):
{
"bet_id": "uuid",
"status": "pending",
"amount": 20.00,
"potential_payout": 38.18,
"bet_type": "straight",
"book": "draftkings",
"legs": 1,
"scan_session_id": "uuid-or-null",
"created_at": "2026-03-21T18:05:00Z"
}
POST /api/bets/screenshot
Image upload. Server extracts bet data via OCR, then creates the bet.
Request: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
| image | file | yes | Screenshot image (JPEG, PNG, max 5MB) |
| book | string | yes | Sportsbook the screenshot is from |
| scan_session_id | string | no | Link to a prior scan |
Response (201):
{
"bet_id": "uuid",
"status": "pending",
"extracted": {
"legs": [
{ "player": "Nikola Jokic", "stat_type": "points", "line": 26.5, "direction": "over", "odds": -110 }
],
"amount": 20.00,
"potential_payout": 38.18,
"bet_type": "parlay",
"confidence": 0.85
},
"needs_confirmation": true,
"message": "We extracted this from your screenshot. Confirm or edit before saving."
}
Note: For MVP, the OCR pipeline uses a stub that returns a structured placeholder. Full OCR (Tesseract or Claude Vision) is a Phase 3 enhancement. The endpoint accepts the image, stores the raw file reference, and returns extracted data for user confirmation.
POST /api/bets/screenshot/confirm
User confirms or edits the OCR extraction, then saves the bet.
Request body:
{
"extracted_bet_id": "uuid",
"legs": [
{ "player": "Nikola Jokic", "stat_type": "points", "line": 26.5, "direction": "over", "odds": -110 }
],
"amount": 20.00,
"book": "draftkings",
"bet_type": "parlay",
"scan_session_id": "uuid-or-null"
}
Response (201): Same shape as quickslip response.
POST /api/bets/sync
Stub for future sportsbook OAuth sync. Returns a "coming soon" response.
Response (200):
{
"status": "coming_soon",
"message": "Sportsbook sync is coming soon. Use quick slip or screenshot for now.",
"supported_books": ["draftkings", "fanduel", "betmgm"]
}
PATCH /api/bets/:id/settle
Settle a bet with outcome. Updates the bet status and creates an outcome record for each leg.
Request body:
{
"status": "won",
"leg_outcomes": [
{ "player": "Nikola Jokic", "stat_type": "points", "actual_value": 28.0, "result": "hit" }
]
}
Response (200):
{
"bet_id": "uuid",
"status": "won",
"settled_at": "2026-03-22T06:00:00Z",
"amount": 20.00,
"potential_payout": 38.18,
"profit": 18.18
}
GET /api/bets
List user's bets with optional filters.
Query params:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| status | string | no | all | Filter: pending, won, lost, push, void |
| book | string | no | all | Filter by sportsbook |
| limit | int | no | 20 | Pagination limit (max 100) |
| offset | int | no | 0 | Pagination offset |
Response (200):
{
"bets": [
{
"id": "uuid",
"amount": 20.00,
"potential_payout": 38.18,
"book": "draftkings",
"bet_type": "straight",
"status": "pending",
"slip_data": { "legs": [...], "total_odds": -110 },
"submission_method": "quickslip",
"scan_session_id": null,
"placed_at": "2026-03-21T18:00:00Z",
"settled_at": null
}
],
"total": 15,
"limit": 20,
"offset": 0
}
GET /api/bets/performance
Returns aggregated performance stats. Recalculates from settled bets and upserts into performance table.
Response (200):
{
"weekly": { "roi": 12.5, "win_rate": 60.0, "sample_size": 10, "total_wagered": 200.00, "total_profit": 25.00 },
"monthly": { "roi": 8.3, "win_rate": 55.0, "sample_size": 40, "total_wagered": 800.00, "total_profit": 66.40 },
"all_time": { "roi": 6.1, "win_rate": 52.0, "sample_size": 120, "total_wagered": 2400.00, "total_profit": 146.40 }
}
Error Responses
| Status | When | Body |
|---|---|---|
| 400 | Missing required fields, invalid bet_type, empty legs | { "error": "legs array is required" } |
| 401 | No auth token | { "error": "Authentication required" } |
| 404 | Bet not found or not owned | { "error": "Bet not found" } |
| 413 | Screenshot too large (> 5MB) | { "error": "Image must be under 5MB" } |
| 422 | Invalid settlement (already settled, invalid status) | { "error": "Bet already settled" } |
Payout Calculation
For straight bets:
If odds < 0: payout = amount + (amount / (|odds| / 100))
If odds > 0: payout = amount + (amount * (odds / 100))
For parlays, multiply individual leg payouts:
parlay_multiplier = product of (1 + single_payout_ratio) for each leg
payout = amount * parlay_multiplier
Performance Calculation
Triggered on each bet settlement. Recalculates from all settled bets for the user.
For each period (weekly, monthly, all_time):
- Filter settled bets within the period
- total_wagered = sum of amount for all settled bets
- total_profit = sum of (payout - amount) for won bets - sum of amount for lost bets
- roi = (total_profit / total_wagered) * 100
- win_rate = (won_count / (won_count + lost_count)) * 100
- sample_size = total settled bets
Upsert into performance table (unique index on user_id + period).
Period boundaries:
- weekly: current ISO week (Monday–Sunday)
- monthly: current calendar month
- all_time: all settled bets ever
Scan Session Linking
When a user submits a bet with scan_session_id:
- Validate the session exists and belongs to the user
- Store the reference in
bets.slip_data.scan_session_id - This enables "how many bets followed BetonBLK grades" analytics later
Service Architecture
src/
├── services/
│ ├── betService.js # Create bet, settle bet, list bets
│ ├── payoutCalculator.js # Payout math for straight/parlay/teaser
│ ├── performanceService.js # Recalculate and upsert performance stats
│ └── ocrStub.js # Stub OCR for screenshot extraction (MVP)
├── routes/
│ └── bets.js # All bet endpoints
- betService.js — Core CRUD: create from quickslip/screenshot, settle, list with filters.
- payoutCalculator.js — Pure function. Calculates potential payout from odds + amount + bet type.
- performanceService.js — Queries settled bets, computes ROI/win_rate/profit, upserts performance table.
- ocrStub.js — MVP stub. Returns a "needs_confirmation" response. Real OCR layered in Phase 3.
Acceptance Criteria
POST /api/bets/quickslipcreates a bet with correct slip_data, amount, payout, and status "pending"POST /api/bets/screenshotaccepts image upload and returns extracted data withneeds_confirmation: truePOST /api/bets/screenshot/confirmsaves the confirmed bet to the databasePOST /api/bets/syncreturns "coming_soon" stub responsePATCH /api/bets/:id/settleupdates bet status and creates outcome recordsPATCH /api/bets/:id/settlerejects settling an already-settled bet (422)GET /api/betsreturns user's bets with status/book/limit/offset filtersGET /api/bets/performancereturns ROI, win_rate, sample_size for weekly/monthly/all_time- Payout calculation correct for straight bets (positive and negative odds)
- Payout calculation correct for parlays (multiplied leg payouts)
- Performance recalculated on each settlement
- Scan session linking validated (session must exist and belong to user)
- All endpoints require auth (401 without token)
- Image upload rejects files > 5MB (413)
Test Plan
Unit Tests (payoutCalculator.js)
- Straight bet with negative odds (-110, $20) → correct payout
- Straight bet with positive odds (+150, $20) → correct payout
- Parlay payout with 2 legs → multiplied correctly
- Parlay payout with 3 legs → multiplied correctly
- Edge case: even odds (-100) → doubles the bet
Unit Tests (performanceService.js)
- Calculates ROI correctly from mix of won/lost bets
- Win rate = won / (won + lost) * 100 (excludes push/void)
- Weekly period filters to current week only
- Empty settled bets returns zeroes
- Upserts into performance table (not duplicate rows)
Unit Tests (ocrStub.js)
- Returns structured placeholder with needs_confirmation: true
- Includes book from request
Integration Tests (routes/bets.js)
- Quickslip: create straight bet → 201 with correct payout
- Quickslip: create parlay → 201 with multiplied payout
- Screenshot: upload image → 201 with needs_confirmation
- Screenshot: confirm → 201 with saved bet
- Sync: returns coming_soon stub
- Settle: pending bet → won → 200 with settled_at
- Settle: already settled → 422
- List: returns user's bets with pagination
- List: status filter works
- Performance: returns stats for all 3 periods
- Auth: no token → 401
- Validation: missing legs → 400
Open Questions
- Screenshot storage: For MVP, we don't actually store the image file — the stub extracts nothing and the user manually enters data via the confirm endpoint. When real OCR is added (Phase 3), we'll need file storage (Supabase Storage or S3). No infrastructure needed now.