USL Mix Stats

Public API

Read-only JSON with permissive CORS: any community site can build its own mix ELO or stats pages on top of the same data. No key needed for GET.

Read endpoints

GET/api/usl-mix/games

Recent games newest first, each with a full player list - alias, side, class, kills, deaths, shots fired / landed, accuracy, bio dart hits, the heal split, opening kills, per-weapon kills, and the captain / shotcaller / vocal flags. Enough to build league-wide aggregates without a detail fetch per game.

limitdefault 25, max 100
offsetpagination offset
kindmix | pub | test | all (default mix + pub)
mapmap key, e.g. els, kp, apollo
aliasonly games this player was in
sinceISO date lower bound
ratedlegacy. Every mix is rated now, so kind=mix is the filter you want - rated=true additionally drops the handful of mixes played before the opt-in was retired
curl "https://www.freeinf.org/api/usl-mix/games?kind=mix&limit=5"
GET/api/usl-mix/games/{id}

One game in full: teams, every player row (kills, deaths, class time, shots fired / landed, accuracy, per-weapon hits, heals, weapon kills, rating change) and every kill event with weapon attribution.

idgame UUID or the zone script match_id
curl "https://www.freeinf.org/api/usl-mix/games/<uuid>"
GET/api/usl-mix/players

Rating leaderboard (mix games) merged with career totals (mix + pub).

limitdefault 50, max 200
minGamesminimum rated games, default 3
sortrating | games | kd | winrate | kills
qalias search
curl "https://www.freeinf.org/api/usl-mix/players?minGames=5&limit=20"
GET/api/usl-mix/players/{alias}

One player: rating + peak, record, career K/D and accuracy, class / weapon / map breakdowns, full rating history, recent games.

gamesrecent games to include, default 20
curl "https://www.freeinf.org/api/usl-mix/players/SomeAlias"
GET/api/usl-mix/insights

Aggregates for charts: Titan vs Collective win rate overall and per map, class win rate / K-D / accuracy per class (and a class × map matrix), kills by weapon with LAW shrapnel rolled up to the LAW.

maprestrict class/weapon stats to one map
kindmix | pub | all
curl "https://www.freeinf.org/api/usl-mix/insights?map=els"
GET/api/usl-mix/leaders

Top players over a rolling window: per-player totals (kills, K/D, hits, accuracy, heals, opening kills, rating change), category leaders, the best player in every class, and single-game records (most kills / opening kills / hits, best accuracy, most healing in one game).

periodweek | month | year | all (rolling 7 / 30 / 365 days; default week)
kindmix | pub | all
maprestrict to one map
minGamesgames needed for the K/D, win-rate and per-class rankings; default depends on period: week 5, month 12, year 22, all 22
boardflat mode, one ranked list: kills | kd | kills_per_game | win_rate | heal | hits | accuracy | opening_kills | rating_gain | class:<name> | record:<kills|opening_kills|hits|accuracy|heal>
limitentries in flat mode, default 10, max 100
curl "https://www.freeinf.org/api/usl-mix/leaders?period=month"
curl "https://www.freeinf.org/api/usl-mix/leaders?board=kills&period=week&limit=10"
curl "https://www.freeinf.org/api/usl-mix/leaders?board=class:medic&period=all"
GET/api/usl-mix/ratings

Current rating, games and provisional flag for a batch of aliases, in the order asked. Unknown players come back at the base rating with 0 games. The zone calls this during the draft to show captains the team averages.

aalias, repeatable: ?a=G&a=THE (max 64)
aliasescomma list alternative
formattext - one line per alias: alias, rating, games (tab separated)
curl "https://www.freeinf.org/api/usl-mix/ratings?a=G&a=THE%20MOUNTAIN"
GET/api/usl-mix/live

Live snapshot of every USL arena that reported in the last ~2.5 minutes: the player list grouped by team (side T/C from the '- T' / '- C' suffix, spec and np as their own teams), each player's class and whether they are spectating, captains, the game clock / score, the draft state, and the in-game ticker lines as a spectator sees them. Zones post once a minute; responses are cached 20s. Same shape as /api/live (which also carries CTF).

freshseconds of silence tolerated before an arena is dropped, 30..600 (default 150)
empty1 to include arenas with nobody in them
curl "https://www.freeinf.org/api/usl-mix/live"
# -> { success, generated_at, fresh_window_s, arenas: [ { game, zone, arena, map, players_total, players_playing,
#      state: { running, mode, label, time_left_ms, score: [{ team, side, kills }] },
#      tickers: [{ idx, text, remaining_cs }],
#      teams: [{ name, side, kills, players: [{ alias, class, spec, captain? }] }],
#      mix: { label, phase, team_size, captains, turn, pool, teams } | null, updated_at, age_s } ] }

Ingest (game server → site)

POST/api/usl-mix/ingest

Called by the USL zone script at Game.End. Requires the shared ingest key as x-api-key, Authorization: Bearer, or auth_key in the body. Re-posting the same match_id is a no-op (idempotent). action=test returns an echo without storing anything.

