UGG-Tracker

Roblox XP Tracker (Discord Bot)

Tracks player XP manually via slash commands, since the bot has no direct access to the game’s data. Users self-report their current XP with /checkin, and the bot calculates gains, pace, and progress toward a weekly requirement (default: 20,000 XP/week).

How it works

Setup (Windows)

  1. Install Python (skip if you already have it)
    • Download from https://python.org/downloads
    • Run the installer. On the first screen, check the box “Add python.exe to PATH” before clicking Install — this is the step people usually miss.
    • After installing, close any open terminal windows and open a new one so the PATH change takes effect.
  2. Create a Discord application & bot
    • Go to https://discord.com/developers/applications → New Application
    • Go to the “Bot” tab → Add Bot → copy the Token (keep this private, don’t share it)
    • Turn on the “Server Members Intent” toggle on this same “Bot” page, under “Privileged Gateway Intents.” This is required for /importxp to see everyone in your server — without it, the bot will either fail to start or only see a small cached subset of members instead of your whole server.
  3. Invite the bot to your server
    • Go to the “OAuth2 → URL Generator” tab
    • Scopes: bot, applications.commands
    • Bot Permissions: Send Messages, Embed Links, Use Slash Commands, Manage Roles (only needed if you plan to use XP milestone roles — see below)
    • Open the generated URL and add the bot to your server
  4. Open a terminal in the bot’s folder
    • Open the folder containing bot.py in File Explorer
    • Hold Shift and right-click on empty space inside the folder → “Open PowerShell window here” (or “Open in Terminal” on Windows 11)
  5. Install dependencies
    pip install -r requirements.txt
    

    If you get 'pip' is not recognized, try python -m pip install -r requirements.txt instead.

  6. Configure your token
    • In File Explorer, copy .env.example and paste it in the same folder
    • Rename the copy to .env (if Windows won’t let you start a filename with a dot, rename it to .env. with a trailing dot — Windows will drop the trailing dot automatically)
    • Open .env in Notepad and replace your-bot-token-here with the token you copied in step 2

    While testing, also set GUILD_ID in .env to your server’s ID so slash commands sync instantly (right-click your server icon in Discord → Copy Server ID — you may need to enable Developer Mode first under Discord Settings → Advanced). Leave it blank for production — global sync takes up to an hour but works across every server the bot is in.

  7. Run the bot
    python bot.py
    

    Leave this terminal window open — closing it stops the bot. If you get 'python' is not recognized, revisit step 1 and make sure “Add python.exe to PATH” was checked, or try py bot.py instead.

Commands

“Owner” below means restricted to the bot’s Discord application owner specifically (or its team, if the application is team-owned) — not “Manage Server” in whichever server the command is run in. This covers store/SKU commands since they’re tied to your actual storefront and Discord Monetization SKUs, not something that makes sense for each server’s own admins to reconfigure. Everything else uses normal per-server “Admin” permissions, so if you invite this bot to other servers, their own admins can manage their own tracking/roles/display settings without being able to touch your store setup. See “Packaging the bot for other servers” near the end of this document for more on this.

⭐ marks a Premium command — it still uses normal per-server “Admin” permissions, but additionally requires that specific server to have Premium access (a paid guild subscription, or a free exemption you’ve granted — see “Premium features” below). A non-Premium server’s admins see these commands like any other, they just get told the server needs Premium when they try to use one.

