README.md

September 19, 2026 · View on GitHub


Game of Thrones x Jev

A Game of Thrones roleplay where you play Jon Snow. Each turn a story model writes the next scene, and then TypeSafe's Jev reads that scene back and answers five questions about it: where Jon now stands, what kind of scene it was, how much danger he is in, what should play under it, and whether the prose stayed inside the fiction.

Those answers are values — a location id from a closed set, a probability distribution, a score. The header, the soundtrack, the artwork behind the page and the next turn's prompt are all functions of the same five answers.

How a turn works

 your action ──► Narrator (GPT-5.6 Luna, via OpenRouter) ──► the scene

                              ┌────────────────────────────────┘

                     Jev — multiple questions, evaluated in parallel
                          location · beat · mood · danger · inFiction

      ┌───────────────────────┼────────────────────────┐
      ▼                       ▼                        ▼
 Decision.resolve       verify & retry            stored answers
 (pure, probabilities)  (inFiction < 0.5          (re-resolved later
      │                  → regenerate once)        without re-asking Jev)

 Location header · music · backdrop · next turn's prompt

Jev never writes anything. It labels the story scenario.

Running it

Requires Node 20.9+ (24 recommended) and pnpm.

Stories live in Redis, so start one first.

docker run -d --name story-redis -p 6379:6379 redis:alpine

pnpm install
cp .env.example .env.local     # then fill in the three required values
pnpm dev                       # http://localhost:3000
VariableDefaultNotes
TYPESAFE_API_KEYRequired. From typesafe.ai
OPENROUTER_API_KEYRequired. From openrouter.ai
REDIS_URLRequired. The Docker Redis above, or a rediss:// endpoint in a deployment
TYPESAFE_MODELjev-latestPin a version if you tune thresholds against one
OPENROUTER_MODELopenai/gpt-5.6-lunaAny OpenRouter chat model
STORY_TTL_DAYS7An untouched story expires on its own
MAX_TURNS15Turn 15 is narrated as a closing chapter
MAX_TURNS_PER_MINUTE10Per caller
MAX_TURNS_PER_IP_PER_DAY60Per caller. A new tale mints a new session, so only the address is durable
MAX_TURNS_PER_DAY500Process-wide spend cap
NEXT_PUBLIC_CDN_URLOptional. Serve music and artwork from a CDN; unset uses public/
pnpm test
pnpm typecheck

API

GET /api/story/:idThe transcript so far, or the prologue for a new session. Writes nothing.
POST /api/story/:id/turn{ action, turn }{ position, mood, beat, danger, text, turn, turnsRemaining, ended }
GET /api/health{ ok: true }

All model calls happen server-side; the browser never sees a key. turn is the number of turns the client has seen — a mismatch answers 409 turn_conflict, which is how two open tabs are kept honest without a lock.

Notes

  • Play is capped at 15 turns per session and 200 characters per action — both to bound spend on a public demo, alongside per-IP rate limiting and a daily budget.
  • Your action is passed to the narrator as delimited data, never as instructions, and inFiction checks the result. Attempts to talk to the narrator get redirected in-world rather than refused.
  • Text you type is sent to TypeSafe and OpenRouter. Don't put anything private in it.
  • Jon's world is George R. R. Martin's. This is a non-commercial technical demonstration.