About
Alts tend to be an issue in a community I’m in, so I made an alternate account detection concept using Python that graphs a user’s history of getting badges in games using Roblox’s APIs. It charts the overall count of badges and plots when they were obtained. So instead of manually going through a profile’s badges looking for a low number of badges or a spam of badges from a badge walk game, the graph makes it easy to see at a glance.
How to use
You can run it yourself using the Google Colab Link, it’s a relatively simple plotting script. 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).
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.
Attempting to fake a badge history requires being able to consistently get badges over periods of years, and at that point, you can basically consider it as a main account. The graph is based on the assumption that active accounts often play games that give them new badges (even randomly) and faking it requires too much play time to consistently work around.
You can find periods of time where a user was inactive or played a lot, and compare their badge history to what they say they did on Roblox. It’s not perfect, but it covers a lot of low effort alt attempts. Be careful using this alone to declare if a user is an alt, it’s recommended to combine this with other common analysis methods.
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
In the future, someone could possibly make a machine learning method to detect alts with this. 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. From use cases I’ve seen, it seems fairly stable and accurate for other groups in the community to check for alts.
Full Code Below:
"""
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
"""
"""
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.