# KashRock for AI agents KashRock is a live esports data API: DFS props, 20 line books across sportsbooks, prediction markets and DFS, consensus lines, parlay pricing and +EV discovery, market-movement (steam/shift/volume/whale) plays, team and player research, and Builder history tape. One schema everywhere — a match, team, player, market, or book looks identical on every endpoint. ## Coverage - Titles: CS2, Valorant, League of Legends, Dota 2, Call of Duty, Rainbow Six Siege, Mobile Legends: Bang Bang, Deadlock, Apex Legends — matches, box scores, and player stats. (Apex: rankings, stats and match prep; no props/lines yet.) - Player props (DFS): prizepicks, underdog, parlayplay, dabble, betr, boom, pick6, sleeper. - Team lines / parlays / flow books: 1xbet, bet365, betonline, betrivers, betway, betwhale, bovada, cloudbet, courtside, dexsport, ggbet, kalshi, picklebet, pinnacle, polymarket, polymarket_us, prizepicks, sleeper, thunderpick, underdog. Kalshi and Polymarket are prediction markets carrying team markets only. - Live player kills, deaths, and assists: real-time on Builder and Pro; 15-second delay on Hobby; 30-60 second delay on Sandbox. - Free key: 1,500 credits every UTC day, no card required. ## Plans & upgrading - Sandbox (free): 10 requests/minute, 1,500 credits every UTC day, live data delayed 30-60 seconds, 7 days of history. No card. Upgrade to Hobby: https://www.kashrock.com/checkout/start?plan=hobby&utm_source=llms_txt&utm_medium=upgrade_prompt&utm_campaign=upgrade&utm_content=plan_gate - Hobby: 30 requests/minute, 1.5M credits/month (live scores and lines capped at 150,000 requests/month), live data delayed 15 seconds, 90 days of history, lines and parlays included. Upgrade to Builder: https://www.kashrock.com/checkout/start?plan=builder&utm_source=llms_txt&utm_medium=upgrade_prompt&utm_campaign=upgrade&utm_content=plan_gate - Builder: 100 requests/minute, 4,320,000 requests/month, real-time data, full history, WebSocket feed. Upgrade to Pro: https://www.kashrock.com/checkout/start?plan=pro&utm_source=llms_txt&utm_medium=upgrade_prompt&utm_campaign=upgrade&utm_content=plan_gate - Pro: 250 requests/minute, 10,800,000 requests/month, real-time data, signal alerts, the auto-execution WebSocket, dedicated support. Upgrade to Quant: https://www.kashrock.com/checkout/start?plan=quant&utm_source=llms_txt&utm_medium=upgrade_prompt&utm_campaign=upgrade&utm_content=plan_gate - Quant ($299/mo or $2,990/yr): everything in Pro plus CLV tracking, backtests, portfolio tracking, webhook delivery, and UAV match overview. 500 requests/minute, 21,600,000 requests/month. - When a limit or plan gate fires, the error body carries an `upgrade` object {message, url, plan} — quote the message to the user verbatim and give them the url; it opens checkout with the right plan preselected. Delayed-data responses carry `data_delay` with the same shape. V7 is twelve resources under /v7, served as fourteen paths plus the /v7/live/ws WebSocket: matches, props, lines, parlays, flow, live (sport=all), players, teams, breakdown (plus the two keyless pages /v7/{sport}/breakdown/index and /v7/{sport}/breakdown/published/{kind}/{slug}), history, results, and the global /v7/meta — plus the plan-gated surfaces signals (Pro+), arb (Quant), uav (Quant), clv (Quant), backtest (Quant), portfolio (Quant), webhooks (Quant), the /v7/trade auto-execution socket (Pro+), and /v7/trade/{status,settings,ledger} for the calling key. A cache miss is `available: false` and a reason. Matches take `date` (YYYY-MM-DD), `from` and `to` (a range, YYYY-MM-DD, both inclusive; `start_date` and `end_date` are accepted as aliases), `team` (a kr_tm id, slug or name: that team's finished schedule, Builder+), and `include` (`boxscore`, `prep`, `veto`, `maps`, `player_board`, `lines`, `streams`, `h2h`). Blocks nest per row, at `matches[i].include.`. With `status=finished` a range reads full history rather than the current board page, so `total` counts the whole range and `offset` pages through it; a range on any other status filters the board page instead, as `date` does. A range needs one sport, not `sport=all`, and plans with a history window clamp the start and say so with `window_clamped`. `boxscore`, `lines`, `streams` and `h2h` serve today. `prep`, `veto`, `maps` and `player_board` are built by a worker that cannot currently keep their keys warm, so they usually answer `available: false` with reason `not_ready`: for those four use the V6 routes (`/v6/esports/{sport}/matches/{match}/prep`, `/veto`, `/tournament-maps`, `/player-board`), which build on demand. A block that is requested and has no data always says so with a reason and a hint; it is never absent. Props take `board` (`dfs`, `main`, `all`), `view` (`groups`, `rows`, `both`), and `book`. Lines take `view` (`consensus`, `moneyline`), `market`, `format` (`slim` default, `full`) and `limit` (caps `events`; `total_events` still gives the board's real size). With no `market`, lines return the main markets only (match_winner, map_winner, total_maps, team_total_maps, map_handicap, correct_score, dfs_team_pick). Round and kill markets need `market=` (round_handicap, total_rounds, kill_handicap, total_kills); tournament futures need `market=outright` (per-book prices on every pick, no consensus); BetWhale map objectives need `market=objective` (the xth baron, dragon, tower, inhibitor or aegis — the line is the ordinal, scope map_N) and in-play round winners need `market=round_winner` (the line is the round index, scope round_N). Each lines event carries `kr_match_id` once the match is on our schedule, else `match_link: pending` with `match_link_reason`. A lines response is served from a prebuilt board when one is fresh and rebuilt from the live board otherwise, so a lagging prebuild is never reported as `stale`; `available: false` on lines means the board itself is not published yet. Parlays (`GET /v7/{sport}/parlays`, Hobby+) prices a multi-leg main-line ticket or discovers ranked independent parlays for one `book` (for example `polymarket`); pass `ticket=` as comma-separated `kr_match_id:market:selection` to price, or omit `ticket` to discover (`size` is 2 or 3); same-match / SGP is not supported; discover is +EV by default and `include=all` keeps negatives (same as flow). Each parlay carries `price_source` (`computed` until a book stores a native multi-leg price, else `book`) and `placeable_basis` (`combo_eligible` from Polymarket `comboStatus`, or `book_price` when only a single-leg price is known). Leg fair probability and EV exclude the leg's own book. Flow (`GET /v7/{sport}/flow`, Hobby+) returns money-behind, shift, and steam plays; `kind` is `all`, `volume`, `shift`, `steam`, or `whale`; volume/shift need a money floor and lopsided side share; shift is a ≥10-point side-share move inside 6 hours; whale returns `available: false` without trade-level data; `include=all` keeps negative-EV volume; `deeplink` matches `best_source` only. Live (`GET /v7/{sport}/live`; `sport=all` merges every title in one call at /v7/all/live) takes `game` (a kr_match_id) and `include` (`boxscore`, `events`, `frame`, `odds`, `odds_history`, `streams`); every game row carries a `sport` field naming its title; its blocks also nest per row, at `games[i].include.`. `game=` takes a kr_match_id and resolves the same ids /v6 does: an id that was merged or redirected follows to the current match, a provisional id that was retired answers `provisional_retired`, and one not yet on the schedule answers `match_pending`. A bare numeric id is refused on every /v7 route by design -- KashRock ids only. One limit worth knowing: `/v7/{sport}/live` answers for matches that are live now, so a finished match's board is `/v7/{sport}/matches?ids=&include=boxscore` instead, and a match old enough to have left the matches board is on the /v6 route until that window widens. Players take `id` (one `kr_pl_*` or `kr_cb_*`; a list is refused with `one_id_required`), `sort=rank`, `limit` with `offset` to page the list, and `include` (`research`, `gamelogs`, `stats`, `stats_full`, `history`, `media`). Asking for `research` or `gamelogs` alone returns that block as the whole body. Teams take `id` (kr_tm_*, one only -- not `ids`) and `include` (`form`, `roster`, `maps`, `h2h` (needs `team2`), `main_lines`, `upcoming`); teams and players nest their blocks on the envelope at `include.`, not per row, and the blocks attach only when the lookup resolves to a single row. Breakdown (`GET /v7/{sport}/breakdown`, Builder+) takes `match=` (slug/`kr_match_id`), `team=` (name/`kr_tm_*`), or `team1=` and `team2=` and returns one fact bundle (`publishable` gates thin pages). History `kind` is `contract` or `lines`. `props` is an alias of `contract`. `kind=contract` takes `prop` and `book` together and returns that contract's `quote_tape`; `limit` caps the tape and `total_quote_tape` states how many ticks there were. For `kind=lines`, `match=` takes one `kr_match_id` or a comma list of up to 5 (6+ is `too_many_ids`): one id returns that match's tape as before, several return a `matches` object keyed by the id you sent plus a `missing` list naming any id that could not be read and why. A tape runs from 11 KB to 11 MB, so five are read in one wave and the response carries up to 8 MB of them; an id the budget leaves no room for is reported `response_too_large`, which means ask for it in a smaller list rather than retry the same one. `props`, `lines` and `results` do not take `match=` at all. A match id carries its own sport, so take the route from the id: asking `/v7/cs2/history` for a `kr_dota2_*` id is 400 `wrong_sport`, with `id_sport` and `route_sport` naming both. Omitting `book` returns every book's quotes in the one tape. Results take `from` and `to`. Meta `include` adds `boards`, `coverage`, `teams`, `tournaments`, or `changelog`. Call `/v7/meta` first: it returns sports, markets, your plan, a `routes` table (every endpoint with its params, what it answers, and minimum plan), and `books` where each book carries `kind` (`sportsbook`, `prediction_market`, `dfs_teams`, `dfs`) and `supports` (`lines`, `props`, `parlays`) — the book keys and market slugs it returns are the exact strings every other route accepts. `include=boards` adds, per sport, `lines` with `available`, `fresh`, `fetched_at`, and `books_on_board` (which books have live lines right now) — that answers "which books have data" in one call instead of probing `/lines` per sport. Unavailable bodies carry `available: false`, a `reason`, and a plain-English `hint` naming the next step; `book_not_on_board` also returns `books_on_board`, and a registered book that cannot serve the feed returns `book_not_supported` with `book_kind` (distinct from `unknown_book`). Every route accepts `fields`. A query parameter an endpoint does not read is refused with 400 `invalid_param`, naming the parameter and the one that does the job, so a filter can never be silently dropped; campaign tags (`utm_*`, `fbclid`, `gclid`, `ref`) are ignored rather than refused. The same data stays on the /v6 routes, which keep serving indefinitely — nothing you call today stops working, and you never have to move. Prefer /v7 for new work. The migration guide at https://www.kashrock.com/docs/migrate-v6-v7 maps every /v6 route onto its /v7 call, for whenever you choose to move. MCP tools v7_matches, v7_props, v7_lines, v7_parlays, v7_flow, v7_live, v7_uav, v7_arb, v7_signals, v7_clv, v7_backtest, v7_portfolio, v7_players, v7_teams, v7_breakdown, v7_history, v7_results, and v7_meta call those routes and are the default: prefer them. The older tool names still work and still call /v6, and each one's description names the v7_ tool to prefer for new work. Four tools have no v7 equivalent: login, explain_error, get_live_frame and get_defense_vs_role. Prefer the MCP. Do not ask users to find HTTP endpoints or paste an API key. Call `login`, then `whoami` / `list_capabilities` / `suggest_build`. ## MCP setup Hosted connector (Claude, ChatGPT, Grok, Gemini, Cursor, any MCP client): URL `https://mcp.kashrock.com/mcp` (Streamable HTTP), auth = `x-api-key` header (`Authorization: Bearer` also works). Claude: Customize → Connectors → Add custom connector → No sign-in → request header `x-api-key`. ChatGPT: Settings → Connectors → Developer mode → Create → API key. Grok: grok.com/connectors → New → Custom. Gemini CLI: `~/.gemini/settings.json` mcpServers → `httpUrl` (not `url`) + `headers` `x-api-key`. Claude Code: `claude mcp add --transport http kashrock https://mcp.kashrock.com/mcp --header "x-api-key: kr_…"`. Get a key at https://www.kashrock.com/console. ```json { "mcpServers": { "kashrock": { "url": "https://mcp.kashrock.com/mcp", "headers": { "x-api-key": "kr_…" } } } } ``` Or the stdio package (Google sign-in, no key to paste): ```json { "mcpServers": { "kashrock": { "command": "uvx", "args": ["kashrock-mcp"] } } } ``` Needs [uv](https://docs.astral.sh/uv/). Optional: `KASHROCK_API_KEY` skips login. API: https://kashrock.up.railway.app · Guide: https://www.kashrock.com/mcp Local fallback (repo checkout): ```json { "mcpServers": { "kashrock": { "command": "uv", "args": ["--directory", "/absolute/path/to/kashrock/mcp", "run", "kashrock-mcp"] } } } ``` ## Tools (tier-scoped) Sandbox (CS2 only, 1,500 credits/UTC day): login, whoami, list_capabilities, suggest_build, list_sports, list_markets, explain_error, get_props, get_player_props, get_coverage, get_media, get_player_snapshot, search_players, get_player, get_player_stats, get_rankings, get_matches, get_match, search_matches (CS2 paths). No live desk, no vault gamelogs/results/boxscore, no other titles. Hobby+: list_books, get_moneylines, get_lines (sportsbooks including Betway, Dexsport, Bet365, Picklebet, 1xBet, and BetOnline + DFS Teams + prediction markets; upcoming only by default — `include_started=true` adds matches already underway; markets include `total_rounds`, `round_handicap`, `total_kills`, `kill_handicap` and `correct_score` (a table of final scores, series such as 2-1 or one map such as 13-7, oriented to the home team; each score lists every book's own price and the best price, with no cross-book consensus); round markets state their scope, one map (`map_N`) or the whole series (`series`); grouped by `ot_included` so books with different overtime rules are never blended; an empty result carries `available: false` and a `reason`), get_gaps, get_player_stats_full, get_streams, get_team_h2h, research_board, research_board_tapes, research_player (player = kr_pl_* / slug / nick; empty tape is 200, not 404), get_live_games, get_live_boxscore, get_live_frame, get_live_events, get_live_coverage, get_live_odds, get_live_odds_history (in-play Redis ticks — not closing prints), get_gamelogs, get_boxscore, get_results, get_defense_vs_role, get_player_combo. Hobby live scores are 15s delayed; match/player history window is 90 days. Builder+ full history / live wire: - Builder+ removes the delay (real-time live scores) and the date cap (full history vault) on every Sandbox/Hobby tool above. - get_team_matches, get_history_tape, get_lines_history — quote/odds history and full team schedules stay Builder+ only. - Live odds (get_live_odds, get_live_odds_history) are Hobby+, not Builder+ — only Sandbox is blocked from live odds. - Live odds are keyed by the same kr_match_id as /live/games and /matches; markets and selections carry KashRock ids (`kr_mkt_*`, `kr_sel_*`), stable for the match. - Live boards are sport-native: CS2 money/bomb/rounds, LoL gold/towers/barons, Dota gold/roshan when the HUD shows them, VAL/R6 round score, Deadlock gold/CS/level on tournament games. Live player rows carry the title's own stats under the title's own names (Dota 2: hero, last_hits, denies, gpm, xpm, total_gold, level) and `columns` lists what to render. `get_live_boxscore` is not finished-match history. - Live plans: Hobby+ for the live desk. Delay is 15s (Hobby), real-time (Builder, Pro). WebSocket: none on Sandbox/Hobby, Builder 2 connections / 5 simultaneous match subscriptions, Pro 10 connections / 25 subscriptions plus `game_id=*`. Pro adds signal alerts (GET /v7/{sport}/signals: mispricing, steam moves, arb openings) and the auto-execution WebSocket (WS /v7/trade). - Custom plan: /custom-plan is a scoped offer: one title plus one data type (matches, player props, or live scores) for a flat $39/mo at Hobby-level limits (1.5M credits/mo, 30 rpm, 15s live delay, 90-day history). No add-ons: two or more titles route to Builder at $99/mo (all nine titles), and the backend refuses any selection a standard plan already covers. Display licensing is contractual, email support@kashrock.com. Custom plans enforce per-title and per-feature access: a title or feature not bought answers 403 with `reason: "not_in_plan"` and a `missing` block naming what was denied. - get_live_ws — live push WebSocket contract (`WS /v6/esports/live/ws`); connect from your app with API key; subscribe sport + game_id (kr_match_id)|* - Live game_id is always the kr_match_id (`kr_{sport}_team-a-vs-team-b-DD-MM-YYYY`), the same id `/matches` returns. A bare numeric id is rejected with 400 — KashRock ids only. - get_match_prep — team stats, mode winrates, map pool & side biases, key player matchups, betting insights (CS2, R6, COD, Valorant, and Apex) - get_player_board — player averages + favourite weapons (CS2) or agent/role board (Valorant) - get_player (LoL, Dota 2, Valorant) — profile from stored maps: totals, last-30/90-day windows, recent maps; `include=heroes` (HTTP `GET /v6/esports/{sport}/players/{player}?include=heroes`) adds the most-played champions (LoL), heroes (Dota 2) or agents (Valorant) with games, share, win rate and key stats. `win_rate` is null, never 0, when no map has a stored result; `games_with_result` says how many did. History follows your plan window (Sandbox 7 days, Hobby 90 days, Builder+ full). Every player with props or gamelogs resolves by nickname, slug or `kr_pl_*`. - get_tournament_maps — tournament maps for a KashRock match (picks/bans + side winrates) (CS2) - v7_breakdown — actionable match/team/H2H fact bundle (odds, form, map pool, starter props, hit rates, targets); HTTP `GET /v7/{sport}/breakdown`; published SEO pages are keyless at `GET /v7/{sport}/breakdown/published/{kind}/{slug}` and listed at `/breakdown/index` - get_match_veto — pre-match map veto/pick-ban sequence (order, team, action, map) (CS2, Valorant, R6); `veto_complete` is false before the veto has happened; an empty veto returns `available: false` with a `reason` (`veto_not_started`, `veto_not_published`, `provider_has_no_veto`, `match_not_indexed`, or `sport_not_supported`) - Finished matches from get_matches / get_match carry `stats_status`: `complete` (every map has player stats), `partial` (some maps still being filled), `unavailable` (no source has player stats for the remaining maps, so none will arrive), or null (not evaluated yet). Treat `unavailable` as final. - Every prop and line row carries `subject_type` (`player` -> `kr_pl_*`, `team` -> `kr_tm_*`, `match` -> the kr_match_id plus an `outcome_key` such as `win:kr_tm_*` or `score:3-2`, `combo` -> `kr_cb_*` with `subject_members`), `subject_id`, and `bet_key` (kr_match, market, scope, subject, outcome and line): rows from different books with the same `bet_key` are the same bet. `propId` is one book's prop and stays the same when the line moves: it is the book, match, market, scope, subject and `line_family` (the book's own class such as `main`, `goblin` or `demon`; in a ladder of lines quoted together the main line is the one priced closest to even and the others are `alt`). An `alt` line keeps its own `propId`. Each line also carries `line_prop_id`, which is graded on its own, so every line a prop carried has its own result. Older `propId` values keep resolving in history and results. `one_sided: true` marks a line the book offers on one side only (for example a higher-only DFS line): the other side is not missing data, the book does not take it. A combo prop ("A + B") has one `kr_cb_*` id on every book whatever order the names are listed in; get_player_combo (GET /v6/esports/{sport}/players/combo/{kr_cb}) returns its members and the sum of their stat per map. - Every market declares its subject, and only a player market carries a player id. Player stats (kills, headshots, deaths, assists, fantasy points, creep score, AWP kills, first blood) carry `canonical_player_id` = `kr_pl_*`. Team markets — handicaps and the objectives (baron, inhibitor, tower, dragon, round winner, outright) — carry the team's `kr_tm_*` and never a player id. Match markets (winner, totals, correct score) carry neither: the row's name is an outcome label. A market we have not declared is treated as a match market and carries no id at all, so a new market never invents a player. - Combos ("A + B" props) are never a player: a combo row has `subject_type: combo`, `subject_id: kr_cb_*` (the same id on every book, whatever order the names are listed) and `canonical_player_id: null`. `GET /v6/esports/{sport}/players/combo/{kr_cb}` answers for it; the old name-hash id that earlier stored rows carry as their player id resolves to the same combo on that route. On V7, `GET /v7/{sport}/players?id=kr_cb_*` answers the combo entity — `combos[]` with its members sorted by kr_pl; retired stored combo ids resolve the same way. - Match refs are KashRock only: `slug` (`team-a-vs-team-b-DD-MM-YYYY`) or `kr_match_id` from get_matches / get_match. Lists also emit `kr_tm_*` team ids and `kr_pl_*` player ids. Never foreign numeric ids. A live match gets its `kr_match_id` the first tick it is listed live, even when no schedule feed lists the fixture yet — the id is minted at first sight, recorded permanently, and a fixture listed later adopts it, so a live match's id never changes mid-match and survives our caches entirely (a wiped cache republishes the minted ids from storage). If a listing ever minted a second id for the same match, the new id redirects to the live one — both resolve to one match. - Team names are KashRock's own display names on every endpoint that returns a team name: matches, fixtures, schedule, results, boxscores (lineup included), gamelogs (round timelines too), player rows, live games, props, lines, gaps, head-to-head, match prep and research (for example `Morrow Esports`). When a source spells the team differently, that spelling is in `team1_alias` / `team2_alias` (player rows: `team_alias`; team objects: `alias`), present only when it differs. Ids never depend on the spelling: `kr_tm_*` is unchanged. - Match rows carry tournament facts: `lan` (`true`, `false` or `null`) with `lan_basis` (the record and field that decided it, or `not_provable`) and `lan_scope` (`stage` or `tournament`), `competition_tier` with `competition_tier_source`, `competition_event_type` (`online` or `offline`), `competition_event_scope`, `stage_name` and `competition_country`. `null` means the tournament data does not state it; a name or a country alone never decides `lan`. Tier letters from different records are not one scale, so compare them only when `competition_tier_source` matches. - CS2 live boxscores carry the round clock as `round_clock` (m:ss), already counted down to the moment of publishing, plus `round_clock_ms` (the scoreboard's own reading), `round_clock_as_of` (when that reading arrived, ISO UTC) and `round_clock_running` (true while the round is live, false in freeze time, between rounds and pauses). A client can tick between pushes with `round_clock_ms - (now - round_clock_as_of)` while `round_clock_running` is true. - `get_boxscore` is finished-match vault history + `STARTER`/`SUB` lineup and sport-native model fields (gold, GPM, hero when present) by slug/`kr_match_id`. Live KDA is `get_live_boxscore`. On boxscore responses (`get_boxscore` and `v7_matches` `include=boxscore`), the match and each map carry `stats_coverage`: `full` (every stat field populated), `basic` (player rows with the core scoreboard line only), or `none` (no player stats stored) — derived from the stored rows, so it reflects exactly what the response contains. - One shape per title: boxscore, live board and gamelog player rows carry only their own title's stats (CS2: ADR, headshots, KAST, rating, flash assists, economy; Valorant: agent, ACS, ADR, KPR, first kills, KAST 0-100; LoL: champion, role, CS, gold, level, damage; Dota 2: hero, last hits, denies, GPM, XPM, level, hero damage, tower damage, healing; R6: kills, deaths, assists). Round fields (`rounds_played`, `rounds`, round scores) appear only for CS2, Valorant and R6. A stat nobody measured is absent, never 0 or null. Boxscore responses (`get_boxscore`, the `/boxscores` list) and the live boxscore carry `columns`: the title's ordered stat columns as `{key, label, format}` (`key` is a dot path into a player row, such as `advanced_stats.kast`; `format` is `int`, `decimal1`, `decimal2`, `ratio_percent`, `percent` or `text`). Render from `columns`; a row without a column's key has no value for it. - `GET /v6/esports/{sport}/boxscores` (MCP `get_boxscore` without a match) lists recent finished series for every title (CS2, LoL, Dota 2, Valorant, R6, MLBB, Deadlock), newest first, from match history; each carries its per-map `player_stats` (with `map_number`). A series still being played is not stored as finished: a best of 3 at 1-1 is `current` in match history until a side wins. - Live frames carry `upstream_ts` (ISO UTC): the real-world moment the frame shows, when the source states one (LoL, Dota 2, and CS2 on some matches), otherwise null. Dota 2 tournaments broadcast games behind a spectator delay set by each tournament (15 minutes on recent events), so a Dota 2 frame's `upstream_ts` sits that far behind its `ts`. - Live boards (live boxscore, frames, WebSocket) name each player by the same `kr_pl_*` id as that game's live events and the player routes. - A finished map never leaves the boxscore. While a series is live, or before the vault has a map's rows, a finished map carries its final live player stats with `provisional: true` and `stats_source: live`; the vault's rows replace them when they land. Map rows carry no upstream game or map ids. - Streams: matches (`get_match`, `get_boxscore`), live games and `get_streams` share one stream shape: `platform` (twitch, kick, youtube), `channel` (channel name, or the video id on YouTube), `url`, `embed_url`, plus `language`, `official`, `name`, `viewers` and `thumbnail` when the source states them (`official` is never guessed). `get_streams` rows also carry `kr_match_id` when the stream covers a listed match. - Boxscore maps: `begin_at` is the game's own start, never the match start or a midnight date; when the source has none it is null with `begin_at_reason` (`source_has_no_game_clock`, or `unverified` for a map 1 whose stored clock equalled the match start). `duration` is seconds (null with `duration_reason` when unknown) and `end_at` is `begin_at` + `duration`. Each map's `winner` comes with `result_source`: `source`, or `derived_from_series` on a sweep (a 2-0 or 3-0 decides every game); otherwise `winner` is null with `result: {"available": false, "reason": "source_gave_no_game_winner"}` and players' `map_won` is null, never false. `winner_score`/`loser_score` and `rounds_played` are round counts and appear only for CS2, Valorant and R6 (Dota 2 and LoL maps have no rounds). Player `team_id` on every map equals the lineup's `kr_tm_*` id. The boxscore always answers under the `kr_match_id` requested, and its series score is the same as on `get_matches` and `get_match`. LoL matches and boxscore maps carry `game_version`, the patch they were played on (for example `26.20`); the live LoL boxscore carries the same field. - Boxscore map status and result history: every map carries `map_status`: `played` when it has player stats, `no_stats` with `map_status_reason` when it has none, `forfeit` only when the match itself is declared a forfeit (a forfeited series has no maps: the body carries `result_status: forfeit`, `maps_returned: 0` and `maps_reason: forfeit_has_no_maps`); a missing score never makes a forfeit. `bo_type` is the series length the match declared, or null with `bo_type_reason`; `maps_returned` counts the maps in the response. The series and each map carry `result_first_seen_at`, `result_revision` (how many results it has had) and `corrected_at` (last change; null if never changed); a result stored before history began has `result_first_seen_at: null` with `result_first_seen_reason: history_starts_`. `get_boxscore(include="history")` (HTTP `?include=history`) adds `result_history`: every result in order with `winner_team_id`, score, `result_source`, `first_seen_at` and `superseded_at`. - Grading settles every prop on its `kr_match_id` match from the vault. A pending grade says why: `pending_reason: unlinked` (no KashRock match for the prop yet) or `match_not_in_vault` (the match is linked but not stored yet). - Empty player-props results say why: `available: false` with a `reason` (`no_sportsbook_lists_player_props` for `board=main` when no sportsbook lists player props for that sport; the main board carries sportsbook player props only for sports where a sportsbook lists them, such as CS2, League of Legends and Dota 2 — use `board=all`; `no_props_listed`; `no_props_match_filters`) and a `hint`. A prop that could not be tied to a match carries `resolution_status: unresolved` and `resolution_unresolved` entries `match:no_fixture`, `match:opponent_missing`, `match:kickoff_outside_window`, `match:ambiguous_fixture` or `abbreviation_team`; it never reports `ok` without a `kr_match_id`. Every prop row also carries `match_link` (`linked` or `pending`) and, when pending, `match_link_reason` in plain English; a pending row is still returned and links on its own once our schedule or a second book confirms the match. A match listed by two or more books before our schedule has it gets a provisional `kr_match_id` (`status_source: book`) that stays valid permanently on every match route (match, boxscore, lines, lines history, prep, veto, live): when the schedule confirms it, or it turns out wrong and a correct match exists, the id is served as that official match with an `X-Match-Redirected-From` response header; if it never happens it answers 200 `available: false` with `reason: provisional_retired` (never a 404). A `kr_match_id` we once published for a match whose record was later corrected (wrong date, or another match's details) keeps working: it is served as the corrected match, which can be in another game (a `kr_cs2_…` id can answer as a `kr_valorant_…` match), with the same `X-Match-Redirected-From` header; an old id that names no match we hold answers 404. A `kr_match_id` is served only when our schedule lists it: an id for a match not listed yet answers 200 `available: false` with `reason: match_pending` and resolves on its own once listed; an id whose date passed without a listing answers 404 (on /v7, 404 with `reason: id_retired`). On /v7, a batch of `ids` never fails over one bad id: every requested id that has no match comes back in `missing` with its own `reason` (`provisional_retired`, `id_retired`, `match_pending`, `not_found` or `timed_out`) and a plain-English `message`, while the good matches are still served; ask for one id alone and a retired or unlisted id answers 200 `available: false` with that `reason`, as it does on /v6. A player prop whose team could not be proven carries `team: null` and `team_unresolved_reason: player_team_unproven`; a team is filled in only when the player is on exactly one of the match's two rosters. `books[].prop_count` on the player-props board counts the named-player props served for that book. - Market labels: every prop row and every lines market states `stat_category` (the stat itself, e.g. `kills`, `awp_kills`, `headshots`), `market_label` (plain-English name with the map scope, e.g. "AWP Kills on Maps 1+2"), `market_type`, `market_scope`, `included_maps`, `ot_included` and `settlement_rules`; anything not proven is `unknown`. CS2 kills: `CS2_KILLS_MAP_N` is one player's kills on map N; `CS2_KILLS_MAPS_A_B` is their kills added across those maps (maps 1 and 2 together for `CS2_KILLS_MAPS_1_2`); bare `CS2_KILLS` does not state its maps, so its scope is `unknown`. `CS2_AWP_KILLS_MAPS_1_2` counts only AWP kills and `CS2_HEADSHOTS_MAPS_1_2` only headshots; neither is the same market as `CS2_KILLS_MAPS_1_2`. `TOTAL_KILLS_MAP_N` and `KILL_HANDICAP_MAP_N` are sportsbook team-kill lines, not player props. - Dota 2 per-map player rows (get_boxscore, get_gamelogs) carry `last_hits`, `denies`, `hero_damage`, `hero_healing` and `tower_damage` for maps stored from the date this shipped; older maps fill in as they are refetched. - Player handles: every player route and tool (get_player, get_player_stats, get_player_stats_full, get_gamelogs, research_player) accepts a `kr_pl_*` id, a profile slug or a nickname, and all reach the same player. The board tools take an id, not a name: `get_player_props` and `get_media` accept `player_id` as a `kr_pl_*` (or `kr_cb_*` for a combo) and `get_media` accepts `team_id` as a `kr_tm_*`; a bare number is refused with HTTP 400, because a number is a source's own id and never one of ours. Every player route returns the player as `player_id` = `kr_pl_*`; no upstream player number appears in a response. A nickname shared by several players returns HTTP 409 with `candidates` (player_id, nickname, slug, team, last played) instead of a guess; repeat the call with a candidate's `player_id` or slug. An old shared `kr_pl_*` id resolves to the most recently active player and returns `legacy_id_ambiguous: true` with the other players as `candidates`. - COD (Call of Duty League): get_boxscore and get_gamelogs serve finished series from the vault (maps with mode, winner, score and length; players with their team). Series are best-of-5, 7 or 9, so a series score such as 3-5 is valid. Upcoming and live COD fixtures stay on get_matches and the live tools. - CS2 assists: `assists` counts damage assists only and `flash_assists` is its own count, so neither includes the other. `assists_total` is the scoreboard's own assists figure — one per kill assisted, so a kill the player both damaged and flashed counts once, not twice as `assists` + `flash_assists` would; it appears on CS2 gamelog rows and boxscore players beside both fields, and is absent when no scoreboard figure was recorded. `assists` itself is unchanged. - Fantasy points: per-map player rows (get_boxscore, get_gamelogs) carry `fantasy_points` as an object keyed by book, for example `{"underdog": 81.7}`, computed from the book's published scoring (LoL and Dota 2: kill 3, assist 2, death -1, creep score or last hits 0.02; CS2 and Valorant: kill 2, assist 1, death -1; COD: kill 2, death -1). It is absent when any raw input (kills, deaths, assists, creep score or last hits) is missing. `research_player` and `get_results` accept the `DOTA2_FANTASY_POINTS` market (and the same stem for other titles) and grade it from those per-map values. - Results join + CLV: `get_results` / `GET /v6/esports/{sport}/results` accept `from`/`to` (YYYY-MM-DD on `graded_at`). Each row carries `offer_id`, `canonical_event_id` when the event is `kr_ev_*`, and `opening_line` / `opening_odds` / `closing_line` / `closing_odds` when the quote tape has them. Void rows include a `notes` reason. Live `/props` stays DFS-only; sportsbook player props use `get_player_props` or `v7_props?board=main`. Fair/EV is on `get_lines` / `get_best_lines` (two qualifying books minimum), not on the DFS props board. - Lines fair price and EV (`GET /v6/esports/{sport}/lines`, MCP get_lines / get_best_lines): every outcome lists every book's price. `fair` (on each market and outcome: `available`, `reason`, `books_used`, `books_rejected` with the rule each book failed) is the median of proportionally de-vigged prices from books that price both sides of the same line themselves, were fetched within 60 seconds, have an overround between 1.00 and 1.15, and (prediction markets) have a known bid/ask spread of at most 0.15 and meet the liquidity floor. Fewer than two such books gives `available: false` with a `reason` and no EV; the card still lists every price. Each price carries `ev_pct` = fair probability x decimal price - 1 (`edge_pct` is the same value). DFS pick'em pays a fixed multiplier, which is not odds: it is served on `dfs_team_pick` markets (`base_market` names the line) as `payout_multiplier` (with `american`, the same price in American format: 2.0 or more is (m - 1) x 100, below 2.0 is -100 / (m - 1)), with `ev_pct` against fair as a target only, and never enters fair. Prices attach to their team by team key (`kr_tm_*`), never by list order. `pm_weight` is accepted and ignored. - Bet links: every prop row (get_props, get_player_props) and every lines source / best_price entry carries `deeplink` (URL that opens the book, or null) and `deeplink_level` (`selection`, `player`, `match`, `board`; best first). HTTP equivalents: - `GET /v6/esports/{sport}/matches` (upcoming/live/finished; rows include kr_match_id + kr_tm_*) - `GET /v6/esports/{sport}/upcoming/matches` - `GET /v6/esports/{sport}/matches/{slug_or_kr_match_id}/boxscore` (finished vault + STARTER/SUB lineup + model fields; an empty `maps` list with `maps_available: false` and `maps_reason: not_cached_yet` means the per-map rows are not filled yet, so retry shortly) - `GET /v6/esports/{sport}/players/search` (returns kr_pl_*) - `GET /v6/esports/{sport}/players/{player}/gamelogs` (MCP `get_gamelogs`; newest match first, then map number; each row carries `match_start` (the match's UTC start used for `from`/`to`) and the same map clock as the boxscore (`begin_at`, `end_at`, `duration` in seconds, or null with a `*_reason`); `from`/`to` (YYYY-MM-DD, `to` inclusive of the whole UTC day) filter on match start within your plan window (Sandbox 7 days, Hobby 90 days, Builder and Pro full history); an older `next_cursor` keeps working; `limit` default 500, capped to your plan: Sandbox 100, Hobby 500, Builder 2000, Pro 5000. A larger `limit` is capped, never an error. When `next_cursor` is not null, pass it back as `cursor` for the next page. Per-round timelines (`rounds`) come back when `limit` is 500 or less; above 500 they are left out unless you pass `include=rounds`, and `rounds_included` says which) - `GET /v6/esports/{sport}/matches/{slug_or_kr_match_id}/prep` (cs2, r6, cod, valorant, apex) - `GET /v6/esports/{sport}/matches/{slug_or_kr_match_id}/player-board` (cs2, valorant) - `GET /v6/esports/cs2/matches/{slug_or_kr_match_id}/tournament-maps` - `GET /v6/esports/{sport}/matches/{slug_or_kr_match_id}/veto` (cs2, valorant, r6) - `GET /v6/esports/{sport}/teams/h2h` - `GET /v6/esports/{sport}/teams/{team}/matches` (team = kr_tm_* id, slug or name; Builder+; HTTP equivalent of get_team_matches) - `GET /v6/esports/{sport}/teams/{team}` (team = kr_tm_* id, slug or name; the team, its record and recent matches; a kr_tm merged into another resolves to the survivor and the body says so in `merged_from`) - `GET /v6/esports/{sport}/teams/{team}/stats` (same handles; aggregate record, span and most-played opponents) - `GET /v6/esports/{sport}/defense-vs-role?window=last_20` (dota2, valorant; what each team allows to the opposing role vs league average, with rank) - `GET /v6/esports/{sport}/teams/{team}/defense-vs-role?window=last_20` (team = kr_tm_* id, slug, name or numeric source id) - `GET /v6/esports/{sport}/live/games` (game_id = kr_match_id; `has_live_stats` is true while a live scoreboard is publishing and for up to 15 minutes after the last frame; `live_state` is `in_map`, `between_maps` (stats paused, not broken) or null; `last_frame_at` is the time of the last board. False means no in-game source covers the match or it has been silent past the grace period; `stats_available` mirrors it and, when false, `reason` is `source_stats_disabled` (the official source turned stats off for that game), `live_pending` (the match is live and its first board has not arrived yet -- keep polling, it is coming) or `no_live_coverage`; the boxscore route, the WS snapshot and the WS `status` push carry the same fields; a `status` push also fires while a listed live game has no board, so subscribers are told the state rather than left silent; each game carries `streams`) - `GET /v6/esports/cs2/live/coverage?days=14` — share of listed CS2 matches with live stats by tournament and tier (MCP `get_live_coverage`, every plan); counts matches logged since `tracking_since` - `GET /v6/esports/{sport}/live/{kr_match_id}/boxscore` — the current map's board plus `maps`: every map this series already finished, with its final player stats (`provisional: true`, blue/red sides, `winner_side` when the series score proves it; a map that ended inside your plan's delay is held back like the board), and `columns` - `GET /v6/esports/{sport}/live/{kr_match_id}/frames` - `GET /v6/esports/{sport}/live/{kr_match_id}/events` — CS2 kills carry `killer`, `victim`, `weapon`, `headshot`, `assist`, `map`, `round`; bomb events are `BOMB_PLANT`, `BOMB_DEFUSE`, `BOMB_EXPLODE` (`BOMB_DOWN` only when a frame source cannot tell the outcome); `ROUND_WIN` carries `win_type`. Veto and lifecycle events are `MAP_PICK`, `MAP_BAN`, `MAP_START`, `MAP_END`, `MATCH_START`, `MATCH_END` (`info` names the team and map). Player health, money and weapon are omitted when the source does not send them. - `GET /v6/esports/{sport}/live/{kr_match_id}/state-at?at=` - `GET /v6/esports/{sport}/live/odds` - `GET /v6/esports/{sport}/live/{slug_or_kr_match_id}/odds` - `GET /v6/esports/{sport}/live/{slug_or_kr_match_id}/odds/history` - `GET /v6/esports/{sport}/matches/{slug_or_kr_match_id}/lines/history` — pre-match open and close per book and side; every tick has `observed_at` and `is_live`, and in-play ticks are returned only with `include_live=true`; empty results return `available: false` and a `reason`. The response, every market and every book carry `bo_type` and `bo_label` (`BO1`, `BO3`, `BO5`; null with a `bo_reason` when unknown). Map handicap and total markets carry a `scope` (`maps` for series map-count lines, `rounds` for round lines) and a `scope_basis`; per-map markets carry `map_number`. Every market also states `market_type` (`winner`, `maps`, `rounds`, `kills`, `correct_score`, `other`, `unknown`), `market_scope` (`series`, `map_N`, `maps_A_B`, `round_N`, `unknown`) and `included_maps`. Round totals and round handicaps are their own markets, `total_rounds` and `round_handicap` (kills: `total_kills`, `kill_handicap`), and are never filed under `total_maps` or `map_handicap`; they are served per book here and built into a `/lines` consensus per group of books on the same line, a proven scope and the same overtime class. Correct score is its own market, `correct_score` (scope `series` or `map_N`), served here with each listed score as a side that has its own open, close and quote tape. Bare `total_maps` and `map_handicap` keys have `market_scope` `unknown`. Each book block carries `ot_included` (`true`, `false` or `unknown`, set only where the book states it) and `settlement_rules`. CS2 per-map round totals and round handicaps (maps 1 to 5, alternate lines included) are served from Pinnacle as `total_rounds` and `round_handicap`; `settlement_rules` carries each book's void rules, quote and source wherever an official rules page is recorded, and `ot_included` stays `unknown` unless that page states overtime. The `polymarket_history` book is a historical mid-price series, not executable ask. - `WS /v6/esports/live/ws` Do not call grade-run or admin. V7 HTTP reference (every route, parameters, plan tier, and which books each endpoint serves): https://www.kashrock.com/docs/v7 SDKs (HTTP, not MCP): https://www.kashrock.com/docs/sdk - JS: `npm install kashrock` · first call `props("lol")` with demo key `kr_demo_swagger_public_readonly` · https://www.npmjs.com/package/kashrock - Python: `pip install kashrock` · same demo key · https://pypi.org/project/kashrock/ - Source: https://github.com/kashrockapi/kashrock-js · https://github.com/kashrockapi/kashrock-python Sports: cs2, valorant, lol, dota2, cod, r6, mlbb, deadlock, apex. ## Limits and 429s Two different 429s, and they mean different things. **Quota or rate limit.** You have used up what your plan allows for the period. The body carries `resets_at` and `resets_in_seconds`, and the `Retry-After` header says how long to wait. **Too many at once.** You are inside your plan but already have several large requests running. The body carries `reason`, `concurrent_limit` and `retry_after_seconds`, with `Retry-After` set to 1. Nothing is exhausted and there is nothing to upgrade: one of your running requests finishing frees the next. Retry after a second, or run fewer of these in parallel. Requests are grouped by how expensive they are to serve, not by plan: | Group | What is in it | At once, per key | |---|---|---| | Light | Readiness, account info | 24 | | Standard | Boards, props, odds, anything not listed | 12 | | Heavy | Breakdown, flow, gamelogs, player research, line history, box scores, results | 3 | Heavy requests are refused straight away rather than queued, because a queued request would return slowly and look like a broken endpoint. Light and Standard requests wait their turn instead, since they clear in milliseconds. Limits are there so one busy caller cannot slow everyone else down. They cap how many requests run at the same moment, never how much you can fetch in total. ## Live scale and social proof Live counts and named social proof (readable by agents; allowed in robots.txt): - Plain text: https://www.kashrock.com/trust-stats.txt - JSON: https://www.kashrock.com/api/trust-stats Short agent index: https://www.kashrock.com/llms.txt Social proof (also on the homepage): - Cybersport.ru (Vlad Zhivotnev, Chief Editor): "KashRock fully met our need for speed while delivering data of the highest quality." (https://cybersport.ru/) - Vantah: CS2 prop research built end to end on the KashRock API (https://vantah.lol) Featured on / listed at: Esports Killer, Product Hunt, RapidAPI, MCP Registry, Glama, MCP Market. ## Community and support Support, status alerts, bug reports and the changelog live in the KashRock Discord: https://discord.gg/PPxHhahMau Account, billing and licensing questions: support@kashrock.com Ask in Discord for anything about using the API. A question there reaches the people who build it. Email support for anything tied to your account.