MetaRoll

Champions Usage API

The game's own ranked ladder — all 235 ranked Pokémon with their moves, held items, abilities, natures, Stat Point spreads and common partners — as static JSON. No key, no rate limit, CORS open to any origin.

Basehttps://metaroll.app/api/v1

Endpoints

Six files. Every one repeats the fields of index.json, so a single request tells you both the data and how old it is. The last of them ranks players rather than Pokémon — see Trainer ladder.

GET/api/v1/index.json 641 B What this is, when it was captured, what the movement is measured against.
GET/api/v1/seasons/{season}/… as above A season that has ended, as a whole copy of this same tree. Ask index.json for pastSeasons to know which exist.
GET/api/v1/ranking.json 17 kB The ladder alone: rank, id, name and previous rank for all 235.
GET/api/v1/pokemon/{id}.json ≈7 kB One Pokémon in full — every distribution the game publishes for it.
GET/api/v1/ladder.json 346 kB The trainer ladder: the top 1000 players, with rating, record and country.
GET/api/v1/singles/ladder.json 346 kB The same, for the singles ladder. Published here and shown nowhere on this site.
GET/api/v1/all.json 1.3 MB Every entry in one file, for when you want the lot.
ranking.json 17 kB
one Pokémon 7 kB
all.json 1.3 MB

Drawn to scale. Seeing who sits where costs a seventy-sixth of the whole file; looking one Pokémon up costs a two-hundredth. That is the only reason the data is split at all.

Fetching

BASE=https://metaroll.app

curl -s $BASE/api/v1/index.json

curl -s $BASE/api/v1/ranking.json \
  | jq -r '.entries[:5][] | "\(.rank)  \(.name)"'

curl -s $BASE/api/v1/pokemon/kingambit.json | jq '.entry.moves[:3]'
// Works from a browser on any origin — Access-Control-Allow-Origin: *
const res = await fetch('https://metaroll.app/api/v1/pokemon/kingambit.json')
const { entry, capturedAt } = await res.json()

entry.rank          // 1
entry.previousRank  // 1
entry.moves[0]      // { name: "Sucker Punch", percent: 99.3 }

Naming a Pokémon

An id is the name lowercased, with every run of non-alphanumeric characters collapsed to a single hyphen. Build the path from a name you already have — there is no lookup step.

Kingambit            → kingambit
Tauros-Paldea-Aqua   → tauros-paldea-aqua
Mr. Rime             → mr-rime
Ninetales-Alola      → ninetales-alola

Alternate forms are separate entries with separate ids. The ranking really does list four Gourgeist sizes and six Rotom forms in different places.

A full response

GET /api/v1/pokemon/tauros-paldea-aqua.json — trimmed to the first item of each list.

{
  "version": 1,
  "game": "Pokémon Champions",
  "source": "in-game ranked ladder",
  "format": "doubles",
  "season": "M-5",
  "pastSeasons": [],
  "capturedAt": "2026-08-17T06:27:02.238Z",
  "comparedTo": "2026-08-16T14:01:20.495Z",
  "count": 235,
  "entry": {
    "id": "tauros-paldea-aqua",
    "name": "Tauros-Paldea-Aqua",
    "rank": 139,
    "previousRank": 137,
    "rankHistory": [{ "d": "2026-08-16", "r": 137 },
                     { "d": "2026-08-17", "r": 139 }],
    "usageHistory": [{ "d": "2026-08-17",
                       "m": { "Protect": 77.2 },
                       "i": { "Life Orb": 18.7 },
                       "a": { "Intimidate": 54.6 } }],
    "abilities": [{ "name": "Intimidate", "percent": 54.6 }],
    "items":     [{ "name": "Life Orb", "percent": 18.7 }],
    "moves":     [{ "name": "Protect", "percent": 77.2 }],
    "natures":   [{ "name": "Adamant", "percent": 60 }],
    "teammates": [{ "name": "Farigiraf", "percent": null }],
    "matchups": {
      "beats":     ["Sneasler", "Sylveon"],
      "beatsWith": [{ "name": "Earthquake", "percent": 48.2 }],
      "losesTo":   ["Basculegion", "Archaludon"],
      "beatenBy":  [{ "name": "Blizzard", "percent": 6.2 }]
    },
    "spreads": [{
      "statPoints": { "hp": 2, "atk": 32, "def": 0,
                       "spa": 0, "spd": 0, "spe": 32 },
      "percent": 29.1
    }],
    "confidence": {
      "formGuessed": false,
      "identifiedByElimination": false
    }
  }
}

