A self-hostable X leaderboard. Pick a list of accounts, refresh their recent posts through the X API, export JSON snapshots, and deploy the React dashboard as a static site from GitHub to any CDN.
Production is static by default: no server, no database connection, and no API token in the browser.
Prerequisites: Bun >= 1.3 and an X API v2 bearer token with
pay-per-use/read access for user lookup, recent search, and post lookup. The
bearer token starts with AAAA; it is not an xAI key.
bun install
cp .env.example .env
# Paste X_BEARER_TOKEN into .envEdit the typed config in xrank.config.ts:
export default defineXRankConfig({
title: "Acme X Rank",
roster: [
{ handle: "thdxr", timeZone: "America/Los_Angeles" },
{ handle: "kitlangton", timeZone: "Asia/Bangkok" },
{ handle: "opencode", color: "#34d399" }
],
schedule: {
every: "4 hours",
command: "bun run publish --skip-if-fresh"
}
})Only handle is required. Set the optional timeZone to the account's IANA
timezone (for example, Asia/Bangkok) so workday streaks use that person's
local calendar; it defaults to UTC. team and color are optional and kept
for future grouping/customization.
Agents can update the roster without hand-editing TypeScript:
bun run config -- set thdxr kitlangton opencode
bun run config -- add jayair adamdotdev
bun run config -- listCheck setup before spending X API credits:
bun run doctorGenerate and preview the static site:
bun run refresh # fetch X data into ./data/snapshots.db
bun run backfill -- --days 90 # one-time historical import (includes authored replies)
bun run export # write public/snapshot.json and public/snapshots/*
bun run dev:static # optional local UI preview using exported snapshots
bun run build # build ./dist for any static hostThe deploy artifact is dist/. Upload it to any static CDN:
- Vercel
- Netlify
- Cloudflare Pages
- GitHub Pages
- S3/R2 plus a CDN
- any server that can serve static files
Connect the GitHub repository's main branch to Vercel, Cloudflare Pages, or
another static host. Set the build command to bun run build and the output
directory to dist.
Use the host's native Git integration as the only deployment path. Do not add a parallel token-based GitHub Actions deployment; it creates duplicate production deploys and fails when the provider token expires or is rotated.
bun run publish refreshes and exports the snapshots, commits only
public/snapshot.json and public/snapshots/, then pushes to origin/main.
That push triggers the connected deployment. The checkout must be on main,
Git author name/email must be configured, and the origin remote must have
non-interactive push authentication (SSH is recommended for local schedulers).
Use the static artifact. No Worker or D1 is required.
bun run refresh
bun run export
bun run build
bunx wrangler pages deploy dist --project-name x-rankIn the Cloudflare dashboard, set the Pages build settings to:
- Build command:
bun run build - Build output directory:
dist
If Cloudflare builds from Git, add X_BEARER_TOKEN as a Pages environment
variable only if the build job also runs bun run refresh. Most setups should
refresh locally or in CI and then deploy the static output.
Provider-agnostic flow:
bun run refresh
bun run export
bun run build
# deploy ./dist with your host's CLIGive this prompt to an agent after forking the repo:
Set up this x-rank fork for my organization.
1. Ask me for the X handles to track and the deployment target.
2. Update `xrank.config.ts` directly or run `bun run config -- set <handles>`.
Only add `team` or `color` if I provide them.
3. Help me create `.env` from `.env.example` and set `X_BEARER_TOKEN`; never
commit the token.
4. Run `bun install`.
5. Run `bun run doctor` and fix any reported issues.
6. Connect my static host to the GitHub repository's `main` branch with build
command `bun run build` and output directory `dist`.
7. If I chose Cloudflare Pages, run `bun run refresh`, `bun run export`,
`bun run build`, then `bunx wrangler pages deploy dist --project-name x-rank`.
8. Otherwise run `bun run refresh`, `bun run export`, and `bun run build`, then
tell me to deploy the generated `dist/` directory.
9. If I want local background publishing, run `bun run schedule:install -- --yes --load`.
10. Run `bun run cost` and summarize the estimated X API spend.Only one scheduler should use a given X API token. Running multiple refresh loops against the same token can double-bill pay-per-use reads.
Good options:
- Manual: run
bun run publishwhen you want fresh data. - Manual retry: run
bun run publish --skip-if-freshto avoid another X API refresh if the latest data is less than an hour old. - Local background publishing: configure
scheduleinxrank.config.ts, then runbun run schedule:install -- --yes --load. On macOS this writes and loads a LaunchAgent; on Linux it installs a labeled cron entry. - Each successful publish commits the exported JSON and pushes
main; use that push as the deployment trigger in your hosting provider.
The production site should still serve static JSON. Avoid request-time X API calls unless you deliberately want a live server and cost controls.
X API pay-per-use pricing changes over time; check console.x.com for the
current rate card. This app estimates cost from the resources it reads. Typical
current public pricing is around $0.01 per user read and $0.005 per post
read, with 24-hour dedupe for repeat reads.
Useful commands:
bun run cost
bun run cost --since 30d --daily
bun run statusbun run dev # Vite with fake data by default; no server or network
bun run dev:static # Vite using exported snapshots from public/
bun run dev:live # Vite + local API server, no refresh daemon by defaultbun run dev:static previews the exported snapshots in public/. bun run dev:live points the UI at the local API server. Live mode is useful while
developing server behavior, but production deploys should use static mode.
bun run server does not schedule background refreshes unless
ENABLE_REFRESH_DAEMON=true is set. REFRESH_INTERVAL=1 hour controls the live
cache TTL and optional daemon cadence.
bun run export uses the existing local DB. Use bun run export --refresh when
you explicitly want export to hit the X API first.
Other commands: bun run xrank --help lists everything (refresh, backfill,
export, publish, cost, status).
xrank.config.ts roster
-> bun run refresh
-> local SQLite cache at ./data/snapshots.db
-> bun run export
-> public/snapshot.json + public/snapshots/*.json
-> git push origin main
-> host runs bun run build
-> static CDNThere is also a Bun API server (bun run server) exposing /api/snapshot and
/api/refresh over Effect HTTP for local/live experiments. It is not required
for the static self-host path.
- Use
bunandbunx. - Keep
xrank.config.tsas the user-editable setup surface. HttpApiinsrc/api.tsis the live API contract.bun run typecheckandbun run buildare the checks.