API

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

ParameterTypeDefaultDescription
pagenumber1Page number (min 1).
perPagenumber25Groups per page. Min 1, max 100.
ParameterTypeDefaultDescription
searchstringCase-insensitive search on player name OR team name. Partial match (contains).

Filters

ParameterTypeDefaultDescription
marketTypestringMarket type key (e.g. "goals", "onTargetScoringAttempt"). See Market Types table below.
linenumberBetting line to filter by (e.g. 0.5, 1.5, 2.5).
eventIdsstring (comma-separated)Specific match/fixture IDs. Single: "12345678". Multiple: "12345678,12345679".
uniqueTournamentIdsstring (comma-separated)League/tournament IDs. Single: "17". Multiple: "17,35,8".
minOddsnumber0Minimum over odds value (decimal odds).
maxOddsnumber0 (no max)Maximum over odds value (decimal odds).
sourcestring (comma-separated)Filter by bookmaker name(s). Single: "Bet365". Multiple: "Bet365,Paddy Power".

Edge Filters

ParameterTypeDefaultDescription
minEVnumber0Minimum over edge percentage. Default only shows > 0% (positive edge). Set to 10 to show only 10%+ edge bets.
maxEVnumberMaximum over edge percentage.
minSourcesnumber0Minimum 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

ParameterTypeDefaultDescription
datestringPreset: "today", "tomorrow", "this-week", "next-week". If omitted (and no specificDate), defaults to upcoming only.
specificDatestring (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

ParameterTypeDefaultDescription
maxLastGameWithTeamnumberMaximum 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

ParameterTypeDefaultDescription
sortBystring"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

ParameterTypeDefaultDescription
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 minEV is set higher)
  • NaN edge values excluded
  • Default to upcoming matches only (unless date or specificDate overrides)

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

FieldTypeDescription
groupKeystring"{playerId}_{marketType}_{line}_{matchId}"
playerIdnumberPlayer's unique ID
playerNamestringPlayer's display name
teamIdnumberPlayer's team ID
teamNamestringPlayer's team name
matchIdnumberMatch/fixture ID
matchDatenumberUnix timestamp in seconds — when the match starts
marketTypestringMarket type key (see table below)
linenumberThe betting line (e.g. 0.5, 1.5, 2.5)
matchSlugstring|nullURL-friendly match identifier
homeTeamIdnumber|nullHome team ID
homeTeamNamestring|nullHome team name
awayTeamIdnumber|nullAway team ID
awayTeamNamestring|nullAway team name
modelsarrayAll statistical models for this group (see Models section)
bookmakersarrayAll bookmakers with odds, sorted by overOdds descending (best first)
avgOverEdgenumber|nullAverage over edge % across all model×bookmaker combos
bestOverOddsnumber|nullHighest over odds from any bookmaker
matchOddsobject|null1x2 moneyline odds from Bet365 (see below)
last30Statsnumber[]Last 30 stat values (most recent first), starters only
sampleSizenumberTotal number of starter games available
hitRatesobjectHit rate percentages for L10, L20, L30 windows
recentGamesarrayLast 30 games with context (see below)
activityInfoobject|nullPlayer 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"
}
FieldTypeDescription
modelNamestringName of the statistical model
fairOverOddsnumber|nullModel's calculated fair odds for over the line
fairUnderOddsnumber|nullModel's calculated fair odds for under the line
opponentMultiplierstring|nullWhich 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
}
FieldTypeDescription
l10number|null% of last 10 starter games where stat >= line. Integer 0-100.
l20number|null% of last 20 starter games where stat >= line
l30number|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
}
FieldTypeDescription
statnumberThe stat value for this market type in this game. When superSub=true, includes the replacement sub's stats added on top.
minutesPlayednumberMinutes played in this game
positionstring|nullPosition played (e.g. "ST", "CM", "GK")
isHomebooleanWhether the player's team was the home team
opponentIdnumberOpponent team ID
opponentNamestringOpponent team name
uniqueTournamentIdnumber|nullLeague/tournament ID for this game
hasSuperSubbooleanWhether 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
}
FieldTypeDescription
startedLastGamebooleanWhether the player started (45+ mins) in their team's most recent match
lastGameWithTeamnumber|nullNumber 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.
inPredictedLineupbooleanWhether 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

