RankPortal
Complete Guide
Everything you need to build a fully working Roblox group ranking system — from creating a Clerk account to ranking players live from your game server. One guide, nothing missing.
This guide covers the full stack: Clerk handles user login so only your authorized users can generate API keys. Those keys are then used by your Roblox games to perform ranking actions through your hosted server.
How Everything Connects
There are two separate flows — the portal flow (web users setting up their key) and the game flow (Roblox calling your API at runtime).
Portal Flow — Getting an API Key
Game Flow — Ranking a Player
Roblox game servers can't call Roblox's own ranking API directly (security restriction). Your Express server acts as the authorized middleman, authenticated via the bot account's cookie.
Prerequisites
- ✓A Roblox account with owner/admin access to your group
- ✓A separate Roblox bot account (never use your main)
- ✓A Clerk.com account (free)
- ✓A GitHub account (free) for deployment
- ✓A Railway.app account (free tier available)
- ✓Roblox Studio installed on your computer
- ✓Basic understanding of Lua scripting
Create a Bot Account
The bot account is the Roblox account that performs all ranking actions. Roblox will show rank changes as coming from this account.
Always use a dedicated bot account. Never use your main account. The bot's cookie grants full account access — if your server is ever compromised, you only lose the bot account.
Register a new Roblox account
Go to roblox.com and sign up. Use a descriptive name like
YourGroupRankBot. Use a unique email (a Gmail alias works).Verify the email
Check your inbox and verify. Unverified accounts may get flagged when making API calls.
Log in as the bot and join your group
Visit your group page while logged in as the bot and click Join. Accept from your main account if approval is required.
Promote the bot to a rank with permissions
From your main account go to Group → Members → find the bot → assign a rank. The bot can only set ranks lower than its own, so make sure it sits high enough.
Rank hierarchy rule: If your bot is Rank 200, it can only assign ranks 1–199. Place the bot above all ranks you want to set programmatically.
Get Your Bot's Cookie
The .ROBLOSECURITY cookie is how your server authenticates with Roblox as the bot. Extract it from your browser while logged in as the bot account.
Log into Roblox as the bot account
Open your browser and sign in at roblox.com.
Open DevTools
Press
F12orCtrl+Shift+I.Application → Cookies → roblox.com
Click the Application tab → expand Cookies in the left panel → click
https://www.roblox.com.Copy .ROBLOSECURITY
Find the cookie named
.ROBLOSECURITY. Double-click the Value column and copy the entire value. It starts with_|WARNING:-DO-NOT-SHARE-THIS.
Log into Roblox as the bot
Open Firefox and sign in.
Open DevTools → Storage tab
Press
F12→ click the Storage tab.Expand Cookies → roblox.com
Find and click
https://www.roblox.comin the Cookies section.Copy .ROBLOSECURITY
Click the
.ROBLOSECURITYrow. Copy the full value from the bottom panel.
Never share this cookie with anyone. Only paste it into the RankPortal dashboard. Enable 2FA on the bot account for extra security. Cookies expire if you log out or change the password.
Set Group Permissions
The bot's group role needs specific permissions enabled. Go to your Group → Configure Group → Roles → edit the bot's role.
✅ Manage lower-ranked members → required for setRank, exile
✅ Remove members → required for exile
✅ Post group shouts → required for shout
☐ Invite members → optional, for join requestsSave the role. Changes apply immediately — no server restart needed.
Create a Clerk Application
Clerk handles all user authentication for the portal. It's free for up to 10,000 monthly active users and requires zero auth UI code.
Sign up at clerk.com
Go to clerk.com and create a free account.
Click "Create application"
From the Clerk dashboard, click Create application. Name it
RankPortal.Choose sign-in methods
Select Email + Password at minimum. You can also enable Google, Discord, or GitHub — Clerk handles the OAuth automatically. Click Create application.
You're taken to the API Keys page
Clerk shows you two keys immediately. Keep this tab open — you'll need them next.
Get Your Clerk API Keys
After creating your Clerk app, you'll see two keys on the API Keys page. You can always find them later at Configure → API Keys.
| Key | Prefix | Where it goes | Safe to expose? |
|---|---|---|---|
| Publishable Key | pk_test_ / pk_live_ | Frontend HTML file | ✓ Yes — it's public |
| Secret Key | sk_test_ / sk_live_ | Server .env file only | ✗ Never expose |
Never put the Secret Key in HTML or commit it to GitHub. Only the Publishable Key goes in index.html. The Secret Key lives in .env on the server.
Configure Clerk Settings
Add Allowed Origins
In the Clerk Dashboard go to Configure → Restrictions → Allowed Origins and add:
http://localhost:3000
https://YOUR-APP.up.railway.appSet Redirect URLs
Go to Configure → Paths and set all redirect URLs to / — the portal is a single page app.
| Setting | Value |
|---|---|
| Sign-in URL | / |
| Sign-up URL | / |
| After sign-in URL | / |
| After sign-up URL | / |
Install & Run the Server
# Extract the project zip, then:
cd rankportal
npm install
# Copy the env template
cp .env.example .envOpen .env and fill in your Clerk keys:
# Clerk — from dashboard.clerk.com → API Keys
CLERK_PUBLISHABLE_KEY=pk_test_YOUR_KEY_HERE
CLERK_SECRET_KEY=sk_test_YOUR_SECRET_HERE
# Server port
PORT=3000Open public/index.html and replace the placeholder with your Publishable Key (around line 12):
data-clerk-publishable-key="pk_test_YOUR_PUBLISHABLE_KEY_HERE"Start the server:
npm start
# or for development with auto-reload:
npm run devOpen http://localhost:3000. You should see the portal with a Clerk-powered Sign In button.
Environment Variables
| Variable | Required | Description |
|---|---|---|
| CLERK_PUBLISHABLE_KEY | Required | From Clerk Dashboard → API Keys. Also goes in index.html. |
| CLERK_SECRET_KEY | Required | From Clerk Dashboard → API Keys. Server-side only — never expose. |
| PORT | Optional | HTTP port. Defaults to 3000 if not set. |
Deploy to Railway
Your Roblox game needs a public HTTPS URL to call your server. Railway is free and takes about 5 minutes.
Push code to GitHub
Make sure
.envanddata/are in.gitignore(they are by default).BASHgit init && git add . git commit -m "initial commit" git remote add origin https://github.com/YOU/rankportal.git git push -u origin mainCreate project on Railway
Go to railway.app → New Project → Deploy from GitHub repo → select your repo.
Add environment variables
In Railway click Variables and add all three env vars from the table above.
Get your public URL
Railway gives you a URL like
https://rankportal-production.up.railway.app. Copy it.Add the URL to Clerk's Allowed Origins
Go back to Clerk Dashboard → Configure → Restrictions → Allowed Origins and add your Railway URL.
Set the Publishable Key in HTML
The Clerk browser SDK loads from a CDN via a <script> tag. The publishable key is passed as a data- attribute — Clerk's standard approach for plain JS apps.
<script
async crossorigin="anonymous"
data-clerk-publishable-key="pk_test_YOUR_KEY_HERE"
src="https://cdn.jsdelivr.net/npm/@clerk/clerk-js@latest/dist/clerk.browser.js"
></script>| Environment | Key prefix | Use when |
|---|---|---|
| Development | pk_test_ | Running on localhost |
| Production | pk_live_ | Deployed on Railway / Render |
Test the Portal
Open the portal in a browser
Visit
http://localhost:3000(or your Railway URL). You should see the landing page with a Sign In button.Sign up with a test account
Click Sign In — Clerk's modal appears. Create an account with an email address. Clerk sends a verification code.
Paste your bot cookie and generate a key
After logging in you see the dashboard. Paste your
.ROBLOSECURITYcookie and click Generate. You'll receive an API key likersp_4a7f2e9d...Copy the key
Click COPY and save it somewhere safe. You'll paste it into your Roblox scripts.
You can see all registered users in Clerk Dashboard → Users. Your API keys and cookies are stored in data/keys.json on the server.
Roblox Studio Setup
Enable HTTP Requests
Open your game in Roblox Studio
Launch Studio and open your place file.
Go to Game Settings → Security
Click Home → Game Settings in the top menu.
Toggle "Allow HTTP Requests" to ON
Without this, all HTTP calls will throw an error and nothing will work.
HTTP Requests must be enabled per game. If you publish a new place, enable it again there too.
Script Placement Rules
The ranking module must run on the server. Only Script objects (not LocalScript) can make HTTP calls.
✅ ServerScriptService → Script (correct)
✅ ServerStorage → Script (correct)
❌ StarterPlayerScripts → LocalScript (will NOT work)
❌ StarterGui → LocalScript (will NOT work)Install the Ranking Module
Create a ModuleScript in ServerStorage
In the Explorer panel right-click ServerStorage → Insert Object → ModuleScript. Name it
RankingService.Paste the full module code below
LUA — RankingService ModuleScript (paste entire block)local HttpService = game:GetService("HttpService") local RunService = game:GetService("RunService") -- /api in Studio, /roblox in live server local mainPath = RunService:IsStudio() and "/api" or "/roblox" local rankingService = {} local pendingUsers, rateLimit = {}, {} rankingService.__index = rankingService function rankingService.new(gameKey, host) local self = { host = host or "https://YOUR-SERVER.up.railway.app", gameKey = gameKey, } setmetatable(self, rankingService) return self end function rankingService:makeRequest(path, method, data) local url = string.format("%s%s%s", self.host, mainPath, path) local headers = { authorization = self.gameKey } local body = nil local urlRL = rateLimit[url] if urlRL and urlRL > 0 then task.wait(urlRL - os.time()) elseif method == "POST" then headers["Content-Type"] = "application/json" body = HttpService:JSONEncode(data) end local ok, response = pcall(function() return HttpService:RequestAsync({ Url = url, Method = method, Headers = headers, Body = body }) end) if not ok then warn("[RankingService]", response) if response == "Number of requests exceeded limit" then task.wait(30) return self:makeRequest(path, method, data) end return { success = false, error = response } end rateLimit[url] = nil if response.Headers["retry-after"] then local wait = os.time() + response.Headers["retry-after"] rateLimit[url] = wait task.wait(wait - os.time()) return self:makeRequest(path, method, data) end local parsed local _, err = pcall(function() parsed = HttpService:JSONDecode(response.Body) end) if err then warn("[RankingService] JSON decode error:", err) return { success = false, error = response.Body } end return parsed end function rankingService:setRank(groupId, userId, rank) if pendingUsers[userId] then return { error = "Pending" } end pendingUsers[userId] = true local r = self:makeRequest("/setRank", "POST", { groupId=groupId, userId=userId, rank=rank }) pendingUsers[userId] = false return r end function rankingService:exile(groupId, userId) if pendingUsers[userId] then return { error = "Pending" } end pendingUsers[userId] = true local r = self:makeRequest("/exile", "POST", { groupId=groupId, userId=userId }) pendingUsers[userId] = false return r end function rankingService:handleJoinRequest(groupId, userId, accept) if pendingUsers[userId] then return { error = "Pending" } end pendingUsers[userId] = true local r = self:makeRequest("/handleJoinRequest", "POST", { groupId=groupId, userId=userId, accept=accept }) pendingUsers[userId] = false return r end function rankingService:shout(groupId, message) return self:makeRequest("/shout", "POST", { groupId=groupId, message=message }) end function rankingService:getRank(groupId, userId) return self:makeRequest("/getRank", "POST", { groupId=groupId, userId=userId }) end return rankingServiceCreate a RankConfig Script in ServerScriptService
Right-click ServerScriptService → Insert Object → Script. Name it
RankConfig. Paste and fill in your values:LUA — RankConfig Script-- RankConfig — Script inside ServerScriptService local RankingService = require(game.ServerStorage.RankingService) local ranking = RankingService.new( "rsp_YOUR_API_KEY_HERE", -- from the portal "https://YOUR-SERVER.up.railway.app" -- your Railway URL ) -- Expose globally so other scripts can use it _G.Ranking = ranking _G.GROUP_ID = 12345678 -- your Roblox group ID
Your First Rank Action
Test the full pipeline end-to-end with this touch-to-rank script.
local Players = game:GetService("Players")
local part = script.Parent
local debounce = {}
part.Touched:Connect(function(hit)
local character = hit.Parent
local player = Players:GetPlayerFromCharacter(character)
if not player or debounce[player.UserId] then return end
debounce[player.UserId] = true
-- Wait for RankConfig to be ready
local t = 0
while not _G.Ranking and t < 10 do task.wait(0.5); t += 0.5 end
if not _G.Ranking then debounce[player.UserId] = nil; return end
local result = _G.Ranking:setRank(_G.GROUP_ID, player.UserId, 5)
if result.success then
print("✅ Ranked", player.Name, "to rank 5")
else
warn("❌ Failed:", result.error)
end
task.wait(5)
debounce[player.UserId] = nil
end)See ✅ Ranked [name] to rank 5 in the Output window? The full pipeline is working. If you see an error, jump to the Error Reference.
Ranking API Reference
All methods on the RankingService Lua module. Initialize once in RankConfig then call from any server Script.
local RankingService = require(game.ServerStorage.RankingService)
local ranking = RankingService.new(apiKey, serverUrl)Changes the user's rank to the specified number. The bot must have a higher rank than the target rank.
| Param | Type | Required | Description |
|---|---|---|---|
| groupId | number | Yes | Your Roblox group ID |
| userId | number | Yes | Target player's UserId |
| rank | number | Yes | Rank number 1–255 to assign |
local result = ranking:setRank(groupId, player.UserId, 5)
if result.success then print("Ranked!") else warn(result.error) endRemoves (exiles) the user from the group. Requires the bot to have Remove Members permission.
| Param | Type | Required | Description |
|---|---|---|---|
| groupId | number | Yes | Your Roblox group ID |
| userId | number | Yes | Target player's UserId |
local result = ranking:exile(groupId, player.UserId)
if result.success then print(player.Name, "exiled.") endAccepts or declines a pending group join request. Only works if your group uses manual approval mode.
| Param | Type | Required | Description |
|---|---|---|---|
| groupId | number | Yes | Your Roblox group ID |
| userId | number | Yes | UserId of the requesting player |
| accept | boolean | Yes | true to accept, false to deny |
ranking:handleJoinRequest(groupId, player.UserId, true) -- accept
ranking:handleJoinRequest(groupId, player.UserId, false) -- denyUpdates the group's shout message. Max 255 characters. Roblox rate-limits this endpoint — don't call it too frequently.
| Param | Type | Required | Description |
|---|---|---|---|
| groupId | number | Yes | Your Roblox group ID |
| message | string | Yes | Shout message (max 255 chars) |
ranking:shout(groupId, "🏆 New server online! Come join!")Returns the player's current rank number. Returns 0 if not in the group. Use this before setRank to avoid downgrading users.
| Param | Type | Required | Description |
|---|---|---|---|
| groupId | number | Yes | Your Roblox group ID |
| userId | number | Yes | Player's UserId to check |
local result = ranking:getRank(groupId, player.UserId)
if result.success then
print(player.Name, "is rank", result.data)
endPortal API Reference
These endpoints are used by the portal frontend (require a Clerk session token in the x-rp-token header).
Validates the Roblox cookie and creates (or updates) an API key for the logged-in Clerk user.
| Body | Type | Description |
|---|---|---|
| cookie | string | Full .ROBLOSECURITY cookie value |
Returns the logged-in user's API key details, Roblox username, request count, and timestamps. Returns hasKey: false if no key exists yet.
Permanently deletes the logged-in user's API key and stored cookie. Irreversible — they'll need to generate a new key.
Common Patterns
Rank on GamePass purchase
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")
local VIP_PASS_ID = 123456789
local VIP_RANK = 50
Players.PlayerAdded:Connect(function(player)
player.CharacterAdded:Wait()
task.wait(1)
while not _G.Ranking do task.wait(0.5) end
local ok, hasPass = pcall(function()
return MarketplaceService:UserOwnsGamePassAsync(player.UserId, VIP_PASS_ID)
end)
if not ok or not hasPass then return end
local current = _G.Ranking:getRank(_G.GROUP_ID, player.UserId)
if current.success and current.data < VIP_RANK then
_G.Ranking:setRank(_G.GROUP_ID, player.UserId, VIP_RANK)
print("Promoted VIP:", player.Name)
end
end)Rank based on score / kills
local function checkPromotion(player, kills)
local targetRank =
kills >= 100 and 50 or
kills >= 50 and 30 or
kills >= 25 and 15 or
kills >= 10 and 5 or nil
if not targetRank then return end
local current = _G.Ranking:getRank(_G.GROUP_ID, player.UserId)
if current.success and current.data < targetRank then
local res = _G.Ranking:setRank(_G.GROUP_ID, player.UserId, targetRank)
if res.success then
print("🎉 Promoted", player.Name, "to rank", targetRank)
end
end
endAdvanced Examples
Admin chat commands
local Players = game:GetService("Players")
local ADMINS = { [123456789] = true } -- your UserId
-- Commands: !rank USERNAME RANKNUM | !exile USERNAME
Players.PlayerAdded:Connect(function(player)
player.Chatted:Connect(function(msg)
if not ADMINS[player.UserId] then return end
local parts = string.split(msg, " ")
local cmd = string.lower(parts[1])
if cmd == "!rank" and #parts == 3 then
local target = Players:FindFirstChild(parts[2])
local rankN = tonumber(parts[3])
if target and rankN then
local r = _G.Ranking:setRank(_G.GROUP_ID, target.UserId, rankN)
print(r.success and "Ranked!" or r.error)
end
elseif cmd == "!exile" and #parts == 2 then
local target = Players:FindFirstChild(parts[2])
if target then
_G.Ranking:exile(_G.GROUP_ID, target.UserId)
end
end
end)
end)Shout when a new server opens
task.spawn(function()
while not _G.Ranking do task.wait(0.5) end
_G.Ranking:shout(_G.GROUP_ID, "⚔️ New server is live — join now!")
end)Error Reference
| Error | Cause | Fix |
|---|---|---|
| Http requests are not enabled | Studio setting off | Game Settings → Security → Allow HTTP Requests → ON |
| 401 Unauthorized | Wrong or missing API key | Check the authorization header in RankConfig matches exactly |
| Invalid Roblox cookie | Cookie expired or wrong | Re-extract .ROBLOSECURITY from browser — log in as bot first |
| ClerkJS failed to load | Wrong publishable key in HTML | Check data-clerk-publishable-key in index.html |
| 401 on /portal/register | Clerk token missing | Make sure x-rp-token is sent with Clerk.session.getToken() |
| CLERK_SECRET_KEY not set | Missing env var on server | Add to Railway Variables or your .env file |
| Allowed origin error | Domain not added in Clerk | Clerk Dashboard → Configure → Allowed Origins → add your domain |
| User is pending a response | Duplicate call same userId | Normal — wait for previous request to complete before calling again |
| Number of requests exceeded | Roblox rate limit hit | Module auto-retries after 30s. Reduce call frequency. |
| Cannot rank above bot's rank | Target rank ≥ bot rank | Raise the bot's group rank or target a lower rank number |
| Server not responding | Free tier cold start | First request after inactivity takes ~10s. Upgrade for production. |
Frequently Asked Questions
Why does my cookie expire?
Roblox cookies expire when you log out of the bot account, change its password, or after extended inactivity. When it expires, log back into the portal and submit the new cookie — your API key stays the same, only the cookie behind it updates.
Can I use one API key for multiple groups?
Yes. Every ranking method takes a groupId parameter. As long as the bot account is in each group with the required permissions, you can manage multiple groups with a single key.
Can multiple game servers use the same key simultaneously?
Yes. There's no per-server limit. All your game servers can call the API concurrently — only Roblox's own rate limits on the bot account apply.
How do I find my Group ID?
Go to your group page on Roblox. The URL will be roblox.com/groups/XXXXXXXX/Name — the number is your group ID.
My game works in Studio but not in the live server
The module auto-selects /api in Studio and /roblox live. Make sure your deployed server mounts both paths (default RankPortal does). Also double-check HTTP Requests is enabled — this setting is per-game and sometimes resets on republish.
How many users can sign up on the portal?
Clerk's free tier supports 10,000 monthly active users. For most group ranking portals this is more than enough. You can see all users in the Clerk Dashboard → Users.
Where are API keys and cookies stored?
In data/keys.json on the server. For production, replace this with a real database (PostgreSQL, MongoDB) and encrypt cookie values at rest.