Jev Plays Snake: Two Small Interfaces for a Decision Model

A hands-on TypeSafe Jev demo: an auto-playing filing cabinet, a 102-step structured Snake replay, and a side-by-side of the System One schema vs OpenAI's Decisions API.

Jev Plays Snake: Two Small Interfaces for a Decision Model

Jev is TypeSafe’s System One model: you send a state and a set of typed questions, and it returns structured answers and probabilities your code can branch on — no generated prose, no JSON parsing, no prompt-and-pray.

That one sentence is easy to nod at and hard to feel. So this post builds two small interfaces that make the contract visible. The first files a document into a folder. The second lets Jev drive a 10 × 9 Snake game, one typed move at a time. Both run in your browser with your own TypeSafe key.


A decision model, not a chatbot

A chat model produces text for a human to read. Jev produces a decision for code to consume. The difference sounds philosophical until you look at the return shape:

PrimitiveQuestion it answersWhat comes back
ChoiceWhich of these options?choice, probabilities, confidence
ScoreWhich level on this rubric?score, legend, probabilities, confidence
NoulIs this statement true?noul (0–1)

Every answer is constrained to the options or levels you supplied. The model cannot invent a sixth folder or a direction outside the grid. Your code never has to recover a value from a sentence.

The other half of the contract is architectural: code owns the workflow. Jev supplies a judgment at one point in that workflow; code decides what to do with it. That is exactly the shape of both exhibits below.

The TypeSafe docs put it well: ask for one snap judgment per question, then compose the answers in code. If a judgment needs several factors, split it, and weight the parts yourself.

Your key, your browser, your bill

There is no backend on this page. Both exhibits open in replay mode: the filing cabinet cycles through structured answers, and the Snake board plays a 102-step trace recorded with Jev. Neither needs a key or a Worker.

The key is only for live mode. When you add one, it is stored in localStorage under jev.key.v1 and sent only to the endpoint you choose. That is convenient, and it is also a real security boundary: any script running on this origin can read localStorage. Use a dedicated key with a spending limit, and revoke it when you are done experimenting.

OPTIONAL // LIVE MODE

Bring your own Jev key

The demos below run without a key. Add one only when you switch an exhibit to live mode. The key stays in this browser; nothing is committed to the repository, and nothing is sent to this site's host.

NO KEY
Paste the HTTPS base URL of your Cloudflare Worker. It is saved in this browser.
ADD A RELAY URL

A CORS reality check. At the time of writing, TypeSafe's API returns Access-Control-Allow-Origin only for https://console.typesafe.ai. A static site on another origin cannot read a direct response, no matter how correct the key is. This demo therefore exposes only the Relay transport: a tiny proxy you control. The page is still pure frontend; the relay is a transport adapter, not a backend. The key goes from your browser to that relay and then to TypeSafe; it never goes to a public CORS proxy. The section below has a 30-line Cloudflare Worker you can deploy for free. If TypeSafe later allowlists your origin, you can point baseUrl() at the API directly and remove the relay.

Exhibit 01 — The filing cabinet

The first exhibit is deliberately mundane: a file lands in a shared drive, and someone has to choose a folder. The interesting part is how little the model needs to know, and how much the code still controls.

It opens in auto demo: five answers recorded with jev-1.13.0 cycle through the starter folders, including an Unknown case, without calling an API. Press Take control when you want to type your own file and run it live. The auto demo stops the moment you edit a field or submit the form.

The state is the file name plus an optional excerpt. The questions are a Choice over the folder set — five starter folders, any custom folders you add, and Unknown — and a Noul for sensitive content. Both are asked in the same request, against the same state, and evaluated independently.

The folder manager is part of the exhibit. Add a folder and give it a one-line description; that description becomes part of the criteria rubric. Custom folders are saved in localStorage, so they survive a reload; Reset restores the five starters. Unknown is always present and cannot be removed. It is the honest “none of these” answer, so Jev can decline to force a file into a bad folder. When it chooses Unknown, the UI tells you to review the file instead of pretending the classification succeeded.

EXHIBIT // 01 · CHOICE + NOUL

The filing cabinet