{
  "action": "game_result", "schema_version": 1, "script_version": "1.0.0",
  "match_id": "9d1f...", "zone_name": "USL - Megamaps", "arena_name": "Arena 1",
  "level_file": "uslMegamap2.lvl", "map_key": "els",
  "game_kind": "mix", "team_size": 8, "rated": true, "first_pick_team": "Bes - T",
  "started_at": "2026-09-05T20:00:00Z", "ended_at": "2026-09-05T20:18:12Z",
  "duration_seconds": 1092, "end_reason": "mercy",
  "teams": [
    { "name": "Bes - T", "side": "T", "kills": 61, "deaths": 31, "result": "win", "captain": "Bes", "shotcaller": "Bes", "player_count": 8 },
    { "name": "Axi - C", "side": "C", "kills": 31, "deaths": 61, "result": "loss", "captain": "Axidus", "shotcaller": null, "player_count": 8 }
  ],
  "players": [
    { "alias": "Bes", "side": "T", "team_name": "Bes - T", "result": "win",
      "is_captain": true, "is_shotcaller": true, "is_vocal": true,
      "primary_class": "Marine", "classes": { "Marine": 1080 },
      "kills": 12, "deaths": 3, "team_kills": 0, "kills_scoreboard": 12, "deaths_scoreboard": 3,
      "shots_fired": 240, "shots_landed": 96, "accuracy": 40.0, "bio_dart_hits": 0,
      "heal_amount": 0, "heal_uses": 0, "play_seconds": 1092,
      "weapon_kills": { "1004": { "name": "LAW", "count": 4 }, "1000": { "name": "Assault Rifle", "count": 8 } },
      "weapon_deaths": {} }
  ],
  "kill_events": [
    { "t": 15230, "killer": "Bes", "victim": "Axidus", "killer_side": "T", "victim_side": "C",
      "killer_class": "Marine", "victim_class": "Medic",
      "weapon_id": 1117, "weapon_name": "Shrapnel", "root_weapon_id": 1004, "root_weapon_name": "LAW",
      "team_kill": false, "kill_type": "Player", "attribution": "matched", "x": 17120, "y": 23504 }
  ]
}

How the rating works

  1. Everyone starts at 1200. Every mix counts - the old per-game captain opt-in is retired, because letting captains choose made the record self-selected rather than merely thin. Casual (pub) games and test snapshots are stored for stats but never move ratings, and a ref can still exclude a one-off with *mix scrim.
  2. Team strength = mean rating of its players. Expected score E = 1 / (1 + 10^((Ropp − Rteam) / 400)).
  3. Base change = K × (S − E) × margin, K = 48 for a player's first 10 games, 32 after. The margin multiplier reaches 1.5× at a 40-kill blowout between evenly matched teams, and fades to nothing as the winner's expected score rises - a favourite stomping an underdog earns no bonus, so stacking a team gains you nothing and nobody ever profits by killing less.
  4. Each captain gets a flat +1 on a rated mix, win or lose. Deliberately negligible - a thank-you for stepping up, not a reason to captain.
  5. Fairness adjustment. In 8v8 the weakest player often decides the game, so each player's change is scaled by their impact (kills − deaths + heals/150) measured against what their class time normally produces: a medic is judged as a medic, a marine as a marine, and a player who switched mid-game against the blend of their minutes in each. The residual is compared within their own team, so a carry gains more on a win and loses less on a loss; a passenger gains less and loses more. Clamped to 0.6×–1.4×.
  6. Every change is logged with E, K and the performance multiplier, and the whole history can be replayed after the constants are tuned.

Individual ratings are not published. The API still returns rating, rating_delta and friends, but a public ladder was pushing mixes toward slow, play-not-to-lose games in a scene that logs on for casual organised play. If you are building on this data, please don't render per-player ratings - everything else (kills, classes, accuracy, heals, opening kills, hits) is fair game. Ratings exist to balance drafts, and nobody is ranked.

Constants live in src/lib/uslMix/elo.ts. GET /api/usl-mix/admin/recompute returns the live values.

Field notes

  • rated is now true for every mix. It survives as a per-game escape hatch for scrims (*mix scrim in the zone), and zone admins can still flip it afterwards with POST /api/usl-mix/admin/set-rated { game_id, rated }, which replays all ratings. Filter on kind=mix rather than rated.
  • first_pick_team is the team that won the coin flip and drafted first, recorded so first-vs-second-pick win rate is finally measurable. Null on games played before 2026-09-09.
  • Side is read from the team name: "- T" / Titan vs "- C" / Collective.
  • map_key is the megamap sub-map the zone had active (*setmap), else the level file name.
  • kills come from death events (enemy kills only, team kills separate); kills_scoreboard is the server's own counter for cross-checking.
  • weapon_id is the item that actually exploded (may be shrapnel); root_weapon_id walks the item chain back to the launcher, so LAW shrapnel counts as LAW.
  • attribution: matched = a shot by the killer landed near the victim in time and space; fallback = the killer's most recent shot; unknown = no shot found.
  • heal_amount is total healing: MediKit repairs plus bio darts, which heal a flat 30 hp each and were previously counted as zero. heal_medikit and bio_dart_heal carry the split, and bio_dart_hits the raw count. The MediKit half is the HP that was missing on nearby team-mates when a heal fired, so it is a potential figure; the dart half is exact. The same field means the same thing on every endpoint - don't add darts on again yourself.
  • is_captain / is_shotcaller / is_vocal are per-player-per-game flags. Shotcaller (?sc) and vocal (?v) are self-declared in the zone and are badges only - they never touch ratings.
  • GET responses are cached for 30–60 seconds.