Skip to content
Back to blog

Team Up Blog

When the Game Ships No Ranked Mode: Running a Roblox Ladder from Discord

| Team Up | 12 min read

Most Roblox experiences have no rating, no matchmaking and no record of who beat whom — so the Discord server is the ranked mode. What that changes, and how a Roblox experience can report its own results through HttpService.

robloxapileaderboardseloguide

Every other competitive game has a ladder of its own. Valorant has ranked, Siege has ranked, CS2 has Premier — and a community bot is a better-tuned alternative to something that already exists. If it broke tomorrow, players would still have a rank.

Roblox is the case where there is nothing to replace. The overwhelming majority of experiences ship no rating, no matchmaking and no persistent record of who beat whom, and the ones that do usually reset it. So when a Roblox community adds a leaderboard to its Discord, that leaderboard is not a companion to the ranked mode. It is the ranked mode — the only place a competitive result is stored at all.

That's a bigger responsibility than "we added a bot", and it changes which decisions matter. Two of them matter far more here than anywhere else: how results get recorded, and what stops someone farming rating on a new account. Both are cheap to solve on day one and expensive to solve after people have stopped trusting the board.

Pick the Shape Before the Settings

Roblox experiences vary in format more than every other game on this site combined, and almost all the configuration follows from which shape you have:

Your experience is… Queue Rating type
Duels, 1v1 arenas /queue_versus, team size 1 Player (Specific Format) at 1v1
Team objective modes /queue_versus at your team size Player (Specific Format) at that size
Rounds, tag, last-one-standing /queue_ffa Player ratings with placements

The third row is the one people get wrong. A round-based or last-one-standing mode is a free-for-all, not a match with a winner — the result is an ordering, and recording it as "X won" throws away everything the round measured. Record the placements. The free-for-all scoring post covers how an ordering becomes rating changes.

Recording Is the Whole Game

Here is the prediction I'd make about any Roblox ladder, sight unseen: it will not die because the ratings were wrong. It will die because recording matches became a chore for one person.

Roblox communities run on volume — far more matches per week than a 10-mans server, with a roster that partly turns over every month. If every result needs an admin to type out ten mentions, the ladder has a half-life of about two weeks. Not because anyone decided to stop; because the admin got bored, and nobody else had permission or knowledge to take over.

So compare the three recording paths by what they cost per match:

Path Who does it Cost per match Needs
/record_match elo An admin, or whoever you trust Typing every player and placement Nothing
/record_match screenshot Any player in the match Attaching a screenshot, mentioning the players, pressing Record Nothing
REST API Nobody — the experience posts it Zero, after you build it once A developer with edit access

The third row is the reason this page exists, and it's genuinely unique to Roblox: the game itself can close the loop. No other title on this site can report its own results, because you can't run code on Riot's or Blizzard's servers. You can run code on yours.

If you don't have a developer: AI screenshot recording

/record_match screenshot takes the end-of-round scoreboard image plus a mention of everyone in the match, reads the scoreboard with a vision model, and posts a proposed result — placements, teams and per-player stats — that a human confirms with a button. The mention list is a hard boundary, not a hint: those are the only Discord members the AI will match in-game names against, so it can never record a result onto someone who wasn't in the game.

For matchmaking queues there's a tighter version: a per-queue opt-in adds a 🤖 Scan Result button to the lobby's result message, so the flow is "play, screenshot, scan, approve" with no command typed at all. That queue setting also has an AI as source of truth option, which auto-records scans that come back completely clean and falls back to human review for anything with an unmatched player or a bad stat read. Turning that on is what gets the admin out of the loop entirely — which, per the prediction above, is the whole ballgame.

The API Path: Let the Experience Report Itself

If someone in your community can edit the experience, this is strictly better. Roblox's HttpService can make an outbound HTTPS request from a server script, and recording a match is one POST.

Two things to set up first:

  1. Enable HTTP requests for the experience — Game Settings → Security → Allow HTTP Requests. Off by default, and RequestAsync throws until it's on.
  2. Generate an API key with /settings api_key generate in your Discord.

Then, in a server-side Script — never a LocalScript, or you have shipped your API key to every client, which is the same as publishing it:

local HttpService = game:GetService("HttpService")

local TEAMUP_API = "https://api.teamupgg.com"
local API_KEY = "<from /settings api_key generate>"
local LEADERBOARD = "ranked"
local RECORDER_DISCORD_ID = "<your Discord user id>"