Type a file name, add an excerpt if you have one, and let Jev choose among the starter folders, your own custom folders, or Unknown. The bars are the full probability distribution.

DEMO
AUTO DEMO 01 / 05
CUSTOM FOLDERS
0 / 8
Inspect the request

    
STATE = FILE NAME + EXCERPT · QUESTIONS = 1 CHOICE + 1 NOUL · ANSWER = FOLDER + PROBABILITIES + CONFIDENCE · UNKNOWN ALWAYS AVAILABLE · ICONS: LUCIDE (ISC)

What the request looks like

The entire integration is one object. The folder names are the keys of criteria; the descriptions are the rubric the model compares against. The sensitive question is speculative in the useful sense: it is nearly free to ask, and the code can ignore it when it is not relevant.

{
  "model": "jev-latest",
  "state": {
    "file_name": "Q3_invoice_acme_2026.pdf",
    "excerpt": ""
  },
  "questions": {
    "folder": {
      "type": "choice",
      "instructions": "Which folder should this file be filed into? Judge from the file name and any excerpt. Pick the single best folder.",
      "criteria": {
        "Finance": "Invoices, receipts, budgets, payroll, tax, expense reports, financial statements.",
        "Engineering": "Source code, architecture notes, incident reports, API specs, infrastructure, technical documentation.",
        "Legal": "Contracts, NDAs, terms of service, compliance policies, licenses, regulatory filings.",
        "Marketing": "Campaign briefs, ad copy, brand assets, social posts, landing pages, market research.",
        "People": "Resumes, offer letters, performance reviews, onboarding, HR policies, employee records.",
        "Unknown": "No listed folder fits, or the file is too ambiguous to file with confidence."
      }
    },
    "sensitive": {
      "type": "noul",
      "instructions": "Does the file name or excerpt suggest it contains personal, confidential, or regulated data?"
    }
  }
}

A representative response is just as small:

{
  "model": "jev-1.13.0",
  "answers": {
    "folder": {
      "type": "choice",
      "choice": "Finance",
      "confidence": 1.0,
      "probabilities": {
        "Finance": 1.0,
        "Engineering": 0.0,
        "Legal": 0.0,
        "Marketing": 0.0,
        "People": 0.0,
        "Unknown": 0.0
      }
    },
    "sensitive": { "type": "noul", "noul": 0.66 }
  },
  "usage": { "input_tokens": 495, "output_tokens": 38 }
}

There is no parsing step. folder.choice is already one of the keys you supplied. sensitive.noul is already a number between 0 and 1. The code decides whether 0.66 is worth a review flag — Jev does not make that policy decision.

Exhibit 02 — Snake, with Jev at the controls

The second exhibit is the fun one, and also the stricter test of the contract. The board is 10 columns × 9 rows. You cannot steer the snake. Jev chooses every move.

It opens as a structured replay: a 102-step trace recorded with jev-1.13.0, stored as about 20 KB of JSON. The browser reconstructs the board from the seed and the recorded moves, then plays it back from the first move to GAME OVER. No key, relay, or API request is involved. Press Go live when you want to run the same loop against Jev with your own key.

Each turn sends the same state shape:

  • A compact matrix of all 90 cells, one string per row: 0 empty, 1 head, 2 body, 3 food.
  • A legend that maps each digit to its meaning.
  • The head coordinate, current direction, food coordinate, score, step count, and the legal moves computed by code.

The question is a Choice whose options are only the legal moves. Code has already removed walls and the snake’s own body from the action space, so a bad answer cannot end the run on a technicality. The model still has to read the grid and pick a direction that moves toward the food.

EXHIBIT // 02 · CHOICE

Jev plays Snake

10 × 9 grid. No arrow keys. The board opens with a structured replay recorded with Jev — 102 moves, no key or Worker required. Switch to live mode to call the API yourself; the run pauses after every ten food points, or use Run to end to skip the gate.