Command Who Description
/help Everyone Browse every command by category via a dropdown — works in any channel even if you’ve restricted others to one.
/store Everyone Shows the product menu and a “Visit Store” link button, if one’s configured. Also works in any channel. See below.
/setstoreurl url:<link> Owner Sets where the /store button goes — your Ko-fi, a website, wherever purchases actually happen.
/clearstoreurl Owner Removes the store link.
/addproduct name: price: [description] [emoji] Owner Adds a product to the /store menu (max 25).
/removeproduct name:<exact name> Owner Removes a product by its exact name.
/settings Admin (Manage Server) Opens an interactive panel with buttons and forms for everything below — no need to remember command syntax. See below.
/checkin xp:<number> [user] Everyone Record current total XP. First checkin starts tracking. The user option lets an admin check in on someone else’s behalf (e.g. if they can’t use Discord themselves) — regular members can only check themselves in.
/status [user] Everyone Show weekly progress, rate, and projection for yourself or another user.
/weeklyleaderboard Everyone Ranked list of everyone’s XP gained this week, with on-track indicators.
/totalleaderboard Everyone Ranked by each person’s actual total XP (starting point + everything gained since), not just the gain alone.
/crewtotals Everyone The whole crew’s combined XP added together — total XP and this week’s gain, summed across everyone tracked (including anyone no longer in the server).
/pastleaderboard [weeks_ago] Everyone A previous week’s full leaderboard — 1 (default) is the most recently finished week, higher numbers go further back. See “Leaderboard history” below.
/crewlevel Everyone The crew’s in-game level (an exponential formula, see below), progress to the next one, and an ETA based on the crew’s combined recent pace.
/setcrewlevelformula base_xp: growth_rate: Admin Sets the level-1→2 XP cost and the per-level growth rate /crewlevel uses. See “Crew level” below.
/adjustcrewxp amount: Admin Manually adds (or subtracts, with a negative number) from the crew’s combined XP total used by /crewtotals and /crewlevel. See “Crew level” below.
/history [user] Everyone Last 10 checkins for a user.
/undo [user] Everyone (own checkins); Admin for others Remove the most recent checkin and revert current XP to the one before it. Also re-checks XP milestone roles against the reverted total — a role earned only because of the undone checkin gets removed. If it was someone’s only checkin, tracking resets entirely and any earned role is stripped the same way.
/hide user: enabled: Admin Hides/unhides a member from /weeklyleaderboard and /totalleaderboard — still fully counted in /crewtotals, /crewlevel, and their own /status//profile. Hiding is about not being listed, not about being untracked.
/ignorerequirement user: enabled: Admin Exempts a member (e.g. an admin or mod) from ever being flagged as behind pace — always shows as on-track, skipped by both inactivity ping buckets and /pingbehindpace. Their real XP is still tracked and shown everywhere normally. Independent of /hide — combine both if needed.
/setlift lift: weight: [user] Everyone (own); Admin for others Records a deadlift/bench/squat number — shows as a badge on /profile.
/setbloxlinkkey api_key: Admin Sets this server’s Bloxlink Guild API Key, used to resolve verified Roblox↔Discord links. See “Roblox auto-tracking” below.
/syncbloxlink Admin Resolves Roblox links for every server member already verified with Bloxlink — no action needed from members who’ve verified before, in this server or any other. Also assigns the configured verified role, if one’s set (see below).
/generatetrackingkey Admin Generates (or regenerates) this server’s key for the Roblox auto-tracking API. Shown once — required before the game can send real XP data.
/revoketrackingkey Admin Immediately disables the Roblox auto-tracking API for this server.
/setbloxlinkrole role: Admin Sets a role /syncbloxlink auto-assigns to anyone it links — including people already linked in a previous sync, if they don’t have the role yet.
/clearbloxlinkrole Admin Stops /syncbloxlink from assigning a role. Doesn’t remove it from anyone who already has it.
/setrequirement xp:<number> Admin (Manage Server) Change the weekly XP goal (default 20,000).
/togglerecentrate enabled:<true/false> Admin Show or hide the “Recent Rate” field server-wide. See note below.
/togglecompactleaderboard enabled:<true/false> Admin Switch /weeklyleaderboard and /totalleaderboard between full-detail and compact one-line-per-person entries.
/setproratethreshold days: hours: minutes: Admin Control how late someone must join to get a prorated requirement (see below). Default: any lateness at all.
/setxpratecap rate: Admin Rejects a self-checkin outright if it implies more than this many XP/hr. See “XP rate cap” below.
/clearxpratecap Admin Turns the XP rate cap off — any self-checkin amount is accepted again.
/addxprole xp:<number> role:<@role> [exclusive] Admin Auto-assign a role once someone’s total XP reaches this amount. Retroactively grants it to anyone who already qualifies. exclusive: true (default) makes it part of a tier ladder — reaching a higher tier removes it. exclusive: false makes it permanent, kept regardless of what else is earned later (e.g. a standing crew role).
/removexprole role:<@role> Admin Stop auto-assigning a role at its XP milestone. Doesn’t strip the role from people who already have it.
/listxproles Everyone Show all configured XP milestone roles and their thresholds.
/syncxproles Admin Re-checks every tracked member’s roles right now — fixes leftover role clutter without waiting for everyone’s next checkin.
/progresschart [user] Everyone Shows a line chart of XP over time, built from that person’s checkin history. Needs at least 2 checkins.
/profile [user] Everyone Shows a rendered profile card: avatar, colored border, Crew XP, rank, this week’s gain, and a progress bar. See below.
/previewtier tier: Everyone ⭐ See what a configured banner tier would look like on your own card, without needing the role. See “Banner tiers” below.
/setbordercolor user: color: [custom_hex] Admin ⭐ Assign a profile border color — a preset from the palette, or an exact hex code. This is the unpaid/default look; see “Banner tiers” below for the store-linked version.
/removebordercolor user:<@user> Admin ⭐ Reset a user’s border color to the default.
/listbordercolors Everyone ⭐ Show all available preset border colors.
/toggleanimatedprofile enabled:<true/false> Admin Turn the animated rainbow-tier /profile card on or off server-wide (static image instead of a GIF). See below.
/addbannertier role: name: mode: color1: [color2] priority: Admin ⭐ Ties a profile banner look to a role — e.g. a role your store/Ko-fi integration grants on purchase. See “Banner tiers” below.
/removebannertier role:<@role> Admin ⭐ Removes a role’s banner tier.
/listbannertiers Everyone ⭐ Shows all configured banner tiers, their roles, and their colors.
/setbasecard mode: color1: [color2] Admin Sets the server’s default /profile color/gradient for anyone with no banner tier and no individually-set /setbordercolor. Not Premium-gated — this is the server’s own default look, not a paid perk.
/addskurole sku_id: role: name: Owner Auto-grants a role while a member has an active Discord purchase/subscription for a SKU, and removes it when that ends. See “SKU role automation” below.
/removeskurole sku_id:<id> Owner Stops auto-granting a role for a SKU.
/listskuroles Admin Shows all configured SKU → role mappings.
/setpremiumsku sku_id: Owner Sets the guild-subscription SKU that grants Premium. See “Premium features” below.
/addpremiumguild [guild_id] Owner Grants a server free Premium — no subscription needed. Defaults to the current server if left blank.
/removepremiumguild [guild_id] Owner Removes a server’s free Premium exemption. Defaults to the current server if left blank.
/listpremiumguilds Owner Shows every server with Premium, split into free (exempt) and paid (subscribed).
/listservers Owner Lists every server the bot is currently in — member count, tracked members, Premium status, when it joined, and its ID. Discord doesn’t expose this anywhere in the Developer Portal, so this is the only way to see it.
/exportdata [file_format] Admin ⭐ Downloads everyone’s current stats (XP, baseline, gains, last checkin) as a CSV or Excel file.
/backup Admin ⭐ Downloads a complete snapshot — every setting plus every tracked user, not just stats. See below.
/restore file:<.json> Admin Restores everything from a /backup file. Replaces the entire server’s data — confirmation required. Not Premium-gated — see “Backup & restore” below for why.
/analytics Admin ⭐ Server-wide activity: retention week-over-week, most active day, top gainer, average gain. See below.
/setadminlogchannel channel:<#channel> Admin Log destructive/data-altering admin actions to a channel. See below.
/clearadminlogchannel Admin Turn off admin action logging.
/addallowedchannel channel:<#channel> Admin Allow /checkin, /status, /weeklyleaderboard, /totalleaderboard, /history, /undo, and /progresschart in another channel. First one added starts restricting; before that, every channel works.
/removeallowedchannel channel:<#channel> Admin Remove a channel from the allowed list.
/listallowedchannels Admin Show every currently-allowed channel.
/clearallowedchannels Admin Remove all channel restrictions — tracking commands work anywhere again.
/setannouncechannel channel:<#channel> Admin ⭐ Post a public congratulations message whenever someone earns an XP milestone role.
/clearannouncechannel Admin ⭐ Turn off milestone announcements.
/setweeklypost channel:<#channel> Admin Auto-post the weekly leaderboard shortly before each week ends. Requires a shared server week.
/clearweeklypost Admin Turn off the auto-posted weekly leaderboard.
/setinactivitychannel channel:<#channel> Admin ⭐ Ping anyone with zero checkins as the week nears its end. Requires a shared server week.
/clearinactivitychannel Admin ⭐ Turn off inactivity reminder pings.
/setinactivitythreshold days: hours: minutes: Admin ⭐ How long before week-end the inactivity ping fires (default: 24 hours).
/toggleinactivitybehindpace enabled:<true/false> Admin ⭐ Whether inactivity pings also include people who checked in but are behind pace, not just people with zero checkins (default: off — zero checkins only).
/pingbehindpace Admin ⭐ Immediately pings everyone who’s checked in this week but isn’t on track — an on-demand version of the behind-pace half of the automatic inactivity ping, for whenever you don’t want to wait for it.
/toggleleaderboardpagination enabled:<true/false> Admin Switch leaderboards between Previous/Next paging and the default single truncated embed.
/setbaseline user:<@user> starting_xp:<number> Admin Set or correct someone’s all-time starting XP — this is what /totalleaderboard calculates total gain from.
/importxp file:<.csv or .xlsx> [existing_users] [announce_milestones] Admin ⭐ Bulk-set XP for many users at once from a spreadsheet. existing_users controls what happens to people already tracked: skip (default), check in (mass check-in, keeps history), or reset. announce_milestones (default: no) controls whether XP role milestones hit during this import get posted publicly. See below.
/undoimport Admin ⭐ Reverts everyone’s data back to exactly how it looked right before the last /importxp ran — the only way to undo a whole batch at once, since /undo only handles one person’s most recent checkin at a time. Only reverts the single most recent import.
/fullreset user:<@user> [clear_history] Admin Reset both weekly and all-time totals at once, starting fresh from their current XP. clear_history: true (default) also wipes their stored checkin history. Unlike /removeuser, they stay registered — no need to re-checkin to restart tracking.
/resetweek user:<@user> Admin Manually restart someone’s weekly window right now (0 days elapsed).
/setweekprogress user:<@user> [days] [hours] [minutes] [week_start_xp] Admin Manually set how far into the week someone is, in plain days/hours/minutes (e.g. days: 3, hours: 12 treats them as if their week started 3.5 days ago). Optionally also override their starting XP for the week.
/setweekprogressall [days] [hours] [minutes] [reset_gains] Admin Same as above, but applies to every tracked user at once — no need to update people one at a time. reset_gains: true also zeroes out everyone’s gained-so-far.
/removeuser user:<@user> Admin Wipe a user’s tracking data entirely. Only works if they’re still in the server — Discord’s picker can’t select someone who’s left.
/removeuserbyid discord_id:<id> Admin Same as above, but by raw Discord ID — works even if the person has left or been kicked.
/removestaleusers Admin Bulk-remove tracking data for everyone tracked who’s no longer in the server. Asks for confirmation first.
/removeallusers Admin Wipe everyone’s tracking data at once. Asks for confirmation with Confirm/Cancel buttons before doing anything — never fires on a single click.

