NAI OS

Prestolex — the consumer web application

An AI-refereed party word game, Jackbox-style: a shared “console” screen creates a room, phones join via QR as answer pads, and Claude rules each typed answer with a one-line reason. Next.js + Supabase + Vercel + Claude API, its own private repository, live at prestolex.vercel.app. This is the one page describing it.

Prestolex (github.com/udopnink/nai-prestolex) is the third web application in this portfolio and the first aimed at consumers rather than at Udo's own consulting practice. It modernizes the mechanic of a classic parlour word race — a category and a letter are revealed, everyone races to type a fitting word — and replaces the argument the paper game always ended in with an AI referee that rules every answer instantly, with a reason. It took over as the primary commercial project on 2026-08-11, inheriting the previous candidate's build envelope, and shipped its first live milestone within days.

Like nai-analysis and nai-onto, it is built with GSD's phase-based discuss→plan→execute→verify loop rather than this workspace's orchestrator/subagent/tool pattern — a persistent product with a database, a deploy pipeline, and now a billing system, not a one-shot dispatch that produces a document. Unlike the other two, an LLM sits inside the product: the referee and the deck generator are Claude calls at runtime, which is why this app has a model policy and cost guardrails the other two never needed.

The game

A round

The console reveals a letter and a category; a 25-second timer runs; every player types an answer on their phone; the referee rules each answer as it arrives. All players answered early? The round ends after a short beat instead of waiting out the clock.

Scoring

Every valid answer scores. The speed bonus goes to the earliest server-side submission timestamp among answers that ultimately validate — async judging latency can never reorder it. Duplicates score less, which keeps answers diverse.

Duplicate matching, v1

Exact match on the normalized form only — Unicode NFC + casefold + trim + collapsed whitespace. No umlaut folding (Bär ≠ Baer), no fuzzy matching; anything smarter is gated behind the referee eval set.

The recap

The signature moment: every answer with the referee's one-line ruling. The argument the paper game always ended in, settled instantly with a reason.

A game

Configurable rounds (default 10) from a chosen deck, a final scoreboard with small awards, and two curated scoring extras — a jackpot rollover and a final-round double.

Bilingual

DE + EN from day one. The deck's language sets the round UI and the judging language.

Showtime

The shared-screen moment is theatrical by design: the letter lands via a slot-machine spin, the category is drawn as a card off a physical-looking deck, and won cards stack visibly per player. Console-only — phones stay fast answer pads.

Time-travel packs

Five bundled decade packs — 80s through 20s — each pairing a curated era deck (DE + EN) with a visual theme worn by the console and every joined phone for the whole game. Curated, not generated: no AI cost, IP-safe by construction.

The AI referee

Haiku for everything

Binding model policy: claude-haiku-4-5 for all app AI calls — referee, deck generation, opponent. Sonnet only as an eval-gated escalation; never an Opus-tier model anywhere in the product. Referee calls are tiny and latency is the UX.

Structured output

Each ruling comes back as {valid, display_safe, reason} via a JSON schema, so parsing can never fail mid-game. The judging rules live in a cached system prompt.

The judgment memo

The cost lever: each distinct (language, category, letter, word) is judged once globally and memoized in Postgres. Party vocabulary is Zipf-distributed, so repeats are free and instant and the hit rate climbs fast.

Never throws

On timeout, refusal, or a missing API key the referee degrades to “unavailable” and the room flips to majority-vote judging by the players — framed as a feature, never an error screen. A referee failure falls back to the letter-check heuristic and refunds the consumed allowance unit.

TV content gate, two layers

Free-text answers on a shared screen pass a synchronous local DE+EN blocklist at submission, then the referee's display_safe flag. An answer failing either is masked on the recap, scores invalid, and its raw text never reaches the console DOM — one module is the single chokepoint for what renders.

Deck generation

Theme in, difficulty-tagged category list out, DE or EN — the freemium product. The generator must refuse protected-IP themes and genericize them; themed decks are their own trademark surface.

Deterministic for tests

REFEREE_MOCK=1 — an answer is valid iff it starts with the letter — is the deterministic referee the unit and e2e suites run against.

Cost guardrails, named before anything can go viral

Every limit is env-configurable, but the point is that each exists and has a name: a per-account monthly judgment allowance (300 free, 8 000 plus, credit packs beyond that), per-room daily caps enforced atomically with the allowance, three deck generations per device per day on the free tier, and a global spend alarm — soft at €10/day, hard stop at €25/day. Exhaustion and the hard stop don't error: they flip judging to majority-vote mode, the same graceful floor the referee falls to when it's unavailable.

