Value Bets V2
The full reference for /api/value-bets-v2 — every query parameter, the response envelope, and what each field means.
The full reference for the Value Bets V2 endpoint — every query parameter, the
response envelope, and the meaning of each field. It is served by
api with no authentication and permissive CORS, which is
what makes it usable directly from a bot or a spreadsheet.
Use V2, not /api/value-bets
/api/value-bets is the older endpoint and is not what the site itself reads.
Integrations should call /api/value-bets-v2.
The generated endpoint reference describes the same operation from the Go handler's own types. This page is the prose version: it explains what the fields mean, which the generated one cannot.
Endpoint
GET {BASE_URL}/api/value-bets-v2- Base URL: The production Statshub domain
- Methods:
GET,OPTIONS(preflight) - Auth: None. Fully open.
- CORS:
Access-Control-Allow-Origin: * - Rate limiting: None
Query Parameters
All parameters are optional. With no parameters, the API returns page 1 of all upcoming value bets with positive over edge, sorted by earliest match first then highest edge.
Pagination
| Parameter | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number (min 1). |
perPage | number | 25 | Groups per page. Min 1, max 100. |
Search
| Parameter | Type | Default | Description |
|---|---|---|---|
search | string | — | Case-insensitive search on player name OR team name. Partial match (contains). |
Filters
| Parameter | Type | Default | Description |
|---|---|---|---|
marketType | string | — | Market type key (e.g. "goals", "onTargetScoringAttempt"). See Market Types table below. |
line | number | — | Betting line to filter by (e.g. 0.5, 1.5, 2.5). |
eventIds | string (comma-separated) | — | Specific match/fixture IDs. Single: "12345678". Multiple: "12345678,12345679". |
uniqueTournamentIds | string (comma-separated) | — | League/tournament IDs. Single: "17". Multiple: "17,35,8". |
minOdds | number | 0 | Minimum over odds value (decimal odds). |
maxOdds | number | 0 (no max) | Maximum over odds value (decimal odds). |
source | string (comma-separated) | — | Filter by bookmaker name(s). Single: "Bet365". Multiple: "Bet365,Paddy Power". |
Edge Filters
| Parameter | Type | Default | Description |
|---|---|---|---|
minEV | number | 0 | Minimum over edge percentage. Default only shows > 0% (positive edge). Set to 10 to show only 10%+ edge bets. |
maxEV | number | — | Maximum over edge percentage. |
minSources | number | 0 | Minimum number of bookmakers that must have +EV odds (bookmaker overOdds > at least one model's fairOverOdds). E.g. minSources=2 means the bet must be +EV on at least 2 bookmakers. |
Date Filters
| Parameter | Type | Default | Description |
|---|---|---|---|
date | string | — | Preset: "today", "tomorrow", "this-week", "next-week". If omitted (and no specificDate), defaults to upcoming only. |
specificDate | string (YYYY-MM-DD, comma-separated) | — | Specific date(s). Takes priority over date. Single: "2025-11-15". Multiple: "2025-11-15,2025-11-16". |
Date priority: specificDate > date preset > default (upcoming matches only)
Activity Filters
| Parameter | Type | Default | Description |
|---|---|---|---|
maxLastGameWithTeam | number | — | Maximum number of team games since the player last played. 0 = played in the most recent game. 2 = played within the last 3 team games. Filters out players who haven't featured recently (e.g. injured/dropped). |
inPredictedLineup | "true" | — | When "true", only returns players who are in the predicted lineup for their upcoming match. Uses the predicted_lineups table. |
Sorting
| Parameter | Type | Default | Description |
|---|---|---|---|
sortBy | string | "overEdgePercent" | "overEdgePercent" or "underEdgePercent". |
sortOrder | "asc" or "desc" | "desc" | Sort direction for edge. |
Default sort: By highest average edge first (AVG(edge) DESC), then by earliest match date (MIN(matchDate) ASC). This means the highest EV bets always appear first regardless of match time, with match date as a tiebreaker.
Super Sub
| Parameter | Type | Default | Description |
|---|---|---|---|
superSub | "true" | — | When "true", stat values combine the starter's stats with the replacement sub's stats for a "full 90" view. |
Built-in Filters (always applied, cannot be changed)
- Only positive over edge (overEdgePercent > 0, unless
minEVis set higher) - NaN edge values excluded
- Default to upcoming matches only (unless
dateorspecificDateoverrides)
Response Structure
{
"data": [/* array of ValueBetGroup objects */],
"pagination": {
"currentPage": 1,
"totalPages": 12,
"totalCount": 291,
"hasNextPage": true,
"hasPreviousPage": false
}
}totalCount— Total number of distinct groups matching filters- Default 25 groups per page (configurable via
perPage, max 100)
ValueBetGroup Object
Each item in the data array is a grouped card representing one unique combination of (playerId, marketType, line, matchId). Multiple bookmakers and models are aggregated into a single object.
{
"groupKey": "12345_goals_0.5_67890",
"playerId": 12345,
"playerName": "Mohamed Salah",
"teamId": 44,
"teamName": "Liverpool",
"matchId": 67890,
"matchDate": 1708531200,
"marketType": "goals",
"line": 0.5,
"matchSlug": "liverpool-manchester-city",
"homeTeamId": 44,
"homeTeamName": "Liverpool",
"awayTeamId": 17,
"awayTeamName": "Manchester City",
"models": [
{
"modelName": "poisson_v2",
"fairOverOdds": 1.65,
"fairUnderOdds": 2.28,
"opponentMultiplier": "opponent_team_stat_vs_league_avg"
},
{
"modelName": "poisson_v3",
"fairOverOdds": 1.72,
"fairUnderOdds": 2.15,
"opponentMultiplier": "1x2_multiplier"
}
],
"bookmakers": [
{
"source": "Bet365",
"overOdds": 1.83,
"underOdds": 1.91
},
{
"source": "Paddy Power",
"overOdds": 1.80,
"underOdds": 1.95
}
],
"avgOverEdge": 8.1,
"bestOverOdds": 1.83,
"matchOdds": {
"home": 2.10,
"draw": 3.40,
"away": 3.20
},
"last30Stats": [1, 0, 2, 0, 1, 3, 0, 1, 0, 2, 1, 0, 0, 1, 2, 0, 1, 0, 0, 1, 2, 1, 0, 1, 0, 0, 1, 2, 0, 1],
"sampleSize": 30,
"hitRates": {
"l10": 70,
"l20": 65,
"l30": 60
},
"recentGames": [
{
"stat": 1,
"minutesPlayed": 90,
"position": "ST",
"isHome": true,
"opponentId": 17,
"opponentName": "Manchester City",
"uniqueTournamentId": 17,
"hasSuperSub": false
}
],
"activityInfo": {
"startedLastGame": true,
"lastGameWithTeam": 0,
"inPredictedLineup": true
}
}Field Reference
| Field | Type | Description |
|---|---|---|
groupKey | string | "{playerId}_{marketType}_{line}_{matchId}" |
playerId | number | Player's unique ID |
playerName | string | Player's display name |
teamId | number | Player's team ID |
teamName | string | Player's team name |
matchId | number | Match/fixture ID |
matchDate | number | Unix timestamp in seconds — when the match starts |
marketType | string | Market type key (see table below) |
line | number | The betting line (e.g. 0.5, 1.5, 2.5) |
matchSlug | string|null | URL-friendly match identifier |
homeTeamId | number|null | Home team ID |
homeTeamName | string|null | Home team name |
awayTeamId | number|null | Away team ID |
awayTeamName | string|null | Away team name |
models | array | All statistical models for this group (see Models section) |
bookmakers | array | All bookmakers with odds, sorted by overOdds descending (best first) |
avgOverEdge | number|null | Average over edge % across all model×bookmaker combos |
bestOverOdds | number|null | Highest over odds from any bookmaker |
matchOdds | object|null | 1x2 moneyline odds from Bet365 (see below) |
last30Stats | number[] | Last 30 stat values (most recent first), starters only |
sampleSize | number | Total number of starter games available |
hitRates | object | Hit rate percentages for L10, L20, L30 windows |
recentGames | array | Last 30 games with context (see below) |
activityInfo | object|null | Player activity info: predicted lineup status, last game appearance (see below) |
Models Array
Each group can have 1 to 6+ models. Each model represents a different statistical approach for calculating fair odds.
{
"modelName": "poisson_v2",
"fairOverOdds": 1.65,
"fairUnderOdds": 2.28,
"opponentMultiplier": "opponent_team_stat_vs_league_avg"
}| Field | Type | Description |
|---|---|---|
modelName | string | Name of the statistical model |
fairOverOdds | number|null | Model's calculated fair odds for over the line |
fairUnderOdds | number|null | Model's calculated fair odds for under the line |
opponentMultiplier | string|null | Which opponent adjustment was used: "opponent_team_stat_vs_league_avg", "1x2_multiplier", or "none" |
Bookmakers Array
Sorted by overOdds descending (best odds first).
{
"source": "Bet365",
"overOdds": 1.83,
"underOdds": 1.91
}Match Odds
1x2 moneyline odds from Bet365 only (not averaged across bookmakers like v1).
{
"home": 2.10,
"draw": 3.40,
"away": 3.20
}Can be null if no Bet365 odds are available.
Hit Rates
{
"l10": 70,
"l20": 65,
"l30": 60
}| Field | Type | Description |
|---|---|---|
l10 | number|null | % of last 10 starter games where stat >= line. Integer 0-100. |
l20 | number|null | % of last 20 starter games where stat >= line |
l30 | number|null | % of last 30 starter games where stat >= line |
Note: V2 uses L10/L20/L30 windows (not L10/L20/L40 like v1).
Returns null for a window if zero starter games exist for that window size.
Recent Games Array
Up to 30 most recent games (starters only), most recent first.
{
"stat": 2,
"minutesPlayed": 90,
"position": "ST",
"isHome": true,
"opponentId": 17,
"opponentName": "Manchester City",
"uniqueTournamentId": 17,
"hasSuperSub": false
}| Field | Type | Description |
|---|---|---|
stat | number | The stat value for this market type in this game. When superSub=true, includes the replacement sub's stats added on top. |
minutesPlayed | number | Minutes played in this game |
position | string|null | Position played (e.g. "ST", "CM", "GK") |
isHome | boolean | Whether the player's team was the home team |
opponentId | number | Opponent team ID |
opponentName | string | Opponent team name |
uniqueTournamentId | number|null | League/tournament ID for this game |
hasSuperSub | boolean | Whether this game's stat includes added sub stats (only when superSub=true) |
Important: V2 only fetches starters (SQL filter: substituted_out IS NULL, meaning the player started the game). This is different from v1 which fetches all games then filters client-side.
Activity Info
{
"startedLastGame": true,
"lastGameWithTeam": 0,
"inPredictedLineup": true
}| Field | Type | Description |
|---|---|---|
startedLastGame | boolean | Whether the player started (45+ mins) in their team's most recent match |
lastGameWithTeam | number|null | Number of team games ago the player last appeared (45+ mins). 0 = played in the last team game. 2 = 2 team games ago. null = no recent appearance found. |
inPredictedLineup | boolean | Whether the player is in the predicted lineup for this specific match (from predicted_lineups table) |
Display guidance:
inPredictedLineup: true→ Show "Predicted" badge (purple)startedLastGame: true→ Show "Started Last" badge (green)lastGameWithTeam: 0→ "Played Last" (blue)lastGameWithTeam: 1-2→ "X+ Games Ago" (amber)lastGameWithTeam: 3+→ "X+ Games Ago" (red — player hasn't featured recently)null→ No appearance data available
Can be null if activity data couldn't be fetched for this player.
Key Differences from V1 API
| Feature | V1 (/api/value-bets) | V2 (/api/value-bets-v2) |
|---|---|---|
| Hit rate windows | L10, L20, L40 | L10, L20, L30 |
| Stats scope | All games (minutesPlayed > 0), filtered to starters client-side | Starters only at SQL level (substituted_out IS NULL) |
| Game history | 40 games | 30 games |
| Match odds source | Averaged across all bookmakers | Bet365 only |
| Default sort | Edge DESC only | Edge DESC, then match date ASC (highest EV first) |
| Date filters | today/tomorrow/this-week/next-week/specificDate | Same (today/tomorrow/this-week/next-week/specificDate, default upcoming) |
| Activity info | startedLastGame, inPredictedLineup, lastGameWithTeam | Same (activityInfo object + maxLastGameWithTeam and inPredictedLineup filters) |
last30Stats array | Not included (only recentStats per-field arrays) | Included (pre-computed stat values for the market) |
recentGames[].stat | Not included (raw stat fields sent separately) | Included (pre-computed stat for this market) |
| Super Sub | Client-side toggle using substitutedPlayerStats | Server-side via superSub=true query param |
| Event ID param name | eventId | eventIds |
| Tournament ID param name | uniqueTournamentId | uniqueTournamentIds |
| Odds param names | minOdd / maxOdd | minOdds / maxOdds |
Market Types
| marketType key | Display Name | What it counts |
|---|---|---|
onTargetScoringAttempt | Shots on Target | shotsOnTarget |
shots | Shots | shotsOnTarget + shotsOffTarget + blockedShots |
goals | Goals | goals |
totalTackle | Tackles | tackles |
fouls | Fouls Committed | fouls |
wasFouled | Fouls Won | wasFouled |
totalPass | Passes | totalPasses |
yellowCard | Yellow Cards | yellowCards |
Note: V2 supports fewer market types than V1. The above are the ones with display labels in the frontend. Other market types may exist in the database but won't have a human-readable label.
Edge Calculation
Edge is calculated client-side (not in the API response per model×bookmaker combo). The formula:
edge = ((bookmakerOdds / modelFairOdds) - 1) * 100For each bookmaker × model combination:
- Take the bookmaker's
overOddsand the model'sfairOverOdds - Positive edge = bookmaker is offering better odds than the model thinks is fair
The API provides:
avgOverEdge— server-calculated average edge across all combos (used for the badge)bestOverOdds— highest bookmaker odds (to quickly identify the best bookmaker)
The bot should calculate per-model edge:
For each model in group.models:
overEdge = ((bestOverOdds / model.fairOverOdds) - 1) * 100The "best model" (most confident) is the one with the lowest fairOverOdds — it thinks the true probability of the over hitting is highest, so it generates the biggest edge when compared to bookmaker odds.
Hit Rate Calculation
Hit rates are computed server-side for starters only:
For each window size (10, 20, 30):
1. Take the first `window` stat values from last30Stats (most recent first)
2. Hit rate = count(stat >= line) / total * 100, rounded to nearest integer- Only starter games are included (substituted_out IS NULL in the SQL query)
- Returns
nullif no games exist for that window sampleSize= total starter games available (up to 30)
Super Sub Feature
When superSub=true is passed:
- The API fetches starters' last 30 games as normal
- For each game where the starter was subbed off (
substitutedIn != null), it fetches the replacement player's stats - The replacement's stats are added to the starter's stats for that game
recentGames[].hasSuperSubistruefor games where sub stats were combined- Hit rates and
last30Statsvalues reflect the combined stats
This gives a "full 90-minute" view: if a starter played 60 mins with 1 shot on target, and their replacement played 30 mins with 2 shots on target, the combined stat = 3.
Filter Options
The V2 page fetches filter options from the V1 API:
GET /api/value-bets?action=filter-optionsResponse:
{
"lines": [0.5, 1.5, 2.5, 3.5],
"marketTypes": ["assists", "goals", "onTargetScoringAttempt", "totalTackle", ...],
"sources": ["Bet365", "Kambi", "Paddy Power", ...],
"opponentMultipliers": ["opponent_team_stat_vs_league_avg", "1x2_multiplier", "none"],
"events": [{"id": 12345678, "label": "Arsenal vs Chelsea", "timeStartTimestamp": 1708531200}],
"tournaments": [{"id": 17, "name": "Premier League"}]
}The bot can use this to discover available lines, market types, and upcoming tournaments/events.
Timestamps
All timestamps are Unix timestamps in seconds (not milliseconds).
matchDate— When the match kicks off
To convert to a JavaScript Date: new Date(timestamp * 1000)
Error Responses
// 500 - Server error
{ "error": "Internal server error" }
// 405 - Wrong HTTP method
{ "error": "Method not allowed" }How the Frontend Displays This Data
Each group from the API becomes a card on the site. Understanding the display helps decide what to show in the bot.
Card Layout
┌──────────────────────────────────────────────────────────────┐
│ [Photo] MOHAMED SALAH [+8.1% Avg EV] │
│ Liverpool │
├──────────────────────────────────────────────────────────────┤
│ [LIV logo] Liverpool vs [MCI logo] Man City │
│ Today 3:00 PM (2h 15m) │
│ [Shots on Target] [Line 0.5] │
│ │
│ 1x2 H 2.10 D 3.40 A 3.20 │
│ ^^^^^ (player's team bolded) │
├──────────────────────────────────────────────────────────────┤
│ Hit Rate L10: 70% (10g) L20: 65% (20g) L30: 60% (30g) │
│ │
│ [Game Strip — 30 mini cards, each showing:] │
│ ┌────┐ ┌────┐ ┌────┐ │
│ │🏆 H│ │🏆 A│ │🏆 H│ ← league icon + H/A badge │
│ │ 2 │ │ 0 │ │ 1 │ ← stat value (green=hit, red=miss) │
│ │90'S│ │85'C│ │90'S│ ← minutes + position │
│ │MCI │ │ARS │ │NEW │ ← opponent abbreviation │
│ └────┘ └────┘ └────┘ │
│ (green bottom border = hit, red = miss) │
│ (teal dot on card = super sub stats included) │
├──────────────────────────────────────────────────────────────┤
│ Bookmaker × Model Edge Table: │
│ │
│ Bookmaker │ Over │ Under │ poisson_v2 │ poisson_v3 │
│ ───────────┼───────┼───────┼───────────────┼────────────────│
│ Bet365 Best│ 1.83 │ 1.91 │ +10.9% │ +6.4% │
│ │ │ │ U: -3.2% │ U: +1.2% │
│ ▼ 2 more bookmakers │
│ Paddy Power│ 1.80 │ 1.95 │ +9.1% │ +4.7% │
│ Sky Bet │ 1.78 │ 1.97 │ +7.9% │ +3.5% │
└──────────────────────────────────────────────────────────────┘Key Display Logic
Edge Badge (top-right): Shows avgOverEdge with "Avg EV" label. Green colored.
Match Row: Home vs Away with team logos. Player's team gets a subtle green ring highlight. Match date shown as relative countdown (Today 3:00 PM (2h 15m), Tomorrow 7:45 PM, Saturday 15th Mar, 3:00 PM).
Market Pills: Gray rounded badges showing market type label + line.
1x2 Odds: Player's team odds shown in bold white, other teams in muted gray.
Hit Rate Badges: Color-coded:
- Green (>= 60%)
- Yellow (40-59%)
- Red (< 40%)
- Shows sample size: "L10: 70% (10g)"
Game Strip: Horizontal scrollable row of mini-cards, each showing:
- League icon + Home/Away badge
- Stat value with green bg (hit: stat >= line) or red bg (miss: stat < line)
- Minutes played + position code
- Opponent team logo + 3-letter abbreviation
- Green/red bottom border for quick hit/miss scanning
- Teal dot indicator when super sub stats are included
Bookmaker × Model Table:
- By default shows only the best bookmaker (highest over odds, marked "Best")
- Expandable to show all bookmakers
- Each model gets a column showing the edge % for that bookmaker×model combo
- Green = positive edge, Red = negative edge
- Model column headers show fair odds:
O: 1.65 / U: 2.28 - Under edge shown as smaller text below over edge when under odds exist
Example Requests
Get all upcoming value bets (default)
GET /api/value-bets-v2Search for a player
GET /api/value-bets-v2?search=salahGet goals market only, page 2
GET /api/value-bets-v2?marketType=goals&page=2Get bets for a specific match
GET /api/value-bets-v2?eventIds=12345678Get Premier League bets with odds between 1.5 and 3.0
GET /api/value-bets-v2?uniqueTournamentIds=17&minOdds=1.5&maxOdds=3.0Get shots on target over 0.5 with super sub enabled
GET /api/value-bets-v2?marketType=onTargetScoringAttempt&line=0.5&superSub=trueGet 10 bets per page (for Telegram's 4096 char limit)
GET /api/value-bets-v2?perPage=10Get high-edge bets only (10%+ edge)
GET /api/value-bets-v2?minEV=10Get bets +EV on at least 2 bookmakers
GET /api/value-bets-v2?minSources=2Get Bet365-only bets with 5%+ edge
GET /api/value-bets-v2?source=Bet365&minEV=5Get today's bets only
GET /api/value-bets-v2?date=todayGet bets for specific dates
GET /api/value-bets-v2?specificDate=2025-03-15,2025-03-16Get bets where player played within last 2 team games
GET /api/value-bets-v2?maxLastGameWithTeam=2Get only players in predicted lineups
GET /api/value-bets-v2?inPredictedLineup=trueCombined: Today's high-value Paddy Power bets, active players only
GET /api/value-bets-v2?date=today&source=Paddy Power&minEV=5&maxLastGameWithTeam=1&perPage=10Combined: Predicted lineup players with 5%+ edge
GET /api/value-bets-v2?inPredictedLineup=true&minEV=5&perPage=10Get filter options (uses v1 endpoint)
GET /api/value-bets?action=filter-optionsTelegram Bot Integration Flow
- On startup / periodically: Call
/api/value-bets?action=filter-optionsto get available leagues, markets, lines - User wants today's bets: Call
/api/value-bets-v2?date=today&perPage=10 - User searches: Call
/api/value-bets-v2?search=salah&perPage=10 - User wants a specific league: Call
/api/value-bets-v2?uniqueTournamentIds=17&perPage=10 - User wants a specific market: Call
/api/value-bets-v2?marketType=goals&line=0.5&perPage=10 - User wants high-value bets: Call
/api/value-bets-v2?minEV=10&perPage=10 - User wants a specific bookmaker: Call
/api/value-bets-v2?source=Paddy Power&perPage=10 - User wants reliable bets: Call
/api/value-bets-v2?minSources=2&maxLastGameWithTeam=1&perPage=10 - Paginate: Use
?page=2,?page=3, etc. Checkpagination.hasNextPage
Formatting a Bot Message
For each bet, a good Telegram message format:
⚽ MOHAMED SALAH (Liverpool)
📊 Goals Over 0.5 — +8.1% Avg EV
🏟️ Liverpool vs Man City — Today 3:00 PM
💰 Best: Bet365 @ 1.83
📈 Models:
poisson_v2: Fair 1.65 → +10.9% edge
poisson_v3: Fair 1.72 → +6.4% edge
🎯 Hit Rate: L10: 70% | L20: 65% | L30: 60%
📋 Sample: 30 starter games
1x2: H 2.10 | D 3.40 | A 3.20Key fields for bot messages:
playerName+teamName— WhomarketType+line— What bet (use MARKET_TYPE_LABELS to get display name)avgOverEdge— Headline edge %homeTeamNamevsawayTeamName— The matchmatchDate— When (convert from unix seconds)bestOverOdds+bookmakers[0].source— Best odds + which bookmaker (bookmakers are pre-sorted)models[]— Each model's name + fairOverOdds, calculate edge per modelhitRates.l10/l20/l30— Historical hit ratessampleSize— How many games the hit rate is based onmatchOdds— 1x2 for context (which team is favored)teamIdvshomeTeamId— Determine if player's team is home or away
Market Type Display Labels
onTargetScoringAttempt → "Shots on Target"
shots → "Shots"
goals → "Goals"
totalTackle → "Tackles"
fouls → "Fouls Committed"
wasFouled → "Fouls Won"
totalPass → "Passes"
yellowCard → "Yellow Cards"