REPLAY
MODE // STRUCTURED REPLAY Loading trace…
GRID 10 × 9 1 = HEAD 2 = BODY 3 = FOOD
SCORE0
STEPS0
TOKENS0
MODELJEV
JEV'S LAST MOVE
— —
WHAT JEV SEES

          
0 EMPTY 1 HEAD 2 BODY 3 FOOD
STEP 000 / 102
Inspect the current request

    
MODE = REPLAY (20 KB TRACE) / LIVE (KEY + RELAY) · STATE = 9 × 10 MATRIX + LEGEND + HEAD + DIRECTION + FOOD + LEGAL MOVES · QUESTION = 1 CHOICE

The compact matrix

The matrix is intentionally boring. A JSON object with 90 named cells would be readable to a human and noisy to the model; a 9-row array of 10-character strings is compact, unambiguous, and easy to log. Here is a real state from the game:

{
  "grid": [
    "0000000000",
    "0000000000",
    "0000000000",
    "0000000000",
    "1221003000",
    "0000000000",
    "0000000000",
    "0000000000",
    "0000000000"
  ],
  "legend": { "0": "empty", "1": "snake head", "2": "snake body", "3": "food" },
  "head": { "row": 4, "col": 3 },
  "direction": "right",
  "food": { "row": 4, "col": 7 },
  "score": 0,
  "steps": 0,
  "legal_moves": ["up", "right", "down"]
}

The instruction tells Jev the coordinate convention — row 0 is the top, column 0 is the left, right increases the column, down increases the row — and tells it to choose only from legal_moves. On this board it chooses right toward the food, with a confidence around 0.95. On a board where the food is behind the snake, it has to turn; the matrix makes the body visible, so the legal set is what keeps it honest.

Design note: the action space is filtered in code, not in the prompt. This is the “code owns the workflow” rule in miniature. Jev chooses a direction; code guarantees the direction is legal. The model is doing judgment, not bookkeeping.

Why the run pauses at ten

Every move is a request in live mode. Jev is cheap — input is 42perbilliontokens,outputisfree—but“cheap”isnot“free,”andalongrunaddsup.Atypicalmoveonthisboardisroughly600inputtokens.A10−foodrunisoften60–100moves,soexpectroughly36,000–60,000inputtokens,orabout42 per billion tokens, output is free — but “cheap” is not “free,” and a long run adds up. A typical move on this board is roughly 600 input tokens. A 10-food run is often 60–100 moves, so expect roughly 36,000–60,000 input tokens, or about 0.0015–$0.0025 at the current price. The pause at ten food points is a budget gate, not a technical limit: you can continue, end the run, or wait and inspect the matrix first. The replay has no gate; it is already paid for.

Run to end is the deliberate bypass. It keeps the same loop but ignores the ten-food gate, stopping only at game over, a full board, an error, or Stop. The button is separate from Start run because skipping a budget gate should be a conscious choice, not the default. If you want to watch the model’s behavior without clicking through every gate, use it; if you want to inspect the matrix at each milestone, stay on the manual run.

The gate also gives you a natural place to watch the model’s behavior. Did it take the shortest path? Did it get trapped by its own body? Did confidence drop before a mistake? Those are the questions worth asking when you evaluate a decision model on your own data.

The pattern behind both

The two exhibits look unrelated, but they share the same four moves:

MoveFiling cabinetSnake
Constrain the answerFolder keys + UnknownOnly the legal directions
Give only relevant stateFile name + excerptMatrix + legend + head + food
Ask independent questionsFolder + sensitive in one callOne move per call, because the board changes
Keep policy in codeWhat to do with sensitiveFiltering illegal moves and gating tokens

The second row is where most prompt-based systems go wrong. More context is not automatically better. Jev’s own documentation lists “large state full of irrelevant detail” as a failure mode: unrelated material acts as a distractor, and accuracy falls. The fix is to filter and structure in code before the request.

The fourth row is the part that makes the system testable. The model returns a probability distribution; the code decides the threshold, the fallback, and the human-escalation path. When the policy changes, you change a number in code — not a paragraph in a prompt.

Two schemas for the same idea

The exhibits above use TypeSafe’s System One schema. As of October 2026 there is a second public schema for the same job: OpenAI’s Decisions API. The two are close enough to compare field by field, and different enough that the comparison clarifies what a decision-model API actually is.