FeatureV1 (/api/value-bets)V2 (/api/value-bets-v2)
Hit rate windowsL10, L20, L40L10, L20, L30
Stats scopeAll games (minutesPlayed > 0), filtered to starters client-sideStarters only at SQL level (substituted_out IS NULL)
Game history40 games30 games
Match odds sourceAveraged across all bookmakersBet365 only
Default sortEdge DESC onlyEdge DESC, then match date ASC (highest EV first)
Date filterstoday/tomorrow/this-week/next-week/specificDateSame (today/tomorrow/this-week/next-week/specificDate, default upcoming)
Activity infostartedLastGame, inPredictedLineup, lastGameWithTeamSame (activityInfo object + maxLastGameWithTeam and inPredictedLineup filters)
last30Stats arrayNot included (only recentStats per-field arrays)Included (pre-computed stat values for the market)
recentGames[].statNot included (raw stat fields sent separately)Included (pre-computed stat for this market)
Super SubClient-side toggle using substitutedPlayerStatsServer-side via superSub=true query param
Event ID param nameeventIdeventIds
Tournament ID param nameuniqueTournamentIduniqueTournamentIds
Odds param namesminOdd / maxOddminOdds / maxOdds

Market Types

marketType keyDisplay NameWhat it counts
onTargetScoringAttemptShots on TargetshotsOnTarget
shotsShotsshotsOnTarget + shotsOffTarget + blockedShots
goalsGoalsgoals
totalTackleTacklestackles
foulsFouls Committedfouls
wasFouledFouls WonwasFouled
totalPassPassestotalPasses
yellowCardYellow CardsyellowCards

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) * 100

For each bookmaker × model combination:

  • Take the bookmaker's overOdds and the model's fairOverOdds
  • 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) * 100

The "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 null if no games exist for that window
  • sampleSize = total starter games available (up to 30)

Super Sub Feature

When superSub=true is passed:

  1. The API fetches starters' last 30 games as normal
  2. For each game where the starter was subbed off (substitutedIn != null), it fetches the replacement player's stats
  3. The replacement's stats are added to the starter's stats for that game
  4. recentGames[].hasSuperSub is true for games where sub stats were combined
  5. Hit rates and last30Stats values 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-options

Response:

{
  "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-v2

Search for a player

GET /api/value-bets-v2?search=salah

Get goals market only, page 2

GET /api/value-bets-v2?marketType=goals&page=2

Get bets for a specific match

GET /api/value-bets-v2?eventIds=12345678

Get Premier League bets with odds between 1.5 and 3.0

GET /api/value-bets-v2?uniqueTournamentIds=17&minOdds=1.5&maxOdds=3.0

Get shots on target over 0.5 with super sub enabled

GET /api/value-bets-v2?marketType=onTargetScoringAttempt&line=0.5&superSub=true

Get 10 bets per page (for Telegram's 4096 char limit)

GET /api/value-bets-v2?perPage=10

Get high-edge bets only (10%+ edge)

GET /api/value-bets-v2?minEV=10

Get bets +EV on at least 2 bookmakers

GET /api/value-bets-v2?minSources=2

Get Bet365-only bets with 5%+ edge

GET /api/value-bets-v2?source=Bet365&minEV=5

Get today's bets only

GET /api/value-bets-v2?date=today

Get bets for specific dates

GET /api/value-bets-v2?specificDate=2025-03-15,2025-03-16

Get bets where player played within last 2 team games

GET /api/value-bets-v2?maxLastGameWithTeam=2

Get only players in predicted lineups

GET /api/value-bets-v2?inPredictedLineup=true

Combined: 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=10

Combined: Predicted lineup players with 5%+ edge

GET /api/value-bets-v2?inPredictedLineup=true&minEV=5&perPage=10

Get filter options (uses v1 endpoint)

GET /api/value-bets?action=filter-options

Telegram Bot Integration Flow

  1. On startup / periodically: Call /api/value-bets?action=filter-options to get available leagues, markets, lines
  2. User wants today's bets: Call /api/value-bets-v2?date=today&perPage=10
  3. User searches: Call /api/value-bets-v2?search=salah&perPage=10
  4. User wants a specific league: Call /api/value-bets-v2?uniqueTournamentIds=17&perPage=10
  5. User wants a specific market: Call /api/value-bets-v2?marketType=goals&line=0.5&perPage=10
  6. User wants high-value bets: Call /api/value-bets-v2?minEV=10&perPage=10
  7. User wants a specific bookmaker: Call /api/value-bets-v2?source=Paddy Power&perPage=10
  8. User wants reliable bets: Call /api/value-bets-v2?minSources=2&maxLastGameWithTeam=1&perPage=10
  9. Paginate: Use ?page=2, ?page=3, etc. Check pagination.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.20

Key fields for bot messages:

  • playerName + teamName — Who
  • marketType + line — What bet (use MARKET_TYPE_LABELS to get display name)
  • avgOverEdge — Headline edge %
  • homeTeamName vs awayTeamName — The match
  • matchDate — 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 model
  • hitRates.l10 / l20 / l30 — Historical hit rates
  • sampleSize — How many games the hit rate is based on
  • matchOdds — 1x2 for context (which team is favored)
  • teamId vs homeTeamId — 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"

On this page