LeetCord is a Discord bot and backend stack for discord communities. It lets users link their LeetCode accounts, view cached stats, track the daily problem, compete on leaderboards, track streaks, and post automated daily updates inside Discord servers.
This repository is a pnpm-powered TypeScript monorepo with separate apps for the Discord bot, HTTP API, and worker, plus shared packages for database access, domain services, and LeetCode integration.
- apps/bot: Discord bot (
discord.js) with slash commands and interactive buttons. - apps/api: Fastify HTTP API for health checks and verification endpoints.
- apps/worker: Cron-based worker for polling LeetCode, caching data, posting daily updates, and refreshing completion state.
- packages/shared: Shared types, constants, utilities, and Zod schemas.
- packages/leetcode-client: LeetCode adapter interface and HTTP-based implementation.
- packages/database: Prisma schema, migrations, and database client.
- packages/core: Core domain services for linking, stats sync, guild settings, leaderboards, and LeetCode integration.
- Link a Discord user to a LeetCode account using a verification code placed in the README section of the user’s LeetCode profile.
- View cached LeetCode profile stats, including total solved, difficulty breakdown, streak, contest rating, and today’s daily status.
- Cache the current LeetCode daily problem in the database and let
/dailybackfill it on demand if it is missing. - Track daily completion status for linked users, and refresh the caller's completion state on demand when
/dailyruns. - Show server leaderboards for total solved, daily completions, and weekly progress snapshots.
- Post the daily problem into a configured server channel.
- Post automatic completion-feed updates when a linked user newly completes today’s daily.
- Let each user opt out of being pinged in automatic completion-feed posts from their own
/meresponse.
| Command | Description |
|---|---|
/ping |
Check if the bot is alive. |
/link username:<your_username> |
Start linking your LeetCode account and generate a verification code to place in the README section of your LeetCode profile. |
/verify |
Complete the link verification by checking the README section of your LeetCode profile. |
/unlink |
Unlink your LeetCode account. |
/me [user] |
Show cached LeetCode stats for yourself or another linked user. On your own /me, a button lets you enable or disable completion-feed pings. |
/daily |
Show today’s LeetCode daily problem, attempt to cache it on demand if it is missing, and refresh your completion status if you are linked. |
/streak [user] |
Show current streak, longest streak, and total completed dailies. |
/leaderboard mode:<total|weekly|daily> |
Show the server leaderboard for all-time solved, this week’s progress, or today’s completions. |
/help |
Show setup instructions and the command list. |
/daily: 60 seconds/link,/verify: 15 seconds/me,/streak,/leaderboard: 10 seconds- All other commands: no cooldown
These commands require the Administrator permission in the Discord server.
| Command | Description |
|---|---|
/setup-daily-channel channel:<channel> |
Set the channel used for daily problem posts and completion-feed updates. |
/setup-timezone timezone:<IANA timezone> |
Store a guild timezone such as America/Toronto. |
/setup-leaderboard enabled:<true|false> |
Enable or disable leaderboard commands in the server. |
When the worker starts, it immediately:
- fetches and caches today’s daily problem
- refreshes cached stats for linked users
- refreshes daily completion state for linked users
- computes weekly leaderboard snapshots
| Job | Schedule | Description |
|---|---|---|
| Fetch and post daily problem | 00:05 UTC daily |
Refreshes the cached daily problem and posts it to configured channels at a fixed UTC time. |
| Daily completion refresh | Every 10 minutes | Checks linked users for new daily completions. |
| Completion feed | Every 10 minutes, when new completions are found | Posts @user just completed today's daily into configured daily channels. If a user disables completion pings, the post uses their LeetCode username instead of pinging them. |
| Stats refresh | Every 60 minutes | Pulls fresh LeetCode stats for all verified users. |
| Weekly leaderboard snapshot | Monday 01:00 UTC |
Computes weekly leaderboard snapshots for all guilds with settings rows. |
A daily recap job exists in the codebase, but it is not currently wired into startup or any scheduler, so it does not run automatically right now.
- Node.js
>= 20 pnpm >= 9
pnpm installCopy the example file:
cp .env.example .envThen fill in the required values.
| Variable | Description |
|---|---|
DISCORD_TOKEN |
Discord bot token from the Discord Developer Portal. |
DISCORD_CLIENT_ID |
Discord application client ID. |
DISCORD_GUILD_ID |
Optional. If set, slash commands are registered to that one guild for fast development updates. |
DISCORD_GUILD_IDS |
Optional. Comma-separated guild IDs for fast multi-server development registration. |
DATABASE_URL |
PostgreSQL connection string. |
API_PORT |
Port for the Fastify API. |
BOT_PUBLIC_URL |
Base URL the bot uses to reach the API for verification and on-demand daily completion refreshes. |
LEETCODE_FETCH_USER_AGENT |
User-agent string for LeetCode HTTP requests. |
LOG_LEVEL |
Pino log level. |
- If
DISCORD_GUILD_IDSis set, the bot registers slash commands to each guild in that comma-separated list. - If
DISCORD_GUILD_IDSis empty butDISCORD_GUILD_IDis set, the bot registers slash commands to that one guild. - If neither is set, the bot registers commands globally for all servers. Global command propagation can take a little while.
- For fast development across multiple servers, prefer
DISCORD_GUILD_IDS.
Example:
DISCORD_GUILD_IDS=123456789012345678,987654321098765432- Docker Compose Postgres:
DATABASE_URL=postgresql://postgres:postgres@db:5432/leetcord?schema=public- Local Postgres outside Docker:
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/leetcord?schema=public- Hosted Postgres or Supabase: Use the exact connection string from your provider.
pnpm prisma:generate
pnpm prisma:migrateRun each service in its own terminal from the repo root:
pnpm dev:apipnpm dev:botpnpm dev:workerRun all three long-lived dev processes in one terminal:
pnpm devStop everything with Ctrl+C.
The app dev scripts import workspace packages from their built dist/ outputs. If you changed or pulled updates in packages/* and ts-node reports missing methods or stale types, rebuild the workspace:
pnpm builddocker-compose up --buildHealthy startup usually includes logs like:
- API:
API listening - Bot:
Registered slash commandsandBot ready - Worker:
Fetched and stored today daily problem
- Open the Discord Developer Portal for your application.
- Go to
OAuth2->URL Generator. - Select scopes:
botapplications.commands
- Select permissions for the bot.
- Easiest for development:
Administrator - Minimum practical set:
View Channels,Send Messages,Embed Links
- Easiest for development:
- Open the generated invite URL, choose your server, and authorize the bot.
- Make sure
.envis configured for that server:- set
DISCORD_GUILD_ID=<that_server_id>for one fast dev guild - or set
DISCORD_GUILD_IDS=<guild_1>,<guild_2>for multiple fast dev guilds - or remove both to use global commands
- set
- Restart the bot so it re-registers slash commands.
The API app exposes:
GET /healthPOST /daily/ensure-cachedPOST /daily/refresh-completionPOST /link/verification/startPOST /link/verification/complete
- Make sure the bot was invited with the
applications.commandsscope. - Check whether
DISCORD_GUILD_IDorDISCORD_GUILD_IDSis pointing at different servers. - Restart the bot after changing
.env. - If using global commands, give Discord some time to propagate them.
The worker fetches the daily problem once during startup, then again on the next scheduled daily run. If your database was paused or unavailable during startup, the worker may miss that startup cache write and continue running.
In that case:
- wait for the database to become available
- restart the worker, or rerun the one-terminal command
Users can run /me and use the button on their own stats response to disable completion-feed pings. After that, automated completion-feed posts use their LeetCode username instead of a Discord mention.
- All code is written in TypeScript with
strictmode enabled. - Prisma is used for the PostgreSQL schema and client.
- Zod is used for environment validation and external response validation.
discord.jspowers the bot and only slash commands are used.- Keep this README in sync with user-facing feature and command changes.
- Fastify is used for the HTTP API.
node-cronis used for scheduled worker jobs.