System One is not just Jev’s private request body. Cloudflare’s Clef and Clef-flash implement the same shape — state, a questions map, and noul / choice / score primitives with criteria — with multimodal extensions. That makes System One a schema in the practical sense: more than one model can speak it. The OpenAI Decisions API is a second schema, served by one endpoint and, today, one model.

Side by side

SPEC MATRIX // DECISION PROTOCOLS

Two contracts, one typed answer

Both endpoints return probabilities instead of prose. The wire format is where they diverge.

PUBLIC DOCS
2026-10-18
S1 // SYSTEM ONE POST /v1/systemone state→ questions{}→ answers{}
OA // DECISIONS POST /v1/decisions input→ questions[]→ answers[]
Field System One Jev / Clef OpenAI Decisions GPT-6 Luna
Evidence state — string, object, or array input — string, messages, or images
Modality Jev: text. Clef: text / JSON / image / video. Text + image
Questions Map keyed by ID. The key is not sent. Array. Optional name is echoed.
Yes / no noul → noul predicate → probability
Choice choice + probabilities{} + confidence choice + probabilities[] + confidence
Score score + legend + probabilities{} + confidence score + probabilities[] + confidence
Options criteria — map / array / object choices[] or levels[]
Instructions Required. String / object / array. Required. String.
Answers answers{} keyed by ID answers[] in question order
Refusal No answer type. Threshold in code. Per-question refusal; optional safety_identifier.
Limits 64k / request; 32k / question; 262k long context. 1–200 questions; 2–255 choices.
Model Jev 1.13 / 1.14 + Clef GPT-6 Luna (public beta)
Price $0.042 / 1M input; output free $0.10 / 1M input; no output or cache charge
Adoption Jev + Clef / Clef-flash OpenAI endpoint only
MONO / HAIRLINE 0.7PX SCHEMA DRIFT: CHECK DOCS JEV · CLEF · GPT-6 LUNA

Two rows are worth correcting from the folklore version of this comparison. First, both schemas require instructions. The real difference is where the options live: System One puts them in criteria; OpenAI puts them in choices or levels. Second, OpenAI’s predicate does not return a separate “condition indicator.” It returns { type, name, probability }; the condition lives in the instructions string. The statistical object is the same as a noul.

The same three questions in both

Here is a support-ticket decision in System One. The state is a structured object, and the questions are keyed by ID:

REQUEST // SYSTEM ONE POST /v1/systemone · JSON
{
  "model": "jev-latest",
  "state": {
    "message": "I was charged twice for order A-104. Please refund the duplicate.",
    "order_id": "A-104"
  },
  "questions": {
    "department": {
      "type": "choice",
      "instructions": "Which team should handle this message?",
      "criteria": {
        "billing": "Payments, invoices, and refunds.",
        "technical": "Bugs, outages, and integrations.",
        "shipping": "Delivery and tracking.",
        "other": "None of the above."
      }
    },
    "refund_requested": {
      "type": "noul",
      "instructions": "Does the customer explicitly request a refund?"
    },
    "frustration": {
      "type": "score",
      "instructions": "How frustrated is the customer?",
      "criteria": ["Calm", "Frustrated but civil", "Very angry"]
    }
  }
}
STATE → QUESTIONS{}APPLICATION/JSON

And here is the same decision in the OpenAI Decisions format. The evidence is message-shaped, the questions are an array, and every question carries a name:

REQUEST // OPENAI DECISIONS POST /v1/decisions · JSON
{
  "model": "gpt-6-luna",
  "input": "Order A-104: I was charged twice. Please refund the duplicate.",
  "questions": [
    {
      "type": "choice",
      "name": "department",
      "instructions": "Which team should handle this message?",
      "choices": [
        { "value": "billing", "description": "Payments, invoices, and refunds." },
        { "value": "technical", "description": "Bugs, outages, and integrations." },
        { "value": "shipping", "description": "Delivery and tracking." },
        { "value": "other", "description": "None of the above." }
      ]
    },
    {
      "type": "predicate",
      "name": "refund_requested",
      "instructions": "Does the customer explicitly request a refund?"
    },
    {
      "type": "score",
      "name": "frustration",
      "instructions": "How frustrated is the customer?",
      "levels": [
        { "label": "Calm", "description": "Neutral, matter-of-fact." },
        { "label": "Frustrated but civil", "description": "Annoyed, still constructive." },
        { "label": "Very angry", "description": "Hostile or threatening to leave." }
      ]
    }
  ]
}
INPUT → QUESTIONS[]APPLICATION/JSON

