A Speed Typing platform for hosting tournaments. It focuses on server-side cheating mitigation rather than client-side (though there are client-side mitigations as well). It's certainly not a perfect filter but it makes the bar for cheating higher. Though it may be susceptible to white-box attacks because of its open-source nature.
It features 2 modes:
- Individual mode: solo async runs against a running leaderboard.
- Heat mode: rolling live lobbies (≤16) with a synchronized countdown and a 2 Hz live race board.
- Go 1.24+, Node 20+ with pnpm, Docker + docker-compose
- A GitHub OAuth app (callback
http://localhost:8080/auth/github/callback)
Make sure to complete the .env file
cp .env.example .env
make up
make migratemake run
make fe-devOpen http://localhost:5173. Sign in with GitHub, create a competition in the organizer console, open it, then share the join code.
For local testing without OAuth, start the server with DEV_AUTH=true and
POST /auth/dev-login to get a session cookie for a synthetic player.
A admin panel can be activated by setting the ADMIN_USER and ADMIN_PASSWORD in .env, it can be accessed through /admin route
- Users: search, edit name/username, toggle organizer, ban/unban, force-logout
- Competitions: list, force state transition, set visibility, delete
- Global settings: hosting lock, public feed, login kill-switch
- Audit log: immutable record of every admin action
- Dashboard: aggregate stats (users, competitions, runs)
NOTE: You can setup behind a reverse proxy for hosting.
- Credit ledger: Players are given attempt credits. With configurable grace failure attempts for genuine reasons like network issue.
- Anti-cheat: see below
- Scoring: net WPM
(correct_chars/5)/minutes; accuracy below the floor scores 0. Best-of-N or average-of-N with qualify gating and accuracy / earliest-qualifying tie-breaks. - Audit: every committed run's raw keystroke stream is stored gzip-compressed for human replay of contested placements.
The anti cheat system uses various techniques to deter and flag cheaters. Policy is to always flag the user rather than kicking them. It is designed around server-side mitigations rather than client-side mitigations which can be easily bypassed. Uses various statistical models as well as deterministic models to flag cheat runs.
Like all anti-cheats it is not perfect, no where near perfect. But it should deter most people and set a higher bar on cheating than simple i.i.d. timing cheats.
I am explaining this on the homepage because it is visible already in the code, also I do not believe in security through obscurity.
It consists of various pieces:
- empty runs
- overtyping (1.5 times the length of the prompt)
- keystrokes timestamped before t = 0
- faster than 50ms/char overall (~240WPM)
- inter-key median < 20ms,
- >= 30% of gaps < 15ms
If client's timestamps exceed servers elapsed time + 750ms slack, so an attacker can't batch all of the keystrokes in one go.
- Coeffecient of Variation of inter-key intervals
- 50 <= ms/char <= 65, flags automatically, its too fast to ignore, but not impossible
- pasted bursts, mid-run local sub-8ms gaps, they can get missed by the deterministic >= 30% < 15ms or the inter-key median
- Structural detectors:
- digraph F-ratio: it basically groups every inter-key gap and computes ANOVA F-ratio, rationale is that "th", "qu", "er", as humans we type each specific pair at a stable, characteristic speed (for example on QWERTY, "th" is usually faster than "rd" as "th" can be typed with two hands while "rd" requires one hand), a bot using uniform timing doesn't follow this.
- lag-1 autocorrelations: A human doesn't generate each keystroke delay independently, consecutive delays are correlated, it captures that correlation like ramp ups, bursts on easy word, slow down. Though on a short test, effect of this probably negligible, but it none the less contributes in unison with other scorers.
- Skewness: determines the shape of the distribution, humans tend to have cluster of fast strokes, while a bot can be uniform.
- Wald-Wolfowitz runs test: basically used to test whether the timing of characters are mutually independent (they shoudn't be) \
A conjunction of these 4 metrics is used to determine rather than these individually, since a human will most likely break one of these.
Detects if the timings between characters are being replayed and flags the user.
- isTrusted guarded, so synthethic KeyboardEvents are dropped. Doesn't protect against someone playing websockets events directly