A small Discord bot that surfaces Claude Code (and other coding-agent) usage inside Discord — leaderboards, per-user stats, and role/server reports.
It is an MVP thin client: it collects nothing and runs no backend of its own. Every user runs the ccclub CLI, which ships privacy-safe usage aggregates to ccclub.dev; this bot only reads that data back and renders it in Discord.
It runs serverless on Cloudflare Workers using Discord's HTTP Interactions model (no gateway, no always-on process) — the same stack ccclub itself uses (a Cloudflare Worker with Hono + KV).
Each user runs ccclub's CLI (npx ccclub join <id>) ──▶ ccclub.dev (collection · storage · pricing)
│ GET /api/rank/:code (public, no auth)
Discord ──POST signed interaction──▶ Worker (Hono) ──────────┘
└─ verify · defer · read rankings · render leaderboards / usage / reports
Only aggregated tokens and cost ever leave a machine — no prompts, code, file paths, or project names. See ccclub for its privacy model.
| Command | Options | What it does |
|---|---|---|
/leaderboard |
period |
Top 10 members of the group by cost. |
/club |
— | Shows the group and its join code, hidden behind a spoiler. |
/usage |
member, period |
One member's tokens / cost / quota (you by default). |
/link |
name |
Bind your Discord account to a ccclub display name. |
/unlink |
— | Remove your link. |
/report |
server | role (+ period) |
Aggregate usage across the whole group or a role's linked members. |
period is one of Today / Yesterday / This week / This month / All time. Most commands default to All time; /leaderboard defaults to This week (configurable via LEADERBOARD_DEFAULT_PERIOD).
This bot is meant to be self-hosted — you run your own copy on your own Cloudflare account, pointed at your own ccclub group. The whole thing is one Cloudflare Worker; a first deploy takes ~15 minutes.
Prerequisites
- Node 20+ and pnpm.
- A free Cloudflare account (the Workers free tier is plenty).
- A Discord application you can administer.
- A ccclub group: run
npx ccclub initto create one — it prints a 6-char join code (e.g.A1B2C3). This is the group whose leaderboard your bot will show. (Or useglobalto read the public global leaderboard.)
git clone https://github.com/<you>/ccclub_dc.git
cd ccclub_dc
pnpm install
pnpm wrangler login # authenticate wrangler to your Cloudflare account
cp wrangler.example.jsonc wrangler.jsonc # your Worker config (gitignored — holds your KV id + group code)wrangler.jsonc is gitignored because it carries two values unique to your deployment (your KV namespace id and your ccclub group code); the committed wrangler.example.jsonc is the shared template. You'll fill in those two values in steps 3–4 — every other field is shared app config, leave it as-is. (Re-copy from the template if it changes upstream, the same way you'd treat .env.example.)
In the Developer Portal → New Application, then collect three values you'll need below:
- Application ID and Public Key — General Information page.
- Bot Token — Bot page → Reset Token (shown once; store it somewhere safe).
On the Bot page, also enable the Server Members Intent (required for /report role to list a role's members).
In your wrangler.jsonc, set CCCLUB_GROUP_CODE to your own 6-char code (from the prerequisites) — in both places it appears: the top-level vars (production) and env.dev.vars (local dev). The template defaults to "global" (the public leaderboard); change it to your code to track your own group. The other vars (CCCLUB_API_URL, TZ_OFFSET_MIN, LEADERBOARD_DEFAULT_PERIOD) are fine at their defaults.
The Discord ⇄ ccclub link store lives in Workers KV. Create your own namespace:
pnpm wrangler kv namespace create LINKSPaste the returned id in place of <your-kv-namespace-id> in wrangler.jsonc — in both kv_namespaces[0].id spots (top-level and env.dev).
Paste each value from step 2 when prompted:
pnpm wrangler secret put DISCORD_PUBLIC_KEY # General Information → Public Key
pnpm wrangler secret put DISCORD_APP_ID # General Information → Application ID
pnpm wrangler secret put DISCORD_BOT_TOKEN # Bot → TokenSlash commands are registered by a small local script (pnpm deploy-commands), which reads a plain .env (not the Worker's secrets):
cp .env.example .envFill in DISCORD_BOT_TOKEN and DISCORD_APP_ID (same values as step 5). Optionally set DISCORD_GUILD_ID to a test-server ID — commands register there instantly; leave it blank to register globally (~1h to propagate).
pnpm deploy-commands # register the slash commands with Discord
pnpm deploy # deploy the Worker — prints your Worker URLCopy the printed Worker URL (e.g. https://ccclub-dc.<subdomain>.workers.dev) into your Discord app's Interactions Endpoint URL (General Information). On save, Discord sends a signed PING; it's accepted only if your Worker verifies the signature (so the secrets from step 5 must be set) and returns PONG.
Build an invite URL (OAuth2 → URL Generator) with the bot and applications.commands scopes, open it, and add the bot to your server. Then each member whose usage you want on the board installs the ccclub CLI and joins your group:
npx ccclub join <your-code>(The /club command shows your join code behind a spoiler for members to copy.) The bot only reads what ccclub has already collected — nothing to configure per member on the bot side.
Run the Worker locally with pnpm dev (= wrangler dev --env dev). It reads secrets from .env.dev — copy .env.dev.example → .env.dev; it only needs DISCORD_PUBLIC_KEY, since DISCORD_BOT_TOKEN/DISCORD_APP_ID are merged in from your .env. Because Discord must reach the endpoint over HTTPS, expose the local server with a tunnel (e.g. cloudflared tunnel --url http://localhost:8787) and set that tunnel URL as the Interactions Endpoint URL while iterating.
Re-run
pnpm deploy-commandswhenever a command's name, options, or description changes, andpnpm deployto ship Worker code orwrangler.jsoncchanges.
pnpm dev— run the Worker locally (wrangler dev)pnpm deploy— deploy the Worker (wrangler deploy)pnpm deploy-commands— register slash commandspnpm typecheck—tsc --noEmitpnpm test— Vitest suite (pnpm test:watchfor watch mode)
| File | Responsibility |
|---|---|
src/worker.ts |
Hono fetch handler: verify signature → PING → defer + dispatch. |
src/interactions.ts |
CommandInput option adapter + ReplyPayload (transport-agnostic). |
src/commands.ts |
All slash commands and their embeds. |
src/ccclub.ts |
Typed read-only client for GET /api/rank/:code — the ccclub contract. |
src/links.ts |
Discord ⇄ ccclub link store (Workers KV). |
src/aggregate.ts |
Pure /report summation logic (unit-tested). |
src/config.ts |
buildConfig(env) from Worker bindings. |
src/env.ts |
Worker bindings (Env) type. |
src/deploy-commands.ts |
Slash-command registration (local script). |
Tests (test/) cover the pure logic seams — format, links (in-memory KV mock), ccclub (mocked fetch), aggregate, and the interactions option adapter. The Hono/Discord glue (worker.ts) is intentionally untested; keep new logic in pure functions/adapters so it stays testable without mocking Discord.
This is a deliberately minimal MVP. Identity is self-asserted (/link trusts a name match against the current rankings) — good enough for a friendly leaderboard, not for anything billing-grade. See CLAUDE.md and AGENTS.md for architecture and contribution conventions.