A web-based prediction game where university students pick winners of NCAA men's college basketball games, earn/lose points based on odds-based scoring (favorites worth less, underdogs worth more), and compete on seasonal leaderboards.
- ✅ .edu authentication - Only university students can sign up
- 🏀 Live game data - Real-time NCAA basketball games from ESPN
- 📊 Odds-based scoring - Log-scaled scoring based on betting odds
- 🏆 Season leaderboards - Compete against other players
- 👤 User profiles - Season points + lifetime points tracking
- ⏰ Pick locking - Picks lock 5 minutes before game time
- 🔄 Auto-scoring - Games scored automatically when final
- Frontend: Next.js 15 (App Router), React 19, TypeScript, Tailwind CSS
- Backend: Next.js API Routes
- Database: Supabase (PostgreSQL)
- Authentication: Supabase Auth
- Data Sources:
- ESPN API (unofficial) for games/scores
- The Odds API for betting lines
- Deployment: Vercel
- Node.js 18+ and npm/yarn/pnpm
- Supabase account (free tier works)
- The Odds API key (free tier: 500 requests/month)
npm install- Create a new project at supabase.com
- Go to SQL Editor and run the migrations in order:
supabase/migrations/001_initial_schema.sqlsupabase/migrations/002_rls_policies.sql
- Get your project URL and keys from Settings > API
- Sign up at the-odds-api.com
- Get your API key from the dashboard
- Free tier includes 500 requests/month
Create a .env.local file:
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# The Odds API
ODDS_API_KEY=your-odds-api-key
# ESPN API (no key needed)
ESPN_API_BASE_URL=https://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball
# Optional: Cron secret for securing cron endpoints
CRON_SECRET=your-random-secret-string
# App URL
NEXT_PUBLIC_APP_URL=http://localhost:3000npm run devTo populate games for testing, manually trigger the cron jobs:
# Sync games from ESPN
curl -X POST http://localhost:3000/api/cron/sync-games
# Fetch odds
curl -X POST http://localhost:3000/api/cron/fetch-odds
# Score finished games
curl -X POST http://localhost:3000/api/cron/score-finalsgit init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin https://github.com/yourusername/basketball-picks.git
git push -u origin main- Go to vercel.com and sign in
- Click Add New Project
- Import your GitHub repository
- Add all environment variables from
.env.local - Deploy!
Vercel will automatically set up the cron jobs defined in vercel.json:
/api/cron/sync-games- Every 15 minutes/api/cron/fetch-odds- Every 20 minutes/api/cron/score-finals- Every 10 minutes
Optional: Restrict cron endpoints by setting CRON_SECRET in Vercel and sending it as Authorization: Bearer {CRON_SECRET} header.
This app uses log-scaled odds-based scoring to make picking underdogs more rewarding while keeping scores balanced.
-
Convert odds to implied probability:
- Negative odds (favorite):
p = |odds| / (|odds| + 100) - Positive odds (underdog):
p = 100 / (odds + 100)
- Negative odds (favorite):
-
Calculate points for correct pick:
points = round(A * ln(1/p) + B)Where:
A = 80(multiplier)B = 20(base points)ln= natural logarithm
-
Calculate points for incorrect pick:
points = -round(correct_points * LOSS_MULT)Where
LOSS_MULT = 0.7
| Scenario | Odds | Probability | Correct | Incorrect |
|---|---|---|---|---|
| Heavy favorite | -300 | 75% | +43 pts | -30 pts |
| Moderate favorite | -150 | 60% | +61 pts | -43 pts |
| Even odds | 0 | 50% | +75 pts | -53 pts |
| Moderate underdog | +150 | 40% | +93 pts | -65 pts |
| Heavy underdog | +300 | 25% | +131 pts | -92 pts |
Key points:
- Picking favorites correctly = lower reward
- Picking underdogs correctly = higher reward
- Losing hurts, but less than winning helps (70% penalty)
- Logarithmic scaling prevents extreme outliers
- users - User profiles (.edu validated)
- seasons - Basketball seasons (e.g., 2026)
- games - NCAA games with scores and odds
- picks - User predictions
- user_season_stats - Fast leaderboard queries
- user_lifetime_stats - All-time points
See supabase/migrations/001_initial_schema.sql for full schema.
GET /api/games- Fetch games with user picksGET /api/leaderboard- Season leaderboardGET /api/users/[username]- User profile
POST /api/picks- Create/update pickGET /api/picks- Get user's picks
POST /api/cron/sync-games- Sync from ESPNPOST /api/cron/fetch-odds- Fetch from Odds APIPOST /api/cron/score-finals- Score finished games
hackathon2026/
├── src/
│ ├── app/ # Next.js pages (App Router)
│ │ ├── api/ # API routes
│ │ │ ├── auth/ # Auth callback
│ │ │ ├── games/ # Games endpoint
│ │ │ ├── picks/ # Picks endpoint
│ │ │ ├── leaderboard/ # Leaderboard endpoint
│ │ │ ├── users/ # User profiles
│ │ │ └── cron/ # Background jobs
│ │ ├── dashboard/ # Main game picking UI
│ │ ├── leaderboard/ # Leaderboard page
│ │ ├── profile/ # User profiles
│ │ └── login/ # Auth page
│ ├── lib/ # Core logic
│ │ ├── supabase/ # Supabase clients
│ │ ├── espn.ts # ESPN API integration
│ │ ├── odds.ts # Odds API integration
│ │ └── scoring.ts # Scoring engine
│ ├── components/ # React components
│ └── types/ # TypeScript types
├── supabase/
│ └── migrations/ # SQL migrations
├── package.json
├── tsconfig.json
├── tailwind.config.ts
└── vercel.json # Deployment config
-
Sign Up (30 sec)
- Go to
/login - Sign up with a
.eduemail - Show validation working
- Go to
-
Browse Games (1 min)
- Dashboard shows upcoming NCAA games
- Filter by upcoming/live/final
- Show odds displayed for each game
-
Make Picks (1 min)
- Click to pick home or away team
- Show pick confirmation
- Try to edit pick before lock time
- Show that picks lock 5 min before game
-
View Leaderboard (1 min)
- Navigate to Leaderboard
- Show rankings by season points
- Show win/loss records and accuracy
-
Profile (1 min)
- Click on your username
- Show season stats vs lifetime stats
- Show recent picks history
-
Scoring Demo (30 sec)
- If a game is final, show how points were awarded
- Explain underdog = more points
- Show both correct and incorrect picks
# Manually trigger game sync
curl -X POST http://localhost:3000/api/cron/sync-games
# Manually trigger odds fetch
curl -X POST http://localhost:3000/api/cron/fetch-odds
# Manually score games
curl -X POST http://localhost:3000/api/cron/score-finals- Client-side: Input validation on signup form
- Server-side: Enforced in signup API (mandatory)
- Email must end with
.eduto create account
- Picks lock 5 minutes before game start time
- Lock time enforced server-side in
POST /api/picks - UI shows "Picks locked" message
- Scoring only processes picks where
points_awarded IS NULL - Safe to run scoring cron multiple times
- Prevents double-awarding points
- Unofficial API - endpoints may change
- No API key required
- Returns today's games by default
- Can specify date range:
?dates=20260301,20260302
- Official API with key
- Free tier: 500 requests/month
- Caches for 5 minutes to reduce API calls
- Fuzzy team name matching (handles variations)
# Check if games are in database
# Run in Supabase SQL Editor:
SELECT * FROM games ORDER BY start_time DESC LIMIT 10;
# If empty, manually trigger sync:
curl -X POST http://localhost:3000/api/cron/sync-games# Verify ODDS_API_KEY is set
echo $ODDS_API_KEY
# Manually trigger odds fetch:
curl -X POST http://localhost:3000/api/cron/fetch-odds
# Check API quota at the-odds-api.com dashboard# Verify game is marked as final:
SELECT * FROM games WHERE status = 'final' AND home_score IS NOT NULL;
# Trigger scoring:
curl -X POST http://localhost:3000/api/cron/score-finals
# Check for unscored picks:
SELECT * FROM picks WHERE points_awarded IS NULL;Make sure you ran BOTH migrations:
001_initial_schema.sql(creates tables)002_rls_policies.sql(enables RLS and creates policies)
- Friends system (send/accept requests)
- Friends-only leaderboard view
- Group/college leaderboards
- Push notifications for game starts
- Historical season archives
- Bracket challenges (March Madness)
- Multi-sport support
MIT
Built for Hackathon 2026. Uses data from ESPN (unofficial API) and The Odds API.