Introduction

Nilsafe is a typed inference primitive. You send it state and a set of named questions; it returns values that fit the shape you declared. Every call. Every time. By construction.

The primitive

Modern software is built on small contracts that refuse to bend. A function returns the type it said it would. A database row has the columns the schema promised. A JSON payload parses, or it does not — there is no in-between where the bytes become suggestions.

Large language models broke that deal. Outputs are free-form prose that happens to look like the thing you wanted, confidence is a vibe, and a hallucination is the price of admission. We are not interested in negotiating that price.

Nilsafe is a stochastic execution primitive that honours the contract. You declare the shape. We return values inside it. The model is the loop, the latency is single-digit millisecond, and the cost is zero. What you wire it into is up to you.

Nilsafe primitives

Every question is one of three types. Each carries its own criteria and produces its own return shape.

Type Goal Returns
choice Pick one of a fixed set of labelled options. choice, probabilities, confidence
score Place input on an ordered rubric of levels. score, legend, probabilities, confidence
noul Estimate a scalar probability in [0, 1]. noul

Atomic questions, composed in code

Each question is small. The interesting work happens when you bundle many of them into a single call, give each one a key, and treat the answer object as a typed struct your code can destructure.

"questions": {
  "department": { "type": "choice", "criteria": { "billing": "…", "technical": "…" } },
  "frustration": { "type": "score", "criteria": ["calm", "angry"] },
  "is_urgent":   { "type": "noul" }
}

Question shape

Every question requires a type. The type determines whether and what shape criteria must take.

type criteria shape
choice required non-empty object, keyed by option id (≤255 keys)
score required non-empty array, ordered rubric levels
noul no criteria

instructions is optional on every type. It is not echoed in the response.

Quickstart

The endpoint accepts a single POST, matches the documented nilsafe request shape, and returns values that conform to it. No auth, no SDK, no model warm-up. Curl from the terminal is the entire getting-started story.

POST /api

Request

{
  "model": "nilsafe-0.0.1",
  "state": "Hi, I've been trying to connect my Stripe account for 3 days and it keeps failing. I'm losing sales. Please help ASAP.",
  "questions": {
    "department": {
      "type": "choice",
      "instructions": "Which team should handle this",
      "criteria": {
        "billing": "Payment or subscription issues",
        "technical": "Bugs or integration problems",
        "sales": "Pricing or account questions"
      }
    },
    "frustration": {
      "type": "score",
      "instructions": "How frustrated the customer appears",
      "criteria": [
        "Frustrated but civil",
        "Calm, just stating facts"
        "Very angry, strong language",
      ]
    },
    "is_urgent": {
      "type": "noul",
      "instructions": "The message conveys urgency or time-sensitivity"
    }
  }
}

Response

{
  "model": "nilsafe-0.0.1",
  "answers": {
    "department": {
      "type": "choice",
      "choice": "billing",
      "probabilities": { "billing": 0.61, "technical": 0.22, "sales": 0.17 },
      "confidence": 0.61
    },
    "frustration": {
      "type": "score",
      "score": 0.05,
      "legend": { "0": "Frustrated but civil", "1": "Very angry", "2": "Calm" },
      "probabilities": { "0": 0.95, "1": 0.05, "2": 0.0 },
      "confidence": 0.95
    },
    "is_urgent": {
      "type": "noul",
      "noul": 0.78
    }
  },
  "usage": { "input_tokens": 0, "output_tokens": 0 }
}

Invariants

The wire format keeps the following guarantees from the nilsafe schema, byte-for-byte:

  • choice is always one of the criteria keys.
  • score is always in [0, n−1]; noul is always in [0, 1].
  • Every key in questions has a matching key in answers.
  • probabilities and legend are keyed the same way. For choice, keys are criterion ids from the request; for score, keys are stringified indices "0".."n−1", ordered by descending mass so the most likely level sits at index "0" and the least likely at index "n−1". The distribution sums to ~1.

Errors