The responses mirror the two collection styles:

RESPONSE // SYSTEM ONE 200 OK · ANSWERS{}
{
  "model": "jev-1.13.0",
  "answers": {
    "department": {
      "type": "choice",
      "choice": "billing",
      "confidence": 0.93,
      "probabilities": {
        "billing": 0.95,
        "technical": 0.02,
        "shipping": 0.01,
        "other": 0.02
      }
    },
    "refund_requested": { "type": "noul", "noul": 0.98 },
    "frustration": {
      "type": "score",
      "score": 1.1,
      "legend": {
        "0": "Calm",
        "1": "Frustrated but civil",
        "2": "Very angry"
      },
      "probabilities": { "0": 0.1, "1": 0.7, "2": 0.2 },
      "confidence": 0.55
    }
  },
  "usage": { "input_tokens": 420, "output_tokens": 40 }
}
KEYED BY QUESTION IDPROBABILITY DISTRIBUTION
RESPONSE // OPENAI DECISIONS 200 OK · ANSWERS[]
{
  "model": "gpt-6-luna",
  "answers": [
    {
      "type": "choice",
      "name": "department",
      "choice": "billing",
      "confidence": 0.93,
      "probabilities": [
        { "value": "billing", "probability": 0.95 },
        { "value": "technical", "probability": 0.02 },
        { "value": "shipping", "probability": 0.01 },
        { "value": "other", "probability": 0.02 }
      ]
    },
    {
      "type": "predicate",
      "name": "refund_requested",
      "probability": 0.98
    },
    {
      "type": "score",
      "name": "frustration",
      "score": 1.1,
      "confidence": 0.55,
      "probabilities": [
        { "value": 0, "label": "Calm", "probability": 0.1 },
        { "value": 1, "label": "Frustrated but civil", "probability": 0.7 },
        { "value": 2, "label": "Very angry", "probability": 0.2 }
      ]
    }
  ],
  "usage": { "input_tokens": 420 }
}
ORDERED BY QUESTIONS[]PROBABILITY DISTRIBUTION

The values are illustrative. The shapes are the point.

What the table hides

state is evidence-shaped; input is message-shaped. System One accepts a JSON object or array directly, so a ticket record and its policy can travel as structured data. OpenAI’s input is a string or a message array; structured evidence has to be serialized into text or a content part. If your state is already a record, System One asks less of your code. If your state is an image or a conversation, the Decisions API is the more natural fit.

Map keys vs array names. System One’s question key is for your code; the model never sees it. OpenAI’s name is part of the question object and is echoed in the answer, and answers arrive in question order. Both are easy to map into code, but they fail differently: a missing System One key is a lookup miss; a missing OpenAI name is an ordered-array position you have to trust.

criteria vs choices / levels. System One compresses the rubric into criteria: a map for Choice, an ordered array for Score, and an optional {true, false} object for Noul. OpenAI makes the options explicit: choices is an array of {value, description}, and levels is an array of {label, description}. The latter is more verbose, but it also lets a choice value be a boolean and keeps labels separate from the values. Both schemas still require instructions.

noul vs predicate. Same idea, different field name. Both return one probability that the statement is true. TypeSafe calls it noul; OpenAI calls it probability. Neither has a separate confidence field for that primitive, because the probability is the uncertainty.

Refusal is a channel, not a probability. OpenAI can return a per-question refusal answer, and the other questions in the same request can still answer. It also accepts an optional safety_identifier — an opaque, caller-provided end-user ID scoped to the verified organization. System One has no dedicated refusal answer type; you express guardrails as Noul questions and decide what to do with the probability and confidence in code. That is a real architectural choice: OpenAI gives you a refusal channel, TypeSafe gives you a number to threshold.

