Every competitive typing site has a leaderboard. Most of them are terrible.
TypeRacer shows you an all-time WPM rank that you'll never crack. MonkeyType has a global top-1000 that's been locked in by dedicated grinders for years. Both create the same problem: if you're not already in the top tier, the leaderboard is demotivating noise.
When I built Clackpit, I wanted a leaderboard that actually worked for a normal person sitting down to practice. That meant rethinking two things: identity and time horizon.
The problem with persistent identity
Most leaderboards require an account. Name, email, password — the whole thing. That's fine for a product where you've already committed to the user relationship. But for a typing site, where someone might show up for a 3-minute race and never return, it's a massive conversion blocker.
I wanted anyone to be competitive immediately. No friction.
The solution I landed on: anonymous handles that persist in localStorage, chosen from a curated wordlist on first visit.
const ADJECTIVES = ['swift', 'nimble', 'sharp', 'quick', 'bright', /* ... */];
const NOUNS = ['falcon', 'arrow', 'spark', 'dash', 'flare', /* ... */];
function generateHandle(): string {
const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
const num = Math.floor(Math.random() * 99) + 1;
return `${adj}${noun}${num}`;
}
// Store in localStorage so it persists across sessions
function getOrCreateHandle(): string {
const stored = localStorage.getItem('clackpit_handle');
if (stored) return stored;
const handle = generateHandle();
localStorage.setItem('clackpit_handle', handle);
return handle;
}
The handle is sticky — come back tomorrow and you're still swiftfalcon42. You can change it, but you don't have to. It's yours without signing up.
The problem with all-time leaderboards
Persistent leaderboards have a nasty property: they compound. The longer they exist, the more locked-in the top positions become. A new user who types 90 WPM sees a leaderboard full of 140+ WPM monsters and correctly concludes that competing is pointless.
The fix is surprisingly simple: reset the leaderboard every night at midnight UTC.
A daily leaderboard has a completely different social dynamic:
- Everyone starts equal each day. The person who showed up for the first time today can be #1 by the end of the day.
- Yesterday's champion is gone. That 140 WPM monster? They have to earn it back.
- Streaks become the real achievement. Staying in the top 10 for a week is more interesting than a stale number on a permanent board.
The implementation
I'm running on Cloudflare Workers with KV storage. The leaderboard is a sorted list stored in KV with a TTL that expires at the next midnight UTC.
// Calculate seconds until next midnight UTC
function secondsUntilMidnightUTC(): number {
const now = new Date();
const midnight = new Date(now);
midnight.setUTCHours(24, 0, 0, 0);
return Math.floor((midnight.getTime() - now.getTime()) / 1000);
}
async function submitScore(
kv: KVNamespace,
handle: string,
wpm: number,
accuracy: number
): Promise<void> {
const key = 'leaderboard:daily';
const ttl = secondsUntilMidnightUTC();
const raw = await kv.get(key);
const entries: LeaderboardEntry[] = raw ? JSON.parse(raw) : [];
// Update or insert this handle's best score
const existing = entries.findIndex(e => e.handle === handle);
if (existing >= 0) {
if (wpm > entries[existing].wpm) {
entries[existing] = { handle, wpm, accuracy, ts: Date.now() };
}
} else {
entries.push({ handle, wpm, accuracy, ts: Date.now() });
}
// Keep top 100, sorted by WPM descending
entries.sort((a, b) => b.wpm - a.wpm);
const top100 = entries.slice(0, 100);
await kv.put(key, JSON.stringify(top100), { expirationTtl: ttl });
}
The TTL does the reset automatically. No cron job, no cleanup task. When the KV key expires, the leaderboard is gone. Next write creates a new one that expires at the next midnight.
What I learned
The daily reset changes behavior in ways I didn't predict.
I expected it to just be "fairer." What I didn't expect was that it makes people check back more often. If you posted a 95 WPM score and there are 3 hours left in the day, you have a reason to come back tomorrow and see if you held on. If the leaderboard were permanent, there's no urgency.
Anonymous handles have a surprising amount of personality. quietarrow71 feels different from swiftfalcon42. People started identifying with their handles even without accounts.
The all-time leaderboard problem is worse than I thought. After watching a few users interact with the board, I noticed that the first thing most people do is scroll to find where they rank. On a daily board, everyone is visible (it only has today's players). On a permanent board, most newcomers wouldn't appear at all. That invisibility is demoralizing.
If you want to see it in action: Clackpit. Sprint mode submits to the daily board after your race. Try it and see where you land before midnight resets it.
Curious what others have done for leaderboards — permanent, daily, weekly? Drop your approach in the comments.
Top comments (0)