Status Trigger
400 Body is not JSON
400 questions is missing, empty, or not an object
400 state is missing or null
400 A question is not an object
400 A choice question has no criteria or an empty criteria object
400 A score question has no criteria or an empty criteria array
400 A question declares a type other than choice, score, or noul
405 Method is not POST (and not GET / OPTIONS)
413 Body exceeds 1 MB
500 Anything else — we did not plan for it
Heads up. Free tier is 100,000 requests per day. The endpoint enforces a 1 MB body cap and a 10 ms CPU budget. If you exceed either, you get a 413 and a JSON explanation.

Coding agents

Agents need a way to gate before they fire. A tool call is a privilege, not a guess. Nilsafe is the gate: cheap enough to sit on the hot path, fast enough that the agent never notices, typed enough that the policy is a JSON schema, not a prompt.

Tool gating, not tool guessing

The pattern is a thin wrapper around any risky action. Before the agent invokes the tool, it asks nilsafe a single noul question about the planned call. If the answer is above your threshold, the call proceeds. If not, the agent re-plans or escalates. The decision is a float, the policy is a number, and the audit trail is a structured log entry.

// pseudocode
const risk = await nilsafe.noul({
  instructions: "This action will delete a production record."
});

if (risk.noul > 0.8) {
  return escalate(plan);
}
return execute(plan);

Classification, scoring, and extraction as function calls

Most agent work is not generation — it is bookkeeping. Sort this ticket. Score this lead. Extract these fields from this blob. Nilsafe turns every one of those into an ordinary function call with a typed return value, the way a database query or an HTTP request already is. No string parsing downstream. No retry loop when the model decides to be creative.

Routing and triage

At single-digit millisecond latency, nilsafe can sit in front of every inbound message and decide which queue, which handler, which human. The routing decision is a choice with a probability mass you can threshold, log, and replay. The hot path stays hot.

Auditable decisions

Every response carries a structured value with a confidence you can store. When something goes wrong at 3am, the question you ask is not "what did the model feel like doing?" — it is "what value did the schema return?" The latter is a row in a table. The former is a vibes report.

When confidence is too low

A low confidence on a choice is a signal, not a complaint. It means the rubric you declared is too coarse for the input — the runtime cannot pick a winner because the criteria do not separate the cases. Do not lower the threshold. Widen the rubric.

When a choice comes back close to uniform across options, the answer is not "uncertain" — the criteria are. Replace the broad labels with a richer set that actually distinguishes the input space, and the distribution sharpens. Each added criterion gives the runtime more surface area to discriminate, without changing the shape your code already expects.

// before — coarse rubric, flat distribution
"department": {
  "type": "choice",
  "criteria": {
    "billing":   "Payment issues",
    "technical": "Everything else"
  }
}

// after — finer rubric, peaked distribution
"department": {
  "type": "choice",
  "criteria": {
    "billing":  "Payment or subscription issues",
    "technical": "Bugs or integration problems",
    "sales":     "Pricing or account questions"
  }
}

The same shape. The same call. A more discriminating answer. Richness is the lever; the schema stays the contract.

Build the policy in code. The threshold, the rubric, the criteria keys — all of it lives in the request body. Change them without redeploying the model. Change them without a prompt template. Change them the way you change a database query.

Read the manifesto →

Returned values are produced by a stochastic execution primitive operating within the constraints of the schema declared in the originating request. Nilsafe warrants solely that returned values conform to that declared shape. Nilsafe makes no representation, warranty, or covenant, express or implied, as to the correspondence of any returned value with any ground truth, reference standard, downstream expectation, business outcome, or subjective notion of usefulness, and disclaims all such representations to the fullest extent permitted by applicable law. The calling system is solely responsible for the interpretation, evaluation, downstream propagation, and consequences of any returned value, and any reliance upon or integration of any returned value is undertaken at the sole discretion and risk of the calling system. Nilsafe’s total aggregate liability, if any, shall not exceed the amount paid by the calling system for the specific request from which liability is alleged to arise, which amount, for the avoidance of doubt, is zero.