Type “what rank is Jynxzi in R6” into Google and you will get a dozen confident-sounding answers, most of them wrong. One site quotes a 1,159 score from a controller-aim leaderboard and calls it his rank. Another links a tracker profile that has not refreshed in weeks. The honest answer, as of October 17, 2026, is that nobody outside Ubisoft can pull a single verified number and call it Jynxzi’s exact Rank Points (RP) at this moment, because Rainbow Six Siege splits “rank” into two different systems that rarely match.
This tutorial walks through why that gap exists and then builds a small Python tool that estimates a player’s real standing, using Jynxzi as the running example. You will pull live data from a public R6 stats API, map Rank Points to the current tier table, and build a simplified skill estimator based on the same win/loss logic Ubisoft itself has described in its own matchmaking documentation. By the end you will have a working script you can point at any Ubisoft username, not just Jynxzi’s, and a much better answer to “what rank is this player really” than any single tracker site gives you.
Why “What Rank Is Jynxzi in R6” Doesn’t Have One Clean Answer
Jynxzi built his audience on Rainbow Six Siege content, but he is a streamer first and a ranked grinder second. He plays casual lobbies, custom scrims, and off-meta loadouts on stream far more often than he plays a full ranked season. That matters because Siege’s Rank Points only move when you queue into ranked matches. A player who streams 40 hours a week but only plays 20 ranked games that season will show a rank that reflects 20 matches, not 40 hours of actual skill on display.
Clips make this worse. A viral clip of Jynxzi hard-stuck in a 1-vs-5 clutch gets shared without any context about which queue, which season, or which account it came from. Viewers assume the clip reflects his “real” rank right now, when it might be from a smurf account, a custom match, or a season that ended months ago. On top of that, a community leaderboard site ranked Jynxzi 20th among controller players with a score of 1,159 based on aim-duel data, a completely separate metric from Siege’s Rank Points that has nothing to do with his ranked tier. Confusing the two is the single biggest reason “what rank is Jynxzi” answers vary so wildly across the internet.
RP vs. Hidden MMR: The Two Numbers Siege Actually Tracks
Rainbow Six Siege runs on two parallel numbers, and almost every confused “what rank is X” thread online stems from mixing them up. The first is Rank Points (RP), the visible number that determines your Copper-through-Champion badge. The second is your hidden Matchmaking Rating (MMR), a skill estimate Ubisoft uses to build matches, which you never see directly in the client.
Ubisoft has actually documented how this works in its own developer blog. According to Ubisoft’s official matchmaking rating explainer, published on its Rainbow Six Siege news page, “Ranked features 36 skill ranks. These ranks are based on a players Matchmaking Ranking (MMR) which is derived from numerical scores generated by the TrueSkill algorithm.” That TrueSkill-based model is the same general approach Microsoft Research originally built for Xbox Live matchmaking, and Ubisoft adapted it for Siege’s ranked ladder starting in Year 1 Season 4.
Here is the part that most tracker sites gloss over. Ubisoft’s own documentation states plainly: “In Season 4, your MMR is proportional to your skill (MMR = 100*μ).” The Greek letter mu (μ) is the mean of a Bayesian skill distribution, not a simple win/loss counter. Every ranked match updates that distribution, and Ubisoft confirms “the MMR is updated solely on the outcome of your match.” RP is a separate, smoothed-out display value derived from that MMR, which is why your visible rank can lag behind a hot or cold streak by several games before it catches up.
Jynxzi described the practical effect of this on his own stream, in a clip transcribed by a fan-run archive. Talking through a ranked match, he explained how the system credits a win: “and the average MMR of what you beat is what you get.” That is a fair summary of how TrueSkill-style systems work. Beat five Champions and your estimated skill jumps hard. Beat five Coppers and it barely moves, even though both count as “a win” in your match history. A streamer who queues solo into a stacked five-man Champion team and loses is often penalized less than a stat sheet would suggest, because the system already expected a loss against that lobby.
The Current Rank Points Tiers, Explained
Siege’s Ranked 2.0 structure groups players into eight named tiers, each split into five sub-ranks (V down to I, with Champion standing alone at the top). The table below reflects the RP bands that trackers and community guides have used consistently across recent seasons. Ubisoft does not publish a single locked RP-to-tier table inside the client, so treat these bands as the widely-used community reference rather than an official per-season guarantee, since exact cutoffs can shift slightly between season patches.
| Tier | RP Range | Sub-Ranks | Typical Population |
|---|---|---|---|
| Copper | 0 – 1,399 | V, IV, III, II, I | New and returning players |
| Bronze | 1,400 – 1,799 | V, IV, III, II, I | Casual ranked grinders |
| Silver | 1,800 – 2,299 | V, IV, III, II, I | Largest population band |
| Gold | 2,300 – 2,999 | V, IV, III, II, I | Core competitive players |
| Platinum | 3,000 – 3,999 | V, IV, III, II, I | Strong mechanical/game sense |
| Emerald | 4,000 – 4,399 | V, IV, III, II, I | Scrim-level grinders |
| Diamond | 4,400 – 5,000 | V, IV, III, II, I | Semi-pro and content creators |
| Champion | 5,000+ | Single tier, ranked by exact RP | Top ladder, pro-adjacent |
Champion is uncapped, which is why leaderboard pages show RP values well past the 5,000 floor. A console leaderboard snapshot from R6 Tracker Network, pulled from its season 37 ranked RP board, lists a player sitting at rank 66 globally with 5,444 RP across 790 matches and a 1.50 kill/death ratio. That single row is a useful sanity check: Champion-tier RP climbs well above the nominal threshold the higher you push, and match count matters as much as RP when judging how “earned” a given rank actually is.
Prerequisites: What You Need Before Starting
This build uses a short Python script, a free public stats API, and about 45 minutes. Nothing here requires paid tooling.
- Python 3.12 or newer installed and on your PATH
- The
requestslibrary (pip install requests) - The
richlibrary for readable CLI output (pip install rich), optional but recommended - Optionally,
matplotlibfor the trend-chart step (pip install matplotlib) - A code editor (VS Code, PyCharm, or any text editor you’re comfortable with)
- A target Ubisoft username, platform, and player ID (we’ll cover how to find these in Step 3)
- A free developer key from an R6 stats provider such as R6 Arenyze, since direct Ubisoft Connect endpoints are not publicly documented for third-party use
Step 1: Set Up Your Project Folder and Virtual Environment
Keep this self-contained so you are not fighting dependency conflicts with other projects on your machine.
mkdir r6-rank-estimator
cd r6-rank-estimator
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install requests rich matplotlib
Create a single file called rank_estimator.py. Every step below adds to this one file, so you end up with one complete, runnable script rather than scattered snippets.
Step 2: Register for API Access
Ubisoft does not publish an open, documented public endpoint for pulling arbitrary players’ ranked stats, which is exactly why third-party aggregators exist in the first place. R6 Arenyze packages that data behind a REST API with an accompanying Node/TypeScript wrapper, and it is the kind of provider this tutorial targets. Sign up for a free developer key at r6.arenyze.com and store it as an environment variable rather than hardcoding it into your script.
# macOS/Linux
export R6_API_KEY="your_key_here"
# Windows (PowerShell)
$env:R6_API_KEY="your_key_here"
If you would rather not register anywhere, you can skip straight to Step 12 and use the cross-reference method with public tracker pages instead of a direct API call. It’s slower and more manual, but it works without an API key.
Step 3: Find the Correct Ubisoft Username and Platform
Every lookup starts here, and it’s the step most people get wrong. Streamers frequently play on a different in-game name than their stream handle, and many maintain multiple accounts across PC, Xbox, and PlayStation. A search for “Jynxzi” on a tracker site like SiegeRadar will often surface several candidate profiles. Cross-check the platform and recent match timestamps against what you saw on stream before you trust a profile is the right one.
- Open siegeradar.com, pick the platform (PC, Xbox, or PlayStation), and search the Ubisoft name or gamertag
- Confirm the profile’s recent match history lines up with content you’ve actually seen on stream
- Note the platform and exact username string, since API calls are case- and platform-sensitive
- If a profile shows no ranked activity in the current season, that account is likely a smurf or an inactive alt, not the main
Ubisoft also allows players to change their display name periodically, so a profile that matched a streamer’s handle six months ago may belong to a completely different person today if that name got freed up and reclaimed. Double-check the player’s platform ID or a persistent account identifier where the tracker exposes one, rather than relying on the display name alone, especially for any lookup you plan to cite publicly.
Step 4: Make Your First API Call
With a username and platform confirmed, write the basic request function. This hits the stats endpoint and returns raw JSON.
import os
import requests
API_KEY = os.environ.get("R6_API_KEY")
BASE_URL = "https://r6.arenyze.com/api/v1"
def get_player_stats(username: str, platform: str = "pc") -> dict:
headers = {"Authorization": f"Bearer {API_KEY}"}
params = {"name": username, "platform": platform}
response = requests.get(f"{BASE_URL}/player/stats", headers=headers, params=params, timeout=10)
response.raise_for_status()
return response.json()
if __name__ == "__main__":
data = get_player_stats("YourTargetUsername", platform="pc")
print(data)
Run it once with python rank_estimator.py and confirm you get back a JSON payload rather than a 401 or 404. If you get a 401, your API key environment variable isn’t being picked up. If you get a 404, the username/platform combination doesn’t match an indexed profile, so go back to Step 3.
Step 5: Parse Rank and RP From the Response
Raw API responses from stats providers typically nest the ranked fields a few levels deep. Write a small parser so the rest of your script doesn’t repeat that navigation logic.
def parse_ranked_summary(payload: dict) -> dict:
ranked = payload.get("ranked", {})
return {
"username": payload.get("username", "unknown"),
"platform": payload.get("platform", "unknown"),
"rp": ranked.get("rankPoints", 0),
"wins": ranked.get("wins", 0),
"losses": ranked.get("losses", 0),
"kills": ranked.get("kills", 0),
"deaths": ranked.get("deaths", 0),
"season": ranked.get("season", "unknown"),
}
Keep the key names flexible. Different stats providers structure their JSON differently, so adjust the dictionary keys above to match whatever the provider you registered with actually returns. Print payload directly during setup to see the real field names before locking in this function.
Step 6: Map RP to a Rank Tier
This turns the raw RP number into the Copper-through-Champion label from the table earlier in this piece.
RANK_BANDS = [
(0, 1399, "Copper"),
(1400, 1799, "Bronze"),
(1800, 2299, "Silver"),
(2300, 2999, "Gold"),
(3000, 3999, "Platinum"),
(4000, 4399, "Emerald"),
(4400, 4999, "Diamond"),
(5000, float("inf"), "Champion"),
]
def rp_to_tier(rp: int) -> str:
for low, high, name in RANK_BANDS:
if low <= rp <= high:
return name
return "Unranked"
Treat this mapping as a best estimate, not gospel. Because Ubisoft doesn't publish a single locked table and these bands can shift slightly between season balance patches, always sanity-check against a live tracker page if the exact sub-rank matters for your use case.
Step 7: Pull Match History for a Skill Trend
RP alone only tells you where someone landed, not how they got there. Pulling recent match history lets you see whether a player is trending up, flat, or sliding, which matters a lot for a part-time ranked player like a streamer.
def get_match_history(username: str, platform: str = "pc", limit: int = 20) -> list:
headers = {"Authorization": f"Bearer {API_KEY}"}
params = {"name": username, "platform": platform, "limit": limit, "mode": "ranked"}
response = requests.get(f"{BASE_URL}/player/matches", headers=headers, params=params, timeout=10)
response.raise_for_status()
return response.json().get("matches", [])
Each match entry should include a win/loss flag and, ideally, an RP delta. If the provider doesn't expose an explicit RP delta per match, you can approximate it by comparing consecutive snapshots of the player's total RP over time, which is why Step 12 recommends polling periodically rather than relying on a single snapshot.
Step 8: Build a Simplified Skill Estimator
Ubisoft's own documentation confirms the ranked system runs on a TrueSkill-derived model where MMR equals 100 times a mean skill estimate (mu), and that the number updates purely based on match outcomes. The exact proprietary formula, including how confidence (sigma) shrinks over time, isn't public. What follows is a simplified, educational approximation of that logic, not Ubisoft's real algorithm, built so you can see directionally whether a player's RP matches their recent performance.
def estimate_trend(matches: list, current_rp: int) -> dict:
"""Simplified, educational approximation inspired by TrueSkill-style
skill updates. This is NOT Ubisoft's real formula -- it's a rough
trend signal built from win rate and a confidence decay factor."""
if not matches:
return {"trend": "insufficient data", "confidence": 0.0}
wins = sum(1 for m in matches if m.get("result") == "win")
total = len(matches)
win_rate = wins / total
# Confidence grows with sample size, capped at 1.0
confidence = min(total / 50, 1.0)
if win_rate > 0.55:
trend = "climbing"
elif win_rate < 0.45:
trend = "declining"
else:
trend = "stable"
projected_rp_range = (
int(current_rp * (1 - (0.5 - win_rate) * 0.1)),
int(current_rp * (1 + (win_rate - 0.5) * 0.1)),
)
return {
"trend": trend,
"win_rate": round(win_rate, 3),
"confidence": round(confidence, 2),
"sample_size": total,
"projected_rp_range": projected_rp_range,
}
This is intentionally conservative. With fewer than 10-15 matches in the sample, the confidence score stays low on purpose, which mirrors the real system's behavior: Ubisoft's placement logic calibrates a starting rating over several matches rather than locking in a rank from game one.
Step 9: Calculate a Percentile Standing
A raw RP number means little without context. Knowing that 4,400 RP sits in Diamond is useful, but knowing roughly what percentage of the ranked population that represents is more useful for understanding how a given rank, including a streamer's, actually compares.
# Rough population distribution bands, community-sourced and approximate.
# Use these as a general-shape reference, not an exact per-season census.
POPULATION_BANDS = [
(0, 1399, 0.12), # Copper
(1400, 1799, 0.18), # Bronze
(1800, 2299, 0.28), # Silver -- largest band
(2300, 2999, 0.22), # Gold
(3000, 3999, 0.12), # Platinum
(4000, 4399, 0.05), # Emerald
(4400, 4999, 0.025),# Diamond
(5000, float("inf"), 0.005), # Champion
]
def estimate_percentile(rp: int) -> float:
cumulative = 0.0
for low, high, share in POPULATION_BANDS:
if rp >= high:
cumulative += share
elif low <= rp < high:
band_width = high - low
progress = (rp - low) / band_width if band_width else 0
cumulative += share * progress
break
return round(min(cumulative, 0.999) * 100, 1)
Label this output clearly as an estimate in anything you build. Population shape varies by platform, region, and season, and no public source in this tutorial's research confirms an exact current census, so treat the output as "roughly top X%" rather than a precise figure.
Step 10: Account for Season Resets
Siege resets and recalibrates ranks each season, which means a snapshot taken in week one of a new season tells you almost nothing about a player's peak from the prior season. The leaderboard example referenced earlier in this piece came from season 37's console ranked board, a useful reminder to always check which season a stat line belongs to before comparing it to anything else.
def is_stale_season_data(data_season: str, current_season: str) -> bool:
return data_season != current_season
def build_report(username: str, platform: str, current_season: str) -> dict:
payload = get_player_stats(username, platform)
summary = parse_ranked_summary(payload)
matches = get_match_history(username, platform)
report = {
"username": summary["username"],
"platform": summary["platform"],
"rp": summary["rp"],
"tier": rp_to_tier(summary["rp"]),
"percentile": estimate_percentile(summary["rp"]),
"trend": estimate_trend(matches, summary["rp"]),
"stale_data_warning": is_stale_season_data(summary["season"], current_season),
}
return report
Set current_season manually at the top of your script and update it each time Ubisoft rotates seasons. There's no reliable public endpoint that returns "the current season name" independent of a specific player's data, so this has to be maintained by hand.
Step 11: Print a Readable CLI Report
Raw dictionaries are fine for debugging but painful to read at a glance. Use rich to format the output as a clean table.
from rich.console import Console
from rich.table import Table
def print_report(report: dict):
console = Console()
table = Table(title=f"R6 Rank Estimate: {report['username']}")
table.add_column("Field")
table.add_column("Value")
table.add_row("Platform", report["platform"])
table.add_row("Rank Points", str(report["rp"]))
table.add_row("Tier", report["tier"])
table.add_row("Estimated Percentile", f"Top {100 - report['percentile']:.1f}%")
table.add_row("Trend", report["trend"]["trend"])
table.add_row("Win Rate (sample)", f"{report['trend'].get('win_rate', 'n/a')}")
table.add_row("Confidence", f"{report['trend'].get('confidence', 'n/a')}")
if report["stale_data_warning"]:
table.add_row("Warning", "Data may be from a prior season")
console.print(table)
if __name__ == "__main__":
report = build_report("YourTargetUsername", "pc", current_season="Y11S4")
print_report(report)
Step 12: Cross-Verify Against a Second Source
Never trust a single API call for a public-facing claim about someone's rank, especially a high-profile streamer whose account activity people will scrutinize. Before you publish or share any result, manually check the same username against at least one other tracker, such as R6 Tracker Network's leaderboard pages or SiegeRadar's profile search. If the two sources disagree by more than a tier, the likely explanation is stale caching on one side, not an actual rank change. Different R6 stats providers pull from Ubisoft-backed data but refresh on different schedules, so a direct call and a cached tracker page can briefly disagree.
Each provider also has a different strength, so picking the right one depends on what you're trying to confirm. The table below summarizes how the three tools referenced throughout this tutorial tend to behave in practice.
| Tool | Best For | Data Refresh | Programmatic Access |
|---|---|---|---|
| R6 Arenyze | Scripted lookups and automation | Near real-time on request | Documented REST API plus a Node/TypeScript wrapper |
| SiegeRadar | Quick manual profile searches | Refreshes on page load | Web interface only, no public API |
| R6 Tracker Network | Global and regional leaderboards | Periodic batch refresh | Web interface and companion Overwolf app |
None of these three is "the official" source, since all three sit on top of Ubisoft's backend rather than being Ubisoft itself. Treat agreement across two or more of them as a reasonably strong signal, and treat a lone outlier reading as something to double-check before you repeat it.
Step 13 (Bonus): Alert Yourself When a Rank Changes
Checking manually every day gets old fast. A lightweight Discord webhook turns your script into a passive monitor that only pings you when something actually moves, which is far more useful than staring at a CLI table you forget to run.
import requests as http
DISCORD_WEBHOOK_URL = os.environ.get("DISCORD_WEBHOOK_URL")
def notify_if_tier_changed(report: dict, last_known_tier: str) -> str:
current_tier = report["tier"]
if current_tier == last_known_tier:
return current_tier
message = {
"content": (
f"Rank change detected for {report['username']}: "
f"{last_known_tier} -> {current_tier} ({report['rp']} RP)"
)
}
if DISCORD_WEBHOOK_URL:
http.post(DISCORD_WEBHOOK_URL, json=message, timeout=10)
return current_tier
Store the last known tier in a small text file or a single-row SQLite table between runs, pass it into notify_if_tier_changed, and wire the whole rank_estimator.py script to a scheduled task once a day. You'll get pinged the day a tracked player actually crosses a tier boundary instead of re-reading the same unchanged table every morning.
Advanced Tip: Chart the RP Trend Over Time
Once you've logged a few weeks of snapshots, a simple line chart tells a clearer story than any single number. Save each report to a CSV file on a schedule (a daily cron job or Windows Task Scheduler entry works fine), then plot it.
import csv
import matplotlib.pyplot as plt
def plot_rp_history(csv_path: str):
dates, rp_values = [], []
with open(csv_path, newline="") as f:
reader = csv.DictReader(f)
for row in reader:
dates.append(row["date"])
rp_values.append(int(row["rp"]))
plt.figure(figsize=(10, 4))
plt.plot(dates, rp_values, marker="o")
plt.title("Rank Points Over Time")
plt.xlabel("Date")
plt.ylabel("RP")
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig("rp_trend.png")
Appending a row to that CSV at the end of your daily build_report call turns this from a one-off lookup into an actual longitudinal record, the same approach streamers' own analytics teams use to track viewer-facing stats over a season.
Sample Output
Running the full script against a test profile with a healthy sample of recent ranked matches produces output similar to this:
R6 Rank Estimate: TestProfile
+----------------------+------------------------+
| Field | Value |
+----------------------+------------------------+
| Platform | pc |
| Rank Points | 4180 |
| Tier | Platinum |
| Estimated Percentile | Top 17.0% |
| Trend | climbing |
| Win Rate (sample) | 0.6 |
| Confidence | 0.4 |
+----------------------+------------------------+
Notice the confidence value of 0.4. With only 20 matches sampled, the script is deliberately cautious about declaring a firm trend, consistent with how Ubisoft's placement system behaves for the first chunk of games each season.
Common Pitfalls to Avoid
- Confusing aggregate leaderboards with Siege RP. A community duel-ranking score (aim stats across multiple games) is not a Rank Points value. Always confirm the metric's source before quoting it as someone's rank.
- Grabbing the wrong platform profile. The same username can exist separately on PC, Xbox, and PlayStation with completely different stat histories. Verify the platform before every lookup.
- Treating a single snapshot as a season average. RP taken on one day doesn't reflect peak rank, decay from inactivity, or a recent hot streak. Pull match history, not just the current total.
- Hardcoding your API key in version control. Always use environment variables; a leaked key can get rate-limited or revoked, and some providers bill per request.
- Ignoring season boundaries. Comparing this season's RP to last season's peak RP without labeling which is which produces a misleading "they dropped ranks" narrative that isn't accurate.
- Assuming RP bands are locked forever. Ubisoft has adjusted tier thresholds across major updates before. Re-verify the table in this tutorial against a live tracker if you're building something long-term.
- Skipping the cross-verification step. A single cached API response can be stale by hours or days. Cross-check before publishing any rank claim publicly.
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
| 401 Unauthorized on every request | API key missing or not exported in current shell session | Re-export the environment variable, then restart your terminal or IDE |
| 404 Not Found for a known username | Wrong platform specified, or username has special characters | Re-check the exact username and platform via the tracker site search first |
| Empty match history list | Player hasn't queued ranked this season, or API limit parameter too low | Increase the limit parameter or confirm the player has ranked activity this season |
| KeyError when parsing the payload | Provider's JSON schema differs from the example in Step 5 | Print the raw payload and adjust dictionary keys to match the actual field names |
| rp_to_tier() returns "Unranked" unexpectedly | RP value is None or missing from the response | Add a default of 0 when the field is absent, and check if the player has placement matches left |
| Script runs but confidence always shows 0.0 | Match history list came back empty due to a bad mode parameter | Confirm the mode filter is set to "ranked" and not "casual" or "all" |
| Rate limit errors (429) on repeated calls | Polling too frequently for the API tier you registered for | Add a delay between calls or reduce your polling schedule to once every few hours |
| Two tracker sources disagree on current tier | Caching lag on one provider, or you compared across two different seasons | Always note the data's season and timestamp before comparing two sources |
| matplotlib chart renders blank | CSV file has a header mismatch or zero rows logged yet | Confirm the CSV column names match exactly, and log at least two data points first |
Why This Matters Beyond One Streamer's Rank
The same confusion that makes "what rank is Jynxzi" hard to answer applies to any competitive player whose public reputation outpaces their verifiable ranked stat line. Esports orgs scouting talent, tournament organizers setting skill brackets, and even casual squads trying to figure out if a new recruit is actually Diamond-level all run into the same RP-versus-skill gap this tutorial walks through. Building a small tool that pulls live data, labels its own confidence, and flags stale seasons is a more defensible approach than screenshotting a single tracker page and calling it settled.
It's also worth remembering that Ubisoft's matchmaking rating system was built specifically to solve the exact confusion this tutorial started with. Before the TrueSkill-based model, Ubisoft's own documentation notes "this led to confusion as the play experiences of players of different ranks would vary greatly," describing the pre-Year 1 Season 4 system. The current MMR-driven approach fixed the matchmaking side of that problem, but it didn't fix the public-facing side: without an open API from Ubisoft itself, fans are still stuck reverse-engineering a skill number through third-party trackers, which is exactly the gap the script in this tutorial is built to narrow.
There's a broader lesson here for anyone building tools around public figures' game stats, not just Siege players. Any time a metric gets detached from its original source and repackaged by a third party, it picks up noise: stale caching, mismatched seasons, or a completely unrelated leaderboard mistaken for the real one. The 1,159 controller-score mix-up that started this whole conversation around Jynxzi is a small, low-stakes example of that pattern, but the same mechanism shows up in bigger contexts, from esports scouting reports to influencer marketing decks that cite a "rank" nobody can actually trace back to a verified match history. Building your own lookup, however small, forces you to confront where a number actually comes from instead of inheriting someone else's unlabeled screenshot.
That habit also pays off the next time Ubisoft ships a ranked season update. Siege has adjusted its ranked structure more than once since launch, and whenever that happens, every static "here's the rank table" article on the internet goes stale overnight while sites still citing the old thresholds keep ranking in search results anyway. A script that reads from a live API, labels its own confidence, and flags season mismatches survives those updates far better than a hardcoded screenshot, because the only thing you need to change is a handful of constants at the top of the file.
The Complete Working Script
Combined into a single file, the full project looks like this. Save it as rank_estimator.py, set your R6_API_KEY environment variable, update the target username, platform, and current season string, then run it.
import os
import csv
import requests
from rich.console import Console
from rich.table import Table
API_KEY = os.environ.get("R6_API_KEY")
BASE_URL = "https://r6.arenyze.com/api/v1"
RANK_BANDS = [
(0, 1399, "Copper"), (1400, 1799, "Bronze"), (1800, 2299, "Silver"),
(2300, 2999, "Gold"), (3000, 3999, "Platinum"), (4000, 4399, "Emerald"),
(4400, 4999, "Diamond"), (5000, float("inf"), "Champion"),
]
POPULATION_BANDS = [
(0, 1399, 0.12), (1400, 1799, 0.18), (1800, 2299, 0.28), (2300, 2999, 0.22),
(3000, 3999, 0.12), (4000, 4399, 0.05), (4400, 4999, 0.025), (5000, float("inf"), 0.005),
]
def get_player_stats(username, platform="pc"):
headers = {"Authorization": f"Bearer {API_KEY}"}
params = {"name": username, "platform": platform}
r = requests.get(f"{BASE_URL}/player/stats", headers=headers, params=params, timeout=10)
r.raise_for_status()
return r.json()
def get_match_history(username, platform="pc", limit=20):
headers = {"Authorization": f"Bearer {API_KEY}"}
params = {"name": username, "platform": platform, "limit": limit, "mode": "ranked"}
r = requests.get(f"{BASE_URL}/player/matches", headers=headers, params=params, timeout=10)
r.raise_for_status()
return r.json().get("matches", [])
def parse_ranked_summary(payload):
ranked = payload.get("ranked", {})
return {
"username": payload.get("username", "unknown"),
"platform": payload.get("platform", "unknown"),
"rp": ranked.get("rankPoints", 0),
"season": ranked.get("season", "unknown"),
}
def rp_to_tier(rp):
for low, high, name in RANK_BANDS:
if low <= rp <= high:
return name
return "Unranked"
def estimate_percentile(rp):
cumulative = 0.0
for low, high, share in POPULATION_BANDS:
if rp >= high:
cumulative += share
elif low <= rp < high:
width = high - low
progress = (rp - low) / width if width else 0
cumulative += share * progress
break
return round(min(cumulative, 0.999) * 100, 1)
def estimate_trend(matches, current_rp):
if not matches:
return {"trend": "insufficient data", "confidence": 0.0}
wins = sum(1 for m in matches if m.get("result") == "win")
total = len(matches)
win_rate = wins / total
confidence = min(total / 50, 1.0)
trend = "climbing" if win_rate > 0.55 else "declining" if win_rate < 0.45 else "stable"
return {"trend": trend, "win_rate": round(win_rate, 3), "confidence": round(confidence, 2), "sample_size": total}
def build_report(username, platform, current_season):
payload = get_player_stats(username, platform)
summary = parse_ranked_summary(payload)
matches = get_match_history(username, platform)
return {
"username": summary["username"],
"platform": summary["platform"],
"rp": summary["rp"],
"tier": rp_to_tier(summary["rp"]),
"percentile": estimate_percentile(summary["rp"]),
"trend": estimate_trend(matches, summary["rp"]),
"stale_data_warning": summary["season"] != current_season,
}
def print_report(report):
console = Console()
table = Table(title=f"R6 Rank Estimate: {report['username']}")
table.add_column("Field")
table.add_column("Value")
table.add_row("Platform", report["platform"])
table.add_row("Rank Points", str(report["rp"]))
table.add_row("Tier", report["tier"])
table.add_row("Estimated Percentile", f"Top {100 - report['percentile']:.1f}%")
table.add_row("Trend", report["trend"]["trend"])
if report["stale_data_warning"]:
table.add_row("Warning", "Data may be from a prior season")
console.print(table)
def log_to_csv(report, csv_path="rp_history.csv"):
import datetime
file_exists = os.path.isfile(csv_path)
with open(csv_path, "a", newline="") as f:
writer = csv.writer(f)
if not file_exists:
writer.writerow(["date", "rp", "tier"])
writer.writerow([datetime.date.today().isoformat(), report["rp"], report["tier"]])
if __name__ == "__main__":
report = build_report("YourTargetUsername", "pc", current_season="Y11S4")
print_report(report)
log_to_csv(report)
Run this once a day, even manually, and the rp_history.csv file becomes a real record you can chart with the advanced-tip plotting function after a week or two of data.
Frequently Asked Questions
Can I find Jynxzi's exact current rank with this tool?
Only if his main ranked account is public on a provider your API key covers and he has enough recent ranked matches logged. Streamers who mostly play casual or custom lobbies will show thin or stale ranked data no matter which tracker you use, because the tool can only report what Ubisoft's backend actually recorded.
Is the 1,159 score some sites list for Jynxzi his Rainbow Six Siege rank?
No. That figure comes from a community aim-duel leaderboard for controller players, a separate metric unrelated to Siege's Rank Points or ranked tier system. Treat any number without a clear RP or tier label with suspicion.
Why does my script show a different tier than a tracker website for the same player?
Usually a caching or season mismatch. Tracker sites refresh on their own schedules, and a profile that recently played a match may show updated RP in a direct API call before a cached web page catches up.
What's the difference between RP and MMR in Rainbow Six Siege?
RP is the visible number tied to your rank badge. MMR is a hidden skill estimate Ubisoft uses for matchmaking, built on a TrueSkill-derived model where, per Ubisoft's own documentation, MMR equals 100 times a mean skill value. RP generally tracks MMR but can lag behind it by several matches.
Do I need a paid API plan to run this tutorial?
No. The script is built against a free-tier developer key. Expect rate limits on free tiers, so space out your requests, especially if you're polling multiple players' profiles in the same script.
Can this estimator work for games other than Rainbow Six Siege?
The RP-to-tier mapping and population bands are Siege-specific, but the overall approach (pull stats, parse a rank value, track win rate, flag stale seasons) generalizes to any ranked ladder with a public or semi-public stats API.
Why is my confidence score always low even with 30+ matches?
The confidence formula caps growth at 50 matches by design, mirroring how real skill-rating systems need a meaningful sample before narrowing their uncertainty window. Thirty matches is a reasonable sample but not yet a high-confidence one.
Should I trust a single tracker site's rank claim about a public figure?
Not without cross-checking. Verify the platform, the season, and ideally a second independent source before repeating any specific rank claim, especially about a high-profile streamer whose numbers get scrutinized and shared widely.
Is it against the rules to build a tool like this?
Respect whichever stats provider's terms of service you register with, including their rate limits and any restriction on redistributing raw data. This tutorial uses a documented third-party REST API rather than scraping Ubisoft's own servers directly, which is the safer and more sustainable approach for a personal project.
