Ratings and rankings
Glicko-2, why the board sorts on a discounted rating, and the sample floors.
Two related surfaces: a team's rating over time, and the leaderboard built from it.
Rating history
GET /v1/{game}/teams/{id}/ratings returns {rating, rd, as_of, match_id} points, oldest-first so
the series plots directly. Default 30 points, max 1000; one point per match the team played.
Naming a window (6m, up to 60m, or all) returns that whole span and raises the default to the
maximum, so ?window=12m is a year of points in one call. match_id is the match that moved the rating
to that point — join it to /teams/{id}/results to say what
happened there.
rating is Glicko-2. rd is the rating deviation — the model's uncertainty. It falls as a team
plays and rises during inactivity, so a returning team has a high rd and its rating should be read
as provisional.
The leaderboard
GET /v1/{game}/rankings?type=team|player over a rolling 3-month window.
Teams rank on glicko2_conservative — the rating discounted by twice its deviation:
value = rating − 2 × rdThat's the important design decision. A team with a high rating the model isn't confident about
sorts below an equally-rated team it is confident about. It rewards demonstrated form over a lucky
run, and it means an inactive team drifts down as its rd grows rather than sitting on a stale
rating forever.
Players rank on rating. Each row reports which via its metric field, so you never have to infer
it.
Sample floors
Entities below a minimum sample are omitted entirely rather than ranked on noise:
- Teams: at least 5 rated matches in the window.
- Players: at least 30 rated maps. A map counts once, from its own per-map rating, and only if it was played on a competitive map; the whole-match summary stored beside a player's maps is not counted.
So absence from the board means "not enough evidence", not "ranked last".
Row fields
rank is 1-based board position. Sorting rows by value reproduces rank exactly.
Teams additionally get:
form— up to 5 recent results as["W","L",…], newest first.rank_delta— movement versus roughly 7 days ago, ordelta_daysago (1–90) when you ask. Positive means moved up.nullmeans the team wasn't on the board then, which is different from "didn't move" (0). The earlier board is rebuilt from the ratings as they stood then, with the deviation inflated for the time since — the same rule as the live board.trend— only with?include=trend: the board metric at each of the last twelve weekly marks, oldest first, ending with the value now. An entry isnullfor a mark where the team had no rating. Absent, notnull, when you do not ask.ratingandrd— only with?include=rating: the Glicko-2 rating and its deviation, each to a tenth, wherevalueisrating - 2 * rd. The deviation is as of now, so it grows while a team is idle — the same step that discountsvaluefor a team that has not played lately. Drawrating ± 2 * rdas a range without a call per team. Absent, notnull, when you do not ask, and never on the player board.external_rank— a separately-sourced world ranking where one exists, reported alongside for comparison. It is not used for ordering.
Sorting and paging
/rankings accepts sort=rank|value|name with a - prefix for descending, and is not
cursor-paginated — limit truncates the computed board.
Re-sorting changes the order rows come back in, never the rank each entity holds. So
?sort=-rank&limit=10 returns the bottom ten with their real ranks, rather than renumbering them
1–10.
The board over time
GET /v1/{game}/rankings/history?weeks=13&top=10 returns the top of the team board at each of the last
weeks weekly marks (1–52, default 13), oldest first, newest being now. Each snapshot lists the top
top teams (1–25, default 10) as {rank, team_id, name, slug, value}.
Each snapshot is the board /rankings would have served at that instant — the latest rating then, its
deviation inflated for the idle time since, and the same activity floor of 5 rated matches in the three
months ending then — so a team appears only if it was active at the time. You always get exactly weeks
snapshots; a mark at which no team met the floor has an empty entries list. It is computed once per ten
minutes and shared across callers, so the newest as_of can be that old.
Round-level figures for the top teams
GET /v1/{game}/rankings/round-stats returns the figures of
/teams/{id}/round-stats for the top teams of the board
in board order, each with its team and rank, in one request. window is a trailing span (default
6m, maximum 12m, no all) and limit is the number of top teams (default 30, max 100). A team with
no recorded rounds in the window reports rounds: 0 and null rates. Also computed once per ten minutes.
Own rank on a detail route
A team or player detail row can carry own_rank, but only when you ask:
curl -H "Authorization: Bearer $ESPORTSODDS_API_KEY" \
"https://api.esportsodds.gg/v1/cs2/teams/natus-vincere?rank=1"It costs a window query over the whole board, so it is off by default. Without the parameter,
own_rank is null meaning "not requested" — not "unranked".