Settings panel

Running /settings opens an interactive panel (admins only) instead of typing out command parameters. As of this version, it’s organized into categories via a dropdown rather than one long row of buttons — pick a category, see just the controls relevant to it, and use ⬅ Back to return to the category list.

Home screen:

⏱️ Requirement & Week

🎨 Display

🏅 XP Roles

🎗️ Banner Tiers

📢 Channels & Automation

🛒 Store

⚠️ Danger Zone

Every button/dropdown expires after 5 minutes of inactivity — just run /settings again if that happens. It works alongside all the individual slash commands (/setrequirement, /resetweek, etc.), which still work exactly as before if you prefer typing them directly.

Bulk importing starting XP from a spreadsheet

If you already have a list of members and their starting XP in Excel or Google Sheets, /importxp can set everyone up in one go instead of them each running /checkin individually.

1. Prepare your file with a header row and these columns (an example import_template.xlsx is included alongside this README if you’d rather start from a working file than build one from scratch):

Column Required? Notes
discord_id Recommended Their numeric Discord user ID — the most reliable way to match. Right-click a user → Copy User ID (needs Developer Mode on).
username Use if no discord_id Their Discord username, current server nickname, or a Roblox username that appears inside their nickname. A leading @ is fine and gets stripped automatically. See matching rules below.
starting_xp (or xp) Required Their starting XP as a whole number.

Example CSV:

discord_id,username,starting_xp
123456789012345678,CoolPlayer123,45000
,another_user,12500

You only need discord_id or username per row, not both — discord_id is checked first if present.

Matching rules (checked in this order):

  1. discord_id — exact, always reliable.
  2. Exact match against their Discord username, server nickname, or global display name.
  3. Substring match — if username appears anywhere inside their nickname, it counts as a match. This covers servers using a bot (like RoWifi or Bloxlink) that formats nicknames as something like DiscordName | RobloxUsername — a plain list of Roblox usernames will match those nicknames without any extra setup.
  4. If a username value matches more than one member (ambiguous), that row is flagged rather than guessed — you’ll need to resolve it manually with a discord_id.

2. Export as .csv or .xlsx — both work directly; .xlsx needs the openpyxl package, which is already in requirements.txt.

⚠️ If you use discord_id: format that column as Text, not a number, before typing IDs into it (in Google Sheets/Excel: select the column → Format → Number → Plain text). Discord IDs are 17-19 digits long, which exceeds what a spreadsheet numeric cell can store exactly — as a real number, the last few digits can get silently rounded off, corrupting the ID. This doesn’t affect the username column, which is plain text either way — if you’re not sure, skip discord_id entirely and just use username.

3. Run /importxp, attach the file, and choose what to do with anyone already tracked via existing_users:

New users are always registered regardless of which option you pick for existing ones. If you have XP milestone roles configured (see “XP milestone roles” below) and want a public announcement for anyone who hits one during this import, set announce_milestones: true — it defaults to false so a routine bulk update doesn’t spam the announcement channel.

4. Check the results — the bot replies with a summary: how many were newly tracked, skipped, ambiguous (matched more than one member), or unmatched. In Check in mode, each existing member’s line shows both their new total and how much that changed by (e.g. CoolPlayer123 (52,000, +7,000)), plus a running total gain for the whole batch in that field’s header — so a routine bulk update actually shows what moved, not just where everyone landed. For unmatched rows, double-check the spelling matches what’s actually in their nickname; for ambiguous ones, add a discord_id for those specific rows. Either way, you can always patch up the leftovers manually with /setbaseline.

Undoing an import: /undo (the regular per-person command) is not the right tool for reverting a bulk import — it only touches one person’s single most recent checkin, and after existing_users: Reset there’s no prior checkin left to revert to; /undo would just delete that person’s tracking entirely rather than restore their pre-import numbers. For reverting the whole batch correctly — including recovering history that a “Reset” import wiped out — use /undoimport instead, which restores everyone’s data to exactly how it looked right before that import ran. It can only undo the single most recent import (not a chain of several), and running a second import overwrites the ability to undo the first.

XP milestone roles

The bot can automatically assign a Discord role once someone’s total XP crosses a threshold you set — e.g. reach 75,000 XP, get the “Veteran” role.

Setup requirements — two things need to be true or role assignment silently fails:

  1. The bot needs the Manage Roles permission. If you invited it before this feature existed, re-invite using an updated OAuth2 URL with that permission checked (see Setup step 3), or just enable it directly for the bot’s role in Server Settings → Roles.
  2. The bot’s own role must be positioned above any role it’s assigning, in Server Settings → Roles (drag it up the list). Discord doesn’t let any bot assign a role ranked higher than its own — this trips people up constantly, so /addxprole checks for it up front and tells you clearly if it’s a problem, rather than failing silently later.

How it behaves:

Roblox auto-tracking (early, in progress)

/setbloxlinkkey api_key:<your server's Bloxlink Guild API Key>
/syncbloxlink

