About
It’s important for community moderators on Roblox to identify alts to keep their spaces safe from rule-breakers bypassing punishments.
Traditionally, mods would check profiles for account age, friends, badges, inventory, favorites, etc., but that has been easily and quickly faked. Badges especially are taken advantage of with “Badge Walk” games, where you can get hundreds of player badges in minutes.
To help solve the issue of badge walks and detect alts, I’m introducing Badge History Graphs.
Instead of manually scrolling how many badges a user has, it graphs how many they earned and when they earned them. This visualizes a player’s “Proof of Time,” making it nearly impossible to fake a legitimate account history without actually playing Roblox for years.
And despite being hard for ban evaders to fake, it’s easy (and free) for you to tell at a glance whether someone is an alt.
Since its release, this opensource script has been put into a couple community Discord Bots, processing over 180,000 checks in 2025 alone.
Detecting Alts
Alternate accounts in these graphs tend to have:
- Few badges in number
- Large spaces of time where they don’t earn a lot (or any) badges
- A sudden spike in badges from badge walking games or collecting them
The graph makes it easy to see a person’s badge history at a glance without scrolling their profile. Compare my account (a_cemaster) to the other badge-walking account (JOHNCENASKELLYMERC).
Video: https://www.youtube.com/watch?v=R80SCfLSGFE
Why This Works
Most anti-alt methods rely on “Proof of Identity” (including Roblox), where you’re trying to identify who’s who.
This tool relies on “Proof of Time”, where you judge the entropy (randomness) of a human playing games over several years. You don’t even need to know whose alt it is, you can just identify it’s an alt with public badge information (also helps with privacy).
This elevates the cost of making effective alts and catches the majority of low-effort, throwaway accounts as an effective filter. With this tool, ban evaders can no longer just buy a real looking account anymore, because they’d have to be maintained or get red flagged.
It’s essentially a time-lock puzzle, where an alt would need to develop for months or years to mimic human slope, introducing a higher barrier of entry to alt ban evasion when it used to just take a few minutes.
Current Use
xTracker (Gun Clan Community) reported over 150,000 badge graphs made in 2025 including 30,000 in their own Discord server.
Clanware (Sword Clan Community) with over 30,000 badge graphs in their Discord server (+ more most likely in other servers).
Problems and Future Work
Some potential issues:
- Users can have an inactivity excuse (or “quit the game”)
- Users can buy an active account and continue using it as their new main
- Users with an extremely high number of badges take longer to graph
- Users can delete their badges
- Users can solely play a single game (or a few) which limits their badge growth
- API can rate limit with too many calls on the same user
- Users need to have public inventory in order to view their badges (Tip: In high-security communities, you can ask a suspicious user to temporarily unprivate their inventory. If they refuse, it is often a red flag in itself.)
- Similar looking badge graphs can be very different depending on the scales
- A badge walk can skew an otherwise active account by being much larger than the surrounding
- A badge check can provide information on a user’s general play history, which could inherently be a privacy concern.
Be careful using this alone to declare if a user is an alt, it’s recommended to combine this with other common analysis methods.
Future Considerations
In the future, someone could possibly make a machine learning method to detect alts with this, although it’s not straightforward to automate into games without public inventories. I started this project a year ago and made the code free to use, so other people have used it for their own work. They’ve made the graph look nicer, the program run faster, and also have put it into Discord Bots to make it easier for them to look up users. Currently considering making a web app to run this on. From use cases I’ve seen, it seems fairly stable and accurate for other groups in the community to check for alts.
How to use
You can run it yourself using the Google Colab Link, it’s a relatively simple plotting script.
Edit (4/22/2026): Use this new Colab link now and follow the instructions in the comment to get an Open Cloud API key to use. You should make a copy of the file, so that you can keep using your own key (It does not save on my Colab link). Link: Google Colab
You can change the username strings of the USERNAMES list to graph different users. In Colab, you can press the triangle run button to see it working, no coding experience required.
It works by gathering all badges that a user has and running the “Awarded Date” API on each one, so that we can get how many badges they earned and when they got it. On the bottom of this post, I also copied the full code, so that you can run it locally as well (requires Python experience).
This code is free and open source. If you run a bot, a cafe group, or a moderation tool, you are encouraged to integrate this logic directly into your infrastructure. The more communities that use Badge Graph Analysis, the harder it becomes for ban evaders to operate on the platform.
Edit (4/22/2026): The script and Colab no longer work due to Roblox’s Badge Web API updates. I posted a newer version in the comments below, which uses Roblox’s Open Cloud API. You will need to get an API key by following the script’s instructions.
Old Code Below (Will not work, gets Forbidden 403 error):
"""
Author: a_cemaster
Date: 2/16/2024
This Python script creates a graph representing the cumulative count of badges earned by a Roblox user over time.
It fetches badge data using the Roblox API and plots the cumulative count of badges against their awarded dates.
If you're running this yourself, a window usually pops up with the graph.
If you're running this in Colab or Jupyter Notebook, then the image will show on the output.
You can also save the plot with the commented code 'plt.savefig(...)'
Some Roblox users have a lot of badges, which makes this script take a long time.
CHANGE LOGS:
- 9/7/2024: Using a proxy for Awarded Dates API for significant speed up, also set STEP = 100
- 3/14/2025: More proxy usage, also added a "can view inventory" print due to player badges being private with hidden inventory, removed concurrency
"""
import requests
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime
from time import sleep
# Put usernames in this list to get their badge information
USERNAMES = ["a_cemaster", "w_armaster", "kielthenoob"]
# Will print current amount of badges tracked if True
# Recommended if user has a lot of badges and you want to see progress
PRINT_PROGRESS = True
BATCH_PER_PRINT = 1000
def get_user_id_from_username(username: str) -> str:
# Use the Roblox users API to get the user ID from the username
url = f"https://users.roblox.com/v1/usernames/users"
params = {"usernames": [username]}
response = requests.post(url, json=params)
data = response.json()
# Check if the response is valid and contains the user ID field
if response.status_code == 200 and data["data"]:
return data["data"][0]["id"]
raise Exception(f"Could not find the user ID for username {username}")
def check_can_view_inventory(user_id: str) -> bool:
"""
Given a Roblox user id, check if the user can view their inventory.
"""
url = f"https://inventory.roproxy.com/v1/users/{user_id}/can-view-inventory"
response = requests.get(url)
# If we get a 429 status, wait and retry with some backoff
retry_after = 5
while response.status_code == 429:
print(f"Rate limited. Retrying after {retry_after} seconds.")
sleep(retry_after)
response = requests.get(url)
retry_after += 5
response.raise_for_status() # Raise an error for other non-200 responses
data = response.json()
return response.json()["canView"]
def fetch_badges(user_id: str) -> list[dict]:
"""
Given a Roblox user id, get the user's badge data.
"""
url = f"https://badges.roproxy.com/v1/users/{user_id}/badges?limit=100&sortOrder=Desc"
badges = []
cursor = None
while True:
params = {}
if cursor:
params['cursor'] = cursor
response = requests.get(url, params=params)
data = response.json()
for badge in data['data']:
badges.append(badge)
if PRINT_PROGRESS and len(badges) % BATCH_PER_PRINT == 0:
print(f"{len(badges)} badges for {user_id} requested.")
if data['nextPageCursor']:
cursor = data['nextPageCursor']
else:
break
return badges
def convertDateToDatetime(date: str) -> datetime:
"""
Given a timestamp string, convert to a datetime object.
"""
milliseconds_length = len(date.split('.')[-1])
# The string dates we get vary, so we have to sanitize to 3 places and 'Z'
# Truncate if more than 3 places & 'Z', else pad zeroes
if '.' in date:
if milliseconds_length > 4:
dotInd = date.find('.')
date = date[:dotInd + 4] + date[-1]
elif milliseconds_length < 4:
date = date[:-1] + '0' * (4 - milliseconds_length) + date[-1]
else:
# There is no decimal portion, so we have to add it
date = date[:-1] + ".000" + date[-1]
return datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%fZ")
def fetch_award_dates(user_id: str, badges: list[dict]) -> list[str]:
"""
Make requests to Roblox's Badge API to get user's badge awarded dates
"""
dates = []
badge_ids = [badge["id"] for badge in badges]
url = f"https://badges.roproxy.com/v1/users/{user_id}/badges/awarded-dates"
STEP = 100 # Adjust the step size as needed, we can't do too many at once
for i in range(0, len(badge_ids), STEP):
try:
params = {"badgeIds": badge_ids[i:i + STEP]}
response = requests.get(url, params=params)
# If we get a 429 status, wait and retry with some backoff
retry_after = 5
while response.status_code == 429:
print(f"Rate limited. Retrying after {retry_after} seconds.")
sleep(retry_after)
response = requests.get(url, params=params)
retry_after += 5
response.raise_for_status() # Raise an error for other non-200 responses
for badge in response.json()["data"]:
dates.append(badge["awardedDate"])
if PRINT_PROGRESS and len(dates) % BATCH_PER_PRINT == 0:
print(f"{len(dates)} awarded dates for {user_id} requested.")
except Exception as e:
print(f"Error fetching data: {e}")
return dates
def plot_cumulative_badges(username: str, user_id: str, dates: list[str]):
"""
Graph the cumulative total of badges over time
"""
# Sort badges by awarded date
y_values = [convertDateToDatetime(date) for date in dates]
y_values.sort()
# Calculate cumulative count at each date and store into a list
curr_count = 0
cumulative_counts = []
for date in y_values:
curr_count += 1
cumulative_counts.append(curr_count)
# Plot the cumulative count over time
plt.style.use('dark_background')
plt.xlabel('Badge Earned Date')
plt.ylabel('Total Badges')
plt.title(f'Badges over Time for {username} ({user_id})')
plt.scatter(y_values, cumulative_counts, marker='o', alpha=0.2)
# Set the X-axis format to 'Year' only
ax = plt.gca()
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
ax.xaxis.set_major_locator(mdates.YearLocator())
plt.figtext(0.05, 0.95, f"Badge Count: {len(y_values)}", ha="left", va="top", color="white", transform=ax.transAxes)
# Save the image, if desired. Must be ran before plt.show()
# plt.savefig(f"BadgeCount-{username}-{user_id}.png", bbox_inches="tight")
plt.tight_layout()
plt.show()
def process_user(username: str):
"""
Run all the functions to get the badge graph for single username
"""
user_id = get_user_id_from_username(username)
can_view_inventory = check_can_view_inventory(user_id)
print(f"{username}: {user_id}, Can view inventory: {can_view_inventory}")
badges = fetch_badges(user_id)
dates = fetch_award_dates(user_id, badges)
plot_cumulative_badges(username, user_id, dates)
print(f"Completed plot for {username} - {user_id}")
def main():
for username in USERNAMES:
process_user(username)
if __name__ == "__main__":
main()
tl;dr I made a Python script to see if an account often plays games and earns badges.







