Reddit weekly trivia bot tutorial for 2026 API#
A weekly trivia thread is the highest-engagement community ritual for game-dev (and adjacent) subreddits. Tuesday post asks the question, Thursday post reveals the answer with a leaderboard of first-correct commenters. The subreddit gets recurring activity, your bigger project (the game you’re actually building) gets a soft promotion in the bot’s signature line. This guide ships the bot in ~120 lines of Node using snoowrap (the standard Reddit API library) + node-cron.
The 2023 Reddit API pricing drama left most “Reddit bot tutorial” blog posts stale — they used pre-pricing free quotas. This guide is current as of 2026: you’ll need a developer account, a small-tier API access key, and you should run the bot on your own infrastructure (Reddit doesn’t host this for you). Cost: $5/mo for a small droplet + Reddit API base tier (currently free under 100 req/min for personal-use bots).
What you’ll build#
A Reddit bot that:
- Tuesday 10am UTC — posts a new thread in your target subreddit titled “Weekly Trivia: $TOPIC”
- Reads top-level comments for the next 48 hours, scores them by first-correct
- Thursday 4pm UTC — replies to the thread with the answer + a leaderboard of top 5 commenters
- No-repeat dedup — tracks question IDs in a JSON file so the subreddit never sees the same question twice
- Per-question attribution — Source / Author / License surfaced in the post body
- Configurable category —
?tags=programmingfor r/gamedev,?category=filmfor r/movies
The mechanic — week-long round + stable IDs#
The bot has two cron jobs (Tuesday post + Thursday reveal) and an in-memory currentRound: { id, correctAnswer, postId, scores: Map }. Tuesday it fetches GET /api/v1/questions/random, posts to Reddit, saves the round to disk (in case the bot restarts mid-week). Wednesday-Thursday it polls the thread’s comments via subreddit.submission.comments, scoring the first-correct per user.
The stable QuizBase id is persisted across the week — if the bot reboots Wednesday at 3am, it reads round.json from disk, knows it’s mid-round, knows which question is currently live. Without the stable ID we’d lose state on every restart. See /docs/api/questions-by-id for the full stable-ID pattern catalogue.
Stack#
- Node.js 20+ + snoowrap (Reddit API client) + node-cron + fs/promises for the JSON store
- Reddit app credentials — create at reddit.com/prefs/apps (developer category: “script”)
- QuizBase publishable key —
qb_pk_*, free tier covers weekly cadence trivially - Small VM — DigitalOcean droplet, Railway worker, Fly machine. Needs to run 24/7 to make the cron triggers fire.
Step 1 — Reddit app credentials + dependencies#
Go to reddit.com/prefs/apps, scroll to “Are you a developer? create an app…”, choose script type. You get a client ID (under the app name) and a client secret.
mkdir reddit-trivia && cd reddit-trivia
npm init -y
npm install snoowrap node-cron dotenv .env:
REDDIT_USER=yourbotaccount
REDDIT_PASS=password
REDDIT_CLIENT_ID=abc123
REDDIT_CLIENT_SECRET=secret456
SUBREDDIT=gamedev
QUIZBASE_KEY=qb_pk_... The bot account is a normal Reddit account you create — give it some karma first (post a couple times manually) so it’s not shadowbanned for fresh-account behaviour.
Step 2 — Wire QuizBase fetch + round state#
// state.ts
import fs from 'node:fs/promises';
const STATE_FILE = './round.json';
export interface Round {
id: string;
correctAnswer: string;
choicesText: string; // pre-formatted A/B/C/D string
postId: string; // Reddit submission id
scores: Record<string, number>; // username → wins this week
}
export async function loadState(): Promise<Round | null> {
try {
const raw = await fs.readFile(STATE_FILE, 'utf-8');
return JSON.parse(raw);
} catch {
return null;
}
}
export async function saveState(round: Round | null) {
if (!round) {
await fs.unlink(STATE_FILE).catch(() => {});
return;
}
await fs.writeFile(STATE_FILE, JSON.stringify(round, null, 2));
} // quizbase.ts
const KEY = process.env.QUIZBASE_KEY!;
const seenIds = new Set<string>(); // load from disk on startup if you want long-term dedup
export async function fetchUnique(retries = 3) {
for (let i = 0; i < retries; i++) {
const r = await fetch(
`https://quizbase.runriva.com/api/v1/questions/random?category=general-knowledge`,
{ headers: { 'X-API-Key': KEY } }
);
const { data } = (await r.json()) as { data: Array<{ id: string; text: string; correctAnswer: string; incorrectAnswers: string[]; attribution: { author: string; license: string } }> };
const q = data[0];
if (q && !seenIds.has(q.id)) {
seenIds.add(q.id);
return q;
}
}
throw new Error('No fresh question after 3 retries');
} Step 3 — Tuesday: post the trivia thread#
import snoowrap from 'snoowrap';
import cron from 'node-cron';
import { fetchUnique } from './quizbase';
import { saveState } from './state';
const reddit = new snoowrap({
userAgent: 'weekly-trivia-bot v1.0',
clientId: process.env.REDDIT_CLIENT_ID!,
clientSecret: process.env.REDDIT_CLIENT_SECRET!,
username: process.env.REDDIT_USER!,
password: process.env.REDDIT_PASS!
});
cron.schedule('0 10 * * 2', async () => {
// Tuesday 10am UTC
const q = await fetchUnique();
const choices = [q.correctAnswer, ...q.incorrectAnswers].sort(() => Math.random() - 0.5);
const choicesText = choices.map((c, i) => `${'ABCD'[i]}. ${c}`).join('\n\n');
const body = `**Weekly Trivia**
${q.text}
${choicesText}
Reply with the letter (A/B/C/D). First correct answer per user counts. Reveal Thursday 4pm UTC.
---
*Source: ${q.attribution.author} (${q.attribution.license})*
*Trivia content from [QuizBase](https://quizbase.runriva.com) — open API, free tier for personal use.*`;
const post = await reddit
.getSubreddit(process.env.SUBREDDIT!)
.submitSelfpost({ title: `Weekly Trivia — ${new Date().toLocaleDateString()}`, text: body });
await saveState({
id: q.id,
correctAnswer: q.correctAnswer,
choicesText,
postId: post.name,
scores: {}
});
console.log(`Posted to r/${process.env.SUBREDDIT}: ${post.url}`);
}); The Source: ... line + the QuizBase backlink in the post body are required. CC BY-SA / CC BY / MIT compliance — see /data. Skipping these puts your bot at risk of moderator removal for “uncredited content”.
Step 4 — Continuously score comments#
snoowrap’s inbox stream isn’t real-time, so we poll comments every 15 minutes. Top-level comments only (no replies); first correct per user counts:
import { loadState, saveState } from './state';
cron.schedule('*/15 * * * *', async () => {
const round = await loadState();
if (!round) return; // No active round
const submission = reddit.getSubmission(round.postId);
const comments = await submission.comments.fetchAll({ amount: 200 });
// Recover the shuffled choices actually shown to users (strip the "A. " prefix)
const choices = round.choicesText.split('\n\n').map((s) => s.slice(3));
const correctIndex = choices.indexOf(round.correctAnswer);
const correctLetter = `abcd`[correctIndex];
for (const c of comments) {
const author = (c.author as any).name;
if (author === '[deleted]' || round.scores[author]) continue;
const lower = c.body.trim().toLowerCase();
if (lower === correctLetter || lower === round.correctAnswer.toLowerCase()) {
round.scores[author] = (round.scores[author] ?? 0) + 1;
}
}
await saveState(round);
}); Step 5 — Thursday: reveal + leaderboard#
cron.schedule('0 16 * * 4', async () => {
// Thursday 4pm UTC
const round = await loadState();
if (!round) return;
const top = Object.entries(round.scores)
.sort(([, a], [, b]) => b - a)
.slice(0, 5)
.map(([user, score], i) => `${i + 1}. /u/${user} — ${score}`)
.join('\n');
const replyBody = `**Answer:** ${round.correctAnswer}
**This week's leaderboard:**
${top || '(No correct answers this week)'}
---
Next round Tuesday. Trivia from [QuizBase](https://quizbase.runriva.com).`;
const submission = reddit.getSubmission(round.postId);
await submission.reply(replyBody);
await saveState(null); // Clear round
console.log('Reveal posted, week reset.');
}); Pitfalls#
- Reddit shadowbans aggressive bot behaviour — your bot account needs natural karma history (post some genuine comments before launch). Fresh accounts posting bot output get filtered out of the subreddit’s view automatically.
- Subreddit moderators must approve bots — message the mods of your target sub explaining what the bot does before launching. Most game-dev subs are bot-friendly with permission.
- Bot uptime requirement — cron jobs only fire if the process is running. Use a VM with
systemdauto-restart or a managed worker (Fly Machines, Railway Worker tier). - 15-minute scoring poll means rapid back-to-back comments after Tuesday post might be missed if your bot crashes mid-poll. Solution: track
lastSeenCommentIdper scoring run, paginate via Reddit’sbeforeparameter.
What next#
- Multi-subreddit deployment — same bot in r/movies, r/history, r/gamedev with different
?category=per subreddit - Persistent all-time leaderboard — track wins across weeks in a SQLite file, surface monthly champions
- Weekly digest cross-post — auto-cross-post the Thursday reveal to your own subreddit / Discord / Slack
- Pair with Slack/Discord variant — same trivia content, different community surface
Ready to ship? Grab a free publishable key, set up Reddit app credentials, copy the snippets, deploy to a small VM. Bug in a question? a Reddit-side POST /api/v1/report integration is one extra cron job — listen for /u/yourbot !report mentions and forward the report by question ID.