DEV Community

Clackpit
Clackpit

Posted on

How I built an anonymous daily leaderboard — and why it resets every night

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;
}
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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)