Object vs array returns. System One keys answers by your question ID, so order does not matter. OpenAI returns answers in the same order as the questions array, with name as the stable identifier. Both return full probability distributions for Choice and Score. System One’s Score answer includes a legend; OpenAI’s Score probabilities carry the label and value on each entry.

Price and modality. Jev lists 0.042per1Minputtokenswithoutputfree;GPT−6LunaDecisionslists0.042 per 1M input tokens with output free; GPT-6 Luna Decisions lists 0.10 per 1M input tokens with no output or cache charges. Tokenization and request size differ, so compare on your own workload rather than multiplying list prices. The bigger difference is input: Jev 1.13 is text-only, while the Decisions API accepts images, and Clef extends the System One shape to images and video.

Adoption and portability. System One has more than one implementation: Jev and Cloudflare’s Clef/Clef-flash. The OpenAI Decisions API is a single-vendor endpoint with one model today. If you want the same schema to survive a model swap, System One is the more portable contract; if you want OpenAI’s model and image handling, Decisions is the direct path.

Which one should you use?

  • Use System One when the evidence is structured JSON, the workload is text-heavy, you want several independent questions in one request, and you want calibrated probabilities you can threshold. It is also the better fit when you want the option to run the same schema on Jev or Clef.
  • Use OpenAI Decisions when you need text plus images, you are already in the OpenAI SDK ecosystem, or you want the safety_identifier / per-question refusal semantics.
  • Use neither as your control flow. Both return decisions; your code should still own routing, thresholds, retries, and the human-escalation path. The schema decides the shape of the answer, not the policy.

The two formats are not wire-compatible, but they are close enough to normalize. A small adapter can map state ↔ input, the question map ↔ array, and noul ↔ predicate; the rest is field renaming. Some API gateways already expose both shapes behind one route. That is a compatibility layer, not a standard.

One caveat before you pin either one: the OpenAI Decisions API is in public beta and currently accepts only gpt-6-luna; TypeSafe’s Jev is at 1.13/1.14, and Clef is an open-weight model with its own limits and prices. The table above reflects the public docs as of 2026-10-18. Check the current docs before freezing a schema in your codebase.

Run it yourself

The HTTP API is one endpoint:

curl https://api.typesafe.ai/v1/systemone \
  -H "Authorization: Bearer $TYPESAFE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "jev-latest",
    "state": { "file_name": "Q3_invoice_acme_2026.pdf" },
    "questions": {
      "folder": {
        "type": "choice",
        "instructions": "Which folder should this file be filed into?",
        "criteria": {
          "Finance": "Invoices, budgets, tax, payroll.",
          "Engineering": "Code, incidents, specs, infrastructure.",
          "Legal": "Contracts, NDAs, compliance.",
          "Marketing": "Campaigns, copy, brand assets.",
          "People": "Resumes, reviews, HR records."
        }
      }
    }
  }'

The replay trace

The replay on this page is one JSON file, not a video. It is about 20 KB:

{
  "source": "jev",
  "model": "jev-1.13.0",
  "seed": 20261018,
  "outcome": { "state": "GAME OVER", "score": 12, "steps": 102, "inputTokens": 64954 },
  "steps": [
    { "step": 1, "move": "right", "confidence": 0.31, "probabilities": { "up": 0.46, "right": 0.54, "down": 0 }, "input_tokens": 645 }
  ]
}

Each step stores only the decision: direction, confidence, probabilities, and input tokens. The board is reconstructed deterministically from the seed, the fixed rules, and the move list, so the file does not need 102 copies of the 90-cell matrix.

Generate it with your own key:

TYPESAFE_API_KEY=... bun run record:snake

The script calls /v1/systemone directly from Node — no CORS, no Worker, no browser key. It writes public/data/jev-snake-replay.json and never stores the key. If you only want a layout fixture, bun run record:snake:mock produces a scripted trace instead; the page labels it SCRIPTED REPLAY.

The committed trace was recorded with jev-1.13.0: 102 steps, 12 food, 64,954 input tokens. Visitors replay it without a key or a relay. The live mode below is unchanged; it still needs both.

A relay you control

The deployed page needs a relay because of the CORS allowlist. Deploy a tiny one; this Cloudflare Worker forwards only two paths and keeps the CORS allowlist tight:

