Fifty-Fifty — the blind voice arena. Two AI voices read your words with no labels and no logos; you crown the one that sounds human, and only then do we reveal who made it. A running scoreboard (win rate + Elo) tracks the most popular engine over time.
Built with Next.js 15 (App Router), Tailwind CSS v4, and framer-motion, styled after the speechify.ai design system: monochrome ink-on-paper, tight display tracking, and the signature animated waveform.
The browser cannot cheat, even by reading the source:
- Pre-vote types (lib/types.ts) contain no provider fields — just opaque sample IDs, TTFB, and estimated cost.
- Audio arrives as anonymous bytes via
/api/audio/[id]. - Provider names enter the client only in the vote response, after the verdict is recorded server-side by the relay.
- A/B order is shuffled by the relay per battle.
Browser ──(same-origin fetch)──▶ Next.js serverless routes ──(Bearer session)──▶ fifty-fifty-relay
/api/battle POST /v1/battles
/api/audio/[id] GET /v1/audio/:id
/api/vote POST /v1/battles/:id/vote
/api/leaderboard GET /v1/leaderboard
The relay (separate repo: fifty-fifty-relay) holds every provider key and
all provider identities. The web app authenticates to it with per-visitor
HMAC-signed session tokens, minted server-side using the shared
RELAY_API_KEY and kept in an httpOnly cookie — no relay credentials or
tokens are ever exposed to client JavaScript.
Start the relay first (see its README), then:
cp .env.example .env.local # point at the relay + shared key
npm install
npm run dev # http://localhost:3000