Living spec. Source of truth is the Claude Doc (see README); this file is exported from it. Last export 2026-09-19.
Plan Maker 2000 is a mobile-web decision game you boot from a stuck group chat: screenshot the argument, an agent turns it into options, everyone places yes and veto chips, Jev pulls the lever, the decision posts, and the table either runs with it or goes another round. Balatro structure (table, run, hand, round), no messaging, no real money.
The pitch line still holds: normal chat vibe on top, hectic probability machine underneath. The probability machine is Jev.
Design for dinner tonight first; the holiday run must fit the same primitives without a second model. Friday dinner falls out of the ledger for free.
| Priority | Use case | People | Hands | Duration | What it stresses |
|---|---|---|---|---|---|
| 1 | Dinner tonight | 2–6 | 1 | minutes | intake speed, reveal, re-share |
| 2 | Holiday planning | 2–6 | 3–10 over days | days | run structure, veto scarcity, expiry |
| 3 | Recurring Friday dinner | 2–4 | 1 per week | weeks | ledger, streaks, rubber-banding |
The couple case (2 people) is the hard-feelings test; the group case (4–6) is the re-share test. Both must work, but only the group produces the success metric.
Three objects, borrowed directly from Balatro's run/hand structure. Everything else in the product hangs off them.
| Object | What it is | Lifetime | Holds |
|---|---|---|---|
| Table | A group of people (phone numbers) | persistent | members, ledger, veto allowance per run |
| Run | An optional sequence of hands toward one goal ("Japan trip") | until last hand accepted, or 14 days idle | ordered hands, remaining vetoes |
| Hand | One decision, looped through rounds until accepted | until accepted or expired (default 24 h) | question, rounds, final decision |
| Round | One pass of options → chips → lever → posted decision | minutes | options, chip placements, Jev's odds, outcome |
Rules:
A phone number is the whole identity. It exists for one reason: so the ledger follows a person across hands and tables.
Why phone and not name-only: the group already exists as phone numbers in WhatsApp, so it costs nothing to ask, and without it "you're paying next time" can't survive a hand.
One LLM agent generates options: from a screenshot at intake, and again mid-hand whenever "More options" wins a round. It is the only text-generating model in the product; Jev takes over the moment options exist.
flowchart LR
A[Screenshot or paste] --> B[Options agent<br/>question + 2-4 options]
B --> C{Ambiguous?}
C -- yes --> D[One clarifying<br/>question]
D --> B
C -- no --> E[Confirm / edit card]
E --> F[Round opens]
M[Manual entry] --> E
G[More options<br/>wins a round] --> B
At intake the agent sees the screenshot and the table's ledger and returns a question, 2–4 options, and any constraint it spotted ("Sam said not Tuesday"). The initiator sees a single editable card and taps share. On a regen it sees the previous round instead: which options had chips, which were vetoed, what Jev's odds were. That is its brief ("more like Thai, cheaper, not Tuesday"); vetoed options never come back.
Rules:
Everyone gets a fresh stack each round. Yes chips say what you'd be happy with and how much; veto chips kill an option outright. Chips don't set the odds directly — Jev reads the whole table, chips included, and sets them.
Yes chips (per round)
Veto chips (per run)
The lever
When everyone has locked or the round's time is up, Jev pulls the lever. Its calibrated probability over the options (the one it has been showing live) is the draw weights; the server samples from it, seeded and logged so the reveal can replay it. Sampling, not top-pick: the underdog can still win, so nobody's choice "got validated by the AI", and a 30 % option landing reads as luck.
Rubber-banding
Jev is the probability machine. It is TypeSafe AI's System One model: it takes a state plus a set of typed questions and returns calibrated probabilities for all of them in parallel, in 70–500 ms, at roughly $0.04 per million input tokens with output free (TypeSafe announcement, LangChain guide). It does not generate text and cannot hallucinate; outputs are schema-guaranteed. Three question types: choice (probability per option + confidence), score (position on an ordered scale), noul (probability a statement is true). Cardinality up to 255 per choice.
Jev is the decider, but a semi-decision-makery one: it weighs the chips heavily, along with constraints and the ledger, produces odds, and the lever samples them. It is not a majority counter and not an oracle. Every chip placement is a state change; Jev re-reads the round and re-answers a fixed question set fast enough that the odds move as you drag.
The hand state Jev sees
Question, options (including "More options"), current yes-chip placements per member, vetoes spent, constraints from intake, rubber-band bonuses in play, previous rounds of this hand (options, chips, what was posted), time to expiry, and the table's ledger (past outcomes, streaks, house rule). No phone numbers.
Question set, asked on every state change
| Question | Type | Used for |
|---|---|---|
| Which option should this table go with? Yes chips are votes and weigh heavily; respect constraints; be fair over the ledger. | choice over options + More options | the odds shown live, and the lever's draw weights |
| Is this round contentious? | noul | triggers the negotiator card when > 0.7 |
| What is the disagreement about? | choice: cost / timing / distance / people / taste / other | picks which canned clarifying prompt to show |
| Is any option effectively dead? | noul per option | greys the option, suggests a veto |
| Is anyone sandbagging (chips spread to avoid commitment)? | noul per member | light nudge to commit |
| How spicy is this round? | score: mild / warm / hot / volcanic | drives the UI escalation ramp |
| Which dealer line fits? | choice over a bank of ~200 pre-written lines | the posted-decision line and ledger line |
Why sample instead of pick
If Jev picked its top option every time, it would be a weighted vote with extra steps and "the AI sided with Sam" becomes the hard feeling. Sampling from calibrated odds keeps the ritual: the lever is a real pull, the odds were on the screen before it, and a loss is bad luck. Jev's influence is in the odds, which everyone can see and argue with by moving chips.
Rules
The ledger is the only thing that persists and the only thing that makes losing fun. It records outcomes and social debt, never chip balances.
Per table, per resolved hand:
| Field | Example |
|---|---|
| Question | Friday dinner |
| Rounds | 2 (More options won round 1) |
| Winner (option) | Thai |
| Who backed it | Sam 8/10, Tom 2/10 |
| Vetoes spent | Tom vetoed pizza |
| Jev's odds at the pull | 62 % Thai |
| Debt line | Tom pays; Sam picks next |
| Dealer line | "Thai. Again. Tom, get your wallet." |
Derived views the table sees:
Not in the ledger: chip counts across hands, any score, any rank. Nothing accrues that could look like a currency.
Calm to hectic is the emotional arc. The Balatro treatment is a ramp driven by Jev's "how spicy is this hand" score, not a theme applied from screen one. A unanimous hand never leaves the poll look; a contested one climbs to full CRT.
| Stage | Look | Motion | Jev avatar |
|---|---|---|---|
| Intake / confirm | plain iMessage-style poll card, system font, flat | none | text bubble, no face |
| Chips placing | felt table, chip stacks, Jev's odds bar live | chips slide, odds bar eases | low-res sprite, 16 px |
| Lock / countdown | felt darkens, options become cards, lever arm appears | cards tilt, CRT scanlines fade in | sprite animates, 32 px |
| Pull (reveal) | full CRT, screen shake, score-tick sound | lever cranks, reels spin on Jev's odds, winner card flips, confetti | full dealer sprite, speaks the line |
| Posted | decision card, accept / go again / more options | none | dealer sprite idles |
| Ledger | back to flat, receipt-style | none | text bubble |
Reveal requirements:
The lever is a slot machine with no money in it, so design it like one: variable reward, near-misses, named winners, a countdown, scarce vetoes, and a house (Jev) that everyone can blame. The one rule that keeps it a game rather than a fight is that every spike resolves inside the hand.
Dopamine: anticipation and variable reward
| Mechanic | Why it works | Build note |
|---|---|---|
| The pull is a variable-ratio reward | Outcome uncertain, odds visible, action is yours (you crank the lever) | The crank is a drag gesture, not a tap. Effort before reward. |
| Stretch the moment | Anticipation beats outcome for dopamine; slots spend 3–4 s on reels for a 200 ms decision | Reels spin on Jev's odds. 2 s minimum before the first reel stops, even if the draw is already known. |
| Near-miss | Losing by a hair drives replay more than losing outright | Reels always pass through the runner-up (by chips) last before landing. If your option was runner-up, your card visibly almost lands. |
| Upsets | A 20 % option landing is a story; a 70 % landing is Tuesday | Jev samples, so upsets happen naturally. When the winner was under 30 %, escalate: "UPSET" banner, double confetti, a ledger badge. |
| Called it | Backing the winner heavily against the odds feels like skill | Ledger line: "Jem called it at 25 %". Titles for repeat callers. |
| Live odds move as you drag | Action → visible consequence at 100 ms is the core slot loop | Jev call per drag, debounced 200 ms. The needle must move on every placement or the loop breaks. |
| Ding ding ding | Sound anchors the reward | Escalating tick as reels stop, a bell on the winner, a groan on a veto. Mute visible, default on. |
Serotonin: status and being seen
| Mechanic | Why it works | Build note |
|---|---|---|
| Winner is a person, not an option | "We're going with Jem's option" attributes the win | Whoever had the most chips on the winner gets their name on the decision card. Ties → earliest placement. |
| Titles | Public, earned, in-group status | Pattern-derived by Jev over the ledger: Table Oracle (calls it), Chaos Agent (backs long shots), Serial Vetoer, The House Always Wins (Jev's favourite). Shown next to the name at the table. |
| Streaks | Visible dominance is status; visible losing streaks trigger the rubber-band so nobody bottoms out | "Sam: 4 Friday dinners in a row" on the ledger. |
| Being seen by Jev | Jev naming what you care about is validation | The negotiator card: "Jev reckons Tom's no is about money, not Thai." Human-authored line, Jev-selected. |
| In-group language | Ritual lines become the table's slang | One fixed ritual line on every pull ("the lever has spoken") plus a rotating dealer line. The fixed line is what gets quoted back in WhatsApp. |
| The screenshot | Social proof and bragging rights outside the app | Capture-safe final frame with the winner's name large. This is the acquisition loop. |
Cortisol: stakes and urgency
| Mechanic | Why it works | Build note |
|---|---|---|
| Round countdown | Time pressure raises arousal; last-to-lock is social pressure | "Everyone's waiting on Tom" card, in-room only. No push. |
| Vetoes are scarce, public, attributed | Spending one is a costly signal; watching someone reach for one is tension | Veto has a hold-to-confirm and a distinct sound. The table sees the hover. |
| Chips vanish at the pull | Loss aversion: unspent is wasted, so place them | Unplaced chips fly off the table at the pull. |
| Social debt | A consequence outside the app is real stakes | Loser pays / winner picks, written on the ledger and the screenshot. |
| Comeback | Hope keeps the loser in | Rubber-band bonus is announced at the start of the next round: "Tom's got +3 this round." |
| Spice ramp | The app visibly gets more intense as the table does; feedback amplifies | Jev's spice score drives the CRT ramp. Volcanic rounds shake. |
Every spike resolves in-hand. Countdown → pull → named winner → dealer line → debt line → accept. Nothing is left hanging when the hand closes. A couple app that manufactures cortisol and doesn't discharge it is a churn machine and a breakup tool; the discharge is the product, not the spike.
Interpersonal dynamics
Jev's natural form, used on purpose
Where it stops being fun
A hand loops through rounds and has two terminal states. Nothing reopens after accept or expiry.
stateDiagram-v2
[*] --> Draft: intake
Draft --> Open: initiator shares
Open --> Open: chips placed / vetoed
Open --> Pulled: all lock or round timer
Pulled --> Posted: Jev's odds sampled
Posted --> Open: refine, or More options<br/>won and agent regenerated
Posted --> Accepted: table accepts<br/>or quiet window passes
Open --> Expired: hand expiry, no chips
Accepted --> [*]
Expired --> [*]
| State | Who can act | Timer |
|---|---|---|
| Draft | initiator only | 1 h then discarded |
| Open | all members: place, move, veto, lock | round timer, default 2 h; hand expiry default 24 h, settable 1 h–7 d |
| Pulled | nobody; draw runs within seconds | n/a |
| Posted | all members: accept, or go again | quiet window 1 h → auto-accept |
| Accepted | nobody; ledger written, reveal replayable | n/a |
| Expired | nobody; default applies | n/a |
Posted → Open is the GOTO. Any member can tap "go again" to start a new round with the same options (chips reset, vetoed options gone) or "more options" to send the agent back in. Accept needs a majority of members, or 1 h of silence. Expiry: if any chips were placed this round, expiry pulls the lever and the posted decision auto-accepts. If no chips were placed at all, the hand expires with the initiator's first-listed option and the ledger notes "decided by default". Reminders are in-room only (a countdown), no push.
These are the lines that don't move. Each one exists because crossing it turns the product into something else (a chat app, a casino, a native app).
In
Out
Stretch (only after an unsolicited share is observed)
Success is one unsolicited share: someone forwards a hand link or a reveal screenshot to a chat the initiator isn't in. If that doesn't happen in 4 weeks, the wedge is wrong, not the UI.
Tested against POST https://api.typesafe.ai/v1/systemone with jev-latest. Six hand states through the full question set, then targeted probes. All of this shapes the build.
| Finding | Evidence | What it means for the build |
|---|---|---|
| Latency is fine for a drag loop, not for 100 ms | 6 questions in one call: 320–420 ms. Single question: ~200 ms. | Debounce chip drags at 200 ms; expect the odds bar to move ~400 ms after the finger. Show it easing, not snapping. |
| Chips dominate the odds by default | Tom 9 Thai / Sam 10 pizza → 45 / 43. Sam 8 concentrated vs Tom spread 3/3/2/2 → pizza 93 %. | Stacking is intensity, as specced. Spread chips read as weak. |
| The fairness dial is the instruction text alone | Same chips (Sam 10 pizza, Tom 6 Thai / 4 ramen, Sam won last 4): no fairness clause → pizza 99 %. "Be fair across the ledger" → thai 54 %, pizza 18 %. "Lean toward the others" → thai 75 %, pizza 5 %. | PICK_INSTRUCTIONS is a config var, hot-reloadable. This is the whole "semi-decision-makery" tuning surface; no code changes needed to playtest it. |
| "More options" works as a votable card | Both members put most chips on it → 94 %. | Regen trigger is reliable. |
| Classification questions want derived features, not raw chips | Format question over raw chip state picked craps for everything, never one_tap or reels. With chip_agreement, live_option_count and spice pre-computed into the state and named in the criteria → one_tap 97 %, swipe 99 %, craps 100 %, reels 72 %. |
The DO computes features before every Jev call. Criteria reference state fields by name. |
| Spice reads low from chips alone | Contentious 0.76 but spice 0.27 on the same round. | Spice needs signals beyond chip position: chip moves per minute, vetoes, re-placements. Or drive the ramp from contentious + format instead. |
| Sandbagging noul is noisy | 0.6 for a member who put 7 chips on More options. | Nudge only, never a callout. Threshold ≥ 0.7. |
| Large choice banks work | 40 dealer tags: 301 ms, correct pick. Duplicate tags split probability. | Bank can be big; tags must be unique and descriptive. |
| Cost is nothing | ~750 in / ~215 out tokens per full call. | Hundreds of calls per hand is well under a cent. |
| Straight sampling punishes unanimity | Local run: both members 10 chips on Thai → Jev 78 % Thai (history in state pulled it down from 99 %), sample landed Ramen at 6 %. | Design change: when format is one_tap the lever does not sample; it confirms. Sampling is for craps and reels. Sharpening (temperature < 1) still open for craps. |
What exists after 19 Sep: a running loop prototype, driven end to end locally. this repo.
Stack as built:
@better-auth-cloudflare/cli (Hono template, D1, no KV/R2).phoneNumber plugin (6-digit OTP, 5 min expiry, sign-up on verify) and anonymous as the zero-friction path. OTP prints to the wrangler console in dev; OTP_MODE=twilio sends SMS.src/hand.ts): state, WebSocket fan-out via the hibernation API, chips/veto/lock, Jev odds recomputed on every state change with a 200 ms debounce, the lever as a seeded weighted sample with the runner-up placed last in the reel order, rounds (again / more / accept with majority).src/jev.ts) with 429/529 backoff. Dealer bank (src/dealer.ts), 22 lines, Jev picks by tag.scripts/drive.sh runs a full two-member hand against a dev server for smoke testing.Dev loop:
pnpm install && cp .dev.vars.example .dev.vars, paste keys.pnpm db:migrate:dev && pnpm dev — local D1 + DO in miniflare, hot reload, OTP codes in the console.wrangler dev --ip 0.0.0.0, open /, jump in, create a hand, share /h/<id>.wrangler.toml [vars].PICK_INSTRUCTIONS; it hot-reloads.pnpm exec wrangler versions upload gives a URL without touching prod. First real deploy: pnpm exec wrangler d1 create decision-maker-2000-db, paste the ID, configure the secrets in README.md, then run pnpm deploy.Still to support the dev loop:
more op.jev_test.py / jev_probe2.py are the seed. Turn them into a fixture set of hand states with expected odds ranges so instruction changes can be regression-tested.TYPESAFE_API_KEY to wrangler secret before the first deploy and roll it.Temp Jev key lives in .dev.vars (gitignored) and in the Claude Doc, not in this repo. Roll it before first deploy.
Order matters: the whole product rests on the drag → odds → pull loop feeling good, and that loop rests on Jev. Prove those two before touching auth, tables or ledger.
Stack, given the shape: Cloudflare Workers + Durable Objects (one DO per hand holds the state and fans out odds over WebSocket), D1 or KV for tables and ledger, Pages for the mobile web client. The Jev call sits inside the DO so every state change is one round-trip.