Models
A model answers your questions. For eachnoul question it returns its probability that the answer is true, and that is all it does. Each question is sent as you wrote it, over the part of the state it reads, and questions that read the same keys share a call. Everything after the answers, every aggregation and outcome, is computed by Moonbounce and is deterministic: the same answers always give the same value and outcome.
Two models are offered:
jev-1.14.0is the default. It is pinned: the id always serves the same model.@cf/cloudflare/clefis Cloudflare’s open-weights decision model, served on Workers AI. It is an alias, not pinned: Cloudflare can change what it serves, so answers can drift without the id changing. It asks each question in its own call and lists a higher price per token, so expect it to cost more per evaluation thanjev-1.14.0.
GET /v1/models lists what this deployment offers. Any scoped key can call it. Trimmed of pricing:
default is the model a decision runs on when nothing else is set. id is what you pass to PUT /v1/decisions/{decision_id}/model. resolved_model is what evaluations report as the model that answered, question_types are the question types it answers (noul today), and modalities are the content types it reads (preview ones are not used by evaluate yet). pricing gives the list price per million input and output tokens and its source.
A decision’s model
Each decision has one model in each environment. The model in effect is that environment’s own stored setting, else the deployment default. The first time a structure is registered for a decision in an environment (an evaluate or run that isn’t a dry run, an import of examples, orPOST /v1/decisions), the default is stored there as revision 1, so a later change of the default never moves a decision on its own.
GET /v1/decisions/{decision_id}/model returns the model in effect for your key’s environment. It answers for any decision that exists. Before anything is stored there it reports the deployment default, with revision null and source default; once a setting is stored, source is setting. It is 404 DECISION_UNKNOWN only when the decision doesn’t exist:
Changing the model
PUT /v1/decisions/{decision_id}/model sets the model for your key’s environment:
- It always answers 200 with the setting and
"changed": truewhen it wrote a new revision. Sending the model that is already in effect writes nothing and answers"changed": false. - A model the deployment doesn’t offer, or a floating alias such as
jev-latestor a-previewid, is 400CAPABILITY_UNSUPPORTED. - The new model applies from the next call: evaluations, runs, imported examples, previews and suggestions all use it.
- Changing back is another revision: setting
jev-1.14.0again makes revision 3, not a return to revision 1.
GET .../binding and POST .../binding/revisions routes answer 410 ENDPOINT_REMOVED, and details.replacement names the new one.
Every evaluation says what answered it. model.id is the setting’s model, model.resolved_model is what actually answered, model.revision is the setting’s revision (null when nothing was stored and the default applied), and model.profile is an opaque string for the models and connectors behind the questions. Compare profiles for equality to know two runs are comparable; never parse one:
The model and your version
The model is not part of the structure hash. Your structure says what to ask, the model setting says who answers, and the evaluation records both. Changing the model mints no new structure version, so your code and your hashes stay as they are, and a change moves all of a decision’s traffic at once. The weights, thresholds and cut points in your structure were chosen against one model’s answers. A sharper or more hedging model can move items across a cut, so check a change before production depends on it.Try a change on test first
The model changes every answer, so check a change before production depends on it. Previews run on the model in effect in your key’s environment: a test key uses test’s own setting, never production’s, which makes test 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
Import examples in test
With a test key, import your labeled examples (
POST /v1/decisions/{decision_id}/examples, or mb import examples.jsonl; see Import examples). That registers the structure, so the test environment stores its own model setting, and gives test previews cases to replay.2
Preview on the current model
Run a preview of your current decision from the test key and note
candidate_correct in by_split: how many of those cases jev-1.14.0 gets right (Previews).3
Change the test model
Send
{"model": "@cf/cloudflare/clef"} to PUT .../model with the test key.4
Preview again
The same preview now replays the same cases on Clef. Compare the counts, and check
cost.5
Change production
If Clef does as well, send the same
PUT 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/validate 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.Your account
Your own settings are on Account: open it from Account at the top of any page. It holds your name, your passkeys, the devices you’re signed in on and the agents connected as you.- Passkeys. The passkeys you sign in with, when passkey sign-in is on. Remove one with a code sent to your email. If it’s your only passkey, you sign in another way afterwards, such as a code sent to your email.
- Signed-in devices. Sign out one device, or every device but this one. Connected agents stay connected.
- Connected agents. Agents you signed in through MCP (Connect your agent). Each acts as you in the team you picked when you connected it. Disconnect removes its access to all your teams within a few minutes; to use it again, connect it again.
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 model setting. Changing test’s model leaves production’s alone, and the other way round. A test key’s evaluations, previews, suggestions and runs use test’s own setting, else the deployment default; they never fall back to production’s.
- The improvement loop. Previews and suggestions learn only from the key’s environment: its reviewed and imported cases, its base version, its action mix and its cases’ splits. 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, each counted per calendar minute across every location: 600 requests a minute per key, and 300 a minute per client IP for requests that carry no valid credential (no key, a malformed or unknown one, or the public hash route). A valid key rarely counts against the per-IP limit: when it is first validated, and again when its cached validation expires after a day without one; while an IP is over its limit, though, even valid keys from it can be refused until the minute ends. Each MCP request counts once against the key and each tool call in it once more, like the REST call it makes, and one MCP request may carry at most 10 messages. A 429 RATE_LIMITED says which limit in details.limit (key_rate or ip_rate) and how long to wait in Retry-After and details.retry_after_seconds; a 503 from the limiter itself also carries Retry-After.
The calls that spend model time (POST /v1/evaluate, runs, imported examples, 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 imported 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 is 422REQUEST_TOO_LARGE, withdetails.too_large: trueand no reset time orRetry-After; split it into smaller requests with fresh idempotency keys.
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, except evaluate, import or preview requests with the same Idempotency-Key: the second waits for the first’s result and is charged only if it runs after all, because the first failed or stopped responding. A preview is charged when it starts: a keyed preview that runs past its 60 seconds answers a retryable 503 (ENGINE_UNAVAILABLE when its replays ran out of time, STORE_UNAVAILABLE when its run could not be saved in time), saves nothing and keeps its charge. Retry it with the same key; the retry is charged again only if it runs (Previews). The per-key limit counts every keyed request, replays and refusals included, and the per-IP limit every request it applies to (see above).
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: every RATE_LIMITED error carries the wait as details.retry_after_seconds too.
Usage
Evaluate responses (including dry runs), evaluation reads, and each imported example’s result report onecost shape, the same on previews and runs: {model_calls, questions, cache_hits, input_tokens, output_tokens, usd_micros}, and on evaluate also calls, what each model call sent. An import’s top-level cost totals those fields across its results. usd_micros is millionths of a US dollar; evaluate and import 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. model_calls counts upstream calls to a model; one call can answer several questions (those that read the same keys share it). questions counts the questions sent to a model. Retries count too. cache_hits is present on previews and case runs, and on evaluate only when the answer cache served a question.
Fresh evaluate and import responses always carry cost. Evaluation reads omit cost if no usage row exists. Replayed costs describe the original request and do not represent new spend.
GET /v1/decisions/{decision_id}/usage reports what a decision’s model 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 model profile and assignment mode; they contribute no per-model traces. They count against the same team admission limits as ordinary evaluations.
evaluations groups by model.profile and assignment mode (primary for evaluate, example for imported examples), with the cost per evaluation as total, mean, median and 95th percentile. models groups the calls by resolved model and adapter, 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 models reported, priced at each model’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, an imported example that failed, 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
state,attribute_valuesandmoonbounce.summaryyou 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 model sees it. Every question is answered by the decision’s model, from the part of the state it reads. Attribute values and
moonbounce.metadatanever reach the model. - The suggestion writer sees some of it. A language model reads the text of imported examples and reviewed cases when it drafts a suggestion. Suggestions carry structure text and ids, and preview results carry ids, values and answers, never content.