Live · Anonymous · Free

Math puzzles that fight back.

1v1 head-to-head competition-math duels. Glicko-2 ratings. LLM-authored problems. No signup, no tracking — just an HTTP cookie and a fight card.

How it works

YOU
Echo-Tensor-7142
RATING 1547
SILVER
VS
OPPONENT
Frost-Bishop-3309
RATING 1561
SILVER
// NUMBER THEORY · TARGET 1500
Find the remainder when 7100 is divided by 5.
00:12
  1. A problem is drawn from the cache, calibrated to your rating.
  2. You face a real player's stored attempt — or a synthetic AI at your level.
  3. Fastest correct answer wins. Your Glicko-2 rating moves accordingly.
  4. Your attempt is recorded as a ghost for future players.

Highlights

Hybrid Duels

Solo-vs-AI by default, opportunistic ghost-pairing against real recent attempts at your rating. Feels live, requires zero WebSocket plumbing.

Glicko-2

Proper rating model with rating deviation and volatility — not naive Elo. New players move fast, stabilize as the system learns their level.

LLM-Authored Problems

Claude Haiku 4.5 drafts problems; Claude Sonnet 4.6 validates every one before it goes live. AMC/AIME-style. Bad drafts get discarded silently.

Spend-Capped Generation

Per-day USD budget guard. When exceeded, the generator pauses and play continues against the existing cache. Aggressive prompt-cache use keeps cost ~$0.01/problem.

Anonymous by Design

One HTTP cookie containing a random UUID — that is the entire identity model. No email, no password, no tracking, no analytics.

Tactical Aesthetic

Dark theme, monospace, red/amber accent, fight-card matchmaking intro. Built to feel like a 1v1 arena game, not a homework site.

Tier Ladder

Iron <1200
Bronze 1200+
Silver 1400+
Gold 1600+
Platinum 1800+
Diamond 2000+
Master 2200+
Grandmaster 2400+

All players start at 1500 / RD 350 / vol 0.06 — Glicko-2 default. The high initial RD means your rating moves quickly during your first few duels.

Stack

Backend
Go 1.23 · stdlib net/http
Database
SQLite (modernc.org/sqlite, no CGO)
Frontend
Vanilla HTML/CSS/JS · KaTeX
Build
go:embed all:web — single static binary
Container
alpine:3.20 · ~25 MB image
Problems
Claude Haiku 4.5 drafts · Sonnet 4.6 validates
Rating
Glicko-2 (custom 150 LoC)
Auth
Cookie-only · UUID · zero accounts

Architecture

flowchart LR B["Browser
HTML/CSS/JS + KaTeX"] N["ximg-web nginx
TLS termination"] P["proofduel
Go binary :8091"] D[("SQLite
/data/proofduel.db")] C["Anthropic API
Haiku + Sonnet"] B <-->|HTTPS| N N <-->|host.docker.internal:8091| P P <--> D P -.->|background
generator| C classDef accent fill:#ff6a00,stroke:#ff6a00,color:#1a0f00 classDef dim fill:#131313,stroke:#2a2a2a,color:#e8e8e8 classDef store fill:#181818,stroke:#ff6a00,color:#e8e8e8 class P accent class B,N,C dim class D store

Single Go binary serves embedded static frontend + JSON API; SQLite holds all state; background workers generate problems via Claude on demand. The whole production stack is one container, one volume, one systemd unit.

API

MethodPathPurpose
GET/api/healthzliveness
GET/api/mecurrent player: handle, rating, RD, tier, W/L
POST/api/duel/startissue a duel — problem + opponent + you
POST/api/duel/answersubmit answer — outcome + new rating
GET/api/leaderboard?limit=Ntop N players by rating (min 3 duels)

What it deliberately is not