The bot can receive XP updates automatically from the Roblox game itself, via POST /api/roblox/checkin, instead of relying only on someone typing /checkin. This reuses the exact same internal functions a manual checkin uses — milestone roles, personal bests, catch-up-checkin flagging, all of it — so an automated checkin behaves identically to a manual one in every way except how the XP number arrived.

Getting a Roblox account linked to a Discord one is the hard part, not the HTTP plumbing — the bot needs some proof a given Roblox account actually belongs to a given Discord member, or anyone could spoof anyone else’s data. Rather than build a custom verification flow, this uses Bloxlink, the most widely used Roblox↔Discord verification service, as the source of truth:

  1. Generate a Guild API Key from Bloxlink’s own dashboard for this server (requires Bloxlink to be added to the server).
  2. /setbloxlinkkey to store it.
  3. /syncbloxlink to resolve every current server member who’s already Bloxlink-verified — including people who verified in a different server at some point, since Bloxlink’s verification is global. This is genuinely “no action needed” for anyone already verified anywhere, not just in this server.

Optional: /setbloxlinkrole to have /syncbloxlink auto-assign a role to anyone it successfully links — a visible “Verified” badge without touching anything else. Re-running /syncbloxlink after setting the role also catches anyone linked in an earlier sync who doesn’t have it yet, not just new links from that run.

  1. Anyone not yet verified needs to actually verify with Bloxlink (bio code or in-game, Bloxlink’s own flow) before a sync will find them — there’s no way around proving ownership somehow, by design.

Rate limits shape the design: Bloxlink’s free tier is 500 lookups/day, which is comfortable for a one-time sync but wouldn’t be for looking someone up on every single XP update. So /syncbloxlink resolves everyone once into the bot’s own storage, and the actual tracking endpoint reads that stored mapping — fast, and still works even if Bloxlink has downtime at the moment someone checks in.

Authentication — how to actually set it up:

/generatetrackingkey

Run this once, then put the key it shows you into your Roblox game’s server-side script as the Authorization header on every request to /api/roblox/checkin. It’s shown exactly once — there’s no way to view it again later, only regenerate a new one (which immediately invalidates the old one, so update the game’s script at the same time). /revoketrackingkey disables the API for this server immediately without issuing a replacement.

The key is what determines which server a request is for — the request body no longer includes or trusts a guild_id at all, which closes off the actual vulnerability the earlier no-auth version had (a spoofed guild_id could otherwise target any server the bot’s in, regardless of who was making the request).

Still worth knowing:

XP rate cap

/setxpratecap rate:10000

Since XP is self-reported (the bot has no way to read it directly from the game), nothing stops someone from typing in an inflated number by mistake — or on purpose. This gives admins a sanity check: if a self-checkin implies more XP per hour than the configured cap, it’s rejected outright — nothing gets recorded, not even partially. The member sees exactly why (their implied rate, the cap, and how much time the gain was spread over) and is told an admin can check them in directly instead, which bypasses the cap entirely — that’s the trusted correction path, not something this is meant to block.

What “does not apply to starting XP” means in practice: someone’s very first /checkin ever has nothing to compute a rate against — there’s no previous checkin, so there’s no implied rate, so the cap can’t apply and never blocks it. It only ever evaluates the gap between two checkins.

The cap only affects self-checkins, not admin corrections. An admin running /checkin user:@someone bypasses it completely, on the theory that an admin is the trusted mechanism for fixing bad numbers — same reasoning as why /setbaseline and /setweekprogress aren’t rate-limited either. Pick a number a bit above what’s actually achievable in your game in an hour; the point is catching typos and obviously-wrong numbers, not being stingy with legitimately fast progress.

Off by default — nothing is rejected until an admin sets a cap with /setxpratecap. Turn it back off anytime with /clearxpratecap.

Weekly date range

Both /status and /weeklyleaderboard show a “📅 Week Runs” field with the actual calendar dates the current week spans (e.g. “July 27 → August 3”), rendered via Discord’s timestamp tags so it automatically displays in each viewer’s own local timezone rather than a fixed UTC string.

On /weeklyleaderboard, this only shows a single shared range if your server uses /setweekprogressall to sync everyone’s week; otherwise it notes that ranges vary per person (check /status for an individual’s own window).

/status also shows a text progress bar (e.g. ████████░░░░░░ 57%) tracking gained-this-week against the requirement — the prorated amount, for anyone in a prorated first week, rather than the flat full number.

Restricting commands to specific channels

