Each round the module reveals one letter from a shared Scrabble bag and runs a 1-second sealed-bid auction. The highest bidder wins the letter and adds it to their private rack. With those letters, bots can play any dictionary word at any time, earning currency that funds future bids. Long words pay a length bonus (1×→3×). A match ends when the bag empties. Matches are formed by a rolling 60-second lobby: bots queue up, and either the lobby fills with real entrants or the timer expires and idle simulated bots fill empty slots.
lobby + lobby_member — the currently open lobby and who's queued in it.match_state / match_participant — match metadata and your per-match
balance / score.auction — open auctions across matches you're in.auction_result — every completed auction (letter, winner_bot_id, top_bid,
paid).word_play — every word played (letters used).my_rack view — your own letters across all matches; clients filter by
match_id. Opponents' racks and remaining bag composition stay hidden, though
both are derivable from public auction + word-play events if you're
motivated.bot + bot_credential — the bots you can act as, plus the full list of
registered bots.join_lobby() // enter the open lobby
submit_bid(auction_id: u64, amount: i64) // bid on an open auction
submit_word(match_id: u64, word: String) // spend letters from rack
You don't manually start matches — the lobby handles that. Just call
join_lobby when you're free (your bot starts a new match or rejoins after its
previous one ends).
balance (spendable on future bids) and score
(ranking).The token authenticates your bot as a credential for your team's bot persona.
Drop it into the SDK's withToken() call:
import { DbConnection } from "./module_bindings";
DbConnection.builder()
.withUri("https://maincloud.spacetimedb.com")
.withDatabaseName("scrabblebot")
.withToken(process.env.BOT_TOKEN) // <-- the token from /team
.onConnect((conn, identity, token) => {
conn.subscriptionBuilder()
.onApplied(() => {
// 1. Find which bot you are.
const cred = conn.db.botCredential.identity.find(identity);
const myBotId = cred?.botId;
if (myBotId === undefined) {
console.error("No bot for this token — get a fresh one from /team");
return;
}
// 2. Hop into the lobby. The match starts when it fills or
// the 60s timer expires.
conn.reducers.joinLobby({});
})
.subscribeToAllTables();
// 3. When a new auction opens, decide your bid.
conn.db.auction.onInsert((_ctx, a) => {
if (a.status.tag !== "Open") return;
// ... your strategy ...
conn.reducers.submitBid({ auctionId: a.id, amount: BigInt(/* your bid */) });
});
// 4. When your rack changes, look for words you can play.
conn.db.myRack.onUpdate(() => {
// ... pick a word from your tiles ...
// conn.reducers.submitWord({ matchId, word });
});
// 5. Re-join the lobby after each match ends.
conn.db.matchState.onUpdate((_ctx, oldRow, newRow) => {
if (oldRow.status.tag !== "Ended" && newRow.status.tag === "Ended") {
conn.reducers.joinLobby({});
}
});
})
.build();
The repo's bot-starter/ directory is a TypeScript skeleton wired up with all
the boilerplate above (token handling, lobby joining, rack tracking, match
lifecycle). Edit src/strategy.ts — it has two functions you'll want to tune:
decideBid(ctx) — returns the amount to bid on the current letter, given your
balance and rack.chooseWord(ctx) — picks a word to play from your tiles, given the shared
dictionary.git clone https://github.com/clockworklabs/scrabblebot
cd scrabblebot/bot-starter
npm install
npm run generate
# Use the token from /team:
BOT_NAME=alice BOT_TOKEN=<token from /team> npm start
The starter persists the token to .token-<BOT_NAME> so future runs don't need
BOT_TOKEN. To rotate, mint a new token on /team.
An admin opens a tournament for registration, and the tournament page shows its id and who has signed up. Your bot registers itself, from the process that plays for it: the module only accepts a registration from one of the bot's own credentials. That's why there's no Register button on the website, which is signed in as you, not your bot.
# With the starter:
BOT_NAME=<name> TOURNAMENT_ID=<id> npm start
# From your own bot, once it's connected:
conn.reducers.registerForTournament({ tournamentId: <id>n });
// Changed your mind before it starts?
conn.reducers.unregisterFromTournament({ tournamentId: <id>n });
auction.onInsert and plays
words when my_rack changes plays them as it is.join_lobby refuses its entrants until they're knocked out or it ends, so
your bot is always free for its next tournament match. The starter just logs
the refusal and waits.SpacetimeDB has SDKs for Rust, C#, and TypeScript. The reducer interface is
language-agnostic; the starter is just a convenience. Generate bindings with
spacetime generate --lang <lang> against this module.