Lesson 02 · TypeSafe Jev — System One models

Your first real call (TypeScript)

Now we turn the mental model into working code. By the end you'll have one function — routeTicket() — that sends a ticket to Jev, gets a typed decision back, and either auto-routes it or escalates to a human based on confidence. That escalation logic is the whole reason to reach for a typed decision model.

~10 min Skill Builds on: Lesson 01 · running example = ticket routing
The win A single Node/TypeScript file: POST a ticket → get { choice, probabilities, confidence } → your code decides "confident enough to act, or hand to a human?". The model judges; you decide.

1. Prerequisites (30 seconds)

  1. Node 18+ — it ships a global fetch, so no HTTP library needed. Check with node -v.
  2. An API key from your TypeSafe dashboard. Put it in your shell, never in code:
    export TYPESAFE_API_KEY="sk-..."
  3. A file routeTicket.ts and either ts-node or tsx to run it (npx tsx routeTicket.ts).
Security — the key is a server secret The API key can spend money and read judgements. It lives in a server environment variable, never in a browser bundle or the request body. All of this runs backend-side.

2. Type the wire format first

TypeScript's payoff: describe the request and response once, and every field is checked and autocompleted. This mirrors the shapes from the cheat-sheet.

// jev.ts — types + a tiny client

// ---- Request ----
interface ChoiceQuestion {
  type: "choice";
  instructions: string;
  criteria: Record<string, string>;   // option name -> description
}

interface JevRequest {
  model: string;                        // "jev-latest"
  state: string;                        // the context to judge
  questions: Record<string, ChoiceQuestion>;   // keyed by names you pick
}

// ---- Response ----
interface ChoiceAnswer {
  type: "choice";
  choice: string;                       // the winning option
  probabilities: Record<string, number>;
  confidence: number;                   // 0..1 — gate on this
}

interface JevResponse {
  model: string;
  answers: Record<string, ChoiceAnswer>;
  usage: { input_tokens: number; output_tokens: number };
}

3. The client — one fetch

No SDK required. This is the entire transport layer:

const ENDPOINT = "https://api.typesafe.ai/v1/systemone";

async function callJev(req: JevRequest): Promise<JevResponse> {
  const res = await fetch(ENDPOINT, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.TYPESAFE_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(req),
  });

  if (!res.ok) {
    // Fail loud: bad key, malformed question, rate limit, etc.
    throw new Error(`Jev ${res.status}: ${await res.text()}`);
  }
  return res.json() as Promise<JevResponse>;
}
Why this stays simple There's no streaming, no token loop, no "parse the assistant message". A decision model returns one JSON object and you're done. The complexity you'd normally spend on parsing prose goes to zero.

4. The router — where your logic lives

This is the heart of the lesson. Jev gives you a choice and a confidence. You decide the threshold. Below it, a human looks; above it, the ticket flows automatically.

const CONFIDENCE_THRESHOLD = 0.75;   // your call, tune it to your risk

type Routing =
  | { action: "auto";     team: string; confidence: number }
  | { action: "escalate"; reason: string; top: string; confidence: number };

async function routeTicket(message: string): Promise<Routing> {
  const response = await callJev({
    model: "jev-latest",
    state: message,
    questions: {
      department: {
        type: "choice",
        instructions: "Which team should handle this ticket?",
        criteria: {
          billing:   "Payments, invoicing, refunds",
          technical: "Bugs, outages, integrations",
          sales:     "Pricing, upgrades, new accounts",
        },
      },
    },
  });

  const a = response.answers.department;

  // The gate: trust the model only when it's sure enough.
  if (a.confidence >= CONFIDENCE_THRESHOLD) {
    return { action: "auto", team: a.choice, confidence: a.confidence };
  }
  return {
    action: "escalate",
    reason: "Low confidence — needs a human",
    top: a.choice,
    confidence: a.confidence,
  };
}

5. Run it

const decision = await routeTicket(
  "Help! My payouts have been failing for 3 days."
);
console.log(decision);
// -> { action: "auto", team: "technical", confidence: 0.82 }

const vague = await routeTicket("hey, quick question about my account");
console.log(vague);
// -> { action: "escalate", reason: "Low confidence ...", top: "billing", confidence: 0.41 }

The clear ticket auto-routes; the vague one gets a human. Same model call — the difference is your threshold. That branch is impossible to do cleanly when an LLM hands you a paragraph.

The pattern to internalise decision = model.judge(state), then action = yourCode.decide(decision.confidence). Every real Jev integration is a variation of this: ask a typed question, gate on confidence, branch in code.

6. Two upgrades you'll want next

Want to…Change
Ask more than one thingAdd keys to questions (e.g. a priority Score, an is_urgent Noul). They run in parallel — that's Lesson 03.
Also consider the margin, not just confidenceEscalate when the top two probabilities are close (e.g. 0.44 vs 0.41), even if confidence looks okay.

Check yourself

Retrieval, not recognition — answer before revealing.

Q1. In Node 18+, the client uses:

Q2. The router escalates to a human when:

Q3. The API key belongs:

Q4. The routing decision itself lives:

I'm your teacher — ask me anything. Good next questions: "Build Lesson 03 — combine Choice + Score + Noul in one call", "How do I add the probability-margin check?", or "Give me 3 Jev-shaped problems in a product like mine so I can pick one."

Primary source to read next: docs.typesafe.ai — the Quick Start & patterns pages (you now have the client to try them against).

Reference: Glossary & cheat-sheet · Prev: Lesson 01 · Next: Lesson 03 — composing multiple questions (ask me to build it).