Field reference

On every response

versionnumber The contract version. 1 will not change shape; a breaking change becomes /api/v2.
capturedAtstring · ISO 8601 When this reading of the ladder was taken. Use it to decide whether to re-fetch.
comparedTostring · ISO 8601 · optional The capture previousRank is measured against. Absent when there was nothing to compare.
formatstring Which ranked ladder this is. Always "doubles": the game ranks doubles and singles separately, and this data is read from the doubles ladder only.
countnumber How many Pokémon the ladder ranked. 235 for season M-5.
seasonstring The in-game season this reading belongs to, as the game names it — "M-5". Seasons run about four weeks and the ranking restarts with each one, so a capture from one season is not comparable with a capture from another.
pastSeasonsstring[] Seasons that have ended and are still served, oldest first. Each is kept at /api/v1/seasons/{season}/ exactly as it stood on its final capture. Empty until the first season ends.

On an entry

ranknumber Position on the ladder, 1 being the most used.
previousRanknumber | null · optional null means it was not in the previous capture. The field is absent entirely when there is no previous capture — so you can tell a new arrival from an unknown.
rankHistory{ d, r }[] · optional The rank this Pokémon held on each day of the season, oldest first: d the UTC day, r the rank. One entry per day, ending on the day of this capture, and the last r is always rank. A day it was not ranked is missing rather than null. Present on /api/v1/pokemon/{id}.json only — it is left out of all.json and ranking.json to keep them small — and absent from files published before it existed.
usageHistory{ d, m, i, a }[] · optional What this Pokémon was played with on each day of the season, oldest first: d the UTC day, then m moves, i items and a abilities, each an object of name to percentage. Only the head of each list is kept — eight moves, six items, three abilities — and a name missing from a day was not in that day's head. One entry per day, the last being this capture's reading, and a new season starts a new series because the percentages are of a different ladder. Present on /api/v1/pokemon/{id}.json only, and absent from files published before it existed.
abilities · items · moves · natures · teammates{ name, percent }[] Ordered most-used first. percent is a share of that Pokémon's appearances, not of the ladder.
matchups{ beats, beatsWith, losesTo, beatenBy, beatsGames, losesToGames } beats and losesTo are species names, most-frequent first. The two move lists belong to different Pokémon: beatsWith are this Pokémon's own moves, and beatenBy are the moves its opponents beat it with. Their percentages are a share of those wins or losses, not a win rate. beatsGames and losesToGames are the number of games behind each entry, aligned with the list beside them — beatsGames[2] belongs to beats[2]. They are games won or lost, not games played: a pair's total is the figure from both directions. Absent on a reading captured before the game began publishing them, and null per row where it published none.
spreads{ statPoints, percent }[] Stat Point distributions, most-used first. See the warnings below — these are not EVs, and they carry no nature.
confidence{ formGuessed, identifiedByElimination } Both are false on current data: the game serves the form outright, so nothing is inferred. Kept for readers who already check them.

Trainer ladder

/api/v1/ladder.json ranks players, not Pokémon. Same capture, same ranked doubles ladder, a different thing counted — so it carries its own capturedAt and its own comparedTo, and moves far more than the Pokémon ranking does: 789 of the thousand changed rank between two readings a day apart.

The game shows the top 300 in its own client. It downloads a thousand, and a thousand is what is published here.

There is a ladder-top.json beside each of these and it is not listed above on purpose. It is the ten rows the front page shows instead of pulling the whole file, which makes it a decision about a layout rather than a dataset — naming it here would freeze "ten" into a contract that only /api/v2 could change. It is reachable, it is simply not promised. Slice entries and pick your own number.

Singles is at /api/v1/singles/ladder.json, in the same shape, with "format": "singles" in its head. It is a genuinely different ladder rather than the same players sorted twice: across one capture the two share 7 trainers out of a thousand, have different leaders, and singles rates higher at the top — 2383 against 2344.

It exists because it costs nothing. A cold start opens the client on Singles and the capture walk switches it to Doubles, so both trainer files arrive in one capture and only one of them used to be read. Nothing on this site shows it, and there is no singles usage data for the same reason there is no cost here: that one would need its own walk through the client every hour.

