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:
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, orPOST /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:
Switching engines
POST /v1/decisions/{decision_id}/binding/revisions appends a revision for your key’s environment:
- 201 with the new revision and
"created": true. Sending the body that matches the current revision writes nothing and answers 200 with"created": false. modelis optional and defaults to the engine’sdefault_model; send it to name the model explicitly. An engine or model the deployment doesn’t offer, or a floating alias such asjev-latestor a-previewid, is 400CAPABILITY_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.
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:
Weights and engines
Weights fit to one engine’s answers are refused on another: evaluating with them on a different engine profile is 409WEIGHTS_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). 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).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. 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 itssecurity 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.
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, plusdecisions:feedbackif it reports outcomes back. - You and your coding agent: a Production key with
decisions:develop, so suggestions and previews learn from your real feedback. Adddecisions:evaluateif the agent should evaluate items itself. Owner feedback, which decides a case’s right answer, needsdecisions: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. 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 includesenvironment (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.
aboutresolves only to evaluations in your key’s environment, byevaluation_idor byitem_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-Keyin 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_idis a 404.
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_LIMITEDwithRetry-After, anddetails.limitisorg_rateororg_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_EXCEEDEDwith noRetry-After:details.reset_atsays when the budget resets. A single request bigger than the whole budget comes back withdetails.too_large: trueinstead; split it into smaller requests.
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 reportcost: {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:
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,contextandsummaryyou 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.
sensitiveis display only. A part marked"sensitive": trueis blurred or linked for reviewers, but the engine still reads it and it is stored like any other part.