Leaderboards — Moderator Guide
Overview
A leaderboard is one live message in a channel of your choice that ranks players by points or by Elo rating and rewrites itself as results arrive. Results come from a game: one the bot hosts itself (picked from the catalogue when you make the board), one you run elsewhere (a browser game, a bot, a script) that submits scores over the API with a game key, or a moderator typing them in by hand. Boards are created and configured on a private web page; the /leaderboard command keeps the actions you want in the heat of a session.
Players reach a game through the ▶ Play button on the board's message or the /play command. For a browser game either hands them a personal link with a 30-minute play session, so the result is credited to the right person without the game ever asking who they are; a bot-run game is played right there in Discord instead, in a public thread the bot opens for it (see Bot-run games). Versus games start with a challenge: one player names another, who accepts from a post in the channel.
A board has one of two modes, chosen at creation and fixed for its life. A points board ranks by score; two further choices decide what a player's results are worth: the order (highest wins or lowest wins, for golf scores and time trials) and the aggregate (sum: every result adds up, right for a puzzle solved; best: only each player's best single result counts, right for an arcade game where one launch is many runs). A lowest-wins board is always best. An Elo board is for versus games: every result is a match with a win, loss or draw, everyone starts at 1000, and a rating moves up or down by how surprising the outcome was (see Elo boards).
Five games ship with the bot: Flappy Francine (an arcade game; best run counts), Word Scavengers (a word search with optional zombie decoys; one puzzle per window, the same puzzle for everyone on the board) and Wordle in Discord, run by the bot, in three modes that are three tiles: Word Bopper (solo; one word per window, the same for everyone), Word Bopper Party (a host and up to three guests race for one word in the open) and Word Bopper Versus (two players race to guess each other's secret word; Elo only).
Two-server setups: boards, keys and results are keyed to the chat server, so every subcommand works the same whether you run it in the chat server or the role server. The server id in API URLs can be either guild id.
Getting started
/leaderboard dashboard— a private link to the setup page (valid 5 minutes).- Press New board and walk the wizard: pick a tile. A hosted game tile (Flappy Francine, Word Scavengers or one of the three Word Bopper modes) needs nothing else from you; External game (API) is for software that will send scores; Manual is for results you type in. Then choose the mode (points, or Elo (versus games)), and for points the ranking order and the aggregate (sum or best; the tile proposes the right one) or for Elo the K factor and whether both players must confirm a match; none of these can change after creation except K. A tile that only makes sense as certain boards offers those as one ranking choice instead (Word Scavengers, Word Bopper and Word Bopper Party), or forces Elo (Word Bopper Versus), and a bot-run tile hides the board controls it fixes (see Bot-run games). Then display mode and top N. A tile with game settings (Word Scavengers and the Word Bopper modes) adds a Game settings card here, prefilled with the defaults. Then name the board and pick its channel.
- Create and post puts the live message in the channel. (
/leaderboard postdoes the same later, or moves it.) If the board has a launchable game, the message carries a ▶ Play button. - For an external game, the wizard lands on the board's Keys card: make a key (or bind an existing one) and hand the public key, the server id and the board slug to whoever runs the game. The API tab shows them the exact URLs and a ready-to-paste request. Give the key a launch URL (
https://only) and the Play button works for that game too. A bot-run game's key shows played in Discord where the launch URL would be; there is nothing to hand out. - For a manual board, record results with
/leaderboard addor the page's Add result form.
/leaderboard subcommand reference
Every subcommand requires the server's moderator role, and every reply is private to you except the board message itself. The board option autocompletes live boards by name; an archived board is addressed as #<id> (the autocomplete shows it that way).
┌──────────────────────────┬──────────────────────────────────────┬──────────────────────────────────────────────────┐
│ Subcommand │ Options │ What it does │
├──────────────────────────┼──────────────────────────────────────┼──────────────────────────────────────────────────┤
│ dashboard │ │ Private link to the setup page (5 minutes) │
│ list │ │ Every board with status, mode, players, results, │
│ │ │ games and a link to its message │
│ post │ board [channel] │ Post the live message here (or in channel); │
│ │ │ running it again moves the message │
│ freeze │ board │ Stop accepting results; the footer says "frozen" │
│ unfreeze │ board │ Accept results again │
│ archive │ board [reveal] │ Final render, stop results, free the slug; │
│ │ │ reveal:True adds Discord names to the render │
│ add │ board player [points] [outcome] │ Record a result by hand (points boards need │
│ │ [opponent] [initials] │ points, Elo boards an outcome; opponent blank = │
│ │ │ solo vs the house; the ceiling does not apply) │
│ strike │ board result-id [reason] │ Soft-delete one result (on Elo boards: its whole │
│ │ │ match) and rebuild; the reason goes to the log │
│ history │ board [player] │ The last 25 results: id, time, player, points or │
│ │ │ outcome and match, source game, struck or not │
│ export │ board │ The whole ledger as a CSV file, attached to a │
│ │ │ private reply (see Exporting the ledger) │
│ rebuild │ board │ Recompute standings from the result ledger │
│ reset │ board confirm │ Strike every result and match (confirm:True); │
│ │ │ players keep their names │
│ key revoke │ game-id │ Revoke a game key immediately; existing results │
│ │ │ stay │
└──────────────────────────┴──────────────────────────────────────┴──────────────────────────────────────────────────┘
Board creation and settings have no command form: they need a game picker and a dozen related fields, which is what the page is for.
For players: /play and /mystanding
/play and /mystanding are the two player commands. Anyone on the server may run them; no role is needed, and every reply is private to the player.
┌──────────────────────────┬──────────────────────────────────────┬──────────────────────────────────────────────────┐
│ Command │ Options │ What it does │
├──────────────────────────┼──────────────────────────────────────┼──────────────────────────────────────────────────┤
│ play │ game [board] [opponent] │ Private launch link for a game with an open │
│ │ │ an open board; opponent:@user makes a challenge │
│ mystanding │ [board] │ The player's own row on one live board, or on │
│ │ │ every live board when board is left empty │
└──────────────────────────┴──────────────────────────────────────┴──────────────────────────────────────────────────┘
game autocompletes only games that are launchable and bound to at least one open board, so a frozen or archived board makes its game vanish from the list. When nothing is running, the reply is "No games are running right now. Watch for a leaderboard post in the server." A game on several open boards asks which board (a picker), or takes board: to say so up front. The ▶ Play button on a board's message does the same thing with the board already chosen, and asks which game when the board has several.
/mystanding answers one line per board: 🏆 **Friday Arcade** — you are #4 of 22 as **Amber Fox**, 1,240 pts over 6 games ▲1 since the last update. On an anonymous or initials board the as **…** part tells the player which name is theirs, which the public message never does (a Discord-names board leaves it out). An Elo board reads rated 1016 (2–1–0); a best or lowest-wins board best 187 over 14 games; the ▲1/▼1 tail appears only when the player moved since the live message was last updated. A player with nothing counted gets no result from you yet. board autocompletes live boards (open or frozen); archived boards are history, not a standing, and are never listed.
The setup page
/leaderboard dashboard links you to a private, phone-friendly page. The moderator role is checked server-side on every request, because the page writes boards and mints keys.
- Boards — every board with its status, mode, games, player and result counts, a link to the live message, and buttons for Post, Freeze/Unfreeze, Archive and Open. New board opens the three-step wizard: pick a tile (a hosted game from the catalogue, External game (API) or Manual), set the Mode (Points, or Elo (versus games); a hosted game that never reports an outcome cannot pick Elo, and a bot-run versus game can pick nothing else), its ranking fields (order and sum-or-best, or K factor and mutual confirmation), display and, when the game has them, its Game settings, then name, slug, channel and, if you want to limit who may play, a required role (see Required role). Picking a hosted game binds the server's key for that game (minted the first time, reused by every later board) and, for a browser game, turns Require a play session on; a bot-run tile hides that control and the mutual-confirmation one (Word Bopper Versus hides the player count too), and on an Elo board offers the K factor as Rating movement (Steady, Normal or Volatile) in place of the number. Opening a board shows its detail cards:
- Settings — name, display mode, top N, the required role (or "Anyone can play"), the "Players: N" line, the per-result ceiling (points boards), and Require a play session (refuse any result that did not come from a Play link). Mode, order and aggregate are shown but fixed. An Elo board shows its K factor, Confirm timeout and Both players must confirm a match instead, all changeable. A board bound to a bot-run game hides the same controls the wizard hid; K stays changeable as a number. Changing display away from Discord names on a board that already has results warns you and asks for a second click, because names have already been on the public message.
- Keys — the keys bound to this board (label, game id, public key, launch URL, per-minute limit, last used, result count, other boards it feeds) with Bind existing, New key, Revoke, Reactivate and Unbind. An external key also shows its signing chip (signing required, signing optional or unsigned) with Require signing / Make signing optional and Rotate secret (see Request signing). The launch URL is what the Play button opens for an external game; hosted games derive theirs from the catalogue, and a bot-run game shows played in Discord instead. A hosted game with settings shows its Game settings form here too, with a Save button (see Game settings).
- Standings — the full ranking, including rows beyond top N (a Rating column on Elo boards), with a moderator-only Discord name column on anonymous and initials boards.
- History — results newest first, filterable by player, game and status, with a Strike button that asks for a reason, an Export CSV link beside the heading (the same file as
/leaderboard export, see Exporting the ledger), and an Add result form (with an opponent field on Elo boards); and a Challenges & matches list showing every match (open, accepted, pending, applied, disputed, unconfirmed, struck, declined, expired, abandoned) with its players and outcome. - Danger zone — Rebuild, Reset (type the board name to confirm) and Archive with the reveal checkbox.
- Keys — every game key on the server, bound to any board or none: create, rename, set the launch URL, change the per-minute limit, revoke, reactivate, bind and unbind. External keys carry their signing controls here too: a new key's secret is revealed once in the box beside its public key, and each key has Require signing / Make signing optional and Rotate secret. When the bot has no
leaderboard.secretKey, the tab says signing is off instead. - API — this server's base URL, each open board's submit and standings URLs, a key picker, and request and response examples with the chosen key and board slug filled in; a key with a secret adds a Signed, from a backend (Node) example. Hand this to whoever is integrating a game, with the integration guide and the API reference.
- Settings — server-wide: the log channel (one line per accepted result, strike, over-ceiling attempt, match conflict, pending, confirmed, disputed, unconfirmed or abandoned match, abandoned party, game thread the bot could not open, confirmed rules change, secret rotation and signing requirement change) and the anonymous-name theme (animals, colours or numbers).
The game dashboard card
The moderator game dashboard at /dashboard/<serverId> (the same cookie login as the rest of the moderator pages) carries a 🏆 Leaderboards card whenever the server has a board that is not archived. Each board shows its mode, status and player count, its top 5 rows (named as the board shows them) and its 5 latest results (Discord names, as in history, since only moderators see this page) with their source and a Strike button that asks for a reason and strikes through the setup page's own API; a struck result shows struck through. The card is for a glance and a quick correction mid-session; its link to the setup page is where everything else lives.
Hosted games
New hosted games are added to the bot's catalogue by whoever maintains the bot; see Adding a hosted game.
The catalogue lists games the bot serves from its own site or plays out in Discord itself, so there is nothing to host and no key to hand out: the wizard tile is the whole setup. A hosted game has no secret; for a browser game the play session is what proves who scored, which is why the board defaults to requiring one, and a bot-run game needs no proof at all because the bot is the referee. Five games are in the catalogue:
- Flappy Francine — tap to jump, dodge the drinks. A launch is as many runs as the player likes and each run is submitted, so the tile proposes a best board: the embed reads "Best run" and shows each player's best score with their run count. Its game settings change only the look: the game title (the start screen heading and the browser tab), the player can be the Francine sprite or an emoji of your choice, and the barriers the drink sprites or solid blocks in a chosen colour. The tile suggests a ceiling of 200, which suits its difficulty curve; raise it if your players are better than that.
- Word Scavengers — a word search: trace the hidden words before the timer runs out, and avoid the zombie decoys, which end the puzzle when traced. There is one puzzle per window (15 minutes by default), built on the server so every player on the board sees the same grid at the same time, and a window can be solved once per player. The tile offers exactly two rankings in place of the mode, order and aggregate choices: Most solves (one point per solve, added up) or Fastest solve (seconds) (each player's quickest solve, lowest first; the game reports the solve time in whole seconds on such a board). Any other shape would reward or punish the wrong thing, so the page does not offer it and the server refuses it. Its game settings are the game title and title emoji (the zombie flavour is only the default; a board can be "Holiday Hunt 🎄"), a colour theme (zombie greens, ocean, sunset, candy or slate), whether zombie decoys are on at all, whether the Intel list is shown (off makes the puzzle harder, so it counts as a scoring change), the word list, the zombie words (empty also means no zombies), the grid size (small 6 × 6, standard 6 × 8 or large 6 × 10; every cell belongs to a word, so the grid decides how many words a puzzle hides and the page refuses a word list whose lengths cannot fill the chosen grid exactly) and the minutes per puzzle. With zombies off the game's wording turns neutral: no survivors, no infection.
- Word Bopper — Wordle, played in Discord with the bot running the game in a public thread per player (see Bot-run games). There is one five-letter word per window, the same word for everyone on the board, and each player gets one game at it. A solve scores Points for a solve plus Bonus per unused guess for every guess left (defaults 5 and 1: nine guesses, solved in four, is 10 points); a loss or a give-up scores 0, and there is no wait between guesses. One result per word, so the tile offers one ranking, Most points (a sum board). Its game settings are Minutes per word (default 30, 5–1440), Guesses (default 9, 3–15), Points for a solve (default 5, 0–1000), Bonus per unused guess (default 1, 0–100) and Post solutions (off by default: each window's word is posted in the board's channel as the window closes).
- Word Bopper Party — a host invites up to three players into one thread and everyone guesses the same random word in the open: every guess is public, letters and all, and the first correct word wins. The tile offers two rankings: Most points (the winner scores for the solve and for the guesses they had left; everyone else scores nothing) or Elo (the winner beats each other player in a pairwise match; a party nobody wins is no match). Its game settings are Guesses each (default 9, 3–15), Points for a win (default 5) and Bonus per unused guess (default 1), both for points boards only, Seconds between guesses (default 60, 0–180; 0 for no wait), Good guesses shorten the wait (on by default: each correct letter knocks 10 seconds off the next wait and each present letter 5, growing with the count) and Party time limit (minutes) (default 120, 5–1440), after which an unfinished party is abandoned with no result.
- Word Bopper Versus — each player seals a secret five-letter word, then they race to guess each other's in a thread of their own; the first to guess wins, and both running out is a draw. It is Elo only: the tile forces Elo mode in the wizard. Players meet through a challenge, and the bot referees, so there is no confirmation step. Its game settings are Guesses each (default 6, 3–6), the same two timer settings as Party (60 seconds, shortened by good guesses), and Match time limit (minutes) (default 120, 5–1440), after which an unfinished match is abandoned with no result.
A bot-run tile offers only the board shapes that suit it and hides the board controls it fixes: Require a play session (there is no session; results come from the bot) and Both players must confirm a match (the bot is the referee) on all three, and Show player count on Versus as well. On an Elo board (Party or Versus) the wizard offers the K factor as Rating movement — Steady (K 16), Normal (K 32) or Volatile (K 64) — instead of a number. The board's Settings card hides the same controls afterwards.
A browser game's launch link is the bot's own page for it; a bot-run game has no link and starts in Discord. Either way Play and /play work the moment the board is posted.
Bot-run games
The three Word Bopper modes and Rock Paper Scissors are bot-run: there is no link and no play session, because the whole game happens in Discord with buttons and modals, and the bot keeps the state and decides the result. Letter tiles are the bot's own custom emoji.
The thread. Every game gets a public thread in the board's channel (the channel the live message is in, wherever Play was pressed), named for the game: Word Bopper · Alice · word 1991790, Word Bopper Party · Alice, Word Bopper Versus · Alice vs Bob, Rock Paper Scissors · Alice vs Bob (the first part is the key's label, so renaming the key renames its threads). The bot posts the game's message in the thread, pings the players there, and then re-posts the live leaderboard message in the channel so it is again the last thing there; the channel itself only ever shows thread starters and the board, however many games are running. When a game ends the thread's message says so and the bot leaves the thread alone: players may keep chatting in it, and Discord's auto-archive retires it after a day's quiet. The bot needs Create Public Threads and Send Messages in Threads in that channel; where it cannot make a thread (no permission, or a channel type without threads) the game is posted in the channel itself and the log channel says why (could not open a thread in #channel (…); the game is posted in the channel instead).
Solo. Play (or /play) on a Word Bopper board opens a thread for that player's game and answers privately "Your word is waiting in #thread. Press Guess there; your board stays private." The public message in the thread shows the player's guesses as coloured squares only — 🟩🟨⬛, one row per guess, no letters — with the guesses left and the time to the next word, and carries one button, Open my board. That opens the player's private board, an ephemeral message only they see, with the letters, the keyboard, and the Guess (a small form to type the word) and Give up (scores 0) buttons. Each move replaces the private board in place and updates the squares on the public message; a dismissed private board comes back from Open my board at any time. The word is never shown in the thread, win or lose, because other players on the board may still be trying it: the word is the same for everyone in the window. A win reads Solved in 4 for 10 points and On the board: #1 with 10 points. A player gets one game per word: Play again while it is open answers "Your game for this word is already open in #thread", and afterwards "You've had your go at this word (10 points)" with the time to the next one. A board that wants the answers out in the open turns on Post solutions: as each window closes, the housekeeping job posts 🔤 Daily · the Word Bopper word for the window that closed 2 minutes ago was CRANE in the board's channel and re-posts the board beneath it (an archived board gets no posts).
Party. Play on a Word Bopper Party board makes the presser the host: the bot opens the thread with a lobby message (who is in, Start and Leave) and answers the host privately with a user picker to invite players, up to ten at a time and as often as they like. Each invitee gets a message in the thread, "Alice invited @Bob", with an Accept button only they can press (anyone else gets "This invitation isn't for you"). Nobody is in the party until they accept, and the first three accepters join (host plus three, four at the table); a fourth is told "That party is full", a repeat "You're already in this party", and an Accept after the start "That party has already started". A guest may Leave before the start; the host cannot (the party ends with its time limit if it is never started). Only the host presses Start, with however many have joined: Start deals one random word, pings everyone in the thread and turns the lobby into the board. Everyone guesses the same word through the same Guess form, each from their own guess allowance and on their own timer (a guess inside the timer is refused with a Guess WORD again button that resubmits it when the time is up, no retyping), and every guess is shown to the whole thread with its letters and colours, so a guess is information given away, a word anyone has already tried cannot be tried again, and the keyboard the table has built between them sits at the bottom of the board. The first correct word wins and ends the party; everyone running out of guesses ends it with no winner, and the message then says what the word was. Every guess re-posts the board at the bottom of the thread (the old copy is deleted), so players chatting in the thread never scroll it out of sight; if Discord refuses the post three times in a row the old board is updated in place instead and the log channel says so. Versus boards move the same way.
Scoring follows the board's ranking. On a points board the winner scores Points for a win plus the bonus for each guess they had left, as one result (@Bob scores 13 points), and every other player gets a zero-point loss result, so they appear on the board with the game counted; a party nobody wins is a zero-point loss for everyone. On an Elo board the winner beats each other player in a separate pairwise match, applied one after another at the same instant: from 1000, a win over two guests reads @Carol 1000 → 1031 · @Alice 1000 → 984 · @Bob 1000 → 985, the second step smaller because the second win was less of a surprise. Each pair is a match of its own in the Challenges & matches list, so strikes and rebuilds work as for any match. A party nobody wins records no match ("No winner, so no match was rated").
Versus. Unchanged in play: the challenge is made and accepted from the channel, and Accept opens the game's thread in the board's channel and posts the game there, with Choose my word, then Guess and Letters used (that player's own letters, privately); the challenge post stays in the channel, since it is how the challenged player gets the Accept button, and the acceptance reply points at the thread.
Rematch. The final board of a versus game (Word Bopper Versus and Rock Paper Scissors alike) carries a 🔁 Rematch button. Either player presses it to offer the other a rematch: the offer is posted in the same thread ("🔁 @Bob wants a rematch against @Alice at
Rock Paper Scissors. A versus game on an Elo board, played as a best of three: challenge and Accept work as for any versus game, and Accept opens the thread with the match board, which carries one button per hand, Rock, Paper and Scissors, or five with the Hands setting on Lizard–Spock (Rock crushes Lizard and Scissors; Paper covers Rock and disproves Spock; Scissors cut Paper and decapitate Lizard; Lizard poisons Spock and eats Paper; Spock smashes Scissors and vaporizes Rock). Pressing a hand is a private pick: the player is told "You picked 🪨 Rock. Waiting for Bob", the board shows only that they have picked (✅), and they may change their mind by pressing another hand until the other player has picked too. Then the round resolves on the board, Round 1 · 🪨 Rock crushes ✂️ Scissors · Alice, and the board is re-posted at the bottom of the thread; a tie is "go again" and does not count. The first player to two round wins takes the match, which is scored like Word Bopper Versus: both results recorded, the ratings moved at once, no confirmation step. The Hands setting applies to the next match, never one in progress; the Match time limit (30 minutes by default) abandons a match nobody finishes.
Results. Results from a bot-run game are trusted, like a moderator entry, and attributed to the game's key, like a key result. They still follow the board's rules: a frozen or archived board refuses them (Play on such a board answers that it is not accepting results), a solo result must belong to the current window or the one just closed, and a solo player gets one result per word. The log channel line reads 🏆 **Daily** · Word Bopper · bot · @A · 10 pts · now #1 for a solo result, one such line per player for a points party (the winner's points, then · 0 pts for each loser), 🏆 **Party** · Word Bopper Party · bot · party #3: @C won (1000 → 1031) against @A (1000 → 984), @B (1000 → 985) for an Elo party, and 🏆 **Ladder** · Word Bopper Versus · bot · match #7: @A win (1000 → 1016), @B loss (1000 → 984) for a versus match (Rock Paper Scissors reads the same way); both players' results are recorded under the match, and the ratings move at once. Strikes work as for any match: strike either row and the match and both rows go. A solo result's meta in history and the export carries the guesses and the word, which is why neither is public.
Housekeeping. The housekeeping job runs every minute. A party past its party time limit (120 minutes by default, counted from the moment the host pressed Play, started or still in its lobby) is abandoned: its thread message gets "⌛ Abandoned: time ran out, no result was recorded" and loses its buttons, and the log channel gets party #3 hosted by @A was abandoned: time ran out, no result. A solo game still open when its window closes is abandoned the same way, with "⌛ The word's window closed before this game finished; no result was recorded" (a guess that arrives first is told the window has closed and to press Play for the new word); nothing is logged, since no score was at stake. A versus game runs on the two clocks every challenge has: the 30-minute expiry while the challenge is unanswered, and the match time limit from acceptance (the game's setting, 120 minutes by default; 2 hours when a game has no such setting), after which the housekeeping job marks the match abandoned, the challenge post reads "abandoned, no result" and the game message gets "Abandoned: time ran out, no result was recorded." A bot restart abandons every game in progress, solo, party and versus, at startup: once the bot is back each game's message gets "🔌 This game was interrupted by a bot restart; no result was recorded", an interrupted match gets a line in the log channel, and interrupted solo games and parties are written to the action log file; since no result was recorded, a solo player presses Play again and starts the word afresh, and a host opens a new party. A button pressed on a game that is gone answers "That game is gone — the bot restarted or the word moved on"; one pressed on an out-of-date view answers "That move is out of date; look at the board again", and someone else's buttons answer "This game isn't yours."
Game settings
Some games have knobs a moderator wants to turn for their server: which words a puzzle draws from, how many, how often. These are game settings (the API calls them preferences). The game declares which it has; you set them per game per board, so Friday's Word Scavengers can run a holiday word list while Saturday's uses the stock one on the same key. They appear as a card in the wizard when you pick the tile and on the board's Keys card afterwards, prefilled with the game's defaults; a field you never touch keeps its default. The game reads them when it launches, so a change reaches players on their next launch.
Word lists are a textarea, one word per line. Words are upper-cased and deduplicated; letters only, within the game's length bounds (3 to 14 letters for Word Scavengers), and the list has to be the right size (at least 14 words for the main list; the zombie list may be empty). A bad line is refused by its line number and content, so it is easy to find. The page shows a live count.
Settings are not secrets. A browser game cannot hide anything from its player: the settings are fetched by the game, so anyone who opens the network tab can read them, zombie words included. Zombies are a deterrent against careless play, not a hidden trap, and a setting that must stay secret from players is not something this feature can offer.
Scoring-relevant settings. Some settings only change the flavour (the word list); others change what a score means (the zombie words, the grid size, the minutes per puzzle), and players before and after the change would be competing under different rules. Changing one of those on a board that already has results asks for an explicit confirmation on the page: results stay, but future results may not be comparable. If you confirm, the log channel gets a line naming the setting with its old and new value, and the live message's footer shows Rules changed
Puzzle windows. Word Scavengers divides time into windows of Minutes per puzzle, and Word Bopper into windows of Minutes per word. The puzzle for a window is seeded by the server when the window opens, so it cannot be worked out ahead of time, and every launch on the board gets the same puzzle until the window ends. A solve is accepted only for the current window or the one that just closed (a player may finish just over the boundary), and a second solve of the same window is a duplicate, so the most a player can score is one solve per window. Shorter windows mean more puzzles and more points per evening; the setting is scoring-relevant for that reason. A Word Bopper game still open when its window closes is over with no result: a guess that arrives first says so and points the player at Play for the new word, and the housekeeping job notes it in the game's thread.
Display modes
Who the board shows is a presentation choice, changeable at any time; the next render picks it up.
- Discord names — the player's server display name.
- Anonymous names — a themed name assigned on a player's first result (Amber Fox, Teal 42, Player 017), unique per board and stable for the board's life. Moderators can see who is who on the Standings card and in
history. - Arcade initials — 1–3 characters the game (or
add) sends with a result. They are uppercased, stripped to A–Z and 0–9 and cut to three characters; a short block list of three-letter words is refused. A player who never sends initials, or sends blocked ones, shows as the first three letters of their anonymous name, never as their Discord name.
Initials are identity, not score: a struck result still updates them, and a rebuild never touches them.
Hiding names later: switching a board from Discord names to anonymous names or initials after it has results is allowed, but the page asks you to confirm — those names have already been public on the live message.
Elo boards
On an Elo board every result is a match: one player's win, loss or draw against one opponent. Everyone starts at 1000. After a match each rating moves by K times how unexpected the result was: with the default K of 32, a 1000 player beating another 1000 player goes to 1016 and the loser to 984; a draw between equals moves nobody; a 1000 player beating a 1400 player gains 29 (and the favourite loses 29). K is how far one match can move a rating (1–400; 16 is steady, 32 usual, 64 volatile). You can change K later from the Settings card; the ratings already on the board stay as they are until you Rebuild, which re-rates every match from 1000 with the current K. The opponent of a match gets a standings row (and an anonymous name) the moment the match applies, even if they never submit anything themselves. Order, aggregate and the ceiling do not apply; points on an Elo result is recorded for history and nothing more.
A result with no opponent is a solo game against the house: a phantom fixed at 1000 that never appears on the board. It is a convenience for games with no second human; a solo board drifts towards wins minus losses, which is fine for a party board. A Word Bopper Party on an Elo board is scored as one ordinary match per loser, the winner on one side of each (see Bot-run games).
Both players must confirm a match (off by default) is the defence against a player reporting a win their opponent never conceded. With it off, the first report scores the match at once. With it on, the match waits as pending until the opponent's report agrees (the mirrored outcome), and only then do the ratings move; a contradicting report marks the match disputed and it never scores; and a pending match that nobody confirms within the confirm timeout (default 10 minutes, settable from 30 seconds to a day; the housekeeping job checks every minute) is marked unconfirmed, logged ("went unconfirmed: @B never reported") and never scores. The cost is that a rage-quitter's opponent loses their win, which is why it is off by default. Solo games and add entries always score at once whatever the setting. The log channel follows each match: … · @A · win vs @B · 1000 → 1016 · now #1 when a report scores, … · ⏳ waiting for @B to confirm while it is pending, … · ✅ confirmed when the mirrored report lands.
Disputes. When confirmation is off and the second report contradicts the first, the match stays applied and the ratings stay where they moved: the log channel flags it ("⚠️ disputed: @A and @B reported different outcomes"), the match shows as applied in the Challenges & matches list, and you decide. If you do not believe the first report, strike either result (see Correcting results); the match and both rows go and the board is re-rated. Nothing reverses a rating on the losing side's say-so alone.
The live message
One message per board. A scoring change (a result, a match, a strike, a rebuild or a reset) re-posts it at the bottom of the channel, so people watching the channel see it as unread; the new message is posted before the old one is deleted, so a hiccup never leaves the channel without a board. Any other change (settings, freeze, a rules change) edits it where it is. It shows the board name, the mode line (Points · top 10, Best run · top 10, Lowest wins · top 10 or Elo · K=32 · top 10), the ranked rows (medals for the first three, then numbers), …and N more players when there are more than top N, the optional Players: N line, and a footer with the time of the last result, the games feeding the board and, after a confirmed rules change, Rules changed <date>. Below the embed sits a ▶ Play button whenever the board is open and at least one of its games can be launched (a hosted game, or an external key with a launch URL); it disappears when the board is frozen or archived.
- Top N is 1–25 (default 10): one embed, kept under Discord's size limit.
- Sum rows read
**DAH** 1,240 pts (6 games); points add up across games, ties go to fewer games, then the better single result. - Best rows read
**DAH** 187 pts best (14 runs); only the best single result ranks, ties go to fewer runs (got there in fewer tries). - Lowest wins rows read
**DAH** 4120 best (3 games); the board has no notion of units, so put them in the name ("Sprint (hundredths)"). Negative points are allowed on these boards. - Elo rows read
🥇 **DAH** 1,016 (1–0–0): the rating, then the win–loss–draw record. Ties go to more games at that rating (a rating earned over more matches is better established), then more wins. - Players: N is a bare count of players with at least one counted result. There is no denominator and no percentage; who is "supposed" to be playing changes from game to game.
- Movement arrows. After every edit the bot remembers the ranks it just showed, and the next edit marks each row against them:
▲2(climbed two places),▼1(fell one),🆕(was not on the message last time) or nothing when unchanged. The very first render is🆕for everyone. The comparison is always with what the channel last saw: a strike or arebuildchanges the ranks but not the remembered ones, so the arrows on the next render show the effect of the correction, and a burst of results coalesced into one edit shows one net move. - Updates are coalesced: a burst of 50 results is one re-post, and a background job catches anything missed across a restart. If the message is deleted or the bot loses permission,
listflags the board as needs re-post; runpostagain.
post into another channel moves the message; the old one is removed.
Keys: what a key is
A game key identifies one game on this server. It is server-scoped (one live key per game id) and bound to one or more boards; a key that is bound to Friday's and Saturday's boards can submit to either URL (or to both at once through the API's fan-out route) and nothing else. A result's duplicate check is per board, so one finished game may be reported to every board the key feeds. The public key looks like lbk_ followed by 32 hex characters, and the game sends it as Authorization: Bearer lbk_… on every request. Hosted games get their key automatically; it shows on the Keys tab like any other and can be revoked the same way.
The key is an identifier, not a secret: anyone who can read a browser game's source can read its key. What it buys you is attribution and a kill switch. Revoke a leaked or retired key with /leaderboard key revoke or the Keys tab; results already recorded stay, and the key can be reactivated from the page. Each key has its own per-minute request limit (default 60), set on the page; a game that submits for many players from one machine wants it higher.
Three guards sit on top of the key. The per-result ceiling on a points board's Settings card is the cheapest: a result above it is recorded struck, not counted, and appears in history and the log channel as an over-ceiling attempt (an Elo board has no ceiling; mutual confirmation is its equivalent). The strongest for a browser game is the play session, below. The third is request signing, for a game with a backend.
Request signing
An external key can also carry a signing secret (lbsec_ followed by 64 hex characters). A game that has it signs every submission with an HMAC over the exact request body and a timestamp (the API guide has the recipe), and a signature that does not verify is refused. The secret is shown exactly once, in the reveal box beside the public key, and never again; copy it to whoever runs the game's backend. Hosted games never have one: their play sessions are what prove who scored.
Signing needs one piece of setup by whoever runs the bot: leaderboard.secretKey in config/config.json, 64 hex characters (openssl rand -hex 32). It encrypts the game secrets at rest, so a copied database file does not reveal them (it is no defence against a compromised bot process, and claims none). Without it, the Keys tab says signing is off on this bot, every signing control refuses with signing_unavailable, and keys and sessions work as normal. With it set:
- Every new external key gets a secret at creation, shown once beside the public key. A key made before the value was configured has none, and shows as unsigned.
- Rotate secret issues a new one, shown once. The old secret stops verifying immediately, so update the game first; the page asks before it does it.
- Require signing refuses any request with no signature (
401 signature_required); the chip reads signing required. Turning it on for a key that has no secret mints one and shows it once, so the toggle is the whole setup for an older key. Make signing optional turns the requirement off; the chip goes back to signing optional. - A revoked key must be reactivated before either action.
A signature is checked whenever it is sent, required or not: a good one marks the result as signed (the log line gets · signed, and history shows it), and a bad or stale one is refused even on an optional key. So a backend can start signing before you flip the requirement, and you flip it once the log channel says its results are signed. Rotation and each change of the requirement are logged to the log channel. A browser game cannot keep a secret, since anyone who reads its source has it; there, treat signing as a deterrent and rely on play sessions.
Required role
A board may name a required role, picked from the server's roles in the wizard's last step or changed later on the board's Settings card ("Anyone can play" is the default and means no limit). With a role set, only members who hold it can start a game on the board, from the Play button, /play, or the game and board pickers, and only members who hold it can be challenged or invited:
- A player without the role who presses Play is told, privately, "This board is for members with the @Role role."
- A challenger who picks an opponent without the role is told "@Bob doesn't have the @Role role this board requires", and no challenge is made. The challenger is checked first, the same way.
- A party host who invites someone without the role sees them named in the reply ("@Carol doesn't have the @Role role this board requires") and no invitation is posted for them; an Accept pressed by someone who has since lost the role is refused.
The live message says 🔒 Open to @Role under the mode line, so players can see who the board is for. The role is read from the member's roles on the chat server, and on the role server when your config names a separate one. The gate is applied where games start in Discord; a result that arrives at the API with a discordUserId and no play session is not checked against it, which is one more reason to keep Require a play session on.
Play sessions
When a player presses ▶ Play or runs /play, the bot replies privately with a link to the game. The link carries a fresh session token that is theirs alone; it lasts 30 minutes from its last renewal, and the game page renews it in the background for as long as it stays open (up to twelve hours), so a long puzzle never loses its score to the clock. The game sends it back with the result, and the result is credited to that player no matter what the game claims. A session may submit to each board it was launched for — the board the player launched from, plus any board you have grouped with it on the key's binding so one play feeds both — and nothing else. While it lives it may submit as many distinct results as the game produces (every Flappy Francine run, say); the board's aggregate decides what they add up to, and a replay of a result that already landed is answered as a duplicate (for 24 hours, even after the session expires), so a flaky connection never costs a player their score and a captured request cannot be counted twice.
Turn on Require a play session on a board's Settings card and the key alone is no longer enough: a result that did not come from a Play link is refused. Hosted boards start with it on. For an external game it means every player must launch from Discord, which also keeps people who are not on the server off the board. Results that arrived through a session show · session in the log channel line, and signed ones · signed; a backend game can send both.
Challenges (versus games)
A versus game that uses challenge matchmaking (the catalogue entry says so) does not launch straight away. Pressing Play asks "Who do you want to play?" with a user picker and, under it, a Challenge anyone — first to accept plays button; /play game:<game> opponent:@user names the opponent in one step. Then:
- The bot creates the match and gives the challenger their own link, with "Waiting for @B to accept." (For a bot-run game there is no link: the reply says the game starts when they accept.)
- It posts "🎮 @A challenged @B to
on in the channel with Accept and Decline buttons. Only the challenged player can accept (anyone else gets "This challenge isn't for you"); the challenger may withdraw with Decline. Nothing in this feature sends a DM — the post is how the challenged player gets a link only they can see.." - Accept gives the challenged player their link, moves the match to accepted and edits the post to "accepted! Game on." For a bot-run game Accept instead opens a thread in the board's channel and posts the game itself there, leaving the challenge post where it is (see Bot-run games). Decline edits it to "declined" and voids the challenger's session. No answer within 30 minutes and the post becomes "no answer, challenge expired." That 30-minute clock applies only while the challenge is unanswered: once accepted, a match cannot expire out from under its players.
- Each player reports their own outcome from their own link. The session decides which match the report belongs to, so a game cannot report into the wrong one. The first report is the match's primary; on an Elo board it scores the match at once (or holds it pending when the board requires mutual confirmation), and the second report confirms it. If the two contradict each other (both claim the win) the log channel flags the match as disputed, with the match number, so you can look and strike; a pending match that is disputed never scores. A bot-run game skips all of this: the bot decides the outcome and applies the match itself, so nothing is pending or disputed.
- An accepted match has a gameplay clock: 2 hours from acceptance, or the game's own timeout when its settings define one. A match that stalls past it — nobody finishes, or one player reports and the other never does — is marked abandoned by the housekeeping job: the post becomes "abandoned, no result", both players' sessions are voided, the log channel gets a line, and the match never scores. On a board with mutual confirmation a third clock runs once the first report is in: the confirm timeout (10 minutes by default), after which a pending match is unconfirmed and never scores.
Open challenges. Challenge anyone posts "🏳️ @A challenges anyone to
Rules: one unanswered challenge per challenger per game, named or open ("You already have a challenge waiting for @X" / "…an open challenge waiting for someone to accept"); you cannot challenge yourself or a bot; a challenge is for one board, chosen the same way a solo launch is. On an Elo board the challenge match is what the ratings are computed from; on a points board the reports score points as they land and the match is a record.
Every challenge, whatever became of it, is in the board's Challenges & matches list on the History card.
The action log file
Beside the log channel, the bot writes every leaderboard action to leaderboard.log in its working directory, in the same winston format as player-actions.log: board creation, settings changes, rebuilds and deletions, key creation, binding, rotation and revocation, challenge creation, acceptance, decline and expiry, and every line the log channel receives (results, strikes, resets, rules changes, abandoned and unconfirmed matches), each tagged with the server id. It is written whether or not a log channel is configured, so it is the place to look when a moderator asks what happened to a board last week.
Correcting results
/leaderboard history board:(optionallyplayer:) lists the last 25 results with their ids, or open the board's History card for the full filterable table./leaderboard strike board: result-id: reason:soft-deletes the result and rebuilds the standings. The result stays in the ledger marked struck, with the reason, so the correction is auditable. On an Elo board the strike is match-level: the match is one scoring event with up to two report rows, so striking either row strikes the match and both rows, and the reply and log line say so ("match #N (both reports…)"). A row the server already struck (an over-ceiling attempt or a match conflict) cannot be struck again./leaderboard rebuildreplays the ledger from scratch: every counted result in arrival order on a points board, every applied match in application order on an Elo board, re-rated from 1000 with the current K. Strikes already rebuild; use this after a K change, an edit on the page or if the board ever looks wrong./leaderboard reset board: confirm:Truestrikes every result on the board, and every scored or pending match, and rebuilds, for a fresh season on the same board. Open and accepted challenges are left to play out. Players keep their anonymous names and initials.
Manual entries (add) run the same acceptance rules as the API except the ceiling, and are recorded with who entered them; on an Elo board each entry is its own match, applied at once, and the reply reads "Recorded a win for @A against @B on Board — now #1, rated 1016 (was 1000)." A disputed match is two reports of one match: strike either and both go.
Exporting the ledger
/leaderboard export board: attaches the board's whole ledger, every result struck or not, as a CSV file in a private reply, named <slug>-<YYYY-MM-DD>.csv; the History card's Export CSV link downloads the same file. It works on archived boards too (board:#<id>). The columns are result_id, created_at (ISO 8601), player_id, player_name (the Discord display name, which is why the file is moderator-only even on an anonymous board), anon_name, initials, points, outcome, opponent_id, opponent_name, match_id, game (the key's label, or manual), session and signed (yes/no), entered_by, struck_at, struck_by, strike_reason and meta (the result's JSON, as sent). A text cell that starts with =, +, - or @ is prefixed with an apostrophe so a spreadsheet never runs a formula a player smuggled into meta or their initials; numbers, including negative points on a lowest-wins board, are written as they are.
Archiving and slug reuse
/leaderboard archive renders the message one last time with Final standings — archived <date>, stops accepting results (the API answers 410 board_archived), and frees the board's slug so next week's board can use the same URL. The message stays in the channel as a record and is never edited again.
reveal:Trueon an anonymous or initials board adds each player's Discord name beside their board name on the final render, for the rows inside top N. The message is public, so this cannot be undone; the confirmation says so. On a Discord-names board it changes nothing.- An archived board is still readable:
history board:#<id>and the page's History card work as before. - An archived board can be deleted from its Danger zone on the setup page: type the board name, confirm, and the board and its whole ledger (results, matches, players, standings, settings) are removed from the database, with a log line saying what went. The archived message in Discord stays, since it was never edited after the final render, and the game keys stay, since they belong to the server. There is no undo, and no command form; archive first if the board is still live.
What is coming later
Nothing from the original plan is outstanding. The catalogue grows as more games are adapted, browser-hosted or bot-run; a new entry is a new tile in the wizard and changes nothing about how a board is set up.