Accounts, caps, and the paywall

Host signs in, players don't

Creating a room or generating decks requires a host account (Google or email magic link, via Supabase Auth) so quotas and purchases have a durable identity. Phone players keep the frictionless anonymous QR + nickname join; signing in is optional and only adds a saved nickname and avatar.

Player caps

3 players free, 13 on the plus tier — stamped into the game state as entitlements and re-resolved at every round start, with a fail-closed-to-free tier resolver.

Stripe

Phone-side checkout for the plus subscription and for judgment credit packs, an idempotent webhook that reopens failed events, and a realtime console unlock when the upgrade lands mid-party.

Owner-gated keys

The Anthropic and Stripe production keys are deliberately not set until the owner adds them — the live referee and billing degrade gracefully in the meantime, because degrading gracefully is a designed-in mode, not an incident.

How it's built

%%{init: {'theme':'neutral', 'flowchart':{'htmlLabels':false,'nodeSpacing':45,'rankSpacing':55}}}%%
flowchart TD
    H["The console - TV, tablet or laptop<br/>host signed in - the single writer"] --> API
    P["Players' phones - anonymous<br/>QR join - submit answers only"] --> API

    subgraph L1 ["Next.js server routes - api/rooms/*"]
        API["applyActions over the pure reducer<br/>version-guarded on rooms.version"]
        REF["Referee module - never throws<br/>memo hit? no API call"]
    end

    API --> REF
    REF --> CL["Claude API<br/>claude-haiku-4-5, structured output"]

    subgraph L2 ["Supabase - service role only for writes"]
        DB[("rooms · answers (append-only)<br/>decks · subscriptions · allowances")]
        MEMO[("judgment memo<br/>each distinct answer judged once")]
        RT["Realtime channel per room<br/>broadcast + presence"]
    end

    API --> DB
    REF --> MEMO
    DB --> RT
    RT --> H
    RT --> P

    classDef orch fill:#dcefe9,stroke:#0f6f63,stroke-width:2px
    classDef tool fill:#f4f1ea,stroke:#8a7a5a
    classDef human fill:#fdf3e0,stroke:#8a5a00,stroke-width:2px
    class H,P human
    class API,REF orch
    class DB,MEMO,RT,CL tool
The distinctive invariant is the console as the single writer: phones only ever submit answers, every state mutation goes through server routes into applyActions under optimistic concurrency on rooms.version, and Supabase Realtime broadcasts the result back to everyone — which is what makes the game survive flaky party Wi-Fi. Game logic is a pure reducer with no I/O; AI calls are server-side only, so the API key never reaches a client; the anon database role is SELECT-only and all writes ride the service-role key inside server routes.

Next.js / React

Next.js App Router on Vercel, installable as a PWA — web manifest, service worker, offline page.

Supabase

Postgres + Auth + Realtime; versioned SQL migrations under supabase/migrations/, twelve so far.

Claude API

The official TypeScript SDK, server-side only, Haiku-only by binding policy.

Stripe

Subscription + credit-pack checkout with an idempotent webhook.

Verification

tsc --noEmit, a Vitest unit suite on the pure reducer (62 tests at the first milestone), and Playwright e2e against a local Supabase stack — the e2e's parallel joins are the standing regression proof for a join-race fix.

Milestones

v0.1.0-m1 (the core game, live) and v0.2.0-m2 (game-flow UX, decade packs, showtime, accounts) are tagged; the player-cap + paywall work is the current branch.

The seam to this workspace

Like nai-onto, and unlike nai-analysis, Prestolex has no seam to this workspace at all — no slash command reads it, writes to it, or knows it exists. What it shares with the agent systems is only the discipline: pinned decisions that don't get relitigated, invariants with a named single home in the code, degraded modes designed as features, and a verify step before anything is called done.

Direction

Store publishability

The current focus: what it takes to ship in the Google Play Store and Apple App Store — wrapper paths, review policies for user-generated content on a shared screen, and how in-app-purchase rules interact with the freemium model.

Voice mode paused

The experimental voice referee is explicitly on hold — no spend on it.

Cheapest capable model

A standing cost comparison for the referee/deck-gen workload (tiny prompts, high volume) across providers; the Haiku-first policy stands until that analysis says otherwise and the owner re-decides.

NextThe pattern & the promise