By default every command works in any channel. If you’d rather keep XP tracking confined to one or more dedicated channels (e.g. #xp-tracking) instead of cluttering general chat:

/addallowedchannel channel:#xp-tracking

The first channel you add is what starts the restriction — before that, every channel works normally. Add more with the same command to allow several channels at once (e.g. one per team, or a general channel plus an overflow channel). This restricts /checkin, /status, /weeklyleaderboard, /totalleaderboard, /history, /undo, and /progresschart — the commands people use often — to whichever channels are on the list. Trying to run them elsewhere gets a clear message listing which channels are actually allowed, instead of silently failing.

/removeallowedchannel takes one channel back off the list. /listallowedchannels shows the current list. /clearallowedchannels wipes the whole restriction, back to “works anywhere.”

Admin/config commands are deliberately exempt/settings, /addxprole, /setrequirement, and so on still work in any channel, since admins often manage settings from a separate admin-only channel.

Exporting data

/exportdata file_format:CSV

or

/exportdata file_format:Excel (.xlsx)

Downloads a file with everyone’s current stats: Discord ID, username, current XP (under a starting_xp header), baseline XP, total gained, this week’s starting XP, this week’s gain, and the timestamp of their last checkin. Useful for backups, spreadsheet analysis outside Discord, or handing records to someone who doesn’t have bot access.

The starting_xp column name matches what /importxp expects, so an exported file can be re-imported directly with no reformatting — export → (optionally edit) → /importxp that same file. What re-importing does depends on which existing_users mode you pick: check in treats each person’s exported current_xp as a fresh checkin (history preserved) — the right choice for most re-imports; reset treats it as a full restart (history wiped), better suited to disaster recovery than routine use.

Backup & restore

/exportdata only exports current stats — it doesn’t capture your settings (requirement, week sync, XP roles, channels, toggles). For a true full snapshot:

/backup

Downloads a JSON file containing everything: every setting in /settings, every XP milestone role, and every tracked user’s full history — not just their current numbers. Keep this file somewhere safe (not just in Discord, where messages can be deleted).

/restore file:<the .json from /backup>

Uploads that file back and completely replaces the server’s current settings and tracked data with it — shows you how many users are in the backup and when it was taken before asking for confirmation, since this discards everything that’s happened since that backup, not just undoes one action. This is real disaster-recovery, not the scoped undo that /undoimport provides for a single import.

A backup taken with an older version of the bot (missing settings that didn’t exist yet) restores fine — any keys not present in the file get filled in with current defaults rather than causing an error.

/restore is deliberately not Premium-gated, even though /backup is. Premium status lives in the same shared data file as everything else — if that file is ever lost (a botched redeploy, a host wiping storage, etc.), the server’s premium status is lost right along with it, which would make /restore itself unreachable at exactly the moment it’s needed most: fixing a lost data file requires premium, but premium was part of what got lost. Since actually running /restore requires already possessing a legitimate backup file for that specific server — which only exists if the server was using the bot in the first place — there’s no real exploit here, just closing off a lockout trap. The bot owner also always passes any Premium check regardless (see require_premium() in the code), as a second layer of the same protection.

Crew level

/crewlevel
/setcrewlevelformula base_xp:1000 growth_rate:1.15

Roblox gym crews often have an in-game “crew level” that levels up as the crew’s combined XP grows — this recreates that as a bot command, since the game itself doesn’t expose it anywhere convenient to check. It’s the same exponential math as the standalone Crew Level Tracker spreadsheet (a calculator built earlier in this project, kept as a separate tool since not everyone wants this wired into the bot) — set the same base_xp/growth_rate in both places and they’ll always agree.

How the curve works: base_xp is how much combined XP the crew needs to go from level 1 to level 2. growth_rate is how much more every level after that costs than the one before it, as a multiplier — 1.15 means each level needs 15% more than the last. This compounds fast: with those defaults, level 16 alone requires roughly 47,500 combined XP, not a simple 16× multiple of the base.

Calibrating it to match the real in-game numbers: the defaults (1,000 / 1.15) are a starting guess, not a known-correct value — Roblox doesn’t publish the actual formula. Run /crewlevel, compare the level it reports against your crew’s real level in-game, and adjust base_xp/growth_rate with /setcrewlevelformula until they match. Two known real data points (e.g. “we were level 8 at X combined XP, hit level 9 at Y”) pin down both numbers precisely — the same calibration approach the spreadsheet’s “Calibration Check” cell is built around.

What “combined XP” means here: the same definition as /crewtotals’ “Total XP (crew)” — every tracked member’s current XP added together, including members no longer in the server, and members explicitly removed from tracking (/removeuser, /removeuserbyid, /removeallusers, /removestaleusers) — their final XP is banked into a permanent running total before their entry is deleted, so the crew’s level can never go down just because someone stopped being individually tracked. That matches reality: the crew’s actual in-game total doesn’t shrink when the bot stops watching one member. Not /totalxpgained-style “since they started tracking” — this is the crew’s actual current total, since that’s what the real in-game level is presumably based on.

The ETA is a real projection, not a guess: it sums each member’s own current-week rate_per_day (the same rate /status shows individually) into one combined crew-wide daily rate, then divides the remaining XP to the next level by that rate. A member with a currently negative rate (e.g. right after an admin correction) is floored to 0 for this calculation rather than dragging the whole crew’s estimate down — one person’s data cleanup shouldn’t make the ETA worse. If the crew’s combined rate is 0 (no one’s gained anything recently), it says so plainly instead of showing an infinite or nonsensical ETA.

Next Milestone field — alongside the immediate next level, /crewlevel also shows XP remaining and an ETA for the next round level: 10, 20, 30, … 100, 110, and so on. If the crew’s already sitting exactly on a multiple of 10, this correctly targets the next one rather than the level they’re already at.

/adjustcrewxp amount:50000
/adjustcrewxp amount:-10000

For manually correcting the crew’s combined XP total — reconciling against a real in-game number, or backfilling contributions from removals that happened before bank_removed_user existed (nothing can automatically recover XP from a removal that predates this feature; this is the manual fix for that). Positive adds, negative subtracts, and it’s a running total — each use adds to whatever adjustment is already on file rather than replacing it, so undoing a specific adjustment is just running the same command with the sign flipped. Shows up in both /crewtotals and /crewlevel, and in the admin log if one’s configured.

Analytics

/analytics

A Premium-gated snapshot of how the server’s actually doing, not just individual stats:

Retention and most-active-day are both derived from stored checkin history, which is capped per user (MAX_CHECKINS_STORED, 50 by default) to keep the data file from growing forever — for a very long-tracked, very active member, their oldest checkins may have aged out, which can slightly skew those two numbers for that person specifically. Doesn’t affect Tracked Members, Active This Week, or Avg Gain, which are based on current totals rather than history.

Admin action log

With this many destructive commands now available (/fullreset, /removeallusers, /undoimport, /restore, and more), it’s worth having a record of who did what, especially once more than one person has admin access.

/setadminlogchannel channel:#admin-log

Posts a log entry to that channel whenever an admin runs a data-affecting command — who did it, what it was, and relevant details (e.g. old value → new value, or how many users were affected). Covers: /fullreset, /removeuser, /removeuserbyid, /removeallusers, /removestaleusers, /resetweek, /setweekprogress, /setweekprogressall, /setbaseline, /setrequirement, /addxprole, /removexprole, /importxp, /undoimport, /restore, and admin-performed /undos (not self-undos — those aren’t really “admin actions” on someone else).

Routine display toggles (/togglerecentrate, /togglecompactleaderboard, etc.) are deliberately not logged, to keep the log meaningful rather than noisy — it’s focused on things that change tracked data or affect the whole server, not cosmetic preferences.

Turn it off with /clearadminlogchannel.

This same channel also flags multi-week catch-up checkins — a fairness gap worth understanding: the week-rollover logic only runs when a member actually checks in, and it has no way to know when within a gap their XP changed. If someone goes 7+ days without checking in and then checks in once, the entire gain since their last checkin gets counted toward whichever week they finally show up in — a two-week grind compressed into one week’s numbers, effectively giving them a head start the following week. Rather than guessing how to split that gain across the weeks it might have actually happened in (which the bot fundamentally can’t know), a checkin after a 7+ day gap with real XP gained posts an “⚠️ Catch-Up Checkin Flagged” note to the admin log channel — who, how long the gap was, how much XP was counted — so an admin can look at the specific case and decide for themselves, e.g. adjusting with /setweekprogress, rather than the bot silently either allowing it or rewriting someone’s numbers on their behalf. Nothing is changed automatically; this is purely informational. A checkin that comes back to zero real gain (someone just confirming they’re still at the same XP) doesn’t trigger it — only an actual multi-week gap with real progress attached does.

Progress charts

/progresschart user:@SomeMember

Renders a line chart of that person’s XP over time, built from their stored checkin history, and posts it as an image. Needs at least 2 checkins to plot anything — with only 1, there’s no trend to show yet, so the bot says so instead of generating an empty chart. Like everything else, this respects the channel restriction if one is set.

Chart generation depends on the matplotlib package (already in requirements.txt). If it’s somehow missing, the command explains what to install rather than crashing.

Profile cards, animation & banner tiers

/profile user:@SomeMember

Renders an image card — avatar with a colored ring, name, rank, Crew XP, this week’s gain, and a progress bar toward the weekly requirement — instead of a plain text embed. Rank is calculated the same way as /totalleaderboard: by actual total XP, so it includes each person’s starting point, not just XP gained while tracked.

Rank #1 gets a gold star and gold-accented rank text regardless of banner tier, so the top spot always stands out. Rank text otherwise uses a brightened, boosted-saturation version of the tier’s primary color — the raw color looks great as a thick border/ring, but some colors (dark, muted ones especially) are too hard to read comfortably as plain text against the card’s dark background, so text specifically gets a readability boost while the border, ring, and progress bar keep the true color. The card is drawn at high internal resolution and downscaled for smooth, anti-aliased edges rather than the jagged look raw drawing would otherwise produce, then shipped at 1560×630 — a higher native resolution than the card’s actual design size, so it still looks sharp in Discord’s small inline chat preview rather than blurry. Long display names are truncated based on actual rendered pixel width, not a fixed character count, so wide and narrow character names both fit correctly instead of some overflowing while others get cut short unnecessarily, and truncation leaves extra room automatically when a tier badge (below) is also on the card, so the two never overlap.

If a member holds a banner tier, its name appears as a small colored badge in the top-right corner (e.g. “SUPPORTER”, “ELITE”) — pulled straight from the name you gave it in /addbannertier, colored with the tier’s own primary color, with the badge’s text automatically flipped between near-black and near-white depending on which reads better against that particular color.

The rank-1 star and the rank-change up/down indicators are drawn as actual shapes, not Unicode symbol characters (★/▲/▼) — Windows’ Arial, a common fallback font, doesn’t reliably include those glyph blocks, which could make them silently fail to render (or show as a blank box) depending on what font the host has available. Drawing them as vector shapes instead means they always render correctly regardless of platform or installed fonts.

Personal bests — if a member has ever set a new single-day XP record, a “Best Day X” pill chip shows up under their rank. Tracked automatically, not something anyone has to set: the most XP gained between one checkin and the next on a given calendar day (UTC). A brand-new member with no record yet simply doesn’t get the chip at all, rather than showing “Best Day 0.” Best-week is still tracked the same way (the highest weekly total a completed week has ever closed with) — it’s just no longer shown on the card itself, freed up for a bigger lift list.

Lift badges — a small aligned list (Deadlift / Bench / Squat, full names, values right-aligned in kg) stacked under the tier badge, or in its place if there’s no tier. Only appears once at least one lift has been set with /setlift — not a permanent row of dashes for members not using the feature.

Weekly progress shows the percentage inside the bar itself rather than as separate text, and the label row includes seven small dots — one per day of that member’s current tracking week, filled if they checked in that day. Since this bot’s “week” is a rolling per-member window (see /setweekprogress), not a fixed calendar Monday–Sunday, day 1 of the dots is whenever their week actually started, not necessarily a Monday.

Daily rank change — next to the rank, the card shows how much someone’s rank has moved since the last daily snapshot: ▲2 in green for climbing, ▼1 in red for dropping, or — for no change. A background task takes one snapshot per UTC day for every server with tracked users (no shared week required for this specific feature, unlike the scheduled post and inactivity pings). The indicator only appears once at least one snapshot has happened — a server’s very first day using the bot won’t show a rank-change badge yet, since there’s nothing to compare against.

/addbannertier role:@Supporter name:"Supporter" mode:gradient color1:#FF3EA5 color2:#3498DB priority:1
/addbannertier role:@Elite name:"Elite" mode:rainbow color1:#FFD700 priority:2

Each tier ties a profile banner look to a Discord role — the intended flow is: someone buys a tier from your store (/store, below), your store’s own integration (e.g. Ko-fi’s Discord role sync) or you manually grants them the matching role, and the bot picks up on that role the next time they run /profile. There’s no payment processing inside the bot itself; it only reacts to the role.

Three modes:

If a member holds roles for more than one tier (e.g. they kept an old role after upgrading), priority decides which one wins — higher number = more premium, and only the single highest one is ever shown, never a blend. Set it however you like; it just needs to be higher for the tiers you consider more valuable. /listbannertiers shows everything currently configured, highest priority first. /removebannertier takes a tier away from a role.

/previewtier tier:Elite lets anyone see what a tier would look like on their own card — real avatar, name, rank, and stats, with that tier’s colors/animation applied — without needing the role first. The tier field autocompletes from whatever’s configured in that server. This is meant as a pre-purchase preview (marketing, basically: “here’s what buying Elite actually gets you”), so the response is ephemeral — only the person who ran it can see it, both to keep channels clean and so a preview never gets mistaken for someone’s actual current profile. It’s still gated by /setpremiumsku//addpremiumguild like the rest of the banner tier commands, since previewing a look the server can’t currently render anyway wouldn’t be very useful.

Members with no tier role fall back in this order: whatever /setbordercolor has set for them individually (a preset or custom hex, always solid) — then, if that’s never been set either, the server’s own default look, set once for everyone with /setbasecard mode: color1: [color2] (solid or gradient, admin’s choice). /setbordercolor still works exactly as before and always wins over the server default when both exist — it’s a good fit for a free/no-purchase-required look, or for one-off manual color assignments outside the tier system. /setbasecard is for setting what unconfigured members look like server-wide, instead of everyone defaulting to plain Slate — and unlike banner tiers or /setbordercolor, it’s not Premium-gated, since it’s the server’s own baseline appearance, not a paid perk layered on top of it.

/toggleanimatedprofile enabled:false is a server-wide override for the rainbow mode specifically — solid and gradient tiers are always static regardless of this setting. Turn it off if you’d rather trade the animation for lower bandwidth/faster /profile responses (an animated card takes a few seconds longer to render and is several megabytes, versus well under a second and under 200KB for a static one), or for accessibility if anyone in your server is sensitive to on-screen motion. With it off, rainbow-tier members still get their tier’s card, just as a static frame at color1 instead of animating.

Card rendering depends on the Pillow package (already in requirements.txt). If it’s somehow missing, /profile explains what to install rather than crashing. Font rendering automatically adapts to whatever’s available on the host — it checks common Linux, Windows, and macOS font locations and falls back to a built-in font if none are found, so the card looks right on your Windows PC during testing and on whatever you eventually host it on.

SKU role automation (Discord purchases & subscriptions)

/addskurole sku_id:1234567890123456789 role:@Supporter name:"Supporter"
/addskurole sku_id:9876543210987654321 role:@Elite name:"Elite Monthly"

If you’re selling through Discord’s own Monetization system (SKUs configured in the Developer Portal — one-time purchases or subscriptions bought right inside Discord, not through an external site), the bot can grant and remove roles automatically as those purchases happen, with no manual step:

Combine this with a banner tier on the same role (see above) and the whole chain becomes automatic: someone buys a SKU in Discord → they get the role within seconds → their next /profile shows the tier’s banner look. No manual /setbordercolor or role-clicking needed anywhere in that chain.

This is separate from the /store/Ko-fi setup below — that’s for external storefronts where you (or a third-party integration) still have to grant the role yourself. If you’re selling through Discord’s native purchase flow instead, this is the fully-automated version of the same idea. A server can use either, both, or neither; they don’t conflict since they both just end up granting a role.

Requires discord.py>=2.4.0 (already the pinned minimum in requirements.txt) and a bot application with Monetization enabled and at least one SKU created in the Developer Portal. sku_id is the numeric SKU ID from that page, not a product name.

Premium features

/setpremiumsku sku_id:1122334455667788990
/addpremiumguild

A separate tier from the per-user SKU roles above: this one is a guild subscription — a whole server gets unlocked, not one member. /setpremiumsku tells the bot which SKU ID (from the Developer Portal) represents that subscription. Once set, a server counts as Premium if it either has an active subscription for that SKU, or is on the free-exemption list.

The ⭐-marked commands in the table above are Premium-gated: mass data management (/importxp, /exportdata, /undoimport), inactivity pings (/setinactivitychannel and friends), data safety (/backup, /restore), milestone announcements (/setannouncechannel, /clearannouncechannel), and customizable profiles (/addbannertier, /removebannertier, /listbannertiers, /setbordercolor, /removebordercolor, /listbordercolors). A non-Premium server’s admins can still see and try these commands — they’re told the server needs Premium, same tone as a normal permission error, not hidden entirely.

This isn’t just a command-time check. If a server’s subscription lapses, three things that were already running stop on their own rather than quietly continuing forever: inactivity pings stop firing, milestone announcements stop posting, and /profile cards revert to the default look (banner tiers and custom border colors both fall back) — all re-checked live, not just when they were originally configured. The alternative — checking only at setup time — would let a server subscribe once, configure everything, cancel, and keep every perk permanently.

Free exceptions, e.g. your own server:

/addpremiumguild

Run with no arguments in a server to exempt that server specifically — this is the common case, including your own. Pass a guild_id to exempt a different server without being in it. /removepremiumguild undoes it, and /listpremiumguilds shows everything currently exempt or subscribed, by name where the bot can resolve it.

Like the SKU role automation, this is kept in sync by two layers: on_entitlement_create/on_entitlement_delete/on_entitlement_update react within seconds, and reconcile_premium_guilds re-derives the truth from Discord’s actual entitlement list once an hour as a safety net (catches anything the gateway events alone can’t fully cover — see “SKU role automation” above for why that safety net matters). All four premium commands (/setpremiumsku, /addpremiumguild, /removepremiumguild, /listpremiumguilds) are Owner-only, same restriction as the store commands — this is a business decision about who gets access, not a per-server setting.

Store

/setstoreurl url:https://ko-fi.com/yourpage
/addproduct name:"Gold Border" price:$5 description:"Unlocks the Gold profile border" emoji:🏆

/store shows everyone a menu of whatever products you’ve configured, plus a Visit Store button linking wherever purchases actually happen — a Ko-fi page, a website, a Discord shop channel, wherever you’re set up to take payments. The bot doesn’t process payments itself; this is a catalog and a signpost, not a checkout.

This is the other half of the banner tier system described above — /store is where members find out what’s for sale and where to buy it, and the role they receive after buying is what /addbannertier keys off of to show the right banner on their /profile card. If you’re selling through an external storefront, that role still has to come from somewhere: your store’s own Discord role-sync integration if it has one (e.g. Ko-fi’s), or granted manually. If you’re selling through Discord’s own Monetization/SKU system instead, see “SKU role automation” above for the fully automatic version — no manual step at all.

Products are stored per-server (up to 25, a Discord embed limit) with a name, price (any free-text format — $5, 500 Robux, Free, whatever fits how you actually sell things), an optional description, and an optional emoji. Remove one with /removeproduct using its exact name, or via the settings panel’s dropdown if you’d rather not worry about exact spelling.

Public milestone announcements

/setannouncechannel channel:#achievements

Once set, the bot posts a congratulations message to that channel whenever someone earns a new XP milestone role (see “XP milestone roles” above) — e.g. “@Someone just earned @Veteran!” Only role additions trigger an announcement, never removals (so someone getting demoted between tiers doesn’t generate an awkward public message).

Bulk operations stay quiet by default, on purpose. Retroactively granting a newly-added role with /addxprole, running /syncxproles, and importing via /importxp all skip individual announcements by default — otherwise a big batch would flood the channel with dozens of messages at once. /addxprole’s retroactive grants and /syncxproles always stay quiet (they report their own summary instead, e.g. “X roles granted”). /importxp is the one exception: it has an announce_milestones toggle if you do want a public post for every milestone hit during that specific import — useful for a big “everyone’s XP is now official” batch update where you want the celebration, not just for routine data corrections.

Run /clearannouncechannel to turn announcements off again.

Scheduled weekly leaderboard post

/setweeklypost channel:#xp-results

Auto-posts the weekly leaderboard to that channel once, shortly before each week ends — a background check runs every 15 minutes looking for that moment.

Requires a shared server week (set up via /setweekprogressall) — without one, there’s no single “week end” moment for every tracked person to schedule around, so the command explains this and won’t let you set it up until you have one.

Timing detail that matters: the post captures the week’s final results, not the just-reset numbers of the new week. It fires in roughly the last 20 minutes before the actual rollover — deliberately before the boundary, since checking after it would mean the data has already reset to the new week’s near-zero starting point. Run /clearweeklypost to turn this off.

Inactivity pings

/setinactivitychannel channel:#xp-reminders
/setinactivitythreshold days:0 hours:24 minutes:0

As the week nears its end, the bot @mentions relevant members in the channel you set, once — not repeatedly. /setinactivitythreshold controls how early this fires (default: 24 hours before week-end); set it earlier for a bigger heads-up, or closer to the deadline for a final-hours nudge. Like the scheduled post, this also requires a shared server week.

By default, this only catches people with zero checkins that week — it doesn’t look at pace at all. Someone who checked in once but is way behind on XP gets no ping by default, same as someone who’s comfortably on track. To also flag people who have checked in but are behind pace to hit the requirement:

/toggleinactivitybehindpace enabled:true

With this on, the ping message splits into two clearly labeled groups — “haven’t checked in at all” and “checked in, but behind pace” — so it’s obvious which situation each person is in rather than lumping everyone into one undifferentiated list.

Don’t want to wait for the scheduled ping? /pingbehindpace does the “behind pace” half of this on demand, right now, regardless of /toggleinactivitybehindpace’s setting or how close the week is to ending — useful for a mid-week nudge rather than only at the automatic threshold. It only ever pings people who’ve checked in but aren’t on track; it doesn’t touch the “zero checkins” group, which stays exclusive to the automatic ping.

Run /clearinactivitychannel to turn pings off entirely.

Leaderboard history

/pastleaderboard weeks_ago:2

Every week’s final leaderboard is automatically saved, indefinitely — full detail, same data the live leaderboard shows (rank, gain, on-track status, prorated flag), not just a summary. /pastleaderboard browses it: 1 (the default) is the most recently completed week, 2 is the week before that, and so on back through everything that’s ever been recorded.

Requires a shared week to work at all. A snapshot is taken automatically, once, right before each week boundary — but “the week” only has one clear ending moment for the whole crew if /setweekprogressall has established a shared clock (week_anchor). Without that, different members’ weeks end at different times, and there’s no single “the leaderboard for week N” to save. If your crew has never used a shared week, /pastleaderboard will have nothing to show until that’s set up and at least one full week completes under it.

Independent of /setweeklypost. That feature announces the leaderboard publicly to a channel; this one just remembers it internally. You can have history without ever setting up the public post, or the public post without necessarily thinking about history — they happen to share the same trigger moment, but one doesn’t require the other.

Worth knowing about growth: since this keeps every week forever by design, the data file does grow a little each week — roughly proportional to server size (a snapshot is a small record per tracked member). For a typical crew this adds up to single-digit megabytes over a couple of years, not a practical concern, but it’s not nothing either, and it’s not something this bot currently prunes or caps automatically. If that ever becomes worth revisiting, that’s a real conversation to have deliberately rather than something to assume away.

Leaderboard pagination

By default, a leaderboard that’s too long to fit in one Discord embed gets truncated with a note about how many entries were cut. As an alternative:

/toggleleaderboardpagination enabled:true

Switches /weeklyleaderboard and /totalleaderboard (and the scheduled auto-post) to Previous/Next buttons instead, paging through 10 entries at a time so nobody gets cut off the list. Turn it back off with enabled:false to return to the single-embed view.

Bot status (presence)

The bot’s Discord status automatically rotates every 45 seconds between a few useful lines shown under its name — “Watching 12 XP trackers,” “Listening to /checkin,” “Watching 3 servers,” and a pointer to /settings. This needs no setup and isn’t configurable per-server since a bot only has one global status across every server it’s in.

Worth knowing: this is the extent of what a Discord bot can do here. True Rich Presence — the image, buttons, and party info you see on some Discord user profiles for games — is a feature of Discord’s RPC SDK for user game clients, not something available to bots through the bot API. A bot’s status is limited to one activity verb (Playing/Watching/Listening/Competing) plus a short text line, which is what’s implemented here.

To customize the rotation text, edit the list inside build_presence_statuses() near the top of bot.py.

Notes & limitations

Packaging the bot for other servers

The bot already supports being added to more than one server with no code changes — every setting (requirement, roles, banner tiers, channels, etc.) is stored per-server, so servers never see or affect each other’s data or configuration. What needed deliberate handling was the store and SKU commands (/setstoreurl, /clearstoreurl, /addproduct, /removeproduct, /addskurole, /removeskurole): those aren’t really “per-server settings” the way a weekly XP goal is — they’re tied to your actual storefront and your Discord Monetization SKUs (configured once, application-wide, in the Developer Portal). If any admin in any server could reconfigure them, someone could point /store’s button at their own link, list fake products, or map a role to a real SKU ID and have it silently pick up entitlements from your actual paying customers. So those six commands — and their /settings panel equivalents — check specifically for you (or your team, if the application is team-owned), not “Manage Server” in whichever server the command happens to run in. Everything else stays normal per-server admin control.

Two ways to get the bot into another server:

If you’re adding it yourself (e.g. your own alt server, or a community’s admin invites you to set it up): go to the Discord Developer Portal → your application → OAuth2 → URL Generator. Under Scopes, check bot and applications.commands. Under Bot Permissions, check at minimum Send Messages, Embed Links, Use Slash Commands, and Manage Roles (needed for XP roles, banner tiers, and SKU role automation). Copy the generated URL and open it — it’ll prompt you to pick which server to add the bot to, same as any bot invite.

If you want other people to be able to add it to their own servers: the same invite URL works for them too, as long as your application’s Bot → Public Bot toggle (in the Developer Portal) is turned on. Share that URL however makes sense — your own server, a website, wherever. Whoever adds it will need “Manage Server” in their own server to complete the invite, same as any bot, but once it’s in, their local admins only get the “Admin” commands from the table above — the store/SKU ones stay locked to you specifically, everywhere, automatically, with no per-server setup needed on your end.

One thing worth knowing: /settings is an ephemeral response (only visible to whoever ran it) specifically so its buttons and dropdowns can’t be seen or clicked by anyone else in the channel — Discord doesn’t restrict component interactions to the original command invoker on its own, so a public settings message would otherwise let anyone who could see it attempt to click through it.

Keeping it running

The bot only runs while python bot.py is active in a terminal window. A few options for keeping it up without babysitting a terminal:

Setting up Git

Doing this now — even before you host anywhere — makes every future update a git push instead of manually re-copying files.

1. Install Git (skip if git --version already works in your terminal)

2. Initialize the repo — in your project folder:

git init
git add .
git commit -m "Initial commit"

The .gitignore in this project already excludes .env (your bot token) and xp_data.json (your live tracking data) — those should never end up in version control, since .env is a secret and xp_data.json is per-deployment data, not source code.

3. Create a GitHub repo and push

  1. Go to https://github.com/new, create a repo (private is fine, and probably what you want here)
  2. GitHub will show you commands like these — run them in your project folder:
    git remote add origin https://github.com/<your-username>/<repo-name>.git
    git branch -M main
    git push -u origin main
    

From now on, updating the bot looks like:

git add .
git commit -m "describe what changed"
git push

No more downloading files and dragging them into a folder — new versions of bot.py just replace the old one locally, then this pushes the change up.

Hosting on Railway (free tier available)

Railway can run the bot 24/7 and auto-redeploy every time you git push, once connected.

  1. Go to https://railway.app and sign up (GitHub login is easiest)
  2. New Project → Deploy from GitHub repo → pick your repo
  3. Railway auto-detects Python and installs requirements.txt automatically
  4. Set your environment variable: in the project → Variables tab → add DISCORD_TOKEN with your bot token as the value (this replaces your local .env file — Railway injects it the same way python-dotenv does locally, so no code changes needed)
  5. Set the start command if Railway doesn’t infer it: Settings → Deploy → Start Command → python bot.py
  6. Deploy. Check the Logs tab for Synced N commands. Logged in as ... to confirm it’s running

One important gotcha: xp_data.json lives on Railway’s filesystem, which is not persistent by default — a redeploy can wipe it. Before relying on this for real, add a Volume (Railway’s persistent storage feature, free tier includes one) mounted at your project folder, so xp_data.json survives redeploys. Without this, every git push would silently reset everyone’s tracked XP.

Fly.io and Render work similarly (GitHub-connected auto-deploy, environment variable for the token, persistent volume for the data file) if you’d rather compare options — the setup shape is nearly identical across all three.