local function teamup(action, body)
    local response = HttpService:RequestAsync({
        Url = TEAMUP_API .. "/" .. action,
        Method = "POST",
        Headers = {
            ["Content-Type"] = "application/json",
            ["X-API-Key"] = API_KEY,
        },
        Body = HttpService:JSONEncode(body),
    })

    -- 409 means this result was already recorded. Not an error — see below.
    if response.StatusCode == 409 then
        return nil, "duplicate"
    end
    if not response.Success then
        error(("%s -> %d %s"):format(action, response.StatusCode, response.Body))
    end
    return HttpService:JSONDecode(response.Body)
end

-- `ordering` is a list of lists of Discord ids, best-placed first.
-- A 1v1 is { {winnerId}, {loserId} }; an 8-player FFA is eight one-element lists.
local function recordMatch(ordering, matchId)
    local matchResults = {}
    for place, team in ipairs(ordering) do
        table.insert(matchResults, { team = team, place = place })
    end

    local recorded, duplicate = teamup("record_match", {
        leaderboard = LEADERBOARD,
        matchResults = matchResults,
        -- Idempotency key. record_match claims this string before it writes
        -- anything, so a retry (or a second server reporting the same round)
        -- comes back 409 instead of applying Elo twice.
        discord_message_id = "roblox-" .. matchId,
    })
    if duplicate then return end

    -- record_match ONLY moves ratings. This second call re-renders the pinned
    -- leaderboard embeds, syncs tier roles and writes the audit log.
    -- Note the field name: `teams`, not `matchResults`.
    teamup("sync_post_match", {
        leaderboard = LEADERBOARD,
        teams = ordering,          -- the id lists themselves, NOT the {team, place} objects
        audit_log = true,
        source = "api",
        recorded_by = RECORDER_DISCORD_ID,
        match_results = matchResults,
    })
end

Three things about that snippet are load-bearing:

The idempotency key is not optional. discord_message_id is claimed atomically before anything is written, so a retried request — or two game servers both deciding they own the round — returns 409 rather than applying the rating change twice. It never has to be a real Discord message id. Build it from something stable about the round (the match id, the server's JobId, a round counter) and a replay becomes a no-op instead of an incident.

Two calls, not one. record_match moves ratings and writes history. sync_post_match is what makes the Discord side reflect it: pinned leaderboard embeds re-render, tier roles get granted and revoked, and the audit log records the result as API-sourced so you can tell automated results from typed ones.

Per-player stats ride along. If your experience knows kills, captures or times, pass player_stats keyed by Discord id and stat_definitions describing them, exactly as the CS2 auto-recording shim does — that post is the fuller worked example of this same API, and everything in it transfers except the game server.

The identity problem, which you cannot skip

Team Up rates Discord identities. Your experience knows Roblox UserIds. Nothing bridges those automatically: Roblox is not one of the platforms the API resolves external identities for, and there's no Roblox account linking. So the map from UserId to Discord id is yours to own.

Keep it in a DataStore, populate it however suits your community — a Discord command that hands out a code the player types in-game, an admin-maintained sheet for a small roster, a sign-up form — and then apply one rule in the recording path:

If any player in the round has no mapping, refuse to record the round. Do not guess, and do not silently drop the unmapped player.

Recording a 3v3 as a 3v2 because one player wasn't in the map produces a rating change that is wrong for all five of the others, and it is close to undetectable afterwards. Failing loudly costs you one round; guessing costs you the board's credibility.

Volume Has a Ceiling

The other thing high-volume communities meet is the recording cap, and it's better to know the number than to discover it at 11pm on a Saturday.

Tier Matches per leaderboard per day Leaderboards
Free 50 3
Pro 250 25
Unlimited uncapped uncapped

Going over returns 429 with code TIER_LIMIT_MATCHES. If you're recording through the API, treat that as a failure to handle rather than a message someone will read — a shim that shrugs at a 429 has silently lost a result.

The leaderboard count matters more here than it looks, because two pieces of good advice both consume boards. One leaderboard per experience is right — a rating describes a population playing a fixed thing, and pooling three different Roblox games into one board mostly measures who played the most of whichever was easiest that week. And rolling a season creates a new leaderboard rather than wiping the old one ("Ranked" becomes "Ranked S2", and last season stays live forever as its own archive). Run two experiences and roll one season and you're at four boards, which is past the free tier. Plan the shape of that before you announce a season, not after.

Make Alt Accounts Unprofitable

A new Roblox account costs nothing — no purchase, no phone number, nothing in the way. That makes smurfing and rating-farming structurally easier here than on any other platform, and it is the single most common reason a Roblox ladder loses its community's trust.

The ladder cannot see Roblox account age, so this is moderation rather than automation. Two things carry most of the weight:

  • /set_rating player puts an obvious returning smurf where they belong instead of letting them farm upward through everyone below them. You can set the match count alongside the rating, so the account isn't treated as brand new by the rating curve either.
  • Watch for the pattern, not the person. A new account with a very high win rate against a narrow set of opponents is farming, and the match history makes that visible in a way a spreadsheet never did. The smurf and match-farming post covers the shapes to look for.

Say the rule out loud before you need it: alts get their rating set, repeat offenders lose queue access. A rule announced in advance is enforcement; the same rule announced afterwards is a grudge.

Plan for Turnover

Roblox rosters turn over faster than any other community type here, which makes seasons more useful and not just decorative. A season rollover archives the standings into a board that stays live and starts a fresh one — which does two things at once: it gives returning and new players a ladder they can actually reach the top of, and it stops the top of your board being permanently held by someone who stopped playing in March.

Six to eight weeks is a reasonable first guess for a community with heavy turnover. Rolling more often than that and the ratings never accumulate enough matches to mean anything; much less often and the board ossifies. The competitive season post covers how much rating should carry across the boundary, which is the decision that actually shapes how a new season feels.

Setup, Start to Finish

  1. One leaderboard per experience, named after the experience.
  2. Match the queue to the format/queue_versus for duels and team modes, /queue_ffa for round-based and last-one-standing.
  3. Set the rating type to Player (Specific Format) at the size you actually play.
  4. Get the admin out of the recording loop — turn on AI screenshot recording for the queue, with auto-record on if you want zero human steps for clean scans.
  5. If you have a developer: enable HTTP requests, /settings api_key generate, and post record_match + sync_post_match from a server Script with a stable idempotency key.
  6. Build the UserId → Discord id map before the first recorded match, and refuse to record rounds with unmapped players.
  7. Announce the alt rule in the same message as the ladder.
  8. Pick a season length — six to eight weeks — and check your leaderboard count against your tier before you roll one.

Frequently Asked Questions

Can a Roblox experience record its own match results?

Yes, if you can edit it. HttpService:RequestAsync posts to the Team Up REST API with a server API key, so the experience can report a finished round directly — Roblox is the one platform here where the game itself closes the loop. Enable HTTP requests in Game Settings first, keep the key in a server-side Script (never a LocalScript), and use a stable idempotency key so a retry doesn't apply Elo twice.

How do Roblox players map to Discord accounts?

You maintain that map yourself, usually in a DataStore. Team Up rates Discord identities and there's no Roblox account linking — Roblox isn't one of the platforms the API resolves external identities for. Populate the map with an in-game code, a sign-up form or an admin list, and make the recording path refuse a round containing an unmapped player rather than dropping them from the result.

We run several Roblox games in one server. One leaderboard or several?

Several — one per experience. A rating only means something relative to a fixed population playing a fixed thing, so a combined board mostly measures who played the most of whichever game was easiest that week. Separate leaderboards share the same server, queues and tier roles. Watch the tier cap: free allows three boards, and each season rollover adds one.

What if nobody in our community can edit the experience?

Use AI screenshot recording. /record_match screenshot reads a post-match scoreboard and pre-fills the result for a human to confirm, and matchmaking queues can add a 🤖 Scan Result button to the lobby message so no command is typed at all. It's the difference between recording a match in fifteen seconds and recording it in two minutes, which over a few hundred matches is the difference between a ladder that survives and one that doesn't.

How do we stop people farming rating on alt accounts?

Socially, with the match history doing the detection work. The ladder can't see Roblox account age, so watch for the signature — a new account with a very high win rate against a narrow set of opponents — and use /set_rating player to place obvious returning smurfs where they belong instead of letting them climb through everyone below them. Announce the rule before you need to apply it.

Does it read stats out of the experience automatically?

No. Nothing is read from Roblox — custom stats carry whatever numbers your experience produces, but they're reported to the bot rather than pulled from the game. Via the API that's a player_stats object on the record call; via screenshots it's whatever the scoreboard shows.


Running a Roblox community on Discord? Team Up gives an experience with no ranked mode a real one — Elo leaderboards, queues, match history and tier roles, recorded by screenshot or by the game itself. See the Roblox setup guide for the configuration in brief, read starting a competitive league for the community side, or the CS2 auto-recording post for a fuller worked example of the same API.