export default {
  async fetch(request, env) {
    const origin = env.ALLOWED_ORIGIN || "https://blog.alextrinitywolf.com";
    const cors = {
      "Access-Control-Allow-Origin": origin,
      "Access-Control-Allow-Headers": "authorization, content-type",
      "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
      "Access-Control-Max-Age": "86400",
      Vary: "Origin"
    };

    if (request.method === "OPTIONS") {
      return new Response(null, { headers: cors });
    }

    const url = new URL(request.url);
    if (url.pathname !== "/v1/systemone" && url.pathname !== "/v1/models") {
      return new Response("Not found", { status: 404, headers: cors });
    }

    const upstream = await fetch("https://api.typesafe.ai" + url.pathname, {
      method: request.method,
      headers: {
        authorization: request.headers.get("authorization") || "",
        "content-type": request.headers.get("content-type") || "application/json"
      },
      body: request.method === "GET" ? undefined : await request.text()
    });

    return new Response(upstream.body, {
      status: upstream.status,
      headers: {
        ...cors,
        "content-type": upstream.headers.get("content-type") || "application/json"
      }
    });
  }
};

Set ALLOWED_ORIGIN to your own origin. The relay sees the Authorization header, so self-host it, keep the allowlist narrow, and do not log request bodies. This is not a public CORS proxy; it is a small piece of infrastructure you own.

After deploying, paste https://your-worker.workers.dev into the page’s Relay field (base only, no /v1/systemone) and press Test connection.

For local development, the same idea in Bun:

const ALLOW = "http://localhost:4321";

Bun.serve({
  port: 8787,
  async fetch(request) {
    const cors = {
      "Access-Control-Allow-Origin": ALLOW,
      "Access-Control-Allow-Headers": "authorization, content-type",
      "Access-Control-Allow-Methods": "GET, POST, OPTIONS"
    };

    if (request.method === "OPTIONS") {
      return new Response(null, { headers: cors });
    }

    const url = new URL(request.url);
    const upstream = await fetch("https://api.typesafe.ai" + url.pathname, {
      method: request.method,
      headers: {
        authorization: request.headers.get("authorization") || "",
        "content-type": request.headers.get("content-type") || "application/json"
      },
      body: request.method === "GET" ? undefined : await request.text()
    });

    return new Response(upstream.body, {
      status: upstream.status,
      headers: {
        ...cors,
        "content-type": upstream.headers.get("content-type") || "application/json"
      }
    });
  }
});

Run it with bun run relay.ts, then paste http://localhost:8787 into the page’s Relay field. This local HTTP relay is for local development: an HTTPS page cannot call a loopback HTTP relay (browsers block the private address space), so use the HTTPS Worker for the deployed site.

Do not use a public CORS proxy for this. It would receive your key in the Authorization header. A relay you control is the only version of this pattern I would ship.

Field notes

  • Model version. jev-latest currently resolves to jev-1.13.0. Jev 1.14.0 is available through jev-preview or its versioned ID. If you tune confidence thresholds, pin the versioned ID and switch deliberately.
  • Language. English is the primary training language. Jev accepts other languages, including CJK, but accuracy is lower. Test on your own content before relying on it.
  • Literal reading. Jev answers the question you wrote, not the one you meant. Put boundary cases in criteria, and split a judgment if it needs several independent factors.
  • Numbers and dates. Keep arithmetic, counting, and date comparisons in code. Use Jev for the extraction or the judgment, not the calculation.
  • Cost. Input is $42 per billion tokens; output tokens are free. A Snake move is a few hundred input tokens, so the gate at ten food points is mostly about discipline, not price.
  • Security. The key in this demo lives in localStorage. Use a scoped key, revoke it when done, and never commit it. The test key used to validate this article is not in the repository.

The point of both exhibits is not that a model can play Snake. It is that a model can return a typed decision at a specific point in a workflow, and the rest of the system can stay ordinary code. That is a much smaller, more testable interface than “generate an answer and hope it parses.”

If you build something with it, the docs are at docs.typesafe.ai, and the TypeSafe Playground is a good place to try a state and a few questions before writing the integration.

Copied to clipboard