guide
"Build CS2 fantasy projections from player stats"
Jul 10, 2026
Bottom line: A CS2 fantasy projection is career averages fed through a scoring function you define. The API gives you the inputs —
avg_rating,avg_adr,avg_kast,kd_ratio— per player. The one step people miss is that the career-stats route needs a player UUID, not a slug, so you resolve the slug first. The scoring weights are yours to choose; this guide gives you a sensible starting point and the plumbing around it, built on the EsportsOdds CS2 data API.
The pipeline
Four steps: turn a slug into an ID, pull that player's career stats, run them through a scoring function, and you have a projection.
Step 1: Resolve the slug to an ID
Detail routes on the API take UUIDs. You'll usually start from a human-friendly slug like s1mple, so resolve it against the players list, which accepts an exact ?slug=:
import os
import requests
BASE = "https://api.esportsodds.gg"
HEADERS = {"Authorization": f"Bearer {os.environ['ESPORTSODDS_API_KEY']}"}
def player_id(slug: str) -> str:
resp = requests.get(f"{BASE}/v1/cs2/players", params={"slug": slug},
headers=HEADERS, timeout=10)
resp.raise_for_status()
rows = resp.json()["data"]
if not rows:
raise LookupError(f"no player with slug {slug!r}")
return rows[0]["id"]
This slug-to-ID hop is the same one you use for teams and matches — any time a route wants an ID and you have a name, the list endpoint with ?slug= is the resolver.
Step 2: Pull career stats
Now fetch the career averages from /players/{id}/stats. This is a single-resource call (not paginated), and it returns the season-long numbers:
def career(slug: str) -> dict:
pid = player_id(slug)
resp = requests.get(f"{BASE}/v1/cs2/players/{pid}/stats",
headers=HEADERS, timeout=10)
resp.raise_for_status()
return resp.json()["data"]
The fields you'll use are avg_rating, avg_adr, avg_kast, kd_ratio, and matches (how many games back the averages). The averages are nullable — a player with too little history may return null for some — so treat missing values defensively.
Step 3: Score it
Here's where your fantasy format's rules live. The weights below are a reasonable default that leans on rating (the broadest measure of impact) and rewards consistency via KAST, but tune them to your league:
def project(stats: dict) -> float:
rating = stats.get("avg_rating") or 1.0
adr = stats.get("avg_adr") or 70.0
kast = stats.get("avg_kast") or 0.70 # 0–1 fraction
kd = stats.get("kd_ratio") or 1.0
# Weights are illustrative — set these to match your scoring rules.
return round(
rating * 12.0 # overall impact
+ adr * 0.10 # damage output
+ kast * 8.0 # round-to-round consistency
+ (kd - 1.0) * 5.0, # duel win rate, centred on even
1,
)
The or <default> guards matter: they keep a null average from crashing the arithmetic, falling back to a roughly league-average value instead.
Step 4: Rank a roster
Project a list of players and sort:
ROSTER = ["s1mple", "zywoo", "sh1ro", "donk", "m0nesy"]
projections = []
for slug in ROSTER:
try:
projections.append((slug, project(career(slug))))
except LookupError as e:
print(f"skipping: {e}")
projections.sort(key=lambda x: x[1], reverse=True)
for slug, points in projections:
print(f"{slug:10s} {points:6.1f}")
The output is a ranked slate you can drop into a draft tool or a lineup optimiser:
A note on honesty
A projection built from career averages is a baseline, not a forecast of a specific match. It doesn't know about a roster change last week, a map that suits one team, or who's stood in as a substitute. Treat it as a starting estimate and layer context on top — recent form is a good next input. Present it as "projected from career averages," which is exactly what it is.
Next steps
- Generate a full match preview — add head-to-head and recent form around these projections.
- Analyse per-match stats — go below career averages to map-by-map performance.
Resolve the slug, pull the averages, apply your weights, and rank. The endpoint reference documents every career-stats field and which are nullable.