> ## Documentation Index
> Fetch the complete documentation index at: https://docs.moonbounce.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Engines, keys and environments

> Which engine answers your features and how to switch it, which key can call which operation, and how test traffic stays apart from production.

The guides on building, running and improving a decision follow one decision from spec to accepted suggestion ([How Moonbounce works](https://docs.moonbounce.ai/learn/how-it-works.md)). This page covers the setup around it: the engine that answers the features, the API keys that call the operations, the production and test environments, and the limits, usage and data handling that come with them.

## Engines

An **engine** answers your features. For each boolean feature it returns P(yes), its probability that the answer is yes, and that is all it does. Everything after the answers, `combine` and `calibrate` and the cut against your cut points, is computed by Moonbounce and is deterministic: the same answers always give the same score and outcome.

Two engines are offered:

* **Jev** (`jev`) is the default. Its model, `jev-1.14.0`, is a pinned version: the id always serves the same model.
* **Clef** (`clef`) is Cloudflare's open-weights decision model, served on Workers AI. Its model id, `@cf/cloudflare/clef`, is not pinned: Cloudflare can change what it serves, so answers can drift without the id changing. Clef asks each prompt in its own call and lists a higher price per token, so expect it to cost more per evaluation than Jev.

`GET /v1/engines` lists what this deployment offers. Any scoped key can call it. Trimmed of `pricing`:

```json theme={null}
{
  "default": { "engine": "jev", "model": "jev-1.14.0" },
  "engines": [
    {
      "engine": "jev",
      "adapter": "jev@0.1.0",
      "default_model": "jev-1.14.0",
      "models": [
        { "model": "jev-1.14.0", "resolved_model": "jev-1.14.0", "pinned": true, "feature_types": ["boolean"], "modalities": { "text": "ga", "json": "ga", "image": "preview" } }
      ]
    },
    {
      "engine": "clef",
      "adapter": "clef@0.1.0",
      "default_model": "@cf/cloudflare/clef",
      "models": [
        { "model": "@cf/cloudflare/clef", "resolved_model": "@cf/cloudflare/clef@clef@0.1.0", "pinned": false, "feature_types": ["boolean"], "modalities": { "text": "ga", "json": "ga", "image": "preview" } }
      ]
    }
  ]
}
```

`default` is the engine a new decision is bound to. For each model, `resolved_model` is what evaluations report as the model that answered, `feature_types` are the node types it answers, and `modalities` are the content part types it reads (`preview` ones are not used by evaluate yet, so image parts are refused). `pricing` gives the list price per million input and output tokens and its `source`. The `adapter` is Moonbounce's connector to the engine, with its version.

### The binding

A decision's engine is its **binding**, and each environment has its own. The first time a structure is registered for a decision in an environment (an evaluate with an inline spec, a seed batch, or `POST /v1/decisions/{decision_id}/structures`), the decision is bound to the default engine there. `GET /v1/decisions/{decision_id}/binding` returns the current revision for your key's environment, and 404 before that first registration:

```json theme={null}
{
  "decision_id": "refund-exception",
  "environment": "test",
  "revision": 1,
  "engine": "jev",
  "model": "jev-1.14.0",
  "pinned": true,
  "created_at": "2026-10-20T09:14:03.551Z"
}
```

### Switching engines

`POST /v1/decisions/{decision_id}/binding/revisions` appends a revision for your key's environment:

```bash theme={null}
curl -sS --fail-with-body -X POST "$MOONBOUNCE_API_URL/v1/decisions/refund-exception/binding/revisions" \
  -H "Authorization: Bearer $MOONBOUNCE_API_KEY" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"engine": "clef"}'
```

* **201** with the new revision and `"created": true`. Sending the body that matches the current revision writes nothing and answers **200** with `"created": false`.
* `model` is optional and defaults to the engine's `default_model`; send it to name the model explicitly. An engine or model the deployment doesn't offer, or a floating alias such as `jev-latest` or a `-preview` id, is 400 `CAPABILITY_UNSUPPORTED`.
* The new binding applies from the next call: evaluations, seeds, previews and suggestions all run on it.
* Switching back is another revision: `{"engine": "jev"}` makes revision 3, not a return to revision 1.

Every evaluation says what answered it: `version.binding` is the revision it ran on, `version.engine_profile` maps each feature to its engine, model and adapter (for example `defective=jev:jev-1.14.0@jev@0.1.0`), and `engines` gives the same per feature:

```json theme={null}
{
  "version": { "binding": 2, "engine_profile": "defective=clef:@cf/cloudflare/clef@clef@0.1.0@clef@0.1.0,..." },
  "engines": {
    "defective": { "engine": "clef", "model": "@cf/cloudflare/clef", "resolved_model": "@cf/cloudflare/clef@clef@0.1.0", "adapter": "clef@0.1.0", "cached": false }
  }
}
```

(The profile is shortened here; it lists every feature.) There is no automatic failover: when an engine can't answer a feature the score needs, the call fails with 502 or 503 rather than asking another engine.

### Weights and engines

Weights fit to one engine's answers are refused on another: evaluating with them on a different engine profile is 409 `WEIGHTS_INCOMPATIBLE`, never a silent mix. Every weights version you can hold today (default weights, and weights carried from them by a suggestion) fits any engine, so a switch moves all of a decision's traffic at once.

The engine is not part of the structure hash. Your spec says what to ask, the binding says who answers, and the evaluation records both. Switching engines or models mints no new structure version, so your code, your hashes and the feedback recorded against each feature stay as they are.

### Try a switch on test first

The engine changes every answer, so check a switch before production depends on it. Previews run on the binding of your key's environment (a test key falls back to production's binding until it has its own), which makes the test binding a safe place to try. A test key's previews replay test cases only, so the comparison runs on examples you send to test:

**1. Seed the decision in test**

With a test key, send your seed examples (`POST /v1/decisions/{decision_id}/seeds`, see [Seeds](https://docs.moonbounce.ai/learn/feedback.md#seeds)). That registers the structure, so the test environment gets its own binding, and gives test previews cases to replay.

**2. Preview on the current engine**

Run a preview of your current spec from the test key and note `candidate_correct` in `by_split`: how many of those cases Jev gets right ([Previews](https://docs.moonbounce.ai/learn/improving.md#previews)).

**3. Switch the test binding**

`{"engine": "clef"}` on the test key's binding revisions.

**4. Preview again**

The same preview now replays the same cases on Clef. Compare the counts, and check `cost`.

**5. Switch production**

If Clef does as well, write the same revision with a production key that has `decisions:develop`.

## Keys

### Getting access

Join the waitlist at [app.moonbounce.ai](https://app.moonbounce.ai). Once you're approved, sign in and create your team. Team admins create and revoke keys at `/team/keys`; other members see the list read-only. The full key is shown once, when it is created.

A key belongs to one team and one environment, Production or Test, chosen when it is created. Neither ever changes, and neither do its scopes: to change any of them, create a new key and revoke the old one.

### Scopes

Each operation needs one scope, listed in its `security` in the OpenAPI document. A key without it gets 403 `INSUFFICIENT_SCOPE`, with the scope it needs in `details.required`. A key with no scope is refused everywhere.

| Scope | Operations |
| - | - |
| `decisions:evaluate` | `evaluate`, `getEvaluation`, `getStructure`, `listEvaluations`, `listStructures`, `listDecisions` |
| `decisions:feedback` | `submitFeedback` from your app (`app_native`, `app_resolution`), `getFeedback` |
| `decisions:develop` | `listDecisions`, `listStructures`, `getStructure`, `registerStructure`, `importSeeds`, the feedback export, the review queue, suggestions, previews, the loop status and the engine binding; also `submitFeedback` and `getFeedback`, including owner feedback and replacing any submission that isn't one of your app's feedback |
| Any scope | `whoami`, `listEngines`, `getUsage` |

`GET /health` and `POST /v1/specs/hash` need no key. MCP tools need the same scope as the operation they call.

A setup that works for most teams:

* **Your app's backend:** a Production key with `decisions:evaluate`, plus `decisions:feedback` if it reports outcomes back.
* **You and your coding agent:** a Production key with `decisions:develop`, so suggestions and previews learn from your real feedback. Add `decisions:evaluate` if the agent should evaluate items itself. Owner feedback, which decides a case's right answer, needs `decisions:develop`, so it comes from this key, not from the app's.
* **Trying things out:** a Test key with all three scopes, as in the [quickstart](https://docs.moonbounce.ai/quickstart.md). Its evaluations, feedback, previews and suggestions stay in test.

### Lifetime, rotation and revocation

Keys don't expire. To rotate one without downtime, create the new key, deploy it, confirm in your own logs that traffic moved, then revoke the old one. A team can hold up to 20 live keys at a time. A revoked key stops working within about a minute everywhere.

## Environments

Every tenant success response includes `environment` (`production` or `test`), taken from the calling key. Call `GET /v1/whoami` (the `whoami` MCP tool) first to see your organization, environment, key id, actor and scopes. Any key with at least one scope can call it; it reads no data and spends nothing.

Production and test are separate inside your team. Each of these belongs to one environment, the environment of the key that made it, and is invisible from the other:

* **Evaluations.** Reading another environment's evaluation is a 404.
* **Feedback.** `about` resolves only to evaluations in your key's environment, by `evaluation_id` or by `item_id`.
* **The review queue.** A test key's queue holds test traffic. The Review page in the web app shows production.
* **Idempotency keys.** The same `Idempotency-Key` in test and production names two different requests.
* **The engine binding.** Switching test's engine leaves production's alone. Until test has a binding of its own, test calls use production's.
* **The improvement loop.** Previews and suggestions learn only from the key's environment: its reviewed and seeded cases, its base version, its action mix and its train and holdout assignments. The same item or text in test and in production is two separate cases. Suggestions, and the loop status, are listed and read only in the environment that made them; another environment's `suggestion_id` is a 404.

Test traffic never counts as production traffic. Only primary production traffic accepts a suggestion, so a test key's suggestions are never accepted, and a test key's loop status lists no acceptances. To check a change against your real feedback, preview it with a production key.

Structures and weights registered by hash are shared across both environments, because they are configuration, not traffic.

## Rate limits

Two limits apply to `/v1` and `/mcp`: 300 requests a minute per client IP, counted before the key is checked, and 600 requests a minute per key. Each MCP tool call counts against the key like the REST call it makes, and one MCP request may carry at most 10 messages. Cloudflare counts both limits per location and approximately. The per-IP limit is the lower one, so a backend calling from one address reaches it first, across all its keys.

The calls that spend engine or model time (`POST /v1/evaluate`, seeds, previews and suggestions) also count against limits for your whole team, across all its keys and both environments:

* **A rate and a cap on calls in flight.** Over either, the call is 429 `RATE_LIMITED` with `Retry-After`, and `details.limit` is `org_rate` or `org_concurrency`.
* **A daily work budget, per UTC day.** Each evaluation and each seed example costs one unit. A preview is charged its largest possible size up front, currently 600 units however many cases it replays, and a suggestion 700, which covers writing it and its preview. When the budget is used up, the call is 429 `QUOTA_EXCEEDED` with no `Retry-After`: `details.reset_at` says when the budget resets. A single request bigger than the whole budget comes back with `details.too_large: true` instead; split it into smaller requests.

These team limits are checked after the `Idempotency-Key` lookup and after validation, so a retry that replays a stored result, or a request refused as invalid, doesn't use them. Two identical requests sent at the same moment can both be charged. The per-IP and per-key limits count every request, replays included.

Over a `RATE_LIMITED` limit, wait `Retry-After` seconds, then retry with the same `Idempotency-Key`. Over the daily budget, retrying before `reset_at` won't help. MCP tool calls don't see headers: a team limit's error carries the wait as `details.retry_after_seconds`, and the per-IP and per-key limits' errors don't, so wait about a minute before retrying those.

## Usage

Evaluate responses (including dry runs), evaluation reads, and each seed result report `cost: {engine_calls, cache_hits, input_tokens, usd_micros}`, matching the preview's shape. A seed summary totals those fields across its results. `usd_micros` is millionths of a US dollar; evaluate and seed amounts come from the same usage rows as the report below, rounded per evaluation, and are reported cost only: the report adds an estimate for calls that reported no usage and breaks it out as `estimated_usd_micros`. `engine_calls` counts upstream engine calls on evaluate and seeds; one call can answer several features (Jev compiles them). On previews it counts features sent to engines instead. Adapter-reported retries count too. Only previews use the feature cache today; evaluate and seeds report `cache_hits: 0`. The separate `usage` token fields are unchanged.

Fresh evaluate and seed responses always carry `cost`. Evaluation reads omit `cost` if no usage row exists. An idempotent replay of an evaluate or seed snapshot stored before costs were reported may omit it until 24 hours after deployment. Replayed costs describe the original request and do not represent new spend.

`GET /v1/decisions/{decision_id}/usage` reports what a decision's engine calls cost, for your key's environment, over a window: `since` (inclusive) and `until` (exclusive), by default the last 30 days, at most 366 days. Any scoped key can read it. Amounts are in millionths of a US dollar:

```json theme={null}
{
  "decision_id": "refund-exception",
  "environment": "production",
  "window": { "since": "2026-09-27T18:00:00.000Z", "until": "2026-10-27T18:00:00.000Z" },
  "evaluations": [
    {
      "engine_profile": "defective=jev:jev-1.14.0@jev@0.1.0,...",
      "assignment_mode": "primary",
      "evaluations": 50,
      "uncached": 50,
      "engine_calls": 50,
      "input_tokens": 30000,
      "output_tokens": 0,
      "usd_micros": { "total": 1260, "mean": 25.2, "p50": 25, "p95": 29 },
      "estimated_usd_micros": 0
    }
  ],
  "engines": [
    { "engine": "jev", "resolved_model": "jev-1.14.0", "adapter": "jev@0.1.0", "calls": 50, "failed": 0, "input_tokens": 30000, "output_tokens": 0, "usd_micros": 1260, "estimated_usd_micros": 0 }
  ],
  "other_spend": [
    { "kind": "uncommitted", "attempts": 2, "engine_calls": 2, "input_tokens": 1200, "output_tokens": 0, "model_input_tokens": 0, "model_output_tokens": 0, "usd_micros": 50, "estimated_usd_micros": 0 }
  ],
  "total": { "usd_micros": 2192, "estimated_usd_micros": 0 },
  "previews": { "runs": 3, "features_run": 210, "input_tokens": 21000, "usd_micros": 882, "estimated_usd_micros": 0 }
}
```

Dry runs store usage without an evaluation, so their totals appear under `unknown` engine profile and assignment mode; they contribute no per-engine traces. They count against the same team admission limits as ordinary evaluations.

`evaluations` groups by engine profile and assignment mode (`primary` for evaluate, `seed` for seed examples), with the cost per evaluation as total, mean, median and 95th percentile. `engines` groups the calls by engine and model, retries and failures included. `previews` covers preview runs, which write no evaluations. The key page shows each key's spend over the last 30 days next to its last use.

Costs are what the engines reported, priced at each engine's list price. A call that may have been billed but reported nothing (abandoned at a deadline, or a dropped connection) has its input cost estimated and shown apart as `estimated_usd_micros`. Evaluations and preview runs that never committed (a failed evaluate or preview, a refused seed batch, the loser of an idempotency race) and suggestion generation (the writer's language model call and its exploratory replays) appear in `other_spend`, outside the per-evaluation numbers, and `total` adds everything recorded. The key page shows the same split per key: evaluations, previews and other spend, with any estimated part.

## Your content

* **It is stored.** The `subject`, `context` and `summary` you send are stored with the evaluation, so your team can review the case and previews can replay it. There is no API to delete evaluations yet, so don't send content you can't have stored.
* **The engine sees it.** Every prompt is answered by the decision's bound engine, from your content.
* **The suggestion writer sees some of it.** A language model reads the text of seed examples and reviewed training cases when it drafts a suggestion; held-out cases never reach it. Suggestions carry spec text and ids, and preview results carry ids, scores and answers, never content.
* **`sensitive` is display only.** A part marked `"sensitive": true` is blurred or linked for reviewers, but the engine still reads it and it is stored like any other part.

## Next

[Connect your agent](https://docs.moonbounce.ai/connect-your-agent.md) sets up your coding agent with these operations as MCP tools, and the [Glossary](https://docs.moonbounce.ai/learn/glossary.md) collects every term from these guides.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.