Skip to content

Repository files navigation

ccclub_dc

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.

Commands

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).

Deploy your own version

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 init to create one — it prints a 6-char join code (e.g. A1B2C3). This is the group whose leaderboard your bot will show. (Or use global to read the public global leaderboard.)

1. Fork, clone & create your config

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.)

2. Create your Discord application

In the Developer PortalNew Application, then collect three values you'll need below:

  • Application ID and Public KeyGeneral Information page.
  • Bot TokenBot 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).

3. Point the bot at your group

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.

4. Create the KV namespace

The Discord ⇄ ccclub link store lives in Workers KV. Create your own namespace:

pnpm wrangler kv namespace create LINKS

Paste 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).

5. Set the Worker secrets (production)

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 → Token

6. Configure the registration script

Slash commands are registered by a small local script (pnpm deploy-commands), which reads a plain .env (not the Worker's secrets):

cp .env.example .env

Fill 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).

7. Register the commands & deploy

pnpm deploy-commands     # register the slash commands with Discord
pnpm deploy              # deploy the Worker — prints your Worker URL

Copy 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.

8. Invite the bot & onboard members

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.

Local development

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-commands whenever a command's name, options, or description changes, and pnpm deploy to ship Worker code or wrangler.jsonc changes.

Scripts

  • pnpm dev — run the Worker locally (wrangler dev)
  • pnpm deploy — deploy the Worker (wrangler deploy)
  • pnpm deploy-commands — register slash commands
  • pnpm typechecktsc --noEmit
  • pnpm test — Vitest suite (pnpm test:watch for watch mode)

Project layout

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.

Notes

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.

About

A thin wrapping of ccclub on Discord. Side-project from our group cuz we like to burn tokens

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages