Setting up LoL Champ Tracker with the official Riot API
Everything from first download to a working sync: installing on both platforms, creating a Riot developer key, what each endpoint returns, how rate limits are respected, and what to do when something returns a 403.
1. How it all fits together #
LoL Champ Tracker is an Electron desktop app. It has exactly one data source — Riot's official developer API — and one place it writes to, a JSON file on your own disk. There is no server in the middle, no account to create, and nothing scraped from a website.
You Electron main process Riot
─── ───────────────────── ────
Riot ID ──▶ account-v1 ─────────────────▶ PUUID
+ API key match-v5 /ids ──────────────▶ last 100 match IDs
match-v5 /{id} ──────────────▶ full match detail ✕N
│
├─ compute mark (locally, no network)
├─ detect duos (locally, no network)
▼
stats-db.json ◀── your disk, nothing else touches it
│
▼
Renderer window (no network access at all)
Two details matter for everything below. First, every network request happens in the
main process — the window itself runs under a content-security policy of
default-src 'self', so even if something went wrong in the UI it could not phone
home. Second, the app never sees a key that isn't yours; it authenticates as
you, against your own rate limit.
2. Installing the app #
Neither build is code-signed — this is a personal project with no certificate behind it — so both operating systems will show a warning the first time. Steps for clearing that are below, and covered in more detail under Gatekeeper & SmartScreen.
Windows 10 & 11 (64-bit)
- Download
LoL-Champ-Tracker.exefrom the download section. - Put it wherever you like — Desktop is fine. It's a portable single file: there is no installer, no Start-menu entry and nothing written to the registry.
- Double-click it. SmartScreen will say "Windows protected your PC" — click More info, then Run anyway.
- The app unpacks to a temporary folder and opens. First launch takes a few seconds; later ones are quicker.
.exe with a newer one. Close the app first — a
running portable build locks its own file, and the copy will fail with "file in use".
Your match database lives elsewhere and is untouched by this.
macOS (Apple Silicon & Intel)
- Open the
.dmgand drag LoL Champ Tracker into your Applications folder. - The first launch must be right-click (or Control-click) → Open, then Open again in the dialog. A plain double-click is refused, because the app is unsigned and un-notarised.
- If macOS instead says the app "is damaged and can't be opened", that's the quarantine flag rather than real damage — see Gatekeeper for the one-line fix.
.dmg isn't shipping on this site at the moment. The
build-from-source guide produces exactly the same app on
your own Mac in two commands, and takes a couple of minutes.
First run
The window opens with the full champion grid greyed out and "Not synced yet" in the header. Nothing will populate until you add a Riot ID and an API key — that's the next section.
3. Getting a Riot API key #
Riot doesn't allow public apps to ship a shared key, and this app has no server to hide one behind — so it uses your key, stored only on your machine.
The key you actually want is a Personal API key, and you get it by registering a product on the developer portal. There's also a development key on the dashboard that works immediately but dies every 24 hours — fine for a first test, miserable to live with.
3.1 Which key you need #
| Key type | Expires | How you get it | Rate limit |
|---|---|---|---|
| Personal ← this one | No | Register Product on the portal, then wait for review. No domain verification needed. | 20 req / sec, 100 req / 2 min |
| Development | Every 24 hours | Already on your dashboard the moment you log in. No application. | Same as personal |
| Production | No | Full application — needs a working prototype and domain verification via a riot.txt file. Review takes roughly two weeks. |
Far higher; irrelevant here |
Personal and development keys carry the same rate limit, so a personal key buys you exactly one thing: it stops expiring. That's the whole reason to bother. Production is for public apps with real users — this app has no users but you, so don't apply for it.
3.2 Register a product for a Personal key #
- Go to developer.riotgames.com and Login with the Riot account you play on. There's no separate developer signup.
- Accept the developer terms if prompted.
- On the main portal page click Register Product.
- Choose the Personal product type (not Production).
- Fill in the form — field by field below — and submit.
- Wait. Riot's Developer Relations team reviews it manually.
What to put in each field
| Field | What to enter |
|---|---|
| Product type | Personal. Picking Production drags you into prototype demos and domain verification you don't need, and it will be rejected for a tool only you use. |
| Product name | Something plain and literal — LoL Champ Tracker (personal). This isn't a marketing pitch; a reviewer should understand what it is from the name alone. |
| Game | League of Legends only. Don't also tick Teamfight Tactics, Legends of Runeterra or VALORANT — requesting access you demonstrably don't use invites questions and slows review. |
| Product description | The field that decides your application. Be specific and honest: who uses it, what it does, which endpoints it calls, where it runs, and where the data ends up. A vague one-liner is the most common reason a personal application stalls. Copy-paste-ready example below. |
| Product URL / website | Your GitHub repo is ideal — it lets a reviewer read the actual code. If you have nothing to link, say so plainly ("Local desktop app, not published anywhere") rather than inventing a URL or linking something unrelated. |
| Expected traffic / rate limit | The default personal limits are plenty. If asked for numbers: a few hundred requests a day at most, in short bursts when syncing. |
A personal desktop application (Electron) that I use to review my own League of Legends match history. It calls account-v1 to resolve my Riot ID to a PUUID, then match-v5 to fetch my recent matches. Matches are stored in a JSON file on my own computer. It computes a per-game performance score by comparing my stats against the other participants in the same match, and shows per-champion win rates. It is used only by me. There is no website, no public deployment, no user accounts, and no server — the app runs entirely on my machine and does not redistribute or display Riot data to anyone else. Source: https://github.com/furkannurovic/lolChampTracker
Riot revises this form from time to time, so a label may not match word-for-word. The substance is stable: what it is, who uses it, which game, where it lives.
3.3 The development key (works right now) #
While you wait for the personal key — or if you just want to try the app once — the
dashboard at developer.riotgames.com
always has a Development API key on it, right under your account details.
It starts with RGAPI-. Click Regenerate API Key if the field is
empty or expired, then copy it.
It behaves exactly like a personal key in this app. It just dies after 24 hours, and the instant you regenerate it.
3.4 Put the key in the app #
- In the app, click Change next to the player name.
- Fill in your Riot name, your #tag (without the
#— the field adds it) and pick your region. - Paste the key into the
RGAPI-…field. - Press Test. A working key answers with "Key works — yourname #tag found." Anything else is listed under API errors.
- Press Save, then Sync.
Your Riot ID is the name#tag pair shown in the League client, not your old
summoner name. If you're unsure of the tag, it's in the client under your profile — it's often
your region code (#EUNE, #NA1) but it can be anything.
3.5 When a key stops working #
A development key dies 24 hours after it's issued, and the moment you regenerate it. When that happens the app stops syncing and shows a 403. The fix is always the same:
- Open the portal, click Regenerate API Key.
- Copy the brand-new
RGAPI-…value. - Change → paste over the old key → Test → Save.
If a personal key starts returning 403, expiry isn't the cause — it doesn't expire. Check that it wasn't regenerated on the portal, and that your product wasn't deactivated for inactivity.
Nothing you've already synced is lost — the local database is independent of the key. Expiry only blocks fetching new games.
4. Regions & regional routing #
Riot's newer endpoints (account-v1 and match-v5) aren't addressed by
your game server. They use four regional routing values, and your platform
maps onto one of them. Pick the wrong one and you get a 404 even though the Riot
ID is spelled perfectly — this is the single most common setup mistake.
The app does this mapping for you from the region you pick in the dropdown:
| Routing value | Platforms it covers | Host used |
|---|---|---|
| europe | EUNE, EUW, TR, RU | europe.api.riotgames.com |
| americas | NA, BR, LAN, LAS | americas.api.riotgames.com |
| asia | KR, JP | asia.api.riotgames.com |
| sea | OCE, VN, PH, SG, TH, TW | sea.api.riotgames.com |
The region dropdown in the app offers EUNE, EUW, NA, KR, BR, TR, OCE, LAN, LAS, RU, JP and VN. Anything it doesn't recognise falls back to europe, so if you play somewhere exotic and get a 404, that fallback is why.
eun1.api.riotgames.com. The two
look interchangeable and are not. This app only ever calls regional hosts, so if
you're testing by hand, use the ones in the table above.
5. The endpoints the app calls #
Three authenticated Riot endpoints, plus one public CDN. Every one of them is a
GET, and every authenticated one carries your key in a header — never in the
query string.
X-Riot-Token: RGAPI-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
5.1 account-v1 — Riot ID → PUUID #
Everything starts here. Riot's match endpoints are keyed by PUUID, a stable per-account identifier, so the name and tag you typed have to be resolved first.
https://{regional}.api.riotgames.com/riot/account/v1/accounts/by-riot-id/{gameName}/{tagLine}
Both path parts are URL-encoded by the app, so spaces and non-Latin characters in a name are fine. A successful response is small:
{
"puuid": "kY3f…a very long opaque string…9Qz",
"gameName": "yourname",
"tagLine": "tag"
}
The app caches that PUUID against your Riot ID, so later syncs skip this call entirely unless you change who you're tracking.
5.2 match-v5 — the list of match IDs #
https://{regional}.api.riotgames.com/lol/match/v5/matches/by-puuid/{puuid}/ids?start=0&count=100
Returns a plain array of match-ID strings, newest first, e.g.
["EUN1_3612345678", "EUN1_3612300000", …]. count is capped at
100 by Riot, and the app always asks for the maximum.
start=0 only — so a single sync sees your latest 100
matches, never older ones. Because everything already fetched stays in the local
database, your history still grows without limit going forward; you just can't
back-fill games older than the 100 most recent at the moment you first sync. Riot's
match-v5 archive is itself finite, so very old games aren't retrievable
regardless.
5.3 match-v5 — the match detail #
https://{regional}.api.riotgames.com/lol/match/v5/matches/{matchId}
This is the expensive one — one request per game, and where the sync budget
goes. The response is large; the app keeps only what it needs from
info.participants:
| Field taken | Used for |
|---|---|
championId | Matching the game to a champion (it's the Data Dragon key). |
win | Win/loss records and A-to-Z ticks. |
kills / deaths / assists | KDA, and the biggest single input to the mark. |
totalMinionsKilled + neutralMinionsKilled | CS. |
totalDamageDealtToChampions, goldEarned, visionScore, damageDealtToObjectives | The other mark metrics — read for every participant, not just you. |
teamId / playerSubteamId | Grouping teammates. Arena uses subteams of two. |
riotIdGameName / riotIdTagline | Naming your duo partners. |
profileIcon | Fetching your summoner picture. |
info.queueId | The queue label — Solo, Flex, ARAM, Arena, URF, Clash… |
5.4 Data Dragon — champion and summoner icons #
Riot's asset CDN. No API key, no rate limit — it's a plain static host, and it's the only thing the app talks to besides the API itself.
# current patch list — the first entry is the newest https://ddragon.leagueoflegends.com/api/versions.json # your summoner icon https://ddragon.leagueoflegends.com/cdn/{version}/img/profileicon/{iconId}.png # fallback that is always current, no version needed https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/profile-icons/{iconId}.png
Champion portraits aren't fetched at runtime at all — all 173 are bundled inside the app, so
the grid works with no connection. Only your summoner icon is downloaded, and it's converted
to a data: URL before it reaches the window, which is what keeps the renderer's
strict CSP intact.
6. Testing your key by hand #
If the app reports something you don't believe, go around it. These call the same endpoint the Test button does — substitute your routing value, name and tag.
$key = "RGAPI-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" $name = "yourname" $tag = "tag" Invoke-RestMethod -Uri "https://europe.api.riotgames.com/riot/account/v1/accounts/by-riot-id/$name/$tag" ` -Headers @{ "X-Riot-Token" = $key }
A working key prints your puuid, gameName and tagLine. A dead one throws with the status code in the message.
KEY="RGAPI-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" curl -i -H "X-Riot-Token: $KEY" \ "https://europe.api.riotgames.com/riot/account/v1/accounts/by-riot-id/yourname/tag"
-i prints the status line and headers, which is what you want here — the rate-limit headers come back on every response.
Once that returns a PUUID, you can walk the same path the app does:
# 1. the last 5 match IDs curl -H "X-Riot-Token: $KEY" \ "https://europe.api.riotgames.com/lol/match/v5/matches/by-puuid/$PUUID/ids?start=0&count=5" # 2. one full match curl -H "X-Riot-Token: $KEY" \ "https://europe.api.riotgames.com/lol/match/v5/matches/EUN1_3612345678"
7. Rate limits #
Development and personal keys carry the same small budget. At the time of writing Riot publishes 20 requests per second and 100 requests every 2 minutes for both — the second number is the one that actually constrains a match sync, since each game is a separate request. Getting a personal key stops the daily expiry; it does not give you more headroom. Check the dashboard for the limits attached to your own key; Riot can and does change them.
The app is built around that ceiling rather than fighting it:
| Guard | Value | Why |
|---|---|---|
| Requests per sync | 90 match fetches | Leaves headroom under the 100-per-2-minutes window. |
| Spacing between requests | 130 ms | Roughly 7–8 requests a second — comfortably inside the per-second limit. |
| On a 429 | Stop immediately | The sync ends early and keeps what it already fetched. Nothing is lost; run it again in a minute. |
| Already-stored matches | Never re-fetched | Only genuinely new match IDs cost a request. |
In practice: a first sync on a busy account will hit the 90-game cap and stop. Wait two minutes, hit Sync again, and it continues where it left off. After that, routine syncs are a handful of requests.
8. What a sync actually does #
- Resolve. Riot ID → PUUID via
account-v1, unless a cached PUUID for that exact Riot ID already exists. - List. Fetch the latest 100 match IDs and subtract everything already in the database. What's left is new.
- Fetch. Pull each new match in turn, up to the 90-request cap, pausing 130 ms between them.
- Back-fill. If budget remains, re-fetch old matches that predate a feature — games stored before v1.2.0 have no mark or teammate data — so history heals itself over a few syncs.
- Derive. Compute each game's mark, group teammates, tally anyone who appears on your side twice or more into duo partners. All local, no requests.
- Icon. If your summoner icon changed, download it from Data Dragon and store it as a
data:URL. - Save. Write
stats-db.jsonand refresh the UI.
The status line under your name reports the outcome — "+6 new, 12 back-filled via Riot API" — or the exact error if something failed. A sync already in progress can't be started twice.
9. How the mark is computed #
No external "average player" data is involved. Each game is scored purely against the other players who were in it.
- For every participant, seven metrics are computed: KDA, kill participation, damage to champions, gold, CS, vision score and objective damage.
- For each metric you get a percentile rank among all participants — 0 = worst in the lobby, 1 = best. Ties share the average rank.
- Those ranks are blended with fixed weights: KDA 24%, kill participation 20%, champion damage 18%, gold 12%, CS 10%, vision 10%, objective damage 6%.
- A small nudge is applied: +0.05 for a win, −0.03 for a loss, plus +0.06 for a pentakill or +0.03 for a quadra.
- The result is clamped to 0–1, multiplied by 10, and mapped to a letter: S+ ≥ 9.0, S ≥ 8.0, A ≥ 7.0, B ≥ 5.8, C ≥ 4.2, otherwise D.
Arena is handled specially — teammates come from playerSubteamId (squads of two)
rather than teamId, so duo detection and kill participation behave sensibly.
10. Where your data lives #
Everything — matches, settings, your API key, your cached PUUID — sits in one JSON file:
| Platform | Path |
|---|---|
| Windows | %APPDATA%\lol-champ-tracker\stats-db.json |
| macOS | ~/Library/Application Support/lol-champ-tracker/stats-db.json |
It's plain, readable JSON — open it, back it up, sync it between machines, or delete it to start over. The app rewrites it after every sync.
What's in it
- matches — one entry per game: champion, result, KDA, CS, duration, queue, position, mark, and the teammates you were grouped with.
- player — your Riot name, tag, region, and your summoner icon as an embedded
data:URL. - riot — your API key in plain text, plus the cached PUUID.
Deleting the file resets the app completely: no matches, no key, no Riot ID. Nothing is recoverable from anywhere else, because nothing was ever uploaded anywhere.
11. Troubleshooting #
11.1 API errors #
These are the messages the app surfaces, and what each one really means.
| Status | What the app says | What to do |
|---|---|---|
| 403 | "Riot rejected the key (403). Development keys EXPIRE every 24h…" | By far the most common one, and on a development key it's almost always expiry — it aged out, or you regenerated it and pasted the old value. Get a fresh key from the portal and re-paste it. On a personal key, which never expires, look instead at whether it was regenerated or the product deactivated (§3.5). |
| 401 | "Riot got no valid key (401)." | The key field is empty or mangled. Make sure you copied the whole thing including the RGAPI- prefix — a partial paste is the usual cause. |
| 404 | "Riot ID not found for this region." | Name, tag or region is wrong. Check the spelling of both, and confirm the region maps to the routing value you expect (see §4). Wrong region is more often the culprit than a typo. |
| 429 | "Riot API rate limit hit — wait a minute and sync again." | Expected on big first syncs. Everything fetched so far is saved. Wait ~2 minutes and sync again. |
| 400 / 415 | "Riot rejected the request." | Usually a stray character in the name or tag field — a leading space, or a # typed into the tag box. |
| 5xx | "Riot API error (HTTP 5xx)." | Riot's side. Nothing to fix locally — try later. |
| — | "Riot API request timed out" | Network or firewall. The app allows 15 seconds per request. Check that nothing is blocking *.api.riotgames.com. |
RGAPI-.
You've probably pasted a PUUID, a summoner name, or an incomplete clipboard.
11.2 Gatekeeper & SmartScreen #
Both builds are unsigned. This is a warning about provenance, not about the contents — but you should only clear it for a binary you got from a source you trust, or better, build it yourself.
Windows — "Windows protected your PC"
Click More info, then Run anyway. If the button isn't there, right-click the .exe → Properties → tick Unblock at the bottom → OK.
macOS — "can't be opened because Apple cannot check it for malicious software"
Right-click (or Control-click) the app → Open → Open. This only needs doing once.
macOS — "is damaged and can't be opened. You should move it to the Trash."
Nothing is damaged. macOS attaches a quarantine flag to anything downloaded from the internet, and for unsigned apps that message is what you get. Strip the flag:
xattr -dr com.apple.quarantine "/Applications/LoL Champ Tracker.app"
Then open it normally. An app you built yourself on your own Mac never picks up the flag in the first place.
11.3 App behaviour #
| Symptom | Cause & fix |
|---|---|
| Sync says "no new games" | Working as intended — everything in your last 100 matches is already stored. Play a game and sync again. |
| Older games have no mark | They were stored before v1.2.0. Each sync back-fills a batch of them; run a few syncs and they'll fill in. |
| A duo partner is missing | Duos need two or more games together in your stored history. One shared game looks identical to a random teammate. |
| A random player showed up as a duo | Possible but rare — it means you were genuinely matched with the same account twice. Common on small servers. |
| No summoner icon | The icon downloads on the sync after the app first sees your profile icon ID. Sync once more. |
| Champion missing from the grid | The bundled champion list is from a fixed patch. A champion released after that patch needs a rebuild — see §12. |
Can't overwrite the .exe |
The running app locks its own file. Close every instance first (Task Manager → end LoL-Champ-Tracker if needed), then copy. |
12. Building from source #
This is how you get a macOS build, and the honest answer to "why should I run your unsigned binary?" You need Node.js LTS and nothing else — no Xcode, no Visual Studio, no native compilation.
# from the project folder
npm install
# run it live, without packaging npm start # build the portable exe -> dist/LoL-Champ-Tracker.exe npm run dist:win
The portable target skips code-signing automatically — you'll see "no signing info identified" in the log, which is expected and not an error.
# run it live, without packaging npm start # build .dmg + .zip for Apple Silicon AND Intel -> dist/ npm run dist:mac
Output lands in dist/ as
LoL-Champ-Tracker-1.2.0-arm64.dmg and
LoL-Champ-Tracker-1.2.0-x64.dmg, plus matching .zip files.
Signing is disabled in the build config, so no Apple Developer account is needed.
.dmg from
Windows or Linux. That's the entire reason this site doesn't ship one.
Refreshing the champion list
The bundled champion data is pinned to a patch. When Riot releases a new champion, regenerate the list and its icons — this hits Data Dragon only, so no API key is involved:
node fetch-data.js # rewrites champions.json + champions-data.js, downloads new icons node make-icon.js # regenerates the app icons (only if you changed them)
Project layout
| File | Role |
|---|---|
main.js | Electron main process — window, IPC handlers, all networking. |
riot.js | Every Riot API call, the mark calculation, Data Dragon fetches. |
db.js | The JSON database — storage, roll-ups, duo detection. |
preload.js | The window.lol bridge. The renderer's only route to anything. |
renderer.js / index.html / styles.css | The UI. No network access of its own. |
champions-data.js | All champions, embedded as a global — a file:// page can't fetch() a local JSON file. |
13. Riot's rules #
Using the API means agreeing to Riot's developer terms. The parts that matter for a tool like this one:
- Your key is yours. Don't share it or ship it inside anything you distribute. This app deliberately has no bundled key for that reason.
- Respect the rate limits. Don't work around a 429 by hammering it — back off and retry, which is what the app does.
- Don't sell it or gate it. Riot's terms rule out charging for access to their data.
- Nothing that affects gameplay. Reading your own match history after the fact is fine; anything that acts during a live game is not.
- Display the disclaimer if you publish something built on the API — the one in this site's footer is the standard wording.
The current terms are at developer.riotgames.com/policies/general, and the full endpoint reference at developer.riotgames.com/apis. Riot changes both from time to time; they're the authority, not this page.
Still stuck? The app prints the exact message Riot sent back — Riot's own wording is usually in the status line, after the app's explanation. That text is the fastest thing to search for.