A Discord bot for managing Roblox experiences via Roblox Open Cloud APIs - with both slash commands and natural language input powered by Claude (Anthropic).
- Moderation - Ban and unban players, check ban status, list active bans
- DataStore Management - Read, write, list keys, and delete standard datastore entries
- Leaderboards - View and remove entries from ordered datastores
- Natural Language Interface - Use
/askwith plain English commands; Claude Haiku 4.5 parses intent and presents a confirmation before executing - Multi-universe Support - Manage multiple Roblox experiences from one bot instance, each with its own API key
- Privacy - Sensitive player data in bot responses is automatically redacted after 2 minutes;
/forgetmelets admins wipe stored data - Security - All commands require Administrator permission; NLP commands include a confirmation step, prompt-injection defence, LLM rate limiting (5 req/60s per user), and batch write protection
| Variable | Required | Description |
|---|---|---|
DISCORD_TOKEN |
Yes | Discord bot token |
ENCRYPTION_KEY |
No | 64-char hex string (32 bytes) for AES-256-GCM encryption of persisted API keys. Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))". Without this, keys are held in memory only and cleared on restart. |
NODE_ENV |
No | Set to "production" on the GCP VM. Controls default log level. |
LOG_LEVEL |
No | "debug", "info", "warn", or "error". Defaults to "info" in production, "debug" otherwise. |
RATE_LIMIT_MAX |
No | Max Roblox API requests per universe per window (default: 50). |
RATE_LIMIT_WINDOW_MS |
No | Rate limit window in ms (default: 60000). |
Minimal .env to get started:
DISCORD_TOKEN=your-discord-bot-token
npm install
npm startAll other secrets are set at runtime via slash commands. When ENCRYPTION_KEY is configured, keys are encrypted and persisted to data/keystore.enc — they survive bot restarts. Without ENCRYPTION_KEY, keys are held in memory only and must be re-entered after each restart.
Register a Roblox universe:
/setapikey universeid:<universeId>
A secure modal will appear prompting you to paste your API key. Run this once per experience you want to manage. The key must have the appropriate Open Cloud permissions (DataStore read/write, User Restrictions write).
Register an Anthropic API key (required for NLP):
/setllmkey
A secure modal will appear prompting you to paste your Anthropic API key. The bot validates the key by making a test call before storing it.
All commands require Administrator permission and are ephemeral or guild-only.
| Command | Parameters | Description |
|---|---|---|
/setapikey |
universeid |
Register a Roblox Open Cloud API key for a universe. API key is entered via a secure modal. |
/setllmkey |
- | Register the Anthropic API key for NLP processing. Key is entered via a secure modal. |
| Command | Parameters | Description |
|---|---|---|
/ask |
prompt |
Issue a command in plain English via AI. Claude Haiku 4.5 parses your intent and shows a confirmation before executing. |
| Command | Parameters | Description |
|---|---|---|
/ban |
userid, reason, universeid, duration*, excludealts* |
Ban a player. Duration format: 7d (days), 2h (hours), 1m (months), 1y (years). Omit for permanent. excludealts: when true, alt accounts are excluded from the ban; when false (default), alts are also banned. |
/unban |
userid, universeid |
Remove a player's ban |
/checkban |
userid, universeid |
Check the ban status of a player |
/listbans |
universeid |
List all active bans in a universe (paginated) |
| Command | Parameters | Description |
|---|---|---|
/showData |
key, universeid, datastore |
Read a datastore entry by key. Full data is always attached as a .txt file. |
/setdata |
key, universeid, datastore, value, scope* |
Set or update a datastore entry (upsert). Value can be JSON or a plain string. |
/deletedata |
key, universeid, datastore, scope* |
Delete a datastore entry. Requires button confirmation; attaches a snapshot of the deleted value as a file. |
/listkeys |
universeid, datastore, scope* |
List all entry keys in a datastore (paginated) |
| Command | Parameters | Description |
|---|---|---|
/listleaderboard |
leaderboard, universeid, scope* |
List top entries from an ordered datastore (paginated) |
/removeFromBoard |
userid, leaderboard, universeid, key* |
Remove a player's entry from an ordered datastore |
| Command | Parameters | Description |
|---|---|---|
/forgetme |
scope* ("personal" or "server") |
Delete data the bot holds. Both scopes require a confirmation button click. Personal (default): clears your NLP command history. Server: wipes all API keys, LLM key, data processing consent, and command history for the entire guild. |
| Command | Parameters | Description |
|---|---|---|
/about |
- | Show bot version, uptime, storage mode, NLP consent status, and data practices |
* Optional parameter
Use the /ask slash command with a plain-English prompt. Claude Haiku 4.5 parses your intent, displays a confirmation embed, and executes only after you click Confirm.
Only Administrators can use
/ask. A rate limit of 5 requests per 60 seconds applies per user. The bot also checks for relevant command keywords before forwarding to the LLM.
/ask prompt:ban player 12345 for exploiting in universe 98765
The bot replies with a confirmation embed showing the parsed parameters. Click Confirm to execute or Cancel to delete the message. Read-only commands (showData, checkBan, listBans, listKeys, listLeaderboard) skip the confirmation step and execute immediately.
/ask prompt:ban user 12345678 for speed hacking, permanent, in universe 111222333
/ask prompt:ban player 87654321 for 7 days for chat abuse in universe 111222333
/ask prompt:ban 11223344 and 55667788 for exploiting in universe 111222333
/ask prompt:unban player 12345678 in universe 111222333
/ask prompt:check if player 12345678 is banned in universe 111222333
/ask prompt:list all active bans in universe 111222333
/ask prompt:show data for key 12345678 in datastore PlayerCoins universe 111222333
/ask prompt:get entry 87654321 from PlayerStats datastore in universe 111222333
/ask prompt:set PlayerCoins for key 12345678 to 500 in universe 111222333
/ask prompt:update PlayerStats for player 87654321 to {"coins":100,"level":5} in universe 111222333
setDatacreates entries that don't exist yet (upsert behaviour). The value can be JSON or a plain string.
Update specific fields inside a JSON entry without touching anything else:
/ask prompt:set Gold on key 12345678 in PlayerStats datastore universe 111222333 to 400
/ask prompt:set Gold to 400 then set Money to 100 on key 12345678 in PlayerStats universe 111222333
Multiple field changes on the same entry are automatically merged into a single API call (one fetch + one write), regardless of how many fields you change. The result embed shows a compact before→after table:
Changes
Gold 250 → 400
Money 50 → 100
You can also chain updateData with other actions in the same request:
/ask prompt:set Gold on that entry to 400, then show me the data
Use
updateData(natural language field changes) when you want to change specific properties. UsesetDataonly when you want to replace the entire entry value.setDatacannot be batched.
/ask prompt:list all keys in PlayerCoins datastore in universe 111222333
/ask prompt:list keys in PlayerStats in universe 111222333
Deletion is not available via NLP. Use the
/deletedataslash command instead - it requires a confirmation button click and attaches a snapshot of the deleted value.setDatacannot be batched via NLP - it must be run one at a time. MultipleupdateDatafield changes on the same entry are automatically merged into one API call.
/ask prompt:show the top entries on the WinsLeaderboard in universe 111222333
/ask prompt:list global leaderboard KillsBoard in universe 111222333
/ask prompt:remove player 12345678 from WinsLeaderboard in universe 111222333
/ask prompt:ban players 111, 222, and 333 for hacking in universe 111222333
/ask prompt:unban 444 and 555 from universe 111222333
Batches are capped at 10 commands per request. Read-only actions (
showData,listKeys,checkBan, etc.) and field-level updates (updateData) can be batched freely. Full-replacement writes (setData) cannot be batched.
Production deploys automatically on every push to master via .github/workflows/deploy.yml:
- SSH into the GCP VM
git fetch --all && git reset --hard origin/master && git clean -fd- Overwrite
.envwithDISCORD_TOKEN,ENCRYPTION_KEY, andNODE_ENV=productionfrom GitHub Secrets npm install --omit=dev- Kill the PM2 daemon (
pm2 kill), then start fresh viapm2 start ecosystem.config.js pm2 saveto persist the process list across VM reboots
Required GitHub Secrets: REMOTE_HOST, REMOTE_USER, SSH_PRIVATE_KEY, DISCORD_TOKEN, ENCRYPTION_KEY.
| Layer | Technology |
|---|---|
| Runtime | Node.js 16+ |
| Discord framework | discord.js v14 |
| Command handler | WOKCommands v1.5 |
| NLP / intent parsing | Anthropic Claude Haiku 4.5 (claude-haiku-4-5-20251001) via @anthropic-ai/sdk |
| HTTP client | axios |
| Environment | dotenv |
| Deployment | GitHub Actions → PM2 on GCP VM |
Per-universe API keys and the Anthropic key are never logged. When ENCRYPTION_KEY is set, they are encrypted with AES-256-GCM and persisted to data/keystore.enc so they survive restarts. Without ENCRYPTION_KEY, they are held in memory only and cleared on restart.
RoAdmin follows a defense-in-depth approach — multiple overlapping layers so that no single failure compromises the system.
- Every command requires Discord Administrator permission, enforced both in command metadata and modal handlers.
- All data is guild-scoped — API keys, LLM keys, consent, and command history set in one server are inaccessible from another.
- NLP commands require explicit data processing consent (button click) from a server admin before the guild can use
/ask.
- Persisted keys use AES-256-GCM (authenticated encryption).
- The encryption key is derived via HKDF-SHA256 from the
ENCRYPTION_KEYenv var, using a per-deployment random salt stored indata/keystore.salt. - Keystore writes are atomic (write → temp file → rename) to prevent corruption.
- File permissions are set to owner-only (
0o600) on both the keystore and salt files.
- Keys are entered exclusively via Discord modals — they never appear in message history or channel text.
- Keys are validated against the Roblox API before being stored (401 = invalid, 403 = insufficient permissions).
- Keys are never echoed back, logged, or included in embeds.
userIdanduniverseIdmust be positive integers;scopeandleaderboardNameare restricted to[a-zA-Z0-9_-].- Duration strings are validated against
\d+[dmyh]and capped at 10 years. - All string fields are capped at 1,000 characters; ban reasons at 500.
- NLP-parsed parameters are type-coerced and re-validated after LLM output — the bot never trusts the model's types directly.
- The LLM system prompt enforces a strict action whitelist. Any unrecognized action is rejected and logged as a possible injection attempt.
- Game names and command history injected into the prompt are sanitized (control characters, newlines, and
<>stripped, values truncated). - Data passed to the LLM for field-level patches is wrapped in explicit
<untrusted_data>/<instruction>XML blocks, and XML-like tags within the data are stripped beforehand. universeIdvalues returned by the LLM are cross-checked against the guild's configured universe list — hallucinated IDs are rejected.deleteDatais never available via NLP; it can only be run through the/deletedataslash command with a confirmation button.
- Roblox API: Sliding-window limiter — 50 requests per 60 seconds per universe (configurable via env vars). Checked before every API call.
- LLM: 5 requests per 60 seconds per user, preventing
/askspam and runaway API costs.
- Batch NLP commands are capped at 10 per request.
setData(full entry replacement) cannot be batched — only one per request.updateDataverifies that only the requested fields changed before writing; unintended mutations are rejected.- All mutating batches require an explicit Confirm button click.
/forgetme personalclears a user's NLP command history;/forgetme serverwipes all keys, consent, and history for the guild (requires confirmation).- Command history is kept in memory only (max 20 entries per user/channel pair, max 10,000 keys total) and is never persisted to disk.
- All output goes through a structured logger gated by
LOG_LEVEL. In production,debugis suppressed entirely — no response bodies, request URLs, or credentials are written. - Raw error objects are never surfaced in Discord; users see formatted embed messages only.
