Every score is verified server-side before being accepted:
1. Server generates a random seed → sends to client with engine config
2. Client creates WASM GameEngine(seed, config) + InputRecorder
3. Client plays — all physics run in the deterministic WASM engine
4. Each frame: recorder captures {thrust, rotate_left, rotate_right, shoot}
5. Game over: client submits score + bitpacked input log + SHA-256 hash
6. Server decodes input log, replays GameState::new(seed, config) → tick(inputs[0..N])
7. Server checks: replayed score == claimed score
8. Match → accept. Mismatch → reject.
The engine uses fixed-point arithmetic (Fixed type, no floats) and a deterministic xorshift64 RNG. Same seed + config + inputs = same game on any platform.
Input logs are compact: 4 bools per frame = 4 bits, 2 frames per byte. A 5-minute game at 60fps ≈ 9KB.
| Attack | Why It's Caught |
|---|---|
| Fabricated scores | Server replays inputs and derives the real score independently |
| Modified client (invincibility) | Server replay hits the collision → different score → mismatch → rejected |
| Modified client (auto-aim) | Any client modification that changes game state diverges from the server's replay |
| Favorable seed selection | Seed is server-generated per session |
| Session replay abuse | One input log per session, ownership verified |
A bot can produce a perfectly valid input log — the inputs are legitimate, just computed by software. The server replay verifies correctly because the gameplay was real.
Why per-account rate limiting doesn't work: Nostr accounts are free (just generate a keypair).
Compares frame count against wall-clock time between session creation and score submission. At 60fps, 3600 frames should take ~60 seconds. Submitting 3600 frames after only 20 seconds is physically impossible → rejected.
Uses server timestamps — the client can't fake it.
The client reports timing data, and the server independently measures elapsed time. If the client claims "60 seconds of play" but the server only saw 30 seconds pass, the client is lying about their timing data → rejected.
Every session stores the client's IP. On score submission:
- More than 5 distinct accounts from the same IP in a rolling hour → flagged
- More than 20 sessions from the same IP in a rolling hour → flagged
Note: these are soft signals, not hard rejections — VPNs and shared networks produce legitimate multi-account IPs.
Client samples performance.now() every 60 frames. Server analyzes variance (human: 5-50ms jitter, bot: <1ms) and mean offset (detects slow-motion play).
Client-reported data can be faked by sophisticated bots, but catches lazy automation.
All bot detection signals are stored in score_metadata for monitoring via the admin dashboard (/admin, WireGuard-only).
- Economic deterrent: Every game costs real sats (configurable, default 1000 sats for 5 plays with a 60-minute expiry window). A bot needs to consistently win the daily competition to be profitable — a single human player winning breaks the bot's ROI.
- Session ownership: Verified against
user_id - Nostr authentication: Every API call requires a signed NIP-98 event
- Nostr audit ledger: Every verified score is published with seed, frames, and input hash — anyone can independently replay
Cryptographically signed record of all game activity (Kind 10100 events):
| Event | What It Proves |
|---|---|
game_entry |
Player paid the entry fee |
score_verified |
Score was replay-verified. Seed + input hash enable independent replay. |
competition_result |
Daily winner, pool size, prize amount |
prize_payout |
Prize was paid. Payment ID for cross-referencing. |
For players: Verify prize pool math, that the highest score won, and that payouts were made.
For disputes: Gaps in the event chain (missing prize_payout after competition_result) are visible.
For verification: Anyone with the game engine can replay a score_verified event independently.
- Reaction time heuristics: Consistently dodging asteroids within 1-2 frames of threat → beyond human reaction time (~200ms = ~12 frames)
- Input entropy scoring: Bots have unnaturally smooth rotation patterns. Human input is noisier.