{
  "version": 1,
  "source": "in-game ranked ladder",
  "format": "doubles",
  "ranks": "trainers",
  "capturedAt": "2026-08-19T08:16:09.000Z",
  "comparedTo": "2026-08-18T06:25:09.000Z",
  "count": 1000,
  "entries": [
    {
      "rank": 1,
      "previousRank": 4,
      "id": "biAcxGh7EW2hA1lfddc8abf5bd2cbbae",
      "name": "Nontaro",
      "rating": 2275.236,
      "wins": 262,
      "losses": 154,
      "draws": 0,
      "battles": 416,
      "winRate": 63.0,
      "country": { "code": 509, "name": "Thailand", "flag": "🇹🇭", "iso": "th" },
      "language": { "code": 20, "name": "English", "flag": "🇺🇸", "iso": "us" },
      "avatar": 22
    }
  ]
}

Three things that will catch you

idstring Track a player by this and never by name. Names are not unique — 972 distinct across 1000 rows — and are the reason movement here is keyed on the id.
countryobject code is a region code, not a country code: the leading digit is a continental block, and x99 means "Other" inside it, so seven different codes all mean "Other". The USA holds four (202–205). Group by name, never by code. iso is ISO 3166-1 alpha-2, and is absent on "Other".
winRatenumber · nullable Wins over battles as a percentage, and null rather than zero for a player with no games. Both it and battles are derived — the game stores only W, L and D — and are published so every reader does not divide by zero separately.
ratingnumber Already divided: the game stores it multiplied by a thousand. This is the ladder's sort key.
avatarnumber The Pokémon icon the player picked, in the game's own numbering — which is not the national dex. Published raw; no image is served for it.

Before you build on it

Four things that will cost you an afternoon if you meet them by surprise.

statPoints are Stat Points, not EVs. Champions replaced EVs with Stat Points: 0–32 per stat, 66 across all six. Feed them to an EV formula and every number you produce is wrong, with nothing in the data to signal it.

Spreads carry no nature. The game publishes natures and spreads as two separate distributions rather than as observed pairs — not one of the 7,020 spreads in this capture carries a nature. natures is the whole of what the game says on the subject.

This differs from Smogon usage stats, where nature and spread come paired. Joining them here would invent data that looks measured.

teammates always has percent: null. The game ranks a Pokémon's common partners without quantifying them. That is the source being silent, not a reading that failed.

confidence is now always false, and that is the point. The ladder shows a species' alternate forms under one shared name and picture — all four Gourgeist sizes read No. 711 Gourgeist on screen — so while these entries were read off the screen the forms had to be inferred, and inference was sometimes wrong: two Paldean Tauros were once published under each other's names.

The data the game serves carries the form outright, so nothing is guessed any more. Both flags are kept, and kept false, rather than removed: a field that vanishes breaks whoever was reading it.

Where the data comes from

Pokémon Champions publishes no usage API. It does serve its own Battle Data to its own client, though, and that is what this is: a capture drives the game, records the ranking files it fetches, and decodes them. The numbers below are the ones the game published, not a reading of them — the ids it uses for moves, abilities and species turn out to be the canonical Pokémon indices, and the Stat Point spreads are hexadecimal.

Earlier readings were taken by photographing the Battle Data screens and running OCR over the frames, which is why confidence exists. It is kept, and reports false throughout, because a field that disappears breaks anyone reading it — but there is nothing left to be unsure about.

capturedAt is the moment the reading was taken. Captures run hourly and publish only when something has changed: the game rebuilds this data every fifteen minutes or so, but between two consecutive builds the rank order was identical and only decimals deep in the lists moved.

The ladder itself moves slowly. Over a full day, 98 Pokémon changed position and the largest move was five places.

Using it, and crediting it

Free to use, in anything — a tool, a spreadsheet, a video, a paid product. No key, no sign-up, no quota. Two things asked in return.

Say where it came from. If your project shows these numbers, name MetaRoll as the source and link to metaroll.app somewhere a reader can find it — a footer, an about page, a video description. That is the whole licence. It is asked rather than enforced, and it is what keeps a capture rig running on someone's machine every hour worth doing.

Cache what you fetch. This is served off a small machine on a home connection. Every response carries Cache-Control: public, max-age=300; honouring it costs you nothing, because the data cannot change faster than the capture that produces it. If you need the whole dataset, take all.json once rather than 235 files in a loop.

What the credit cannot do is imply an endorsement. This is an independent fan project reading a game it has no relationship with, and anything built on it inherits that: name MetaRoll as a source, not as a partner, and do not present the numbers as official.