This is the full developer documentation for Mouse Decision API (mouse-1)
# Overview
> mouse-1 scores agent options in one forward pass (~100ms) and returns calibrated probabilities with zero data retention.
`mouse-1` is a decision model built for agent harnesses and AI-powered software. Instead of generating text token by token and parsing prose back into control flow, it reads your `state` and evaluates up to 128 typed `questions` in a single non-autoregressive forward pass—returning a calibrated probability distribution across your options in \~40–100 ms.
Keep control flow, deterministic rules, and side effects in your application code. Call `mouse-1` at narrow decision points where your system needs programmable common sense over unstructured text or structured program state.
## Primitives
[Section titled “Primitives”](#primitives)
Every question in a request uses one of three primitives, and every answer returns a constrained, schema-valid distribution:
| Primitive | Question asks | Answer fields | Maps in code to |
| ------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------- | -------------------- |
| [`choice`](/docs/concepts/questions#choice) | Which of these named options (up to 1,024)? | `choice`, `probabilities`, `confidence`, `mouse` | `switch` / router |
| [`noul`](/docs/concepts/questions#noul) | Is this binary condition true? | `noul` (`0.0` to `1.0`), `mouse` | `if` / gate |
| [`score`](/docs/concepts/questions#score) | Which of these 2–10 ordered rubric levels applies? | `score`, `legend`, `probabilities`, `confidence`, `mouse` | threshold / rank key |
Every question in a single `POST /v1/decisions` (or `POST /v1/systemone`) call is evaluated **in parallel and in isolation** against the same `state`:
* **Speculative fan-out is nearly free:** `state` tokens are billed once per request regardless of how many questions you attach. Ask every question a downstream branch might need and let your code ignore the rest.
* **No cross-question contamination:** Adding or removing a question never shifts another question's probabilities.
* **5-minute prefix caching:** Repeating the same `state` within 5 minutes bills `state_tokens` at the cached rate (**$0.02 / 1M** instead of **$0.10 / 1M**).
* **Calibrated abstention:** Fit a signed `calp1.` profile on 200+ of your own labeled examples via `POST /v1/calibrations` so every answer includes `mouse.abstain` (`true` when top probability falls below the out-of-fold cutoff for your target error rate).
* **Zero data retention by default:** Every request sends `Mouse-Data-Retention: none` and `Cache-Control: no-store`. Request states, questions, answers, and calibration examples are never logged or stored.
## Endpoints
[Section titled “Endpoints”](#endpoints)
Base URL: `https://api.mouse.dev` (US-East)
| Method & path | Description |
| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`POST /v1/decisions`](/docs/api-reference#post-v1systemone) | Evaluate `choice`, `noul` (yes/no), and `score` questions in parallel against a `state` (`POST /v1/systemone` is served as an identical alias for TypeSafe System One SDKs). |
| [`POST /v1/calibrations`](/docs/api-reference#post-v1calibrations) | Fit a signed confidence cutoff profile (`calp1.`) on 200–5,000 labeled examples using 5-fold out-of-fold cross-fitting. |
| [`GET /v1/models`](/docs/api-reference#get-v1models) | List immutable model snapshots (`mouse-preview-2026-10-13`) and active aliases (`mouse-1`, `mouse`). |
| [`GET /healthz`](/docs/api-reference#get-healthz) | Unauthenticated liveness and region probe. |
## Example request
[Section titled “Example request”](#example-request)
This single request routes an agent turn across four tools (`choice`) and checks whether a proposed shell command is safe to run without human approval (`noul`):
```bash
curl https://api.mouse.dev/v1/decisions \
-H "Authorization: Bearer $MOUSE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "mouse-1",
"state": {
"task": "Rename exported function `parseToken` to `verifyToken` in src/auth.ts.",
"proposed_command": "pnpm --filter @mouse/relay test"
},
"questions": {
"next_tool": {
"type": "choice",
"instructions": "Which tool should handle `task` next?",
"criteria": {
"edit_file": "Modify source code in a file",
"grep_search": "Search for symbols across the repository",
"run_tests": "Execute the test suite",
"ask_user": "Ask the user for clarification"
}
},
"safe_to_run": {
"type": "noul",
"instructions": "Is `proposed_command` safe to run without human approval?"
}
}
}'
```
## Example response
[Section titled “Example response”](#example-response)
```json
{
"id": "dec_8f92a1c04e1b902d",
"object": "systemone.response",
"model": "mouse-preview-2026-10-13",
"answers": {
"next_tool": {
"type": "choice",
"choice": "edit_file",
"confidence": 0.88,
"probabilities": {
"edit_file": 0.91,
"grep_search": 0.06,
"run_tests": 0.02,
"ask_user": 0.01
},
"mouse": {
"calibrated": false,
"p_top": 0.91,
"abstain": null
}
},
"safe_to_run": {
"type": "noul",
"noul": 0.98,
"mouse": {
"calibrated": false,
"p_top": 0.98,
"abstain": null
}
}
},
"usage": {
"input_tokens": 74,
"state_tokens": 26,
"question_tokens": 26,
"framing_tokens": 22,
"image_tokens": 0,
"cached_tokens": 0,
"output_tokens": 0,
"cost_usd": 0.0000074
},
"latency_ms": 38.2
}
```
## Next steps
[Section titled “Next steps”](#next-steps)
* **[Quickstart](/docs/quickstart)** — Create an API key and run your first multi-question call in 60 seconds.
* **[Build with coding agents](/docs/agents)** — Copy one prompt to deploy `mouse-1` using Cursor, Claude Code, or Codex, or point your agent at [`/llms-full.txt`](/llms-full.txt) and [`/llm.txt`](/llm.txt).
* **[API Reference](/docs/api-reference)** — Complete request schemas, answer shapes, headers, and status codes.
* **[Question types](/docs/concepts/questions)** — Learn how `choice`, `noul`, and `score` map to control flow and how `confidence` is calculated.
* **[Migrate from Jev or OpenAI Decisions](/docs/guides/migrate)** — Side-by-side comparison and migration recipes.
* **[Console Playground](/console/playground)** — Test states and questions interactively in your browser.
# API reference
> Complete HTTP API reference for mouse-1 — POST /v1/systemone, POST /v1/calibrations, GET /v1/models, and GET /healthz.
Evaluate a `state` against a map of typed `questions` (`choice`, `noul`, `score`) in a single forward pass (\~40–100 ms) and receive structured `answers` with calibrated probability distributions.
* **Base URL:** `https://api.mouse.dev` (US-East)
* **Authentication:** `Authorization: Bearer sk_mouse_...` ([create an API key](/console/keys))
* **Data retention:** Zero data retention by default on every request (`Mouse-Data-Retention: none`, `Cache-Control: no-store`)
* **OpenAPI 3.1 spec:** [`/openapi.json`](/openapi.json) · **Interactive Playground:** [`/console/playground`](/console/playground)
| Method & endpoint | Auth | Description |
| ----------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`POST /v1/decisions`](#post-v1systemone) | `Bearer sk_mouse_...` | Evaluate 1 to 128 typed `choice`, `noul` (yes/no), and `score` questions in parallel against a `state` (`POST /v1/systemone` is served as an identical alias). |
| [`POST /v1/calibrations`](#post-v1calibrations) | `Bearer sk_mouse_...` | Fit a signed confidence cutoff profile (`calp1.`) on 200–5,000 labeled examples using 5-fold out-of-fold cross-fitting. |
| [`GET /v1/models`](#get-v1models) | `Bearer sk_mouse_...` | List immutable model versions (`mouse-preview-2026-10-13`) and current aliases (`mouse-1`, `mouse`). |
| [`GET /healthz`](#get-healthz) | None | Unauthenticated liveness and region probe. |
***
## `POST /v1/systemone`
[Section titled “POST /v1/systemone”](#post-v1systemone)
Evaluate up to 128 typed `questions` in parallel and in isolation against a shared `state`.
```http
POST https://api.mouse.dev/v1/systemone
Authorization: Bearer
Content-Type: application/json
```
### Request body
[Section titled “Request body”](#request-body)
| Property | Type | Required | Default | Description |
| ----------------- | ------------------------------------------------ | ------------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `state` | `string \| object \| array \| number \| boolean` | **Required** | — | The content to evaluate (up to **65,536 tokens**). Pass a string for a single message or passage, or a JSON object/array for structured application state, tool proposals, and records. Never truncated; states over 65,536 tokens return `422 state_too_long`. |
| `questions` | `Record` | **Required** | — | Non-empty map of 1 to **128** question IDs you choose to typed [`ChoiceQuestion`](#choice-question), [`NoulQuestion`](#noul-question), or [`ScoreQuestion`](#score-question) objects. Question IDs are returned as keys in `answers` and are not sent to the model during inference. |
| `model` | `string` | Optional | `"mouse-1"` | Model alias (`"mouse-1"`, `"mouse"`) or immutable snapshot ID (`"mouse-preview-2026-10-13"`). |
| `calibration` | `string \| null` | Optional | `null` | Optional signed calibration profile token (`"calp1.."`) returned by [`POST /v1/calibrations`](#post-v1calibrations). |
| `order_invariant` | `boolean` | Optional | `false` | Score every multi-option `choice` question a second time with its options reversed and average the two distributions, removing option-order sensitivity. The reversed questions are billed again (their `question_tokens` and `framing_tokens` roughly double); the `state` prefix is shared and billed once. `noul` and `score` are never permuted. |
Unknown top-level fields (for example `"temperature"`) are ignored and listed in the response's `warnings` array. Unknown fields inside a question object are refused with `422 invalid_schema`.
#### Accepted aliases
[Section titled “Accepted aliases”](#accepted-aliases)
So that a migration is a base-URL change, the request parser also accepts, and normalizes before validation:
| You send | Read as |
| ---------------------------------------------------------- | ------------------------------------------- |
| `input` | `state` |
| `questions` as an array of objects carrying `id` or `name` | `questions` map keyed by that `id`/`name` |
| `type: "predicate"` or `type: "boolean"` | `type: "noul"` |
| `text` | `instructions` |
| `options: ["a", "b"]` | `criteria: { "a": null, "b": null }` |
| `choices: [{ value, description }]` | `criteria: { [value]: description }` |
| `levels: [{ label, description }]` | `criteria: [": ", ...]` |
Aliases change only how the request is read; the response is always the Mouse shape documented below.
#### Example request (`cURL`, `TypeScript`, `Python`)
[Section titled “Example request (cURL, TypeScript, Python)”](#example-request-curl-typescript-python)
```bash
curl https://api.mouse.dev/v1/systemone \
-H "Authorization: Bearer $MOUSE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "mouse-1",
"state": {
"task": "Rename exported function `parseToken` to `verifyToken` in src/auth.ts.",
"proposed_command": "pnpm --filter @mouse/relay test"
},
"questions": {
"next_tool": {
"type": "choice",
"instructions": "Which tool should handle `task` next?",
"criteria": {
"edit_file": "Modify source code in a file",
"grep_search": "Search for symbols across the repository",
"run_tests": "Execute the test suite",
"ask_user": "Ask the user for clarification"
}
},
"safe_to_run": {
"type": "noul",
"instructions": "Is `proposed_command` safe to run without human approval?"
},
"blast_radius": {
"type": "score",
"instructions": "What is the impact of `proposed_command`?",
"criteria": [
"Read-only inspection or local test execution",
"Reversible local workspace edit",
"Irreversible deletion, remote git push, or external mutation"
]
}
}
}'
```
```ts
const res = await fetch("https://api.mouse.dev/v1/systemone", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MOUSE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "mouse-1",
state: {
task: "Rename exported function `parseToken` to `verifyToken` in src/auth.ts.",
proposed_command: "pnpm --filter @mouse/relay test",
},
questions: {
next_tool: {
type: "choice",
instructions: "Which tool should handle `task` next?",
criteria: {
edit_file: "Modify source code in a file",
grep_search: "Search for symbols across the repository",
run_tests: "Execute the test suite",
ask_user: "Ask the user for clarification",
},
},
safe_to_run: {
type: "noul",
instructions: "Is `proposed_command` safe to run without human approval?",
},
blast_radius: {
type: "score",
instructions: "What is the impact of `proposed_command`?",
criteria: [
"Read-only inspection or local test execution",
"Reversible local workspace edit",
"Irreversible deletion, remote git push, or external mutation",
],
},
},
}),
});
const data = await res.json();
console.log(data.answers.next_tool.choice, data.answers.safe_to_run.noul);
```
```python
import os
import requests
res = requests.post(
"https://api.mouse.dev/v1/systemone",
headers={"Authorization": f"Bearer {os.environ['MOUSE_API_KEY']}"},
json={
"model": "mouse-1",
"state": {
"task": "Rename exported function `parseToken` to `verifyToken` in src/auth.ts.",
"proposed_command": "pnpm --filter @mouse/relay test",
},
"questions": {
"next_tool": {
"type": "choice",
"instructions": "Which tool should handle `task` next?",
"criteria": {
"edit_file": "Modify source code in a file",
"grep_search": "Search for symbols across the repository",
"run_tests": "Execute the test suite",
"ask_user": "Ask the user for clarification",
},
},
"safe_to_run": {
"type": "noul",
"instructions": "Is `proposed_command` safe to run without human approval?",
},
"blast_radius": {
"type": "score",
"instructions": "What is the impact of `proposed_command`?",
"criteria": [
"Read-only inspection or local test execution",
"Reversible local workspace edit",
"Irreversible deletion, remote git push, or external mutation",
],
},
},
},
timeout=5.0,
)
print(res.json()["answers"])
```
***
### Question types
[Section titled “Question types”](#question-types)
A `Question` is one of three primitives determined by its `type` field (`"choice"`, `"noul"`, or `"score"`).
All three primitives accept `instructions` as a `string`, a structured `object`, an `array`, or `null`. Point a question at a specific nested field inside `state` (or inside a structured `instructions` object) using backticks, such as ``"Does `ticket.messages[0].text` request a refund?"``.
#### `choice` question
[Section titled “choice question”](#choice-question)
Picks one option from a set of 1 to **1,024** named options and returns the full probability distribution.
| Property | Type | Required | Description |
| -------------- | --------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `"choice"` | **Required** | Always `"choice"`. |
| `criteria` | `Record` | **Required** | Map of option keys to rubric descriptions. Use `null` when an option key needs no extra detail. Passing an array instead of an object returns `422 invalid_schema`. |
| `instructions` | `string \| object \| array \| null` | Optional | What the model should decide. |
```json
{
"department": {
"type": "choice",
"instructions": "Which team should handle this ticket?",
"criteria": {
"billing": "Payments, invoices, duplicate charges, or refunds",
"technical": "Bugs, outages, or API integration problems",
"sales": "Pricing, upgrades, or new accounts"
}
}
}
```
#### `noul` question
[Section titled “noul question”](#noul-question)
Evaluates a yes/no predicate and returns the probability (`0.0` to `1.0`) that the statement is `true` (yes).
| Property | Type | Required | Description |
| -------------- | --------------------------------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `"noul"` | **Required** | Always `"noul"`. |
| `instructions` | `string \| object \| array \| null` | Optional | The yes/no question or statement to evaluate against `state`. Phrase positively so a value near `1.0` means yes. |
| `criteria` | `{ "true"?: JSONContent, "false"?: JSONContent } \| null` | Optional | Optional descriptions clarifying what `"true"` (yes, near `1.0`) and `"false"` (no, near `0.0`) mean. Only `"true"` and `"false"` keys are allowed. |
```json
{
"is_urgent": {
"type": "noul",
"instructions": "Does this message convey time-sensitive urgency?",
"criteria": {
"true": "Production outage, blocked customers, or explicit deadline",
"false": "Routine question, feature request, or non-blocking issue"
}
}
}
```
#### `score` question
[Section titled “score question”](#score-question)
Rates the state along an ordered rubric of **2 to 10** concrete levels (`0` to `N - 1`).
| Property | Type | Required | Description |
| -------------- | ----------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `type` | `"score"` | **Required** | Always `"score"`. |
| `criteria` | `Array` | **Required** | Ordered array of 2 to 10 level descriptions from lowest (`0`) to highest (`N - 1`). Describe concrete situations rather than bare numbers. |
| `instructions` | `string \| object \| array \| null` | Optional | What dimension the model should rate. |
```json
{
"frustration": {
"type": "score",
"instructions": "How frustrated does the customer appear?",
"criteria": [
"Calm, stating facts",
"Frustrated but civil",
"Very angry or threatening to leave"
]
}
}
```
***
### Response body (`POST /v1/systemone`)
[Section titled “Response body (POST /v1/systemone)”](#response-body-post-v1systemone)
| Property | Type | Description |
| ------------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | `string` | Unique decision identifier (`"dec_..."`). |
| `object` | `"systemone.response"` | Fixed object type string. |
| `model` | `string` | Exact immutable model snapshot ID that evaluated the request (`"mouse-preview-2026-10-13"`). Also echoed in the `Mouse-Model-Version` HTTP header. |
| `answers` | `Record` | One typed [`ChoiceAnswer`](#choice-answer), [`NoulAnswer`](#noul-answer), or [`ScoreAnswer`](#score-answer) per question, keyed by the same IDs you passed in `questions`. |
| `usage` | [`Usage`](#usage-object) | Itemized token counts and exact USD cost for the request. |
| `latency_ms` | `number` | End-to-end server evaluation latency in milliseconds. |
| `warnings` | `string[]` (optional) | Present only when unknown top-level request fields were ignored. |
***
### Answer types
[Section titled “Answer types”](#answer-types)
Every answer carries a `type` matching its question, the full `probabilities` distribution, the raw `logits` behind it (`softmax(logits) == probabilities`), and a `mouse` metadata object (`{ calibrated, p_top, abstain, confidence_winning, confidence_normalized }`). `choice` and `score` answers also carry a top-level `confidence` (`0.0` to `1.0`).
Log `logits` and `model` together if you fit thresholds or calibrators offline; `probabilities` alone lose the scale.
#### `choice` answer
[Section titled “choice answer”](#choice-answer)
| Property | Type | Description |
| --------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `"choice"` | Always `"choice"`. |
| `choice` | `string` | The highest-probability option key from `criteria`. |
| `probabilities` | `Record` | Every option key mapped to its probability (`0.0` to `1.0`, summing to `1.0`). |
| `logits` | `Record` | Every option key mapped to its raw log-score. |
| `confidence` | `number` | Normalized concentration score in `0.0` to `1.0`: ((p\_{\max} - 1/k) / (1 - 1/k)) for (k) options (`0.0` for an even split, `1.0` when all probability is on one option). |
| `mouse` | [`MouseAnswerMeta`](#mouseanswermeta-object) | `{ calibrated, p_top, abstain, confidence_winning, confidence_normalized }`. |
```json
{
"type": "choice",
"choice": "edit_file",
"confidence": 0.88,
"probabilities": {
"edit_file": 0.91,
"grep_search": 0.06,
"run_tests": 0.02,
"ask_user": 0.01
},
"logits": {
"edit_file": 2.41,
"grep_search": -0.31,
"run_tests": -1.41,
"ask_user": -2.1
},
"mouse": {
"calibrated": false,
"p_top": 0.91,
"abstain": null,
"confidence_winning": 0.91,
"confidence_normalized": 0.88
}
}
```
#### `noul` answer
[Section titled “noul answer”](#noul-answer)
| Property | Type | Description |
| --------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `type` | `"noul"` | Always `"noul"`. |
| `noul` | `number` | The yes/no probability on a scale from `0.0` (`false` / no) to `1.0` (`true` / yes). A value near `0.5` indicates equal probability for yes and no (certainty is (\|2 \cdot \text{noul} - 1\|), also returned as `mouse.confidence_normalized`). |
| `probabilities` | `{ "false": number, "true": number }` | The same distribution in the shape `choice` uses: `{ "false": 1 - noul, "true": noul }`. |
| `logits` | `{ "false": number, "true": number }` | Raw log-scores for each side. |
| `mouse` | [`MouseAnswerMeta`](#mouseanswermeta-object) | `{ calibrated, p_top, abstain, confidence_winning, confidence_normalized }`, where `p_top = max(noul, 1 - noul)`. |
```json
{
"type": "noul",
"noul": 0.98,
"probabilities": { "false": 0.02, "true": 0.98 },
"logits": { "false": -1.95, "true": 1.94 },
"mouse": {
"calibrated": false,
"p_top": 0.98,
"abstain": null,
"confidence_winning": 0.98,
"confidence_normalized": 0.96
}
}
```
#### `score` answer
[Section titled “score answer”](#score-answer)
| Property | Type | Description |
| --------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `"score"` | Always `"score"`. |
| `score` | `number` | Probability-weighted mean level index: (\sum\_{i=0}^{k-1} i \cdot p\_i), landing continuously between `0.0` and `k - 1`. |
| `legend` | `Record` | Each string level index (`"0"`, `"1"`, …) mapped back to its description from `criteria`. |
| `probabilities` | `Record` | Each string level index (`"0"`, `"1"`, …) mapped to its probability (summing to `1.0`). |
| `logits` | `Record` | Each string level index mapped to its raw log-score. |
| `confidence` | `number` | Ordinal concentration score in `0.0` to `1.0`: (\max(0, 1 - \sum\_i p\_i \|i - m\| / \text{MAD}\_{\text{unif}})), where (m) is the most likely level. |
| `mouse` | [`MouseAnswerMeta`](#mouseanswermeta-object) | `{ calibrated, p_top, abstain, confidence_winning, confidence_normalized }`. |
```json
{
"type": "score",
"score": 0.04,
"confidence": 0.94,
"legend": {
"0": "Read-only inspection or local test execution",
"1": "Reversible local workspace edit",
"2": "Irreversible deletion, remote git push, or external mutation"
},
"probabilities": {
"0": 0.96,
"1": 0.04,
"2": 0.0
},
"logits": {
"0": 3.18,
"1": 0.0,
"2": -6.9
},
"mouse": {
"calibrated": false,
"p_top": 0.96,
"abstain": null,
"confidence_winning": 0.96,
"confidence_normalized": 0.94
}
}
```
#### `MouseAnswerMeta` object
[Section titled “MouseAnswerMeta object”](#mouseanswermeta-object)
| Property | Type | Description |
| ----------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `calibrated` | `boolean` | `true` when a `calibration` profile was passed on the request and matched this question's signature. When `true`, `probabilities` (and every field derived from them) are temperature-scaled on your labels. |
| `p_top` | `number` | Top outcome probability ((\max\_i p\_i)). |
| `abstain` | `boolean \| null` | `null` when uncalibrated; `true` when `p_top` is below the profile's fitted cutoff (signaling your code to escalate or fall back); `false` when `p_top >= cutoff`. |
| `confidence_winning` | `number` | The winning-probability convention ((\max\_i p\_i)), as Cloudflare Clef reports `confidence`. Equal to `p_top`; named so a Clef threshold ports without arithmetic. |
| `confidence_normalized` | `number` | The normalized convention ((p\_{\max} - 1/k) / (1 - 1/k)), as TypeSafe reports `confidence`, on every answer type including `noul`. |
Both conventions are present because the same distribution reads differently under each: `(0.80, 0.15, 0.05)` is `0.80` winning and `0.70` normalized, so a `0.75` threshold flips depending on which one a vendor calls `confidence`. Threshold on the convention your labels were fitted against, or on the full `probabilities`.
#### `Usage` object
[Section titled “Usage object”](#usage-object)
| Property | Type | Description |
| ----------------- | --------- | -------------------------------------------------------------------------------------------------------------- |
| `input_tokens` | `integer` | Total billed input tokens: `state_tokens + question_tokens + framing_tokens + image_tokens`. |
| `state_tokens` | `integer` | Tokens in `state` (counted once per request regardless of how many questions are evaluated). |
| `question_tokens` | `integer` | Tokens across all question `instructions` and `criteria`. |
| `framing_tokens` | `integer` | Structural delimiter tokens: `4` per request + `3` per question + `2` per option (`2` options per `noul`). |
| `image_tokens` | `integer` | `0` for text/JSON requests. |
| `cached_tokens` | `integer` | `state_tokens` served from the 5-minute in-memory prefix cache (`$0.02 / 1M` instead of `$0.10 / 1M`). |
| `output_tokens` | `integer` | Always `0` (output is free). |
| `cost_usd` | `number` | Exact USD cost for the request (`((input_tokens - cached_tokens) * 0.10 + cached_tokens * 0.02) / 1_000_000`). |
***
## `POST /v1/calibrations`
[Section titled “POST /v1/calibrations”](#post-v1calibrations)
Score 200 to 5,000 labeled examples and fit a signed confidence cutoff profile (`calp1..`) using 5-fold out-of-fold cross-fitting. Examples are evaluated in memory and discarded immediately (`Mouse-Data-Retention: none`).
```http
POST https://api.mouse.dev/v1/calibrations
Authorization: Bearer
Content-Type: application/json
```
### Request body
[Section titled “Request body”](#request-body-1)
| Property | Type | Required | Default | Description |
| -------------- | -------------- | ------------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `examples` | `CalExample[]` | **Required** | — | Array of **200 to 5,000** labeled examples. Each distinct question signature requires **50+** labeled examples to be fitted. |
| `model` | `string` | Optional | `"mouse-1"` | Model alias or snapshot ID. The returned `profile` is cryptographically bound to the resolved snapshot ID. |
| `target_error` | `number` | Optional | `0.05` | Maximum acceptable error rate (`0.0 < target_error < 0.5`) among non-abstained predictions. |
Each item in `examples` contains `state` and `questions`, where each question includes its ground-truth `label`:
* **Labeled `choice` (`CalChoice`):** `label` is a `string` matching one of the option keys in `criteria`.
* **Labeled `noul` (`CalNoul`):** `label` is a `boolean` (`true` or `false`).
* **Labeled `score` (`CalScore`):** `label` is an `integer` level index in `0 .. len(criteria) - 1`.
```bash
curl https://api.mouse.dev/v1/calibrations \
-H "Authorization: Bearer $MOUSE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "mouse-1",
"target_error": 0.05,
"examples": [
{
"state": "Customer was charged twice for order #1042.",
"questions": {
"intent": {
"type": "choice",
"instructions": "What does the customer need?",
"criteria": {
"billing": "A charge, invoice, or refund issue",
"shipping": "Delivery status or tracking",
"account": "Login or profile settings"
},
"label": "billing"
}
}
}
]
}'
```
### Response body (`POST /v1/calibrations`)
[Section titled “Response body (POST /v1/calibrations)”](#response-body-post-v1calibrations)
| Property | Type | Description |
| -------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `object` | `"calibration"` | Fixed object type string. |
| `model` | `string` | Immutable snapshot ID this profile was fitted against (`"mouse-preview-2026-10-13"`). |
| `target_error` | `number` | Target error rate used to select the abstention cutoff. |
| `profile` | `string` | HMAC-SHA256 signed profile token (`"calp1.."`). Pass as `calibration` on `POST /v1/systemone`. |
| `n_examples` | `integer` | Total labeled examples processed. |
| `questions` | `CalibrationQuestionReport[]` | Per-question-signature report (`signature`, `type`, `question_ids`, `status`, `before`, and `after` metrics including the fitted `temperature`, `cutoff`, and 5-fold out-of-fold `coverage_at_target_error` and `error_when_covered`). |
| `note` | `string` | Out-of-fold methodology and zero-data-retention note. |
| `usage` | [`Usage`](#usage-object) | Itemized token counts and USD cost for scoring the calibration examples. |
```json
{
"object": "calibration",
"model": "mouse-preview-2026-10-13",
"target_error": 0.05,
"profile": "calp1.k1.eyJmaXRzIjpb...",
"n_examples": 250,
"questions": [
{
"signature": "4f8c12a90e3b7104",
"type": "choice",
"question_ids": ["intent"],
"status": "fitted",
"before": {
"accuracy": 0.912,
"ece": 0.031,
"brier": 0.142,
"nll": 0.264,
"mean_confidence": 0.938,
"coverage": 1.0,
"cutoff": null,
"n": 250
},
"after": {
"accuracy": 0.912,
"ece": 0.031,
"brier": 0.142,
"nll": 0.264,
"mean_confidence": 0.938,
"coverage": 0.864,
"cutoff": 0.78,
"n": 250,
"coverage_at_target_error": 0.856,
"error_when_covered": 0.0421
}
}
],
"note": "'coverage_at_target_error' and 'error_when_covered' are 5-fold out-of-fold. Examples were not stored.",
"usage": {
"input_tokens": 14500,
"state_tokens": 3500,
"question_tokens": 7750,
"framing_tokens": 3250,
"image_tokens": 0,
"cached_tokens": 0,
"output_tokens": 0,
"cost_usd": 0.00145
}
}
```
***
## `GET /v1/models`
[Section titled “GET /v1/models”](#get-v1models)
List active immutable model versions and alias mappings (`mouse-1`, `mouse`).
```bash
curl https://api.mouse.dev/v1/models \
-H "Authorization: Bearer $MOUSE_API_KEY"
```
```json
{
"object": "list",
"alias": {
"mouse-1": "mouse-preview-2026-10-13",
"mouse": "mouse-preview-2026-10-13"
},
"data": [
{
"id": "mouse-preview-2026-10-13",
"released": "2026-10-13",
"status": "preview",
"base": "mouse-1",
"max_state_tokens": 65536,
"modalities": ["text"],
"retire_after": null
}
],
"today": "2026-10-14"
}
```
***
## `GET /healthz`
[Section titled “GET /healthz”](#get-healthz)
Unauthenticated liveness probe returning serving region and engine status.
```bash
curl https://api.mouse.dev/healthz
```
```json
{
"ok": true,
"region": "us",
"engine": "modal"
}
```
***
## Response headers
[Section titled “Response headers”](#response-headers)
Every response from `https://api.mouse.dev` includes these HTTP headers:
| Header | Example | Description |
| -------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------- |
| `Mouse-Request-Id` | `req_8f92a1c04e1b902d73c104ab` | Unique request identifier (`req_` + 24 hex chars). |
| `Mouse-Model-Version` | `mouse-preview-2026-10-13` | Exact immutable snapshot ID that scored the request (`POST /v1/systemone` and `POST /v1/calibrations`). |
| `Mouse-Region` | `us` | Serving region (`us`). |
| `Mouse-Data-Retention` | `none` | Enforced zero data retention on every response. |
| `Cache-Control` | `no-store` | Prevents intermediary caching. |
| `x-ratelimit-limit-requests` | `200` | Sustained requests-per-second limit (`200` standard, `5` playground). |
| `x-ratelimit-remaining-requests` | `399` | Remaining requests in the burst bucket (`400` capacity). |
| `x-ratelimit-reset-requests` | `1` | Seconds until the request bucket resets (`1`). |
| `x-ratelimit-limit-tokens` | `20000000` | Input-tokens-per-minute limit (`20,000,000`). |
| `x-ratelimit-remaining-tokens` | `19999926` | Remaining input tokens in the 60-second bucket. |
| `x-ratelimit-reset-tokens` | `60` | Seconds until the token bucket resets (`60`). |
| `Retry-After` | `2` | Present on `429` and `503` responses; integer seconds to wait before retrying. |
***
## Errors
[Section titled “Errors”](#errors)
All non-2xx responses use a single JSON error envelope. Messages name the limit or offending field and never echo request content:
```json
{
"error": {
"type": "invalid_request_error",
"code": "state_too_long",
"message": "state is 71,204 tokens; mouse-preview-2026-10-13 reads at most 65,536. Nothing was truncated or billed.",
"param": "state"
}
}
```
| Status | `error.type` | `error.code` | Meaning |
| ------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| `400` | `invalid_request_error` | `invalid_calibration_profile` | Malformed, altered, or retired `calibration` profile token. |
| `401` | `authentication_error` | `missing_api_key`, `invalid_api_key` | Missing `Authorization: Bearer sk_mouse_...` header, bad CRC32 checksum, or revoked/expired key. |
| `402` | `billing_error` | `insufficient_credits` | Prepaid credit balance is `$0.00`. Add credits at [`/console/billing`](/console/billing). |
| `404` | `invalid_request_error` | `model_not_found` | Unknown `model` name. See [`GET /v1/models`](#get-v1models). |
| `409` | `invalid_request_error` | `profile_model_mismatch` | `calibration` profile was fitted on a different model snapshot than the one serving the request. |
| `413` | `invalid_request_error` | `request_too_large` | Request body exceeds **4,000,000 bytes** (4 MB). |
| `422` | `invalid_request_error` | `invalid_schema`, `state_too_long`, `too_many_questions`, `too_many_options`, `too_few_examples`, `too_many_examples`, `too_few_examples_per_question` | Request failed schema validation or exceeded a published payload limit. Never billed. |
| `429` | `rate_limit_error` | `rate_limited`, `token_rate_limited` | Exceeded **200 RPS** (burst 400) or **20M TPM**. Honor the `Retry-After` header. |
| `503` | `api_error` | `engine_timeout` | Inference worker timed out (`10s`). Honor the `Retry-After` header. |
# Agent harness integration
> Save up to 65x on decision tokens and cut loop latency by wiring mouse-1 into Cursor, Claude Code, OpenCode, or your own agent harness.
Agent harnesses do not just generate code—they make dozens of narrow control-flow decisions on every turn: **which tool or skill to load**, **whether a shell command is safe to auto-approve**, **whether untrusted output contains a prompt injection**, and **whether test logs actually verify the claim**.
Calling a generative model (`Claude Haiku 4.5`, `Sonnet 5`, `GPT-6 Sol`) for those checks costs **$1.25 to $6.50 per 1,000 decisions** and adds **500–2,000 ms** of token decoding on every step. `mouse-1` scores up to 128 typed questions against your `state` in a single forward pass in **\~40–100 ms** for **$0.10 / 1M input tokens** (**$0.02 / 1M cached**, **$0 output**).
## Where `mouse-1` saves money in an agent loop
[Section titled “Where mouse-1 saves money in an agent loop”](#where-mouse-1-saves-money-in-an-agent-loop)
| Harness decision point | What `mouse-1` replaces | How it saves money & latency |
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **1. Tool & skill routing** ([guide](/docs/guides/tool-routing)) | Stuffing 30–100 MCP tool or skill definitions into every frontier model turn | Score all candidate tools with one `choice` question (`~$0.00001`), then load only the top 1–3 tools into your frontier model—cutting main-model input tokens by **40–70%**. |
| **2. Action gating & auto-approval** ([guide](/docs/guides/gating)) | Interrupting the human on every `Bash` / `Write` call, or paying a chat LLM to judge safety | Evaluate `safe` and `destructive` `noul` predicates in \~40 ms. Safe local commands run unblocked; destructive commands return `deny` with a reason so the agent re-plans immediately. |
| **3. Post-tool verification & stop check** ([guide](/docs/guides/verification)) | Blindly trusting `"done"` claims or burning 3–5 frontier turns repeating a broken fix | Score `{ goal, diff, test_log }` at turn end. Catching a contradicted claim or repeated failure early saves **$0.50–$2.00** in wasted frontier model retries. |
| **4. Prompt-injection guard** ([guide](/docs/guides/injection)) | Exposed control flow when reading untrusted GitHub issues, web pages, or logs | `mouse-1` treats `state` strictly as data (**0% decision flip rate** on planted instructions vs **65.4%** on TypeSafe Jev). |
***
## 1. Drop-in hooks for coding agents (zero prompt changes)
[Section titled “1. Drop-in hooks for coding agents (zero prompt changes)”](#1-drop-in-hooks-for-coding-agents-zero-prompt-changes)
If you use **Cursor**, **Claude Code**, or **OpenCode**, you can wire `mouse-1` into the harness's native lifecycle hooks in under 60 lines. Your system prompt and main model stay untouched:
* **`allow`** → executes immediately without interrupting you.
* **`deny` + reason** → blocks the tool call and feeds the reason back to the coding agent so it re-plans on its own.
* **`ask`** → falls back to the native confirmation prompt when confidence is in the middle band.
### Cursor (`.cursor/hooks.json`)
[Section titled “Cursor (.cursor/hooks.json)”](#cursor-cursorhooksjson)
Cursor supports project-level command hooks on `beforeShellExecution`, `preToolUse`, `postToolUse`, and `stop`.
Create `.cursor/hooks.json`:
```json
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{
"command": ".cursor/hooks/mouse-gate.py",
"failClosed": true
}
]
}
}
```
Create `.cursor/hooks/mouse-gate.py` and make it executable (`chmod +x .cursor/hooks/mouse-gate.py`):
```python
#!/usr/bin/env python3
"""Cursor beforeShellExecution gate powered by mouse-1."""
import json, os, sys, urllib.request
TAU_SAFE = float(os.environ.get("MOUSE_TAU_SAFE", "0.85"))
TAU_DESTRUCTIVE = float(os.environ.get("MOUSE_TAU_DESTRUCTIVE", "0.50"))
def decide(state: dict, questions: dict) -> dict | None:
key = os.environ.get("MOUSE_API_KEY")
if not key:
return None
req = urllib.request.Request(
"https://api.mouse.dev/v1/decisions",
data=json.dumps({"model": "mouse-1", "state": state, "questions": questions}).encode(),
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
)
try:
with urllib.request.urlopen(req, timeout=2.5) as res:
return json.loads(res.read())["answers"]
except Exception:
return None
ev = json.load(sys.stdin)
ans = decide(
{"command": ev.get("command", ""), "cwd": ev.get("cwd")},
{
"safe": {
"type": "noul",
"instructions": "Is this shell command safe to run automatically without asking the user?",
},
"destructive": {
"type": "noul",
"instructions": "Does this command delete data, force-push, or exfiltrate secrets?",
},
},
)
if ans is None:
out = {"permission": "ask", "user_message": "mouse-1 unreachable; review command."}
elif ans["destructive"]["noul"] >= TAU_DESTRUCTIVE:
p = ans["destructive"]["noul"]
out = {
"permission": "deny",
"user_message": f"Blocked destructive command (p={p:.2f}).",
"agent_message": f"mouse-1 blocked this command as likely destructive (p={p:.2f}). Use a safer, non-destructive alternative.",
}
elif ans["safe"]["noul"] >= TAU_SAFE:
out = {"permission": "allow"}
else:
p = ans["safe"]["noul"]
out = {
"permission": "ask",
"user_message": f"mouse-1 confidence below auto-approve threshold (p={p:.2f}).",
}
print(json.dumps(out))
```
### Claude Code (`.claude/settings.json`)
[Section titled “Claude Code (.claude/settings.json)”](#claude-code-claudesettingsjson)
Claude Code fires `PreToolUse` before tool calls and reads `hookSpecificOutput.permissionDecision` (`"allow" | "deny" | "ask"`) plus `permissionDecisionReason` from `stdout`.
Add to `.claude/settings.json`:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Edit|Write",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/mouse-gate.py"
}
]
}
]
}
}
```
Save `.claude/hooks/mouse-gate.py` (`chmod +x .claude/hooks/mouse-gate.py`):
```python
#!/usr/bin/env python3
"""Claude Code PreToolUse gate powered by mouse-1."""
import json, os, sys, urllib.request
TAU_SAFE = float(os.environ.get("MOUSE_TAU_SAFE", "0.85"))
TAU_DESTRUCTIVE = float(os.environ.get("MOUSE_TAU_DESTRUCTIVE", "0.50"))
def decide(state: dict, questions: dict) -> dict | None:
key = os.environ.get("MOUSE_API_KEY")
if not key:
return None
req = urllib.request.Request(
"https://api.mouse.dev/v1/decisions",
data=json.dumps({"model": "mouse-1", "state": state, "questions": questions}).encode(),
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
)
try:
with urllib.request.urlopen(req, timeout=2.5) as res:
return json.loads(res.read())["answers"]
except Exception:
return None
ev = json.load(sys.stdin)
ans = decide(
{"tool": ev.get("tool_name"), "input": ev.get("tool_input"), "cwd": ev.get("cwd")},
{
"safe": {
"type": "noul",
"instructions": "Can this tool call run automatically without asking the user?",
},
"destructive": {
"type": "noul",
"instructions": "Does this delete, overwrite, or exfiltrate sensitive data?",
},
},
)
if ans is None:
decision, why = "ask", "mouse-1 unreachable; falling back to user prompt"
elif ans["destructive"]["noul"] >= TAU_DESTRUCTIVE:
decision, why = (
"deny",
f"mouse-1: likely destructive (p={ans['destructive']['noul']:.2f}); re-plan with a safe command",
)
elif ans["safe"]["noul"] >= TAU_SAFE:
decision, why = "allow", f"mouse-1: auto-approved (p={ans['safe']['noul']:.2f})"
else:
decision, why = "ask", f"mouse-1: requires confirmation (p={ans['safe']['noul']:.2f})"
print(
json.dumps(
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": decision,
"permissionDecisionReason": why,
}
}
)
)
```
### OpenCode (`.opencode/plugins/mouse-gate.ts`)
[Section titled “OpenCode (.opencode/plugins/mouse-gate.ts)”](#opencode-opencodepluginsmouse-gatets)
OpenCode plugins hook `tool.execute.before` (throwing an error blocks the call and returns the message to the agent) and `tool.execute.after` (mutating `output.output` appends verification feedback):
.opencode/plugins/mouse-gate.ts
```ts
const TAU_SAFE = Number(process.env.MOUSE_TAU_SAFE ?? 0.85);
const TAU_DESTRUCTIVE = Number(process.env.MOUSE_TAU_DESTRUCTIVE ?? 0.50);
async function decide(state: unknown, questions: Record) {
const key = process.env.MOUSE_API_KEY;
if (!key) return null;
try {
const res = await fetch("https://api.mouse.dev/v1/decisions", {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ model: "mouse-1", state, questions }),
signal: AbortSignal.timeout(2500),
});
return res.ok ? ((await res.json()) as any).answers : null;
} catch {
return null;
}
}
export const MouseGatePlugin = async () => ({
"tool.execute.before": async (input: { tool: string }, output: { args?: unknown }) => {
const ans = await decide(
{ tool: input.tool, args: output.args ?? {} },
{
safe: { type: "noul", instructions: "Can this tool call run without asking the user?" },
destructive: {
type: "noul",
instructions: "Does this delete, overwrite, or exfiltrate data?",
},
},
);
if (!ans) return;
if (ans.destructive.noul >= TAU_DESTRUCTIVE) {
throw new Error(
`mouse-1 blocked ${input.tool} (p=${ans.destructive.noul.toFixed(2)}). Re-plan with a safer command.`,
);
}
},
"tool.execute.after": async (
input: { tool: string; args?: unknown },
output: { output?: string },
) => {
if (input.tool !== "bash") return;
const ans = await decide(
{ tool: input.tool, args: input.args, output_tail: String(output.output ?? "").slice(-2000) },
{
failed: {
type: "noul",
instructions: "Did the command fail or contradict the expected outcome?",
},
},
);
if (ans && ans.failed.noul >= TAU_SAFE) {
output.output = `${output.output ?? ""}\nCommand output indicates failure (p=${ans.failed.noul.toFixed(2)}). Fix the root cause before repeating. `;
}
},
});
```
***
## 2. Native integration in your own agent harness
[Section titled “2. Native integration in your own agent harness”](#2-native-integration-in-your-own-agent-harness)
When you own the agent loop (TypeScript, Python, LangGraph, Vercel AI SDK, or OpenAI Agents SDK), evaluate every decision point for a step in **one batched request** against the same `state`:
```ts
const res = await fetch("https://api.mouse.dev/v1/decisions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MOUSE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "mouse-1",
// Put shared rubrics/schemas first so repeated turns hit the $0.02/1M prefix cache
state: {
task: userPrompt,
git_status: gitStatus,
last_tool_output: lastToolOutput,
},
questions: {
route_skill: {
type: "choice",
instructions: "Which skill should be loaded into context for `task`?",
criteria: {
database_migration: "Schema changes, Drizzle/SQL migrations, or index tuning",
stripe_billing: "Webhooks, checkout sessions, or credit ledger updates",
ui_verification: "Mobile or web component layout and visual checks",
none: "No specialized skill needed",
},
},
needs_repo_sandbox: {
type: "noul",
instructions: "Does answering `task` require booting a code sandbox to read or edit files?",
},
previous_step_verified: {
type: "noul",
instructions: "Does `last_tool_output` confirm the previous step succeeded without errors?",
},
},
}),
signal: AbortSignal.timeout(3000),
});
```
Because `state` tokens are billed **once per request** regardless of how many questions you attach (up to 128), fanning out routing, sandbox-boot gating, and verification in a single call costs `~$0.00001` and completes in one forward pass.
***
## 3. One-click prompt for Cursor, Claude Code, and Codex
[Section titled “3. One-click prompt for Cursor, Claude Code, and Codex”](#3-one-click-prompt-for-cursor-claude-code-and-codex)
Click **Copy for Agent** in the top docs bar (or copy below) and paste it into your coding agent to wire `mouse-1` into your repository automatically:
```text
Integrate the Mouse Decision API (mouse-1) into this codebase or agent harness to cut LLM decision costs and latency.
Full documentation & schemas:
- Full LLM docs: https://www.mouse.dev/llms-full.txt (also https://www.mouse.dev/llm.txt)
- Docs index: https://www.mouse.dev/llms.txt
- OpenAPI 3.1 spec: https://www.mouse.dev/openapi.json
1. Wire Contract
- Endpoint: POST https://api.mouse.dev/v1/decisions (alias POST /v1/systemone)
- Headers: Authorization: Bearer $MOUSE_API_KEY (sk_mouse_... from https://www.mouse.dev/console/keys), Content-Type: application/json
- Model: "mouse-1" (~40-100ms single forward pass, $0.10/1M input tokens, $0.02/1M cached input, $0 output, Zero Data Retention)
- Question types (up to 128 evaluated in parallel per request; state billed once):
- choice: { "type": "choice", "instructions"?: string, "criteria": { "": "" | null } } -> { choice, confidence, probabilities, mouse }
- noul: { "type": "noul", "instructions": "" } -> { noul, probabilities: { false, true }, mouse }
- score: { "type": "score", "instructions"?: string, "criteria": ["", "", ...] } -> { score, confidence, legend, probabilities, mouse }
2. Your Task
- Inspect this repository to find the highest-leverage agent decision points (replacing slow LLM classification/routing prompts, pruning MCP tools/skills before calling the main model, gating risky tool executions, or verifying tool outputs).
- Or, if configuring a coding agent harness, add a lifecycle hook adapter (.cursor/hooks.json beforeShellExecution/preToolUse, .claude/settings.json PreToolUse, or .opencode/plugins/mouse-gate.ts).
- Implement a zero-dependency client reading MOUSE_API_KEY with a 3s timeout and safe fallback when the key is unset, document MOUSE_API_KEY in .env.example, and add a verification test.
```
## 4. Machine-readable endpoints
[Section titled “4. Machine-readable endpoints”](#4-machine-readable-endpoints)
| URL | Format | Purpose |
| ------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------ |
| [`/llms.txt`](/llms.txt) | `text/plain` | Curated LLM index + quick-reference wire contract (`< 5k` tokens). |
| [`/llms-full.txt`](/llms-full.txt) | `text/plain` | Complete documentation concatenated into a single Markdown file. |
| [`/llm.txt`](/llm.txt) / [`/llm.text`](/llm.text) | `text/plain` | All-in-one agent setup prompt + hook adapters + full docs bundle. |
| [`/openapi.json`](/openapi.json) | `application/json` | OpenAPI 3.1 specification with full request/response schemas and examples. |
| `/docs/.md` | `text/markdown` | Append `.md` to any docs URL (e.g. [`/docs/agents.md`](/docs/agents.md)) for raw Markdown. |
# Quickstart
> Make your first mouse-1 decision call in under a minute.
## Quickstart with your coding agent
[Section titled “Quickstart with your coding agent”](#quickstart-with-your-coding-agent)
Using **Cursor**, **Claude Code**, **Codex**, or **OpenCode**? Copy the prompt below and paste it into your agent—it will inspect your repo, wire up a zero-dependency `POST /v1/decisions` client (or lifecycle hook), and verify the integration for you:
```text
Integrate the Mouse Decision API (mouse-1) into this codebase.
1. Read the full LLM reference if needed: https://www.mouse.dev/llms-full.txt (OpenAPI: https://www.mouse.dev/openapi.json)
2. Wire Contract:
- Endpoint: POST https://api.mouse.dev/v1/decisions (alias POST /v1/systemone)
- Headers: Authorization: Bearer $MOUSE_API_KEY (sk_mouse_... from https://www.mouse.dev/console/keys), Content-Type: application/json
- Body: { "model": "mouse-1", "state": , "questions": { "": } }
- Question primitives (up to 128 evaluated in parallel per request; state tokens billed once at $0.10/1M input, $0.02/1M cached, $0 output):
* choice: { "type": "choice", "instructions"?: string, "criteria": { "": "" | null } } -> answers[k] = { choice, confidence, probabilities, mouse }
* noul: { "type": "noul", "instructions": "" } -> answers[k] = { noul, probabilities: { false, true }, mouse } (noul is P(true) in 0..1)
* score: { "type": "score", "instructions"?: string, "criteria": ["", "", ...] } -> answers[k] = { score, confidence, legend, probabilities, mouse }
3. Task:
- Add a zero-dependency `decide(state, questions)` helper using native fetch / requests / httpx with a 3s timeout and graceful fallback when MOUSE_API_KEY is unset.
- Wire `mouse-1` into the highest-leverage decision point in this project (or create a runnable quickstart example / harness hook if starting fresh).
- Add MOUSE_API_KEY="sk_mouse_..." to .env.example and run a quick verification check.
```
Or run a one-liner directly from your terminal:
```bash
# Claude Code
claude "Fetch https://www.mouse.dev/llms-full.txt and integrate mouse-1 (POST https://api.mouse.dev/v1/decisions) into this repository with a runnable quickstart check."
# Codex CLI
codex "Fetch https://www.mouse.dev/llms-full.txt and integrate mouse-1 (POST https://api.mouse.dev/v1/decisions) into this repository with a runnable quickstart check."
```
***
## 1. Create an API key
[Section titled “1. Create an API key”](#1-create-an-api-key)
Sign in to the [Mouse API Console](/console/keys) and create a secret key. Verified accounts start with **$5.00 in free credits** (\~50,000 typical decisions, no credit card required).
Export your key in your shell environment:
```bash
export MOUSE_API_KEY="sk_mouse_..."
```
## 2. Evaluate a state with multiple questions
[Section titled “2. Evaluate a state with multiple questions”](#2-evaluate-a-state-with-multiple-questions)
Send a `state` (a string, JSON object, or array) and a map of `questions` (`choice`, `noul`, or `score`) to `POST https://api.mouse.dev/v1/decisions` (`POST /v1/systemone` is also supported as an identical alias).
In the example below, we pass a support ticket and evaluate three questions in a single forward pass:
1. `department` (`choice`) — which team should handle the ticket
2. `frustration` (`score`) — where the customer falls on a 3-level rubric (`0` to `2`)
3. `is_urgent` (`noul`) — the probability (`0.0` to `1.0`) that the issue is time-sensitive
### cURL
[Section titled “cURL”](#curl)
```bash
curl https://api.mouse.dev/v1/decisions \
-H "Authorization: Bearer $MOUSE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "mouse-1",
"state": "Our webhook endpoint started returning 500 errors 20 minutes ago and customer orders are stuck.",
"questions": {
"department": {
"type": "choice",
"instructions": "Which team should handle this ticket?",
"criteria": {
"billing": "Invoices, duplicate charges, or refund requests",
"technical": "Bugs, outages, or API integration failures",
"sales": "Pricing, enterprise contracts, or account upgrades"
}
},
"frustration": {
"type": "score",
"instructions": "How frustrated does the customer appear?",
"criteria": [
"Calm, stating facts",
"Concerned or frustrated but civil",
"Very angry or threatening to churn"
]
},
"is_urgent": {
"type": "noul",
"instructions": "Does this message describe a production outage or time-sensitive blocker?"
}
}
}'
```
### TypeScript / Node.js
[Section titled “TypeScript / Node.js”](#typescript--nodejs)
```ts
const response = await fetch("https://api.mouse.dev/v1/decisions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MOUSE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "mouse-1",
state:
"Our webhook endpoint started returning 500 errors 20 minutes ago and customer orders are stuck.",
questions: {
department: {
type: "choice",
instructions: "Which team should handle this ticket?",
criteria: {
billing: "Invoices, duplicate charges, or refund requests",
technical: "Bugs, outages, or API integration failures",
sales: "Pricing, enterprise contracts, or account upgrades",
},
},
frustration: {
type: "score",
instructions: "How frustrated does the customer appear?",
criteria: [
"Calm, stating facts",
"Concerned or frustrated but civil",
"Very angry or threatening to churn",
],
},
is_urgent: {
type: "noul",
instructions:
"Does this message describe a production outage or time-sensitive blocker?",
},
},
}),
});
if (!response.ok) {
throw new Error(`Mouse API error (${response.status}): ${await response.text()}`);
}
const data = await response.json();
console.log("Department:", data.answers.department.choice, data.answers.department.confidence);
console.log("Frustration (0..2):", data.answers.frustration.score);
console.log("Urgent probability:", data.answers.is_urgent.noul);
```
### Python
[Section titled “Python”](#python)
```python
import os
import requests
response = requests.post(
"https://api.mouse.dev/v1/decisions",
headers={"Authorization": f"Bearer {os.environ['MOUSE_API_KEY']}"},
json={
"model": "mouse-1",
"state": "Our webhook endpoint started returning 500 errors 20 minutes ago and customer orders are stuck.",
"questions": {
"department": {
"type": "choice",
"instructions": "Which team should handle this ticket?",
"criteria": {
"billing": "Invoices, duplicate charges, or refund requests",
"technical": "Bugs, outages, or API integration failures",
"sales": "Pricing, enterprise contracts, or account upgrades",
},
},
"frustration": {
"type": "score",
"instructions": "How frustrated does the customer appear?",
"criteria": [
"Calm, stating facts",
"Concerned or frustrated but civil",
"Very angry or threatening to churn",
],
},
"is_urgent": {
"type": "noul",
"instructions": "Does this message describe a production outage or time-sensitive blocker?",
},
},
},
timeout=5.0,
)
response.raise_for_status()
data = response.json()
print("Department:", data["answers"]["department"]["choice"])
print("Frustration (0..2):", data["answers"]["frustration"]["score"])
print("Urgent probability:", data["answers"]["is_urgent"]["noul"])
```
## 3. Inspect the response
[Section titled “3. Inspect the response”](#3-inspect-the-response)
`mouse-1` returns one typed answer per question key, itemized token usage (`state_tokens`, `question_tokens`, `framing_tokens`, `cached_tokens`, `cost_usd`), and the exact immutable snapshot ID (`mouse-preview-2026-10-13`):
```json
{
"id": "dec_4c91e02ab718d30f",
"object": "systemone.response",
"model": "mouse-preview-2026-10-13",
"answers": {
"department": {
"type": "choice",
"choice": "technical",
"confidence": 0.97,
"probabilities": {
"technical": 0.98,
"billing": 0.01,
"sales": 0.01
},
"mouse": {
"calibrated": false,
"p_top": 0.98,
"abstain": null
}
},
"frustration": {
"type": "score",
"score": 0.72,
"confidence": 0.58,
"legend": {
"0": "Calm, stating facts",
"1": "Concerned or frustrated but civil",
"2": "Very angry or threatening to churn"
},
"probabilities": {
"0": 0.28,
"1": 0.72,
"2": 0.0
},
"mouse": {
"calibrated": false,
"p_top": 0.72,
"abstain": null
}
},
"is_urgent": {
"type": "noul",
"noul": 0.99,
"mouse": {
"calibrated": false,
"p_top": 0.99,
"abstain": null
}
}
},
"usage": {
"input_tokens": 114,
"state_tokens": 23,
"question_tokens": 64,
"framing_tokens": 27,
"image_tokens": 0,
"cached_tokens": 0,
"output_tokens": 0,
"cost_usd": 0.0000114
},
"latency_ms": 41.6
}
```
## 4. Branch on answers and confidence in code
[Section titled “4. Branch on answers and confidence in code”](#4-branch-on-answers-and-confidence-in-code)
Use the returned probabilities and `confidence` to decide when to act automatically and when to escalate to a human or a reasoning model:
```python
dept = data["answers"]["department"]
urgent = data["answers"]["is_urgent"]["noul"]
frustration = data["answers"]["frustration"]["score"]
if dept["confidence"] < 0.60:
# Distribution is spread across teams; route to manual triage
route_to_human_triage(ticket)
elif dept["choice"] == "technical" and urgent >= 0.85:
page_oncall_engineer(ticket, high_frustration=(frustration >= 1.5))
else:
assign_to_queue(dept["choice"], ticket)
```
## Key guarantees
[Section titled “Key guarantees”](#key-guarantees)
* **Deploy with coding agents:** Using Cursor, Claude Code, or Codex? Copy the one-shot setup prompt in **[Build with coding agents](/docs/agents)** or point your agent at [`https://www.mouse.dev/llms-full.txt`](/llms-full.txt).
* **Immutable version echo:** The `model` field in the JSON body and the `Mouse-Model-Version` response header always report the exact snapshot ID (`mouse-preview-2026-10-13`) even when you send `model: "mouse-1"`.
* **Loud validation, no silent truncation:** States over 65,536 tokens return `422 state_too_long`. Validation errors (`4xx`) are never billed and never echo your request content.
* **Zero data retention:** Every response carries `Mouse-Data-Retention: none` and `Cache-Control: no-store`.
# Authentication
> Bearer authentication, CRC32 checksum validation, SHA-256 storage, and key lifecycle.
Authenticate every request to `/v1/*` by passing your secret key in the `Authorization` HTTP header:
```http
Authorization: Bearer sk_mouse_...
```
## Key format and pre-database checksum
[Section titled “Key format and pre-database checksum”](#key-format-and-pre-database-checksum)
Every production Mouse API key has a deterministic 58-character structure:
```text
sk_mouse_<43 base62 random characters><6 base62 CRC32 checksum characters>
```
* **Prefix (`sk_mouse_`):** 9-character fixed prefix for secret scanners and log redaction filters.
* **Random body (43 base62 chars):** 256 bits (32 bytes) of cryptographic randomness encoded in base62 (`0-9A-Za-z`).
* **Checksum (6 base62 chars):** Fixed-width base62 encoding of `CRC32(body)`.
Before performing any database lookup, the API verifies the prefix, length, and CRC32 checksum in constant time (`secrets.compare_digest`). Malformed keys, truncated strings, and scanner false positives are rejected immediately with `401 invalid_api_key` without touching Postgres.
## Storage and revocation
[Section titled “Storage and revocation”](#storage-and-revocation)
* **Shown once:** Raw keys are returned once at creation time in the [API Keys console](/console/keys) and never stored in plaintext.
* **SHA-256 hashed:** Only the `SHA-256` digest of the key and a 13-character display prefix (`sk_mouse_xxxx…`) are stored in `api_keys`.
* **Revocation TTL:** Validated keys are cached in worker memory for up to **30 seconds**. Revoking a key in the console or crossing a `$0.00` credit balance takes effect across all workers within 30 seconds.
* **GitHub secret scanning:** If a `sk_mouse_` key with a valid CRC32 checksum is committed to a public GitHub repository, Mouse automatically revokes the key and emails the account owner.
## Key types
[Section titled “Key types”](#key-types)
| Key type | Where created | Rate limit | CORS / Browser access | Expiry |
| ------------ | ----------------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------ | ------------- |
| `standard` | [Console → API keys](/console/keys) | 200 RPS (burst 400), 20M TPM | Server-to-server only (no browser CORS header) | Until revoked |
| `playground` | Minted automatically by [Console Playground](/console/playground) | 5 RPS | Allowed from `https://www.mouse.dev` and `https://mouse.dev` | 1 hour |
Standard keys deliberately omit `Access-Control-Allow-Origin` on `200` responses so production keys cannot be used directly from browser client code. Keep `MOUSE_API_KEY` on your server or edge worker.
## Authentication errors
[Section titled “Authentication errors”](#authentication-errors)
| HTTP status | `error.type` | `error.code` | Cause |
| ---------------------- | ---------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `401 Unauthorized` | `authentication_error` | `missing_api_key` | Missing `Authorization` header or missing `Bearer `prefix. |
| `401 Unauthorized` | `authentication_error` | `invalid_api_key` | Key does not start with `sk_mouse_`, fails CRC32 checksum, is expired, or has been revoked. |
| `402 Payment Required` | `billing_error` | `insufficient_credits` | Key is valid, but the account's prepaid credit balance is `$0.00`. Top up at [`/console/billing`](/console/billing). |
```json
{
"error": {
"type": "authentication_error",
"code": "invalid_api_key",
"message": "This API key is not valid. Check it or create a new one."
}
}
```
# SDKs and compatibility
> Use standard HTTP clients or existing System One SDKs in TypeScript and Python by changing the base URL.
`POST https://api.mouse.dev/v1/decisions` is our primary neutral endpoint, and `POST https://api.mouse.dev/v1/systemone` is served as an identical alias that is wire-compatible with the TypeSafe System One request and response specification. You can call either route with zero dependencies using `fetch` / `requests` / `httpx`, or point an existing `@typesafe-ai/sdk` (TypeScript/JavaScript) or `typesafe-sdk` (Python) client at `https://api.mouse.dev`.
## Wire-level compatibility
[Section titled “Wire-level compatibility”](#wire-level-compatibility)
Every standard System One request and response field works unchanged:
* **Request fields:** `model` (`"mouse-1"` or `"mouse"`), `state` (string, JSON object, or array), and `questions` (`choice`, `noul`, `score` with string or structured JSON `instructions` and `criteria`).
* **Response fields:** `answers` keyed by your question IDs, returning `choice` / `noul` / `score`, `probabilities`, `confidence`, and `legend`, plus `model` and `usage`.
* **Mouse extensions (safely ignored by existing SDKs):**
* Request: optional `calibration` (`"calp1.."`) from [`POST /v1/calibrations`](/docs/api-reference#post-v1calibrations).
* Request: optional `order_invariant: true` (server-side permutation averaging for `choice`), and OpenAI Decisions field names (`input`, array `questions`, `predicate`, `choices`, `levels`) accepted as aliases.
* Answer: `logits` and `mouse: { calibrated, p_top, abstain, confidence_winning, confidence_normalized }` on every answer; `noul` answers also carry `probabilities: { false, true }`.
* Usage: itemized `state_tokens`, `question_tokens`, `framing_tokens`, `cached_tokens`, and `cost_usd` alongside `input_tokens` and `output_tokens`.
* Top-level: `id` (`"dec_..."`), `object` (`"systemone.response"`), `latency_ms`, and optional `warnings`.
## Zero-dependency HTTP (`fetch` and `httpx`)
[Section titled “Zero-dependency HTTP (fetch and httpx)”](#zero-dependency-http-fetch-and-httpx)
Because `/v1/systemone` is a single non-streaming JSON endpoint that completes in \~40–100 ms, a 15-line `fetch` or `httpx` helper gives you full access to both System One answers and Mouse calibration fields (`mouse.abstain`, `usage.cost_usd`):
### TypeScript (`fetch`)
[Section titled “TypeScript (fetch)”](#typescript-fetch)
```ts
export interface MouseMeta {
calibrated: boolean;
p_top: number;
abstain: boolean | null;
}
export async function decide>(
state: unknown,
questions: T,
options?: { calibration?: string; signal?: AbortSignal },
) {
const res = await fetch("https://api.mouse.dev/v1/systemone", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MOUSE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "mouse-1",
state,
questions,
...(options?.calibration ? { calibration: options.calibration } : {}),
}),
signal: options?.signal ?? AbortSignal.timeout(3_000),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err?.error?.message ?? `Mouse HTTP ${res.status}`);
}
return res.json();
}
```
### Python (`httpx`)
[Section titled “Python (httpx)”](#python-httpx)
```python
import os
import httpx
_client = httpx.Client(
base_url="https://api.mouse.dev",
headers={"Authorization": f"Bearer {os.environ['MOUSE_API_KEY']}"},
timeout=3.0,
)
def decide(state, questions: dict, calibration: str | None = None) -> dict:
payload = {"model": "mouse-1", "state": state, "questions": questions}
if calibration:
payload["calibration"] = calibration
res = _client.post("/v1/systemone", json=payload)
res.raise_for_status()
return res.json()
```
## Using `@typesafe-ai/sdk` (TypeScript / JavaScript)
[Section titled “Using @typesafe-ai/sdk (TypeScript / JavaScript)”](#using-typesafe-aisdk-typescript--javascript)
Install `@typesafe-ai/sdk` (Node.js 20+) and set `baseURL` to `"https://api.mouse.dev"` and `defaultModel` to `"mouse-1"`:
```bash
npm install @typesafe-ai/sdk
```
```ts
import { choice, noul, score, TypeSafeClient } from "@typesafe-ai/sdk";
const client = new TypeSafeClient({
apiKey: process.env.MOUSE_API_KEY,
baseURL: "https://api.mouse.dev",
defaultModel: "mouse-1",
timeout: 3_000,
retry: { maxRetries: 1 },
});
const result = await client.systemOne({
state: {
task: "Rename parseToken to verifyToken in src/auth.ts",
command: "pnpm test",
},
questions: {
tool: choice("Which tool should run next?", {
edit: "Modify source code",
search: "Search repository",
ask: "Ask the user for clarification",
}),
safe: noul("Is `command` safe to run without user approval?"),
risk: score("How risky is `command`?", [
"Read-only or local test command",
"Modifies local workspace files",
"Pushes to remote or mutates external state",
]),
},
});
// Answer types are inferred from the question map:
// result.answers.tool.choice has type "edit" | "search" | "ask"
console.log(result.answers.tool.choice, result.answers.tool.confidence);
console.log(result.answers.safe.noul);
console.log(result.answers.risk.score);
```
## Using `typesafe-sdk` (Python)
[Section titled “Using typesafe-sdk (Python)”](#using-typesafe-sdk-python)
Install `typesafe-sdk` (Python 3.10+) and pass `base_url="https://api.mouse.dev"` and `model="mouse-1"`:
```bash
pip install typesafe-sdk
```
```python
import os
from typesafe_sdk import AsyncTypeSafeClient, Choice, Noul, Score, TypeSafeClient
with TypeSafeClient(
api_key=os.environ["MOUSE_API_KEY"],
base_url="https://api.mouse.dev",
model="mouse-1",
) as client:
response = client.system_one(
state="Customer says: my card was charged twice for order #1042.",
questions={
"intent": Choice(
instructions="Which team should handle this?",
criteria={
"billing": "Duplicate charges, invoices, or refunds",
"shipping": "Delivery status or tracking",
"account": "Login or profile settings",
},
),
"refund_requested": Noul(
instructions="Does the customer ask for a refund?",
),
},
)
print(response.answers["intent"].choice, response.answers["intent"].confidence)
print(response.answers["refund_requested"].noul)
```
For async Python services (FastAPI, asyncio workers), use `AsyncTypeSafeClient` with the same `base_url="https://api.mouse.dev"` and `model="mouse-1"` parameters.
## Next steps
[Section titled “Next steps”](#next-steps)
* **[API Reference](/docs/api-reference)** — Full endpoint, parameter, and response documentation.
* **[Migrate from TypeSafe Jev or OpenAI Decisions](/docs/guides/migrate)** — Side-by-side schema comparison table and migration examples.
# Calibration and abstain
> Fit out-of-fold confidence cutoffs on your own labeled data with POST /v1/calibrations and branch on mouse.abstain.
Zero-shot probabilities from any decision model can be over- or under-confident when your production traffic has different class priors or harder boundary cases than the model's training mix. Tuning confidence cutoffs by hand across dozens of questions is tedious and breaks whenever you change a rubric.
`POST /v1/calibrations` automates threshold fitting in a single stateless API call: send **200 to 5,000 labeled examples** (with at least **50 examples** per distinct question signature), specify your `target_error` (default `0.05`, or 95% precision on answered items), and receive a signed `calp1.` profile token plus a 5-fold out-of-fold reliability report.
## How calibration works
[Section titled “How calibration works”](#how-calibration-works)
1. **Question signature matching:** Each question is hashed into a 16-character signature over `(type, rendered_instructions, sorted_option_keys)`. Because option keys are sorted when computing the signature, a profile still matches even if option order changes between requests.
2. **Temperature scaling:** For each signature, a scalar temperature `T` is fitted by minimizing negative log-likelihood of your labels over the model's raw `logits`. Dividing the logits by `T` before the softmax makes the reported probabilities mean what they say on your traffic (an overconfident question gets `T > 1` and flatter probabilities) without ever changing which option wins. `after.temperature` reports the fitted value and `after.ece` the calibration error that remains.
3. **5-fold out-of-fold cross-fitting:** For each question signature with 50+ labeled examples, `POST /v1/calibrations` splits the scored examples into 5 folds. Each fold is evaluated using the temperature and confidence cutoff fitted on the other 4 folds:
* The **cutoff** is the lowest scaled top probability (`p_top`) at which the empirical error rate among covered examples (`p_top >= cutoff`) is at most `target_error`.
* `after.coverage_at_target_error` and `after.error_when_covered` report the honest out-of-fold fraction of traffic automated and the error rate on that automated slice.
4. **Stateless HMAC-SHA256 profile (`calp1.`):** The fitted temperatures, cutoffs, and the exact model snapshot ID (`mouse-preview-2026-10-13`) are serialized and signed with HMAC-SHA256 (`calp1..`). Neither your labeled examples nor the profile are stored on Mouse servers.
Why both a temperature and a cutoff: the temperature fixes *what the number means*; the cutoff fixes *when to act on it*. Fit once, then threshold on calibrated `p_top` (or on `mouse.abstain`) instead of a hard-coded `0.7` that was tuned against a different model's confidence scale.
## Applying a profile on `POST /v1/decisions`
[Section titled “Applying a profile on POST /v1/decisions”](#applying-a-profile-on-post-v1decisions)
Pass the `profile` string as `calibration` on your `/v1/decisions` (or `/v1/systemone`) requests:
```json
{
"model": "mouse-preview-2026-10-13",
"calibration": "calp1.k1.eyJmaXRzIjpb...",
"state": "Customer says: my card was charged twice for order #1042.",
"questions": {
"intent": {
"type": "choice",
"instructions": "What does the customer need?",
"criteria": {
"billing": "A charge, invoice, or refund issue",
"shipping": "Delivery status or tracking",
"account": "Login or profile settings"
}
}
}
}
```
When a question in `/v1/systemone` matches a signature in the profile:
| Field | Uncalibrated (`calibration` omitted or question not in profile) | Calibrated (`calibration` matches question signature) |
| ---------------------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------- |
| `mouse.calibrated` | `false` | `true` |
| `probabilities`, `noul`, `score`, `confidence` | Raw model distribution | Temperature-scaled distribution (same argmax) |
| `mouse.p_top` | Raw top probability ((\max\_i p\_i)) | Calibrated top probability |
| `mouse.abstain` | `null` | `true` if `p_top < cutoff`, `false` if `p_top >= cutoff` |
| `logits` | Raw log-scores | Scaled log-scores (`logits / T`) |
Your application code simply checks `answer.mouse.abstain`:
```ts
const intent = data.answers.intent;
if (intent.mouse.abstain) {
// Below the fitted cutoff for target_error: escalate to human or reasoning model
await escalateToHumanReview(ticket);
} else {
// Guaranteed <= target_error on your validation distribution
await routeAutomatically(intent.choice, ticket);
}
```
## Model version binding
[Section titled “Model version binding”](#model-version-binding)
Because probability distributions can shift across model releases, every `calp1.` profile is cryptographically bound to the exact snapshot ID (`mouse-preview-2026-10-13`) it was fitted on.
* If you pass a profile fitted on `mouse-preview-2026-10-13` to a request that resolves to a different model version, the API refuses the request with `409 profile_model_mismatch` rather than silently applying stale cutoffs.
* Pin the exact snapshot ID (`model: "mouse-preview-2026-10-13"`) in production alongside your `calibration` profile, and re-run `POST /v1/calibrations` on your validation set when upgrading to a newer snapshot.
## Reliability curve and the Fair Coin / Fair Die sanity check
[Section titled “Reliability curve and the Fair Coin / Fair Die sanity check”](#reliability-curve-and-the-fair-coin--fair-die-sanity-check)
Practitioners evaluating decision models rightly test whether a model assigns uniform probability to symmetric random events (the "Fair Coin / Fair Die" check, where uncalibrated models in the wild have reported `86%–87%` on a 50/50 coin flip) and how often reversing option order flips the winning choice.
| Sanity check | Theoretical ideal | How `mouse-1` handles it |
| ------------------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Fair coin (`heads` vs `tails`)** | `50.0%` / `50.0%` | Evaluated via `POST /v1/decisions`; pass `order_invariant: true` to average forward and reversed option orderings in one call. |
| **Fair 6-sided die (`1`..`6`)** | `16.7%` per face | Uniform prior across symmetric options; `confidence` normalized to `0.0` at (1/k). |
| **Option-order flip rate** | `0.0%` under `order_invariant: true` | Setting `order_invariant: true` scores multi-option `choice` questions in both orders and averages the distributions, eliminating positional bias. |
| **10-bin Expected Calibration Error (ECE)** | `0.0` | Reported in `before.ece` and `after.ece` on every `POST /v1/calibrations` call over your own labeled examples. |
> **Reproducibility note:** We only publish empirical Fair Coin, Fair Die, and single-pass flip-rate numbers measured against the attached production checkpoint (reproducible via `apps/decide/scripts/shadow_migrate.py`), never hand-entered estimates.
# Jaggedness and limitations
> Where mouse-1 excels, its known failure modes on arithmetic and counting, and when a supervised fixed-label classifier is a better fit.
Every single-pass decision model has a **jagged capability frontier**: it outperforms slow generative LLMs on narrow, well-scoped judgment calls, and underperforms on tasks that require multi-step scratchpad computation or exact counting. Designing reliable agent harnesses means calling `mouse-1` where a single forward pass is strong and keeping deterministic code or reasoning models where it is not.
## Where `mouse-1` excels
[Section titled “Where mouse-1 excels”](#where-mouse-1-excels)
| Use case | Why a single forward pass wins | Recommended primitive |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
| **Zero-shot tool and intent routing** | Dynamic option sets (`criteria` changes per turn or user), natural-language descriptions, \~40–100 ms latency | `choice` |
| **Pre-execution action gating** | Binary safety/policy check on a proposed command, URL, or diff before execution | `noul` |
| **Prompt-injection resistance** | Untrusted content lives inside `state` and is never decoded as instructions; `mouse-1` only scores your fixed option keys | `noul` + `choice` |
| **Ordinal patch and rubric grading** | Evaluating whether a diff or test log satisfies an objective along 2–10 concrete levels | `score` |
| **Speculative multi-question fan-out** | Answering 5–50 independent questions over one `state` in a single pass (`state_tokens` billed once) | `choice` + `noul` + `score` |
## Known failure modes (do not use `mouse-1` for these)
[Section titled “Known failure modes (do not use mouse-1 for these)”](#known-failure-modes-do-not-use-mouse-1-for-these)
Because `mouse-1` scores options in one non-autoregressive forward pass with zero output tokens (`output_tokens = 0`), it has no chain-of-thought scratchpad. Avoid delegating these tasks to the model:
1. **Exact multi-step arithmetic and financial totals:** Do not ask `mouse-1` whether `invoice.line_items` sum to `invoice.total`. Compute the sum in application code and pass the boolean or discrepancy into `state` if a semantic judgment is still needed.
2. **Exact counting over large arrays or long strings:** Do not ask `"Are there more than 15 failed tests in test_log?"` over raw log text. Parse or count in code (`failed_count: 17`) and pass the structured count in `state`.
3. **Calendar, timezone, and date math:** Do not ask `"Is 2026-10-14 more than 14 business days after 2026-09-22?"`. Compute the delta in code using your runtime's date library.
4. \*\* Cryptographic, regex, or syntax validation:\*\* Checksums, UUID formats, JSON schema validity, and carrier opt-out keywords (`STOP`, `UNSUBSCRIBE`) belong in deterministic validators, never a probabilistic model.
5. **States exceeding 65,536 tokens:** `mouse-1` refuses states above 65,536 tokens with `422 state_too_long` (`$0` billed) rather than silently truncating. Pre-filter relevant files or fields before calling `/v1/decisions`.
## When a fixed-label supervised classifier (DeBERTa / ModernBERT) is a better fit
[Section titled “When a fixed-label supervised classifier (DeBERTa / ModernBERT) is a better fit”](#when-a-fixed-label-supervised-classifier-deberta--modernbert-is-a-better-fit)
A general-purpose decision model is not always the right tool. Consider a fine-tuned encoder (such as DeBERTa-v3 or ModernBERT) instead of `mouse-1` when **all four** of these hold:
* Your label set is **fixed and small** (for example, a permanent 3-way sentiment or spam tagger that never changes per customer or per turn).
* You already have **10,000+ clean labeled examples** for that exact schema and retrain whenever the rubric changes.
* Your input is a **short single text** (`< 512` tokens) rather than a multi-field agent state or 10k-token diff.
* You only ask **one question per input** rather than fanning out multiple `choice`, `noul`, and `score` questions in a single pass.
Conversely, use `mouse-1` when your options or rubrics are dynamic per tenant or turn, when you have 0 to 1,000 labeled examples (using [`POST /v1/calibrations`](/docs/concepts/calibration) once you reach 200 examples), or when you evaluate multiple heterogeneous questions against a shared agent `state`.
# Question types
> How choice, noul, and score primitives work, how confidence is calculated, and how to use structured criteria.
Every entry in the `questions` map of `POST /v1/systemone` specifies one of three primitives via its `type` field: `choice`, `noul`, or `score`.
| Primitive | Question asks | Answer fields | Control-flow equivalent |
| ------------------- | -------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------- |
| [`choice`](#choice) | Which of these 1–1,024 named options? | `choice`, `probabilities`, `logits`, `confidence`, `mouse` | `switch (answer.choice)` |
| [`noul`](#noul) | Is this binary condition true? | `noul` (`0.0` to `1.0`), `probabilities`, `logits`, `mouse` | `if (answer.noul >= threshold)` |
| [`score`](#score) | Which of these 2–10 ordered rubric levels applies? | `score`, `legend`, `probabilities`, `logits`, `confidence`, `mouse` | `if (answer.score >= level)` / sort key |
Question IDs (the keys of your `questions` map) are for your code—they are echoed back in `answers` and are **not** sent to the model. Always write the full question in `instructions` and `criteria`.
***
## `choice`
[Section titled “choice”](#choice)
Use `choice` when the answer is one of a closed set of mutually exclusive options with no inherent numeric order: selecting a tool, routing a ticket to a department, or classifying a failure category.
### Request shape
[Section titled “Request shape”](#request-shape)
```json
{
"next_tool": {
"type": "choice",
"instructions": "Which tool should handle `task` next?",
"criteria": {
"edit_file": "Modify source code in a file",
"grep_search": "Search for symbols across the repository",
"run_tests": "Execute the test suite",
"ask_user": "Ask the user for clarification"
}
}
}
```
* `criteria` is a JSON object mapping each option key (`1` to `1,024` options) to a description string, a structured JSON object, or `null` when the option key is self-explanatory.
* Include an explicit fallback option (such as `"other"`, `"unknown"`, or `"none"`) whenever your option list might not cover every possible input. Without one, probabilities are still normalized to sum to `1.0` across the supplied options.
### Answer shape
[Section titled “Answer shape”](#answer-shape)
```json
{
"type": "choice",
"choice": "edit_file",
"confidence": 0.88,
"probabilities": {
"edit_file": 0.91,
"grep_search": 0.06,
"run_tests": 0.02,
"ask_user": 0.01
},
"logits": {
"edit_file": 2.41,
"grep_search": -0.31,
"run_tests": -1.41,
"ask_user": -2.1
},
"mouse": {
"calibrated": false,
"p_top": 0.91,
"abstain": null,
"confidence_winning": 0.91,
"confidence_normalized": 0.88
}
}
```
* `logits` are the raw per-option log-scores; `softmax(logits) == probabilities`. Log them with `model` if you fit thresholds offline.
* Always threshold on `probabilities`, `mouse.p_top`, or the margin between the top two options, not on a vendor's `confidence` scalar: the same distribution reads `0.91` under the winning convention and `0.88` under the normalized one. Both are returned by name in `mouse` so a threshold fitted elsewhere ports without arithmetic.
* Pass `order_invariant: true` on the request to have Mouse score the question a second time with the options reversed and average the two distributions.
***
## `noul`
[Section titled “noul”](#noul)
Use `noul` for a binary yes/no predicate: whether a command is safe to run without approval, whether a test log supports a completion claim, or whether a message requests a refund.
### Request shape
[Section titled “Request shape”](#request-shape-1)
```json
{
"safe_to_run": {
"type": "noul",
"instructions": "Is `proposed_command` safe to execute without human approval?",
"criteria": {
"true": "Read-only command, local test run, or scoped cleanup inside ./tmp",
"false": "Force push, production mutation, credential access, or destructive rm outside ./tmp"
}
}
}
```
* Phrase `instructions` positively so a value near `1.0` means `true` (yes) and a value near `0.0` means `false` (no).
* Ask one atomic condition per `noul`. Instead of asking `"Is the command safe and did the user ask for it?"`, ask two separate `noul` questions (`is_safe`, `user_requested`) in the same request and combine them with `&&` in code.
* `criteria` (`{ "true": ..., "false": ... }`) is optional; add it when you want to pin down subtle edge cases on each side of the boundary.
### Answer shape
[Section titled “Answer shape”](#answer-shape-1)
```json
{
"type": "noul",
"noul": 0.03,
"probabilities": { "false": 0.97, "true": 0.03 },
"logits": { "false": 1.74, "true": -1.74 },
"mouse": {
"calibrated": false,
"p_top": 0.97,
"abstain": null,
"confidence_winning": 0.97,
"confidence_normalized": 0.94
}
}
```
* `noul` is the probability of `true` (`0.0` to `1.0`). `probabilities` carries the same distribution in the shape `choice` uses, so one code path can read every answer type.
* `mouse.p_top` is (\max(\text{noul}, 1 - \text{noul})) (`0.97` above, because the model is 97% certain the answer is `false`).
***
## `score`
[Section titled “score”](#score)
Use `score` when the judgment falls along an ordered spectrum of **2 to 10 concrete levels**: patch quality, bug severity, customer frustration, or action blast radius.
### Request shape
[Section titled “Request shape”](#request-shape-2)
```json
{
"patch_quality": {
"type": "score",
"instructions": "How completely does the patch address `objective`?",
"criteria": [
"Tests fail, build breaks, or objective is missed",
"Partial fix or missing regression test",
"Addresses objective cleanly and all checks pass"
]
}
}
```
* `criteria` is an ordered array of level descriptions from index `0` (lowest) to `N - 1` (highest).
* **Describe concrete situations, never bare numbers or vague degrees:** Write `"Broken feature, but a workaround exists"`, not `"Medium"` or `"1"`. Each level is scored against `state` on its own wording.
### Answer shape
[Section titled “Answer shape”](#answer-shape-2)
```json
{
"type": "score",
"score": 1.88,
"confidence": 0.85,
"legend": {
"0": "Tests fail, build breaks, or objective is missed",
"1": "Partial fix or missing regression test",
"2": "Addresses objective cleanly and all checks pass"
},
"probabilities": {
"0": 0.02,
"1": 0.08,
"2": 0.90
},
"logits": {
"0": -3.81,
"1": -2.42,
"2": 0.0
},
"mouse": {
"calibrated": false,
"p_top": 0.90,
"abstain": null,
"confidence_winning": 0.90,
"confidence_normalized": 0.85
}
}
```
* `score` is the expectation-weighted average level index: \[ \text{score} = \sum\_{i=0}^{k-1} i \cdot p\_i = 0 \times 0.02 + 1 \times 0.08 + 2 \times 0.90 = 1.88 ]
* To combine multiple `score` questions of different lengths in code, normalize each score to `0.0 .. 1.0` by dividing by `len(criteria) - 1`.
***
## How `confidence` is calculated
[Section titled “How confidence is calculated”](#how-confidence-is-calculated)
`choice` and `score` answers include a `confidence` field in `0.0 .. 1.0` computed deterministically from `probabilities`. Mouse uses the exact System One confidence formulas so thresholds behave identically across compatible clients.
### `choice` confidence
[Section titled “choice confidence”](#choice-confidence)
For a `choice` question with (k) options and top probability (p\_{\max} = \max\_i p\_i):
\[ \text{confidence} = \frac{p\_{\max} - \frac{1}{k}}{1 - \frac{1}{k}} ]
* When probability is split evenly across all (k) options ((p\_{\max} = 1/k)), `confidence` is `0.0`.
* When all probability is concentrated on a single option ((p\_{\max} = 1.0)), `confidence` is `1.0`.
* Note the difference between `confidence` and `mouse.p_top`: on a 3-option `choice` with probabilities `(0.60, 0.30, 0.10)`, `mouse.p_top` is `0.60` while `confidence` is ((0.60 - 1/3) / (2/3) = 0.40).
### `score` confidence
[Section titled “score confidence”](#score-confidence)
Because `score` levels are ordered ((0, 1, \dots, k-1)), probability mass on an adjacent level reduces confidence less than probability mass on the opposite end of the scale. Let (m = \arg\max\_i p\_i) be the mode level:
\[ \text{confidence} = \max\left(0,\ 1 - \frac{\sum\_{i=0}^{k-1} p\_i , |i - m|}{\text{MAD}*{\text{unif}}}\right), \qquad \text{MAD}*{\text{unif}} = \frac{1}{k} \sum\_{i=0}^{k-1} \left| i - \frac{k - 1}{2} \right| ]
* On a 3-level scale (`0, 1, 2`), a split between adjacent levels `(0.0, 0.5, 0.5)` has `confidence = 0.25`.
* A bimodal split between opposite ends `(0.5, 0.0, 0.5)` has `confidence = 0.0`, even though both distributions have the same expected `score` (`1.0` vs `1.5`).
### `noul` certainty
[Section titled “noul certainty”](#noul-certainty)
A `noul` answer has only two outcomes (`false` and `true`), so `noul` itself carries both the direction and the certainty. If you need a `0.0 .. 1.0` certainty scalar on the same scale as `choice` confidence, use:
\[ \text{confidence} = |2 \cdot \text{noul} - 1| = 2 \cdot \text{mouse.p\_top} - 1 ]
***
## Structured `instructions` and `criteria`
[Section titled “Structured instructions and criteria”](#structured-instructions-and-criteria)
`instructions` and every entry in `criteria` accept structured JSON objects and arrays in addition to plain strings.
When two `choice` options are easily confused, pass a structured object with consistent keys (`what`, `not_for`, `examples`) on each option to sharpen the boundary:
```json
{
"return_topic": {
"type": "choice",
"instructions": {
"question": "What is the customer asking about in `ticket.message`?",
"focus": "Distinguish policy questions before a return from status checks on an existing return."
},
"criteria": {
"return_policy": {
"what": "Rules, eligibility window, or fees for returning an item",
"not_for": "Checking on an item that was already shipped back",
"examples": ["Can I return sale items?", "Do you charge a restocking fee?"]
},
"return_status": {
"what": "Where an in-progress return or refund stands right now",
"not_for": "General policy questions before starting a return",
"examples": ["I mailed my return last Tuesday, where is my refund?"]
}
}
}
}
```
# State and fan-out
> Structure your state as text or JSON, reference nested paths in backticks, and evaluate up to 128 independent questions in one forward pass.
`state` is the context `mouse-1` reads before scoring your questions. You pass it in the top-level `state` field of `POST /v1/systemone` alongside a map of `questions`.
Every question in a request evaluates the same `state` in parallel and in isolation.
## Structuring `state`
[Section titled “Structuring state”](#structuring-state)
`state` accepts a plain string, a JSON object, or a JSON array up to **65,536 tokens**.
| Format | Best for | Example |
| --------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| **String** | A single message, log excerpt, diff, or passage | `"Customer says: my card was charged twice for order #1042."` |
| **JSON Object** | Named fields, agent state, tool proposals, and untrusted inputs kept in separate keys | `{"task": "Clean build cache", "proposed_cmd": "rm -rf ./tmp/cache", "git_status": "clean"}` |
| **JSON Array** | Ordered sequences of turns, events, or candidate items | `[{"round": 1, "error": "TS2345"}, {"round": 2, "error": "TS2345"}]` |
Use a **JSON object** whenever your decision depends on comparing two or more pieces of context (for example, comparing a user's `task` against a `proposed_command`, or comparing a `claim` against a `test_log`). Naming each piece of evidence keeps its provenance clear and prevents untrusted text in one field from masquerading as instructions in another.
### Referencing nested `state` fields by path
[Section titled “Referencing nested state fields by path”](#referencing-nested-state-fields-by-path)
When `state` is a nested JSON object or array, point a question at specific fields using a **backticked dot-and-index path** inside `instructions`:
```json
{
"model": "mouse-1",
"state": {
"ticket": {
"subject": "Duplicate charge",
"messages": [
{ "from": "customer", "text": "I was charged twice for order A-104. Please refund the duplicate." },
{ "from": "support", "text": "We are checking the charges." }
]
},
"order": {
"id": "A-104",
"charges": [
{ "amount_usd": 49, "status": "captured" },
{ "amount_usd": 49, "status": "captured" }
]
},
"refund_policy": "Duplicate captured charges on the same order are eligible for an automatic refund."
},
"questions": {
"refund_requested": {
"type": "noul",
"instructions": "Does `ticket.messages[0].text` request a refund?"
},
"policy_supports_refund": {
"type": "noul",
"instructions": "Does `refund_policy` allow the refund requested in `ticket.messages[0].text`, given `order.charges`?"
}
}
}
```
### Keep only relevant context in `state`
[Section titled “Keep only relevant context in state”](#keep-only-relevant-context-in-state)
Include the fields that bear on the questions you are asking, and omit unrelated repository dumps or full environment variables:
* **Faster and cheaper:** Latency and cost scale with `state_tokens`.
* **Higher signal-to-noise:** Focused states avoid diluting attention across thousands of irrelevant lines.
* **Never silently truncated:** If `state` exceeds **65,536 tokens**, `mouse-1` refuses the request with `422 state_too_long` and bills `$0` rather than silently truncating the tail of your input.
## Speculative fan-out
[Section titled “Speculative fan-out”](#speculative-fan-out)
In a single `POST /v1/systemone` request, `mouse-1` runs a single prefill pass over `state` and scores up to **128 questions** in parallel:
1. **`state` is billed once:** In a 2,000-token `state`, adding 5 extra questions adds only \~150 `question_tokens` + `framing_tokens`—making 6 questions in one call \~5× cheaper and \~5× faster than 6 sequential calls.
2. **Questions are strictly independent:** Question `A` never sees Question `B` or its answer. Adding, removing, or rewording one question never alters the probability distribution of any other question in the same request.
3. **Ask speculative questions upfront:** Include follow-up questions whose answers only matter on certain branches (for example, asking `bug_severity` and `refund_requested` in the same call as `department`), then ignore the unused answers in your `if`/`switch` logic.
```ts
const { answers } = await decide(
{
user_prompt: "Deploy the staging branch to production",
git_status: "2 uncommitted files",
ci_status: "passing",
},
{
intent: {
type: "choice",
instructions: "What is `user_prompt` asking the agent to do?",
criteria: {
deploy: "Deploy or release to an environment",
code_edit: "Modify or refactor source files",
question: "Answer a question about the repository",
},
},
is_production_target: {
type: "noul",
instructions: "Does `user_prompt` target a production environment?",
},
workspace_clean: {
type: "noul",
instructions: "Does `git_status` indicate a clean working tree with zero uncommitted files?",
},
},
);
if (answers.intent.choice === "deploy") {
if (answers.is_production_target.noul >= 0.5 || answers.workspace_clean.noul < 0.8) {
await promptHumanConfirmation("Production deploy with uncommitted files requires approval.");
}
}
```
### When to make a second request
[Section titled “When to make a second request”](#when-to-make-a-second-request)
Because questions in a single request are evaluated in parallel, make a second request **only** when the first answer is required to construct the second request's `state` or `criteria`—for example:
* Ranking 200 skills in Request 1, fetching the full documentation of the top 3 winners, and verifying them in Request 2.
* Walking a deep taxonomy tree level by level where the chosen top-level branch determines which child options to pass next.
## 5-minute prefix caching
[Section titled “5-minute prefix caching”](#5-minute-prefix-caching)
When your harness evaluates a second request with a repeated `state` from the same account—typically within **5 minutes (300 seconds)**, best effort in GPU memory—the worker serves the cached tokens at the discounted rate:
* **Structure for cache hits (rubric first, item last):** JSON key order in `state` is preserved exactly as sent (`sort_keys` is not applied). Put shared rubrics, schemas, or policies first and the varying item last.
* Cached tokens are billed at **$0.02 / 1M** (an 80% discount from `$0.10 / 1M`, with no cache-write fee).
* Prefix cache entries live only in GPU memory, are scoped per tenant so another account can never probe your cache state, and are never written to disk ([Zero Data Retention](/docs/reference/security)). You can track your actual cache hit rate in [`/console/usage`](/console/usage).
# Models and versioning
> Immutable model snapshots, the mouse-1 alias, and version pinning.
Mouse serves one model family (`mouse-1`) behind immutable, date-stamped snapshot IDs. Every decision response echoes the exact snapshot ID that scored the request in both the JSON body (`model`) and the `Mouse-Model-Version` HTTP header.
## Active models and aliases
[Section titled “Active models and aliases”](#active-models-and-aliases)
Query [`GET /v1/models`](/docs/api-reference#get-v1models) to inspect all active snapshots and alias mappings:
| Alias | Resolves to snapshot | Max `state` tokens | Modalities | Status |
| --------- | -------------------------- | ------------------ | ---------- | ------- |
| `mouse-1` | `mouse-preview-2026-10-13` | 65,536 | `["text"]` | Preview |
| `mouse` | `mouse-preview-2026-10-13` | 65,536 | `["text"]` | Preview |
You can pass either an alias (`"mouse-1"`, `"mouse"`) or the immutable snapshot ID (`"mouse-preview-2026-10-13"`) in the `model` field of `POST /v1/systemone` and `POST /v1/calibrations`.
## Versioning and pinning policy
[Section titled “Versioning and pinning policy”](#versioning-and-pinning-policy)
1. **Immutable snapshots:** Once a snapshot ID such as `mouse-preview-2026-10-13` is published, its weights, tokenizer, rendering format, and scoring numerics never change.
2. **Always echoed:** Even when you request `model: "mouse-1"`, the response body's `model` field and the `Mouse-Model-Version` header always return the concrete snapshot ID (`"mouse-preview-2026-10-13"`). Log this value alongside your application telemetry.
3. **When to pin a snapshot ID:** If you fit a [calibration profile](/docs/concepts/calibration) (`calp1.`) or tune custom confidence thresholds against your validation set, pin `model: "mouse-preview-2026-10-13"` in production. Calibration profiles are bound to the snapshot they were fitted on and return `409 profile_model_mismatch` if used with a different snapshot.
4. **30-day alias notice & 12-month support window:** Aliases (`mouse-1`, `mouse`) advance to newer snapshots with at least **30 days' advance notice**. Superseded snapshots remain live for at least **12 months** after their successor ships, with `retire_after` populated in `GET /v1/models`.
## Model design
[Section titled “Model design”](#model-design)
`mouse-1` is a proprietary decision model purpose-built for agent-harness control: tool routing, action-gate safety, option-order invariance (<1% order flip rate), and resistance to planted prompt injections in untrusted state (0% flip rate on held-out injection suites).
# Calibrating on your labels
> Fit a signed calibration profile on 200+ labeled examples and automate decisions at a guaranteed target error rate.
When you deploy a decision question in production—such as routing support tickets, gating shell commands, or triaging failures—you want to automate as many decisions as possible while keeping wrong decisions below a fixed error budget (for example, `target_error: 0.05` for ≥95% precision).
Instead of guessing a confidence threshold like `0.75`, send 200 to 5,000 labeled examples from your own workload to `POST /v1/calibrations`.
## Step 1: Prepare 200+ labeled examples
[Section titled “Step 1: Prepare 200+ labeled examples”](#step-1-prepare-200-labeled-examples)
Each item in `examples` has the same `state` and `questions` structure as `POST /v1/systemone`, plus a `label` field on each question:
| Question `type` | `label` type | Valid values |
| --------------- | ------------ | ------------------------------------------------------- |
| `choice` | `string` | Any option key present in `criteria` (e.g. `"billing"`) |
| `noul` | `boolean` | `true` or `false` |
| `score` | `integer` | Level index `0` to `len(criteria) - 1` |
Every distinct question signature (`type` + `instructions` + sorted option keys) requires at least **50 labeled examples** to be fitted, and the total `examples` array must contain **200 to 5,000 examples**.
## Step 2: Fit the profile with `POST /v1/calibrations`
[Section titled “Step 2: Fit the profile with POST /v1/calibrations”](#step-2-fit-the-profile-with-post-v1calibrations)
```python
import os
import requests
# Load 250 labeled examples from your validation set
examples = [
{
"state": row["text"],
"questions": {
"intent": {
"type": "choice",
"instructions": "What does the customer need?",
"criteria": {
"billing": "A charge, invoice, or refund issue",
"shipping": "Delivery status or tracking",
"account": "Login or profile settings",
},
"label": row["true_intent"], # "billing" | "shipping" | "account"
}
},
}
for row in validation_rows
]
res = requests.post(
"https://api.mouse.dev/v1/calibrations",
headers={"Authorization": f"Bearer {os.environ['MOUSE_API_KEY']}"},
json={
"model": "mouse-preview-2026-10-13",
"target_error": 0.05,
"examples": examples,
},
timeout=30.0,
)
res.raise_for_status()
cal = res.json()
print("Signed profile:", cal["profile"])
print("Question report:", cal["questions"][0])
```
### Inspecting the out-of-fold report
[Section titled “Inspecting the out-of-fold report”](#inspecting-the-out-of-fold-report)
```json
{
"object": "calibration",
"model": "mouse-preview-2026-10-13",
"target_error": 0.05,
"profile": "calp1.k1.eyJmaXRzIjpb...",
"n_examples": 250,
"questions": [
{
"signature": "4f8c12a90e3b7104",
"type": "choice",
"question_ids": ["intent"],
"status": "fitted",
"before": {
"accuracy": 0.912,
"ece": 0.031,
"brier": 0.142,
"nll": 0.264,
"mean_confidence": 0.938,
"coverage": 1.0,
"cutoff": null,
"n": 250
},
"after": {
"accuracy": 0.912,
"ece": 0.031,
"brier": 0.142,
"nll": 0.264,
"mean_confidence": 0.938,
"coverage": 0.864,
"cutoff": 0.78,
"n": 250,
"coverage_at_target_error": 0.856,
"error_when_covered": 0.0421
}
}
],
"note": "'coverage_at_target_error' and 'error_when_covered' are 5-fold out-of-fold. Examples were not stored.",
"usage": {
"input_tokens": 14500,
"state_tokens": 3500,
"question_tokens": 7750,
"framing_tokens": 3250,
"image_tokens": 0,
"cached_tokens": 0,
"output_tokens": 0,
"cost_usd": 0.00145
}
}
```
Key metrics in `after`:
* `cutoff` (`0.78`): The fitted `p_top` threshold stored in your signed `profile`.
* `coverage_at_target_error` (`0.856`): Across 5 out-of-fold splits, **85.6%** of examples had `p_top >= cutoff` and were answered automatically.
* `error_when_covered` (`0.0421`): Among those 85.6% automated examples, the out-of-fold error rate was **4.21%** (within your `0.05` target).
## Step 3: Pass `calibration` on `POST /v1/systemone`
[Section titled “Step 3: Pass calibration on POST /v1/systemone”](#step-3-pass-calibration-on-post-v1systemone)
Store the `calp1.` token in your environment or configuration (for example `MOUSE_CALIBRATION_PROFILE`) and pass it alongside the pinned model snapshot on `/v1/systemone`:
```python
res = requests.post(
"https://api.mouse.dev/v1/systemone",
headers={"Authorization": f"Bearer {os.environ['MOUSE_API_KEY']}"},
json={
"model": "mouse-preview-2026-10-13",
"calibration": cal["profile"],
"state": "Where is my tracking link for order #1099?",
"questions": {
"intent": {
"type": "choice",
"instructions": "What does the customer need?",
"criteria": {
"billing": "A charge, invoice, or refund issue",
"shipping": "Delivery status or tracking",
"account": "Login or profile settings",
},
}
},
},
).json()
intent = res["answers"]["intent"]
# intent["mouse"] == {"calibrated": True, "p_top": 0.94, "abstain": False}
if intent["mouse"]["abstain"]:
route_to_human_queue(ticket)
else:
route_to_department(intent["choice"], ticket)
```
Applying a calibration profile on `/v1/systemone` adds **0 tokens** and **$0** to your request cost.
# Action gating
> Gate shell commands, git pushes, and external connector writes using risk-scaled confidence bands.
Before an agent executes a shell command, pushes to a branch, or calls an external write tool (Slack, Linear, GitHub, Stripe), your harness must decide whether to **execute automatically**, **pause for human confirmation**, or **deny**.
`mouse-1` evaluates semantic safety and task alignment in \~40–100 ms so you can gate every tool call on the hot path without slowing down safe local commands.
## Defense in depth: deterministic floor + `mouse-1` judgment
[Section titled “Defense in depth: deterministic floor + mouse-1 judgment”](#defense-in-depth-deterministic-floor--mouse-1-judgment)
Keep your deterministic regex or allowlist rules as an unbypassable floor, and combine them with `mouse-1` judgments:
1. **Deterministic deny floor:** Hardcoded denies (such as reading `.env` secrets or force-pushing to `main` when forbidden by repo policy) always block in code without a network call.
2. **Semantic alignment (`mouse-1`):** Ask atomic `noul` and `score` questions over `{ task, user_constraints, proposed_action }` to catch semantic violations—for example, running `git push` when the user said *"just show me the diff first, don't push"*.
## Request: atomic gating questions
[Section titled “Request: atomic gating questions”](#request-atomic-gating-questions)
Instead of asking a single vague question like `"Is this safe?"`, decompose the gate into specific, auditable questions in one request:
```json
{
"model": "mouse-1",
"state": {
"task": "Clean temporary build artifacts and run unit tests.",
"user_notes": "Do not push or modify git history.",
"proposed_command": "git reset --hard HEAD~2 && git push --force origin main"
},
"questions": {
"matches_task": {
"type": "noul",
"instructions": "Is `proposed_command` a reasonable step toward completing `task`?"
},
"violates_user_constraint": {
"type": "noul",
"instructions": "Does `proposed_command` violate any restriction stated in `task` or `user_notes`?"
},
"blast_radius": {
"type": "score",
"instructions": "What is the reversibility and impact of `proposed_command`?",
"criteria": [
"Read-only inspection or running local tests with no file changes",
"Reversible local workspace edit or deleting temporary build cache files",
"Irreversible local deletion, history rewrite, remote git push, or external service mutation"
]
}
}
}
```
### Response
[Section titled “Response”](#response)
```json
{
"id": "dec_7e04c19a3b82d510",
"object": "systemone.response",
"model": "mouse-preview-2026-10-13",
"answers": {
"matches_task": {
"type": "noul",
"noul": 0.01,
"mouse": { "calibrated": false, "p_top": 0.99, "abstain": null }
},
"violates_user_constraint": {
"type": "noul",
"noul": 0.99,
"mouse": { "calibrated": false, "p_top": 0.99, "abstain": null }
},
"blast_radius": {
"type": "score",
"score": 1.98,
"confidence": 0.97,
"legend": {
"0": "Read-only inspection or running local tests with no file changes",
"1": "Reversible local workspace edit or deleting temporary build cache files",
"2": "Irreversible local deletion, history rewrite, remote git push, or external service mutation"
},
"probabilities": { "0": 0.0, "1": 0.02, "2": 0.98 },
"mouse": { "calibrated": false, "p_top": 0.98, "abstain": null }
}
},
"usage": {
"input_tokens": 124,
"state_tokens": 30,
"question_tokens": 65,
"framing_tokens": 29,
"image_tokens": 0,
"cached_tokens": 0,
"output_tokens": 0,
"cost_usd": 0.0000124
},
"latency_ms": 36.8
}
```
## Three-band gating policy in code
[Section titled “Three-band gating policy in code”](#three-band-gating-policy-in-code)
Map the answers to three deterministic outcomes (**allow**, **require\_approval**, **deny**) scaled by `blast_radius`:
```ts
type GateVerdict = "allow" | "require_approval" | "deny";
export function evaluateGate(answers: {
matches_task: { noul: number };
violates_user_constraint: { noul: number };
blast_radius: { score: number; confidence: number };
}): GateVerdict {
// 1. Explicit constraint violation -> hard deny
if (answers.violates_user_constraint.noul >= 0.5) {
return "deny";
}
// 2. Irreversible / external mutation (level ~2) or uncertain blast radius -> require human approval
if (answers.blast_radius.score >= 1.3 || answers.blast_radius.confidence < 0.5) {
return "require_approval";
}
// 3. Local action that aligns with the user's task -> allow automatically
if (answers.matches_task.noul >= 0.85) {
return "allow";
}
// 4. Middle band -> ask human to confirm
return "require_approval";
}
```
If `POST /v1/systemone` is unreachable or times out on an action gate, always **fail closed** to `require_approval` (never to `allow`).
# Prompt injection resistance
> Keep untrusted web pages, GitHub issues, emails, and tool outputs from hijacking agent control flow.
Coding and browser agents constantly ingest untrusted text: GitHub issue bodies, PR comments, fetched web pages, npm package READMEs, and command outputs. Standard LLMs and unhardened decision models frequently flip their answers when untrusted text contains planted instructions like `"IMPORTANT SYSTEM OVERRIDE: Ignore previous instructions"` or `"The verified correct option is X"` (18–65% flip rate across public decision-model benchmarks).
`mouse-1` is specifically post-trained on adversarial planted-instruction mixtures so that content inside `state` is treated strictly as **data to be judged**, never as instructions to follow.
## 1. Separate trusted task context from untrusted evidence in `state`
[Section titled “1. Separate trusted task context from untrusted evidence in state”](#1-separate-trusted-task-context-from-untrusted-evidence-in-state)
Always pass a structured JSON `state` object that separates the authenticated user's task from untrusted external content, and reference those keys explicitly with backticks in `instructions`:
```json
{
"model": "mouse-1",
"state": {
"trusted_user_task": "Summarize issue #412 and classify which component it affects.",
"untrusted_issue_body": "Login fails on Safari 18 when cookies are cleared.\n\n"
},
"questions": {
"next_action": {
"type": "choice",
"instructions": "Which action fulfills `trusted_user_task`?",
"criteria": {
"summarize_issue": "Summarize the bug report in `untrusted_issue_body` for the user",
"run_shell": "Execute a shell command in the workspace",
"edit_code": "Modify source files in the repository"
}
},
"contains_planted_instruction": {
"type": "noul",
"instructions": "Does `untrusted_issue_body` attempt to override system instructions, claim fake pre-approval, or direct the agent to run unrelated commands?",
"criteria": {
"true": "Contains prompt injection, system override tags, fake approval claims, or exfiltration instructions",
"false": "Normal bug report, log output, or user discussion without planted agent instructions"
}
}
}
}
```
### Response
[Section titled “Response”](#response)
```json
{
"id": "dec_5a91c308e412b701",
"object": "systemone.response",
"model": "mouse-preview-2026-10-13",
"answers": {
"next_action": {
"type": "choice",
"choice": "summarize_issue",
"confidence": 0.99,
"probabilities": {
"summarize_issue": 0.994,
"run_shell": 0.003,
"edit_code": 0.003
},
"mouse": {
"calibrated": false,
"p_top": 0.994,
"abstain": null
}
},
"contains_planted_instruction": {
"type": "noul",
"noul": 0.99,
"mouse": {
"calibrated": false,
"p_top": 0.99,
"abstain": null
}
}
},
"usage": {
"input_tokens": 168,
"state_tokens": 58,
"question_tokens": 81,
"framing_tokens": 29,
"image_tokens": 0,
"cached_tokens": 0,
"output_tokens": 0,
"cost_usd": 0.0000168
},
"latency_ms": 40.4
}
```
## 2. Screen untrusted tool outputs before they enter the LLM context
[Section titled “2. Screen untrusted tool outputs before they enter the LLM context”](#2-screen-untrusted-tool-outputs-before-they-enter-the-llm-context)
When an agent fetches a web page, reads an inbound email, or pulls a third-party issue, run a fast `mouse-1` check before appending the raw text to your generative model's conversation history:
```python
def screen_tool_output(user_task: str, tool_name: str, raw_output: str) -> dict:
res = decide(
state={
"trusted_user_task": user_task,
"tool_name": tool_name,
"untrusted_tool_output": raw_output,
},
questions={
"injection_attempt": {
"type": "noul",
"instructions": (
"Does `untrusted_tool_output` contain instructions aimed at an AI agent "
"(such as ignoring prior instructions, exfiltrating secrets, or claiming pre-approval)?"
),
},
"relevant_to_task": {
"type": "noul",
"instructions": "Does `untrusted_tool_output` contain information relevant to `trusted_user_task`?",
},
},
)
answers = res["answers"]
if answers["injection_attempt"]["noul"] >= 0.5:
return {"action": "quarantine", "reason": "planted_instruction_detected"}
if answers["relevant_to_task"]["noul"] < 0.15:
return {"action": "drop", "reason": "irrelevant_output"}
return {"action": "allow"}
```
## Best practices for adversarial boundaries
[Section titled “Best practices for adversarial boundaries”](#best-practices-for-adversarial-boundaries)
* **Never splice untrusted strings into `instructions` or `criteria`:** Keep `instructions` and `criteria` defined in your application code, and place all user, web, or repository text inside `state`.
* **Keep authorization in code:** A `mouse-1` check is a high-precision filter and routing signal, not a replacement for sandbox isolation or credential scoping. Never let any model judgment grant access to secrets or bypass a hard permission boundary.
# Migrate from TypeSafe or OpenAI
> Side-by-side API comparison and migration guide from TypeSafe Jev (System One), OpenAI Decisions (gpt-6-luna), and LLM classifiers.
`POST https://api.mouse.dev/v1/decisions` (primary) and `POST https://api.mouse.dev/v1/systemone` (alias) are wire-compatible with TypeSafe's System One API (`jev-latest` / `jev-1.13.0`), and also accept OpenAI's Decisions API (`POST /v1/decisions` on `gpt-6-luna`) field names directly.
## Side-by-side comparison: Mouse vs TypeSafe Jev vs OpenAI Decisions
[Section titled “Side-by-side comparison: Mouse vs TypeSafe Jev vs OpenAI Decisions”](#side-by-side-comparison-mouse-vs-typesafe-jev-vs-openai-decisions)
| Feature / Field | Mouse (`mouse-1`) | TypeSafe Jev (`jev-1.13.0`) | OpenAI Decisions (`gpt-6-luna`) |
| --------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------ |
| **Endpoint** | `POST https://api.mouse.dev/v1/decisions` (alias `/v1/systemone`) | `POST https://api.typesafe.ai/v1/systemone` | `POST https://api.openai.com/v1/decisions` |
| **Model name** | `"mouse-1"` (snapshot `mouse-preview-2026-10-13`) | `"jev-latest"` (`jev-1.13.0`) | `"gpt-6-luna"` |
| **Input / context field** | `state` (string, JSON object, or array) | `state` (string, JSON object, or array) | `input` (string or user message array) |
| **Questions container** | `questions` object map (`{ "": Question }`) | `questions` object map (`{ "": Question }`) | `questions` array (`[{ "name": "", ... }]`) |
| **Binary yes/no primitive** | `type: "noul"` → `answer.noul` (`0..1`) | `type: "noul"` → `answer.noul` (`0..1`) | `type: "predicate"` → `answer.probability` (`0..1`) |
| **Multi-option primitive** | `type: "choice"`, `criteria: { key: desc }` (up to **1,024** options) | `type: "choice"`, `criteria: { key: desc }` (up to **255** options) | `type: "choice"`, `choices: [{ value, description }]` |
| **Choice probabilities shape** | Object map: `{ "billing": 0.91, "shipping": 0.09 }` | Object map: `{ "billing": 0.91, "shipping": 0.09 }` | Array: `[{ "value": "billing", "probability": 0.91 }, ...]` |
| **Ordinal rubric primitive** | `type: "score"`, `criteria: [lvl0, lvl1, ...]` (2–10 levels) | `type: "score"`, `criteria: [lvl0, lvl1, ...]` (2–10 levels) | `type: "score"`, `levels: [{ label, description }]` |
| **Score probabilities shape** | Object map: `{ "0": 0.1, "1": 0.7, "2": 0.2 }` + `legend` | Object map: `{ "0": 0.1, "1": 0.7, "2": 0.2 }` + `legend` | Array: `[{ "value": 0, "label": "...", "probability": 0.1 }, ...]` |
| **Confidence formulas** | System One `choice` & `score` formulas + `mouse.p_top` | System One `choice` & `score` formulas | Undocumented internal confidence scalar |
| **Refusal answer type** | Never refuses (`choice`, `noul`, or `score` always returned) | Never refuses | Can return `{"type": "refusal", "name": "..."}` |
| **First-party calibration** | `POST /v1/calibrations` → signed `calp1.` profile + `mouse.abstain` | None (manual thresholding only) | None (manual thresholding only) |
| **Max state / context** | **65,536 state tokens** (422 on overflow, never truncated) | 32k state+longest question / 64k request | Not published |
| **Rate limits** | **200 RPS** (burst 400), **20M TPM** published on every key | 80 RPS / 100K TPS ("adjusting dynamically") | Tier-based RPM |
| **Price per 1M tokens** | **$0.10** input · **$0.02** cached (5m) · **$0** output | $0.042 input · no cache tier · $0 output | $0.10 input (+10% regional) · no cache discount · $0 output |
| **Itemized `usage` & `cost_usd`** | `input_tokens`, `state_tokens`, `question_tokens`, `framing_tokens`, `cached_tokens`, `cost_usd` | `input_tokens`, `output_tokens` | Token counts only |
| **Zero Data Retention (ZDR)** | **Default for every customer** (`Mouse-Data-Retention: none`) | Enterprise plan via sales only | Eligible customers with prior approval |
***
## 1. Migrating from TypeSafe Jev (`POST /v1/systemone`)
[Section titled “1. Migrating from TypeSafe Jev (POST /v1/systemone)”](#1-migrating-from-typesafe-jev-post-v1systemone)
Because Mouse implements the exact `/v1/systemone` request, answer, and confidence specification, migrating from Jev requires changing only the **base URL** and **model name**:
```diff
const res = await fetch("https://api.typesafe.ai/v1/systemone", {
const res = await fetch("https://api.mouse.dev/v1/systemone", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TYPESAFE_API_KEY}`,
Authorization: `Bearer ${process.env.MOUSE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "jev-latest",
model: "mouse-1",
state,
questions,
}),
});
```
Or with `@typesafe-ai/sdk` / `typesafe-sdk`:
```ts
import { TypeSafeClient } from "@typesafe-ai/sdk";
const client = new TypeSafeClient({
apiKey: process.env.MOUSE_API_KEY,
baseURL: "https://api.mouse.dev",
defaultModel: "mouse-1",
});
```
### What stays identical vs what improves
[Section titled “What stays identical vs what improves”](#what-stays-identical-vs-what-improves)
* **Identical:** `questions` (`choice`, `noul`, `score`), structured `instructions` and `criteria`, `answers..choice`, `answers..noul`, `answers..score`, `probabilities`, `legend`, and the exact `confidence` formulas.
* **Additional fields (ignored by existing SDKs):** Each answer includes `logits` and `mouse: { calibrated, p_top, abstain, confidence_winning, confidence_normalized }` (`noul` answers also gain `probabilities`), and `usage` includes `state_tokens`, `question_tokens`, `framing_tokens`, `cached_tokens`, and `cost_usd`.
* **Re-calibrating thresholds & shadow replay:** Even though `confidence` uses the same formula over `probabilities`, `mouse-1` and `jev-1.13.0` have different underlying model weights. A threshold tuned on Jev does not transfer. Replay your existing Jev or OpenAI JSONL logs with `apps/decide/scripts/shadow_migrate.py` (`MOUSE_API_KEY=sk_mouse_... python scripts/shadow_migrate.py logs.jsonl --calibrate`) to inspect top-1 agreement rate, confidence shift, 10-bin ECE, and fit a temperature and an out-of-fold cutoff for `mouse-1` via [`POST /v1/calibrations`](/docs/guides/calibrate), then branch on `mouse.abstain`.
* **Option order:** If you saw answers flip when options were reordered, pass `order_invariant: true` and Mouse averages two passes server-side.
***
## 2. Migrating from OpenAI Decisions (`POST /v1/decisions`)
[Section titled “2. Migrating from OpenAI Decisions (POST /v1/decisions)”](#2-migrating-from-openai-decisions-post-v1decisions)
OpenAI's Decisions API uses the same three conceptual primitives under slightly different field names:
1. Change the base URL to `https://api.mouse.dev` (`POST /v1/decisions` is served) and `"model": "gpt-6-luna"` to `"model": "mouse-1"`.
2. Send the request body unchanged. Mouse reads the OpenAI field names as aliases (`input` → `state`; the `questions` array keyed by `name`; `predicate` → `noul`; `choices: [{ value, description }]` and `levels: [{ label, description }]` → `criteria`). The canonical map form below is what the request becomes; you can convert at your own pace.
3. Read the Mouse response shape: `answers` is a map keyed by your question `name`s, a `predicate` comes back as `answers..noul` (not `answers[i].probability`), and `choice` probabilities are a map (`answers..probabilities[value]`).
4. Remove `answer.type === "refusal"` branches—`mouse-1` never refuses a valid schema and always returns a probability distribution over your options.
### Before (OpenAI `POST /v1/decisions`)
[Section titled “Before (OpenAI POST /v1/decisions)”](#before-openai-post-v1decisions)
```json
{
"model": "gpt-6-luna",
"input": "I was charged twice for my order.",
"questions": [
{
"type": "choice",
"name": "department",
"instructions": "Which department should handle this complaint?",
"choices": [
{ "value": "billing", "description": "Payments, invoices, and refunds." },
{ "value": "technical", "description": "Problems using the product." },
{ "value": "shipping", "description": "Delivery and tracking." },
{ "value": "other", "description": "Requests outside these categories." }
]
},
{
"type": "predicate",
"name": "refund_requested",
"instructions": "Does the customer ask for a refund or report a duplicate charge?"
}
]
}
```
### After (Mouse `POST /v1/decisions`)
[Section titled “After (Mouse POST /v1/decisions)”](#after-mouse-post-v1decisions)
```json
{
"model": "mouse-1",
"state": "I was charged twice for my order.",
"questions": {
"department": {
"type": "choice",
"instructions": "Which department should handle this complaint?",
"criteria": {
"billing": "Payments, invoices, and refunds.",
"technical": "Problems using the product.",
"shipping": "Delivery and tracking.",
"other": "Requests outside these categories."
}
},
"refund_requested": {
"type": "noul",
"instructions": "Does the customer ask for a refund or report a duplicate charge?"
}
}
}
```
***
## 3. Migrating from generative LLM classifiers (`chat/completions` / `responses`)
[Section titled “3. Migrating from generative LLM classifiers (chat/completions / responses)”](#3-migrating-from-generative-llm-classifiers-chatcompletions--responses)
If you currently prompt a chat model (`claude-haiku-4-5`, `gpt-5.6`, `gemini-flash`) with a JSON schema or forced tool call to classify intents, gate actions, or score outputs:
1. Move your dynamic prompt variables into `state` as a clean JSON object (no prompt template boilerplate needed).
2. Turn each boolean schema field into a `noul` question, each enum field into a `choice` question, and each 1–5 rating field into a `score` question with concrete level descriptions.
3. Delete your JSON parser and retry-on-malformed-output loop: `mouse-1` evaluates all questions in one forward pass (\~40–100 ms) with `$0` output token cost and returns calibrated probabilities for every option.
# Tool routing
> Route agent turns across tools, MCP servers, and skills in one forward pass with speculative fan-out.
In an agent loop, invoking a full reasoning LLM just to decide whether a turn needs repository search, a file edit, a test run, or a clarifying question wastes 1–3 seconds and thousands of output tokens. With `mouse-1`, you pass the user's request and current workspace summary in `state`, attach a `choice` question over candidate tools alongside companion `noul` questions, and route in \~40–100 ms.
## Pattern: speculative fan-out for agent routing
[Section titled “Pattern: speculative fan-out for agent routing”](#pattern-speculative-fan-out-for-agent-routing)
Because every question in `POST /v1/systemone` runs in parallel against the same `state`, ask your primary routing `choice` question **and** any follow-up `noul` checks in a single call:
1. `next_tool` (`choice`): Which tool or handler should run next?
2. `needs_sandbox` (`noul`): Does this turn require booting a code execution sandbox?
3. `needs_clarification` (`noul`): Would one missing fact from the user materially change the implementation?
### Request
[Section titled “Request”](#request)
```json
{
"model": "mouse-1",
"state": {
"user_prompt": "Rename exported function `parseToken` to `verifyToken` in src/auth.ts and update all callers.",
"recent_files": ["src/auth.ts", "src/http/middleware.ts"],
"sandbox_status": "hibernated"
},
"questions": {
"next_tool": {
"type": "choice",
"instructions": "Which tool should handle `user_prompt` first?",
"criteria": {
"grep_search": "Find all callers or references across the repository before editing",
"edit_file": "Modify a single known file when all locations are already in context",
"run_tests": "Run the test suite to verify changes",
"ask_user": "Ask the user to clarify an ambiguous requirement",
"direct_reply": "Answer directly without touching the repository"
}
},
"needs_sandbox": {
"type": "noul",
"instructions": "Does fulfilling `user_prompt` require reading or modifying files in the repository?"
},
"needs_clarification": {
"type": "noul",
"instructions": "Is `user_prompt` missing a required decision that only the user can make?"
}
}
}
```
### Response
[Section titled “Response”](#response)
```json
{
"id": "dec_21b90e4a7c12f803",
"object": "systemone.response",
"model": "mouse-preview-2026-10-13",
"answers": {
"next_tool": {
"type": "choice",
"choice": "grep_search",
"confidence": 0.85,
"probabilities": {
"grep_search": 0.88,
"edit_file": 0.10,
"run_tests": 0.01,
"ask_user": 0.01,
"direct_reply": 0.0
},
"mouse": {
"calibrated": false,
"p_top": 0.88,
"abstain": null
}
},
"needs_sandbox": {
"type": "noul",
"noul": 0.99,
"mouse": {
"calibrated": false,
"p_top": 0.99,
"abstain": null
}
},
"needs_clarification": {
"type": "noul",
"noul": 0.02,
"mouse": {
"calibrated": false,
"p_top": 0.98,
"abstain": null
}
}
},
"usage": {
"input_tokens": 132,
"state_tokens": 31,
"question_tokens": 68,
"framing_tokens": 33,
"image_tokens": 0,
"cached_tokens": 0,
"output_tokens": 0,
"cost_usd": 0.0000132
},
"latency_ms": 42.1
}
```
## Branching in TypeScript
[Section titled “Branching in TypeScript”](#branching-in-typescript)
Use `confidence` (or `mouse.abstain` when using a [calibration profile](/docs/concepts/calibration)) as a second axis: dispatch directly when confidence is high, and fall back to your full reasoning model when confidence is low.
```ts
const { answers } = await decide(state, questions);
// 1. Pre-warm sandbox in parallel if repository access is needed
if (answers.needs_sandbox.noul >= 0.6) {
void ensureSandboxWarmed(threadId);
}
// 2. If the prompt is genuinely ambiguous, ask the user before burning tokens
if (answers.needs_clarification.noul >= 0.85) {
return promptUserForClarification(state.user_prompt);
}
// 3. Confidence-gated tool dispatch
const route = answers.next_tool;
if (route.mouse.abstain === true || route.confidence < 0.65) {
// Uncertain route -> let the full reasoning model plan the turn
return runFullReasoningTurn(state);
}
return dispatchTool(route.choice, state);
```
## Routing across large skill or MCP catalogs (100–1,000 options)
[Section titled “Routing across large skill or MCP catalogs (100–1,000 options)”](#routing-across-large-skill-or-mcp-catalogs-1001000-options)
Because `choice` supports up to **1,024 options per question**, you can pass an entire catalog of MCP tools or agent skills in a single `choice` question:
1. **Request 1 (Rank & Gate):** Pass all skill names and 1-line descriptions in a `choice` question alongside an `any_skill_applies` (`noul`) question.
2. **Shortlist in code:** If `any_skill_applies.noul < 0.4`, load zero skills. Otherwise, take the top 3 skills by `probabilities[skill]` and inject only their full instructions into the agent's context.
# Verifying tool results
> Check whether diffs and test logs satisfy task requirements using atomic noul and composite score questions.
Coding agents frequently claim a task is complete when a test actually failed, when a check was skipped, or when the diff only fixed a symptom. Before ending an agent loop or opening a pull request, evaluate the agent's claim against the actual command output and diff summary in a single `POST /v1/systemone` call.
## Pattern: atomic verification + composite scoring
[Section titled “Pattern: atomic verification + composite scoring”](#pattern-atomic-verification--composite-scoring)
Break completion verification into independent, falsifiable dimensions:
1. `logs_support_claim` (`noul`): Does `test_log` support the agent's `claim`?
2. `games_verification` (`noul`): Does the diff weaken, delete, or skip existing assertions to make tests pass?
3. `addresses_objective` (`score`, 3 levels): How completely does the diff address the user's `objective`?
4. `scope_discipline` (`score`, 3 levels): Did the diff stay focused on `objective` without unrelated churn?
### Request
[Section titled “Request”](#request)
```json
{
"model": "mouse-1",
"state": {
"objective": "Reject expired API keys with HTTP 401 in PgKeyStore._lookup.",
"agent_claim": "Implemented expiry check and all unit tests pass.",
"diff_summary": "Modified mouse_api/auth.py (+4 -0) and tests/test_api.py (+12 -0).",
"test_log": "15 passed, 1 failed in 0.84s\nFAILED tests/test_api.py::test_expired_key_is_401 - AssertionError: 200 != 401"
},
"questions": {
"logs_support_claim": {
"type": "noul",
"instructions": "Does `test_log` support `agent_claim`?"
},
"games_verification": {
"type": "noul",
"instructions": "Does `diff_summary` weaken, skip, or delete tests to force a passing result?"
},
"addresses_objective": {
"type": "score",
"instructions": "How well does the work satisfy `objective` given `diff_summary` and `test_log`?",
"criteria": [
"Does not address the objective or failing tests contradict the fix",
"Partially addresses the objective or lacks verification",
"Directly satisfies the objective and all relevant checks pass"
]
},
"scope_discipline": {
"type": "score",
"instructions": "How focused is `diff_summary` relative to `objective`?",
"criteria": [
"Contains unrelated file edits or unrequested refactoring",
"Mostly focused with minor collateral changes",
"Touches only the files and lines required by the objective"
]
}
}
}
```
### Response
[Section titled “Response”](#response)
```json
{
"id": "dec_6c18f204a93d110e",
"object": "systemone.response",
"model": "mouse-preview-2026-10-13",
"answers": {
"logs_support_claim": {
"type": "noul",
"noul": 0.01,
"mouse": { "calibrated": false, "p_top": 0.99, "abstain": null }
},
"games_verification": {
"type": "noul",
"noul": 0.02,
"mouse": { "calibrated": false, "p_top": 0.98, "abstain": null }
},
"addresses_objective": {
"type": "score",
"score": 0.14,
"confidence": 0.79,
"legend": {
"0": "Does not address the objective or failing tests contradict the fix",
"1": "Partially addresses the objective or lacks verification",
"2": "Directly satisfies the objective and all relevant checks pass"
},
"probabilities": { "0": 0.86, "1": 0.14, "2": 0.0 },
"mouse": { "calibrated": false, "p_top": 0.86, "abstain": null }
},
"scope_discipline": {
"type": "score",
"score": 1.94,
"confidence": 0.91,
"legend": {
"0": "Contains unrelated file edits or unrequested refactoring",
"1": "Mostly focused with minor collateral changes",
"2": "Touches only the files and lines required by the objective"
},
"probabilities": { "0": 0.0, "1": 0.06, "2": 0.94 },
"mouse": { "calibrated": false, "p_top": 0.94, "abstain": null }
}
},
"usage": {
"input_tokens": 212,
"state_tokens": 58,
"question_tokens": 116,
"framing_tokens": 38,
"image_tokens": 0,
"cached_tokens": 0,
"output_tokens": 0,
"cost_usd": 0.0000212
},
"latency_ms": 44.0
}
```
## Normalizing and combining composite scores in code
[Section titled “Normalizing and combining composite scores in code”](#normalizing-and-combining-composite-scores-in-code)
When combining multiple `score` questions, divide each `score` by `len(criteria) - 1` so every dimension sits on `0.0 .. 1.0`, and keep hard verification gates (`logs_support_claim`, `games_verification`) as non-compensatory vetoes:
```python
def verify_completion(answers: dict) -> dict:
# 1. Hard vetoes: never average away a contradicted log or a gamed test
if answers["logs_support_claim"]["noul"] < 0.7:
return {
"done": False,
"reason": "test_log contradicts agent_claim; fix the failing check before finishing.",
}
if answers["games_verification"]["noul"] >= 0.3:
return {
"done": False,
"reason": "Diff appears to weaken or skip verification checks.",
}
# 2. Normalize each 3-level score (0..2) to 0.0..1.0 and compute weighted composite
obj_norm = answers["addresses_objective"]["score"] / 2.0
scope_norm = answers["scope_discipline"]["score"] / 2.0
composite = 0.75 * obj_norm + 0.25 * scope_norm
return {
"done": composite >= 0.80,
"composite_score": round(composite, 3),
}
```
# Errors
> Single error envelope, status codes, ZDR error handling, and retry policy.
Every non-2xx response from `https://api.mouse.dev` uses a single JSON error envelope. Error messages name the offending field or limit and **never echo your request content** (`state`, `instructions`, or `criteria`).
## Error envelope
[Section titled “Error envelope”](#error-envelope)
```json
{
"error": {
"type": "invalid_request_error",
"code": "state_too_long",
"message": "state is 71,204 tokens; mouse-preview-2026-10-13 reads at most 65,536. Nothing was truncated or billed.",
"param": "state"
}
}
```
| Field | Type | Description |
| --------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `error.type` | `string` | Broad error category: `invalid_request_error`, `authentication_error`, `billing_error`, `rate_limit_error`, or `api_error`. |
| `error.code` | `string` | Machine-readable error code for programmatic branching (see table below). |
| `error.message` | `string` | Human-readable explanation naming the limit or parameter. Never contains request content. |
| `error.param` | `string` (optional) | Dotted path of the offending request field (e.g. `"state"`, `"questions.intent.criteria"`, `"examples"`) or header (`"authorization"`). |
Every error response includes `Mouse-Request-Id`, `Mouse-Region`, `Mouse-Data-Retention: none`, and `Cache-Control: no-store`. `4xx` responses are never billed.
## HTTP status codes and error codes
[Section titled “HTTP status codes and error codes”](#http-status-codes-and-error-codes)
| Status | `error.type` | `error.code` | `param` | Description & resolution |
| ------ | ----------------------- | ------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | `invalid_request_error` | `invalid_calibration_profile` | `calibration` | The `calibration` token is malformed, truncated, tampered with, or signed with a retired key. Fit a new profile with `POST /v1/calibrations`. |
| `401` | `authentication_error` | `missing_api_key` | — | Missing `Authorization: Bearer sk_mouse_...` header. |
| `401` | `authentication_error` | `invalid_api_key` | — | Key does not start with `sk_mouse_`, fails its CRC32 checksum, is expired, or has been revoked. |
| `402` | `billing_error` | `insufficient_credits` | `authorization` | Prepaid credit balance is `$0.00`. Top up or enable auto-reload at [`/console/billing`](/console/billing). |
| `404` | `invalid_request_error` | `model_not_found` | — | Unknown `model` string. Query `GET /v1/models` for active aliases (`mouse-1`, `mouse`) and snapshots. |
| `409` | `invalid_request_error` | `profile_model_mismatch` | — | The supplied `calibration` profile was fitted on a different model snapshot than the one serving this request. Pin the model version or refit the profile. |
| `413` | `invalid_request_error` | `request_too_large` | `body` | Raw HTTP request body exceeds **4,000,000 bytes** (4 MB). |
| `422` | `invalid_request_error` | `invalid_schema` | `` | Request body failed schema validation (e.g. missing `state` or `questions`, unknown field inside a question, `choice` `criteria` passed as an array instead of an object, or invalid calibration `label`). |
| `422` | `invalid_request_error` | `state_too_long` | `state` | `state` exceeds **65,536 tokens**. Nothing is truncated or billed; trim `state` to the fields relevant to your questions. |
| `422` | `invalid_request_error` | `too_many_questions` | `questions` | More than **128 questions** in a single request. |
| `422` | `invalid_request_error` | `too_many_options` | `questions..criteria` | A question has more than **1,024 options**. |
| `422` | `invalid_request_error` | `too_few_examples` | `examples` | Fewer than **200 examples** sent to `POST /v1/calibrations`. |
| `422` | `invalid_request_error` | `too_many_examples` | `examples` | More than **5,000 examples** sent to `POST /v1/calibrations`. |
| `422` | `invalid_request_error` | `too_few_examples_per_question` | `examples` | No question signature in `POST /v1/calibrations` had at least **50 labeled examples**. |
| `429` | `rate_limit_error` | `rate_limited` | — | Exceeded the per-key request rate limit (**200 RPS**, burst **400**). Includes `Retry-After`. |
| `429` | `rate_limit_error` | `token_rate_limited` | — | Exceeded the per-key input token rate limit (**20,000,000 TPM**). Includes `Retry-After`. |
| `503` | `api_error` | `engine_timeout` | — | The GPU worker did not respond within the 10-second engine deadline. Includes `Retry-After: 2`. |
## Handling retries and timeouts
[Section titled “Handling retries and timeouts”](#handling-retries-and-timeouts)
When calling `POST /v1/systemone` on a user-facing or hot agent path:
1. **Do not retry `400`, `401`, `402`, `404`, `409`, `413`, or `422`:** These are deterministic client errors; fix the request or fall back to baseline harness logic immediately.
2. **Honor `Retry-After` on `429` and `503`:** Both `429 Too Many Requests` and `503 Service Unavailable` include a `Retry-After` header (in integer seconds). On background or batch paths, retry with exponential backoff + jitter after `Retry-After`.
3. **Bound total latency on critical paths:** In interactive agent loops (such as tool-call gating or message routing), set a per-request deadline (e.g. `1,500 ms` to `3,000 ms`) with `0` or `1` retry and fall back to your deterministic floor if the call times out.
```ts
async function systemOneWithFallback(payload: unknown) {
const res = await fetch("https://api.mouse.dev/v1/systemone", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MOUSE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(2_500),
});
if (res.status === 429 || res.status === 503) {
const retryAfterSec = Number(res.headers.get("retry-after") ?? "1");
await new Promise((r) => setTimeout(r, retryAfterSec * 1_000));
// Optional single retry for background jobs
}
if (!res.ok) {
const { error } = await res.json();
throw new Error(`[${error.code}] ${error.message}`);
}
return res.json();
}
```
# Rate limits
> Published payload limits, token-bucket rate limits, and x-ratelimit-* headers.
Every standard API key receives the same published payload and throughput limits. Nothing is silently truncated: any request exceeding a payload limit is refused before GPU execution with `422 Unprocessable Entity` (or `413 Content Too Large`) and is never billed.
## Request and payload limits
[Section titled “Request and payload limits”](#request-and-payload-limits)
| Parameter | Limit | Error code when exceeded |
| --------------------------------------- | -------------------------- | -------------------------------------------------------------------------- |
| `state` length | **65,536 tokens** | `422 state_too_long` |
| `questions` per request | **128 questions** | `422 too_many_questions` |
| Options per `choice` question | **1,024 options** | `422 too_many_options` |
| Levels per `score` question | **2 to 10 levels** | `422 invalid_schema` |
| HTTP request body size | **4,000,000 bytes** (4 MB) | `413 request_too_large` |
| Calibration `examples` per call | **200 to 5,000 examples** | `422 too_few_examples` / `too_many_examples` |
| Labeled examples per question signature | **≥ 50 examples** | Skipped in report (or `422 too_few_examples_per_question` if none qualify) |
| Engine execution timeout | **10.0 seconds** | `503 engine_timeout` |
## Throughput rate limits
[Section titled “Throughput rate limits”](#throughput-rate-limits)
Throughput is enforced per API key using two shared Redis token buckets: a **requests-per-second (RPS)** bucket with a 2× burst capacity, and an **input-tokens-per-minute (TPM)** bucket.
| Key kind | Requests per second | Request burst | Input tokens per minute |
| ------------------------------- | ------------------- | ---------------- | ----------------------- |
| Standard key (`sk_mouse_...`) | **200 RPS** | **400 requests** | **20,000,000 TPM** |
| Console Playground key (1h TTL) | **5 RPS** | **5 requests** | **20,000,000 TPM** |
Need higher sustained RPS or TPM for a batch workload or gateway integration? Contact [sales@mouse.dev](mailto:sales@mouse.dev?subject=Mouse%20API%20Committed%20Throughput) with your key prefix (`sk_mouse_xxxx…`) to provision reserved RPS/TPM capacity on a committed-throughput purchase order at the **same `$0.10 / 1M` input and `$0.02 / 1M` cached token rate**—no enterprise markup.
## Rate limit headers
[Section titled “Rate limit headers”](#rate-limit-headers)
Every authenticated response includes rate-limit state headers:
| Header | Description | Example |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
| `x-ratelimit-limit-requests` | Sustained requests-per-second limit for this key. | `200` |
| `x-ratelimit-remaining-requests` | Remaining requests available in the burst bucket right now. | `399` |
| `x-ratelimit-reset-requests` | Window reset interval in seconds (`1`). | `1` |
| `x-ratelimit-limit-tokens` | Input tokens-per-minute capacity for this key. | `20000000` |
| `x-ratelimit-remaining-tokens` | Remaining input tokens available in the 60-second bucket. | `19999926` |
| `x-ratelimit-reset-tokens` | Window reset interval in seconds (`60`). | `60` |
| `Retry-After` | Included on `429` (`rate_limited`, `token_rate_limited`) and `503` (`engine_timeout`) responses. Integer seconds until enough bucket capacity refills. | `1` |
# Pricing and billing
> Transparent single price list, exact token accounting formula, prefix caching, and prepaid billing.
Mouse has one price list for every customer. There are no seat fees, tiers, per-request minimums, or regional surcharges. Zero data retention and `POST /v1/calibrations` profile application are included at no extra charge.
## Rate card
[Section titled “Rate card”](#rate-card)
**The only decision API with a cache discount** — cached tokens at open-weight prices (`$0.02 / 1M`), with native probabilities, no regional surcharge, and no ZDR gate.
| Token category | Price per 1M tokens | Notes |
| ----------------------------------------------------------------- | ------------------- | --------------------------------------------------------------------------- |
| **Input tokens** (`state`, `instructions`, `criteria`, `framing`) | **$0.10** | `$0.0000001` per token (`$0.0001` per 1,000 tokens) |
| **Cached input tokens** (repeated prefix within \~5 minutes) | **$0.02** | **80% discount** (`$0.00000002` per token; no cache-write fee) |
| **Output tokens** | **$0.00** | Free—`mouse-1` scores options in one pass and never generates output tokens |
### Same price for everyone
[Section titled “Same price for everyone”](#same-price-for-everyone)
Every customer pays the same published rate card:
* **No enterprise markup or tier gate:** Zero Data Retention (`Mouse-Data-Retention: none`) and `POST /v1/calibrations` are included at list price (unlike Vercel's `$0.10 / 1k` ZDR surcharge or enterprise-only ZDR gates).
* **No regional surcharge:** Currently served from **US-East** (`Mouse-Region: us`) at `$0.10 / $0.02` (unlike OpenAI Decisions' `+10%` regional processing multiplier).
* **No cache-write fee:** Writing into the prefix cache costs `$0.00` extra.
* **Cost basis:** We publish our H100 prefill cost basis once measured on the attached production checkpoint rather than asserting unverified figures.
## Cache break-even against other decision APIs
[Section titled “Cache break-even against other decision APIs”](#cache-break-even-against-other-decision-apis)
With uncached input at `$0.10 / 1M` and cached input at `$0.02 / 1M`, your effective rate per 1M input tokens is:
\[ \text{effective\_rate}(f) = $0.10 - $0.08 \cdot f ]
where (f \in \[0, 1]) is the fraction of billed input tokens served from cache (visible per day and per key in [`/console/usage`](/console/usage)).
| Comparator | Published input rate | Cache discount | Cache hit fraction (f) needed for `mouse-1` to beat it |
| --------------------------------------- | ---------------------------- | ------------------- | ------------------------------------------------------ |
| Cloudflare Clef (27B) | `$0.24 / 1M` | None | **(f \ge 0%)** (always cheaper) |
| OpenAI Decisions (`gpt-6-luna`) | `$0.10 / 1M` (+10% regional) | None | **(f > 0%)** (any cache hit beats it) |
| TypeSafe Jev (`jev-1.13.0`) | `$0.042 / 1M` | None | **(f > 72.5%)** |
| Microsoft Decision-1 (9B) | `$0.042 / 1M` | None | **(f > 72.5%)** |
| Cloudflare Clef-flash (9B) | `$0.038 / 1M` | None | **(f > 77.5%)** |
| Perplexity `pplx-decider` / Liquid `d1` | `$0.02 / 1M` | None (`$0.02` read) | Parity at (f = 100%) |
### Structure your `state` for cache hits
[Section titled “Structure your state for cache hits”](#structure-your-state-for-cache-hits)
* **Rubric first, varying item last:** Place shared instructions, tool schemas, or policy rubrics at the **start** of `state` and the per-request item at the end. JSON object key order is preserved exactly as sent over the wire.
* **Tenant-isolated, best-effort \~5-minute window:** Cache entries live in ephemeral GPU memory, are scoped to your account, and typically remain warm for \~5 minutes under normal load (evicting earlier under memory pressure).
* **Inspect your own hit rate:** The [Console Usage](/console/usage) page and CSV export break out `input_tokens` and `cached_tokens` per key so you can measure (f) directly on your workload.
## Exact token accounting
[Section titled “Exact token accounting”](#exact-token-accounting)
Unlike providers that bundle hidden system prompts into `input_tokens`, Mouse publishes the exact token accounting formula used by `apps/decide/mouse_api/billing.py` and itemizes every component in the response's `usage` object:
\[ \text{input\_tokens} = \text{state\_tokens} + \text{question\_tokens} + \text{framing\_tokens} + \text{image\_tokens} ]
1. **`state_tokens`:** Tokens in your serialized `state`. Counted and billed **once per request**, whether you ask 1 question or 128 questions.
2. **`question_tokens`:** Sum of tokens across each question's `instructions` and rendered option descriptions (`criteria`).
3. **`framing_tokens`:** Structural delimiters around the request, questions, and options—no hidden system prompt: \[ \text{framing\_tokens} = 4 + 3 \times Q + 2 \times \sum\_{q=1}^{Q} O\_q ] where (Q) is the number of questions and (O\_q) is the number of options in question (q) (`2` for a `noul` question, `len(criteria)` for `choice` and `score`).
4. **`cached_tokens`:** When an identical `state` is evaluated again within the **5-minute (300 s)** in-memory prefix cache window, `cached_tokens` equals `state_tokens`.
5. **`cost_usd`:** Computed to nanodollar precision (`$0.000000001`) using `ROUND_HALF_UP`: \[ \text{cost\_usd} = \frac{(\text{input\_tokens} - \text{cached\_tokens}) \times $0.10 + \text{cached\_tokens} \times $0.02}{1{,}000{,}000} ]
### Worked example: 1 question vs 5-question fan-out vs cached repeat
[Section titled “Worked example: 1 question vs 5-question fan-out vs cached repeat”](#worked-example-1-question-vs-5-question-fan-out-vs-cached-repeat)
Suppose your agent `state` is **1,000 tokens**, and each question (`instructions` + `criteria` + framing) is **40 tokens**:
| Scenario | `state_tokens` | `question_tokens + framing_tokens` | `cached_tokens` | Cost per call | Cost per 1,000 calls |
| ---------------------------------------------- | -------------- | ---------------------------------- | --------------- | ------------- | -------------------------------------------- |
| 1 question (cold state) | 1,000 | 40 | 0 | `$0.0001040` | **$0.104** |
| 5 questions in 1 call (cold state) | 1,000 | 200 | 0 | `$0.0001200` | **$0.120** (vs `$0.520` as 5 separate calls) |
| 5 questions in 1 call (cached state within 5m) | 1,000 | 200 | 1,000 | `$0.0000400` | **$0.040** |
## Calibration billing
[Section titled “Calibration billing”](#calibration-billing)
* Calling `POST /v1/calibrations` bills the tokens of the labeled `examples` at the same `$0.10 / 1M` input (`$0.02 / 1M` cached) rate. Fitting 250 typical 100-token examples costs about **$0.0025** (one quarter of a cent).
* Passing a `calibration` profile token on `POST /v1/systemone` is **free** (0 extra tokens, $0 surcharge).
* Refused requests (`4xx` validation, auth, or rate-limit errors) are **never billed**.
## Prepaid credits and auto-reload
[Section titled “Prepaid credits and auto-reload”](#prepaid-credits-and-auto-reload)
* Verified accounts receive **$5.00 in free starter credits** at signup.
* Spend draws from your prepaid Mouse credit balance ([`/console/billing`](/console/billing)).
* When your balance reaches `$0.00`, API keys return `402 insufficient_credits` within 30 seconds until you top up or auto-reload runs.
* Usage settles asynchronously within \~60–120 seconds, so a high-RPS burst can briefly take a balance slightly below zero; the next top-up reconciles it.
* **Built-in spend cap:** Optional auto-reload charges your saved payment method at most **once per 24 hours**, bounding worst-case spend from a leaked key to your current balance plus one reload amount per day.
# Data retention and security
> Enforced zero data retention (ZDR) by default, in-memory prefix cache architecture, and key security.
Every Mouse API customer gets **Zero Data Retention (ZDR) by default** on the standard API. No enterprise contract, sales approval, or request header is required.
## What Mouse retains vs discards
[Section titled “What Mouse retains vs discards”](#what-mouse-retains-vs-discards)
| Data element | Retained on disk? | Where it lives | Lifecycle |
| ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `state`, `questions`, `instructions`, `criteria` | **No** | Process memory for the active HTTP request | Discarded as soon as the HTTP response is sent |
| `answers`, `probabilities`, `confidence` | **No** | Process memory for the active HTTP request | Discarded as soon as the HTTP response is sent |
| `POST /v1/calibrations` labeled `examples` | **No** | Process memory during 5-fold cross-fitting | Discarded as soon as the signed `profile` is returned |
| Calibration `profile` (`calp1.`) | **No** | Returned to caller, HMAC-SHA256 signed | Held by the caller and passed back on `/v1/systemone` |
| Prefix cache (KV tensors of recent `state`s) | **Memory only** | GPU VRAM on the inference worker, keyed by `SHA-256(state)` | 5-minute TTL; evicted automatically; gone on worker restart; never written to disk |
| Request metadata (`request_id`, `key_id`, `model`, `status`, `input_tokens`, `cached_tokens`, `questions` count, `latency_ms`, `region`) | **Yes** | Structured logs and billing usage rollups | 30 days in console activity logs; billing aggregates as required by law |
## How ZDR is enforced in code
[Section titled “How ZDR is enforced in code”](#how-zdr-is-enforced-in-code)
Zero data retention in `apps/decide` is enforced mechanically rather than by policy alone:
1. **Allow-listed log filter (`MetadataOnly`):** The API logger strips every attribute outside the fixed metadata allow-list (`event`, `request_id`, `key_id`, `path`, `status`, `latency_ms`, `model`, `region`, `input_tokens`, `cached_tokens`, `questions`, `error_code`) and drops any formatted string arguments before serialization. Even an accidental `log.info(..., extra={"state": ...})` cannot emit request content.
2. **Non-echoing validation errors:** When Pydantic rejects a malformed request body (`422 invalid_schema`), the error handler extracts only the field path (`loc`) and schema rule, never the rejected input value. Automated tests place secret canary strings inside invalid `state` and `criteria` payloads and assert they never appear in error bodies or log streams.
3. **Explicit HTTP headers on every response:** Every response carries:
```http
Cache-Control: no-store
Mouse-Data-Retention: none
```
4. **No response cache:** Only input `state` KV tensors live in ephemeral GPU memory (keyed by SHA-256 digest with a 300-second TTL), which matches the industry ZDR standard for stateless in-memory prompt caching.
## Infrastructure, availability SLO, and subprocessors
[Section titled “Infrastructure, availability SLO, and subprocessors”](#infrastructure-availability-slo-and-subprocessors)
* **Serving region:** `https://api.mouse.dev` runs in **US-East** (`Mouse-Region: us`) on stateless CPU API gateways paired with isolated GPU workers on Modal.
* **Availability SLO (preview):** We target **99.5% monthly availability** on `POST /v1/decisions` and `POST /v1/systemone` during the `mouse-preview-2026-10-13` preview, monitored via `GET https://api.mouse.dev/healthz`. Formal contractual SLAs with service credits ship at General Availability after 30 days of continuous production telemetry.
* **No model training on customer traffic:** Customer requests, states, questions, and calibration examples are never used to train or fine-tune models.
* **Key security:** API keys (`sk_mouse_...`) include a 6-character base62 CRC32 checksum verified before any database query, are stored exclusively as `SHA-256` hashes, and are automatically revoked via GitHub secret scanning if leaked to a public repository.
## 12-Question AI-CAIQ Enterprise Security & Trust Reference
[Section titled “12-Question AI-CAIQ Enterprise Security & Trust Reference”](#12-question-ai-caiq-enterprise-security--trust-reference)
For security and procurement reviews (CSA AI-CAIQ), here are direct answers for `https://api.mouse.dev`:
| # | Security review question | Mouse (`api.mouse.dev`) answer |
| -- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 1 | **Is Zero Data Retention (ZDR) enabled by default?** | **Yes.** Every response includes `Mouse-Data-Retention: none` and `Cache-Control: no-store` on all accounts with $0 surcharge. |
| 2 | **Are customer prompts, `state`, or `questions` used for training?** | **Never.** Neither inference requests nor `POST /v1/calibrations` labeled examples are stored or used for training, distillation, or evaluations. |
| 3 | **Where does calibration data live?** | **Stateless in-memory only.** `POST /v1/calibrations` scores examples in memory, fits temperature `T` and `cutoff`, and returns an HMAC-SHA256 signed `calp1.` token. Examples and profiles are immediately discarded. |
| 4 | **How is the in-memory prefix cache isolated across tenants?** | Cache keys are scoped to `(user_id, state)` (`cache_salt` per tenant) in ephemeral GPU memory (\~5m best-effort TTL) and never written to disk, preventing cross-tenant cache probing. |
| 5 | **What is retained in audit logs?** | Only allow-listed operational metadata (`request_id`, `key_id`, `path`, `status`, `model`, `region`, `input_tokens`, `cached_tokens`, `questions` count, `latency_ms`, `error_code`) enforced by `MetadataOnly` in `apps/decide/mouse_api/zdr.py`. |
| 6 | **Can validation errors leak sensitive payload contents?** | **No.** `RequestValidationError` handlers strip rejected values and return only the schema field path (`param`). |
| 7 | **How are API keys stored and protected against leaks?** | Keys (`sk_mouse_...`) carry a base62 CRC32 checksum (invalid keys are rejected before any DB query), are stored as `SHA-256` digests only, and support automatic revocation on public GitHub exposure. |
| 8 | **How is prompt injection inside `state` mitigated?** | `mouse-1` is non-autoregressive (`output_tokens = 0`) and only scores the fixed option keys you supply in `criteria`. Untrusted text placed in `state` cannot invoke tools or emit free-form output. |
| 9 | **How are model versions pinned and deprecated?** | Every response echoes `Mouse-Model-Version: mouse-preview-2026-10-13`. Immutable snapshots receive at least **12 months' notice** (`retire_after` in `GET /v1/models`) before retirement, and `calp1.` profiles reject mismatched snapshots with `409 profile_model_mismatch`. |
| 10 | **What are theserving regions and data residency options?** | Currently **US-East (`us`)** only. We do not charge regional surcharges; EU residency will be announced in `GET /v1/models` and `/healthz` when live. |
| 11 | **How are rate limits and runaway spend bounded?** | Token buckets enforce `200 RPS` (burst `400`) and `20M TPM` per key with standard `x-ratelimit-*` headers. Prepaid balances stop at `$0.00` (`402 insufficient_credits`), and optional auto-reload fires at most **once per 24 hours**. |
| 12 | **How do committed-throughput upgrades work?** | Keys can be upgraded to reserved RPS/TPM capacity via purchase order at the **same `$0.10 / $0.02` per-token rate** (`mailto:sales@mouse.dev`). |
# Changelog
> Releases and updates for mouse-1 and the Mouse API.
## 2026-10-14
[Section titled “2026-10-14”](#2026-10-14)
* Added `POST /v1/decisions` as the primary neutral decision endpoint alongside `POST /v1/systemone` (wire-compatible alias).
* Updated cached input token pricing to `$0.02 / 1M` (80% discount from `$0.10 / 1M`, with no cache-write fee) and tenant-scoped prefix cache keys.
* Added out-of-fold temperature scaling (`after.temperature`) to `POST /v1/calibrations` profiles (`calp1.`) and `scripts/shadow_migrate.py` for replaying Jev and OpenAI Decisions logs.
## 2026-10-13
[Section titled “2026-10-13”](#2026-10-13)
* Launched `mouse-1` (`mouse-preview-2026-10-13`), a general-purpose decision model for agent harnesses.
* Shipped `POST /v1/systemone`, `POST /v1/calibrations`, and `GET /v1/models` at `https://api.mouse.dev`.
* Launched the [Mouse API Platform Console](/console) with API key management, interactive Playground, usage metrics, zero-data-retention request logs, and prepaid billing.