List your models
One manifest, a published conformance bar for text, image and video, paid prepaid or in arrears.
Sell your inference through the marketplace: one model document, a published conformance bar, pass-through pricing, and automatic payment on the schedule you choose.
Two things to know up front:
- Pricing is pass-through. Buyers pay your listed price; we never mark it up. Your price is your price.
- Routing is earned. Traffic flows on live health and published metrics — the same numbers you can see — not on negotiation.
To apply: email [email protected] with your model document attached. Onboarding is white-glove today: a human plus the automated suite take it from there, usually same-week.
The model document
Everything we need is one JSON document — your endpoint, models, prices, capacity, and data policy. Rather than poll a /v1/models endpoint you host, we accept the same information pushed as a versioned manifest: you send updates when you choose, and nothing changes under you between updates. A self-serve listing changes its own prices, limits and on/off state through the API (see below); a new model is still added by us.
Validate against the published JSON Schema before sending: /docs/provider-manifest.schema.json.
{
"manifest_version": "1",
"provider": {
"id": "acme",
"name": "Acme Inference",
"privacy_policy_url": "https://acme.example/privacy",
"terms_of_service_url": "https://acme.example/terms",
"status_page_url": "https://status.acme.example",
"prompt_logging": "none"
},
"endpoint": {
"base_url": "https://api.acme.example/v1",
"dialect": "openai",
"api_key_env": "ACME_API_KEY"
},
"settlement": {
"mode": "arrears",
"interval": "weekly",
"billing_contact": "[email protected]"
},
"models": [
{
"id": "acme-large-1",
"display_name": "Acme Large 1",
"context_length": 128000,
"max_output_tokens": 16384,
"streaming": true,
"is_ready": false,
"pricing": [
{ "type": "prompt", "unit": "token", "cost_usd_per_million": "0.90" },
{ "type": "completion", "unit": "token", "cost_usd_per_million": "3.60" }
],
"capacity": [
{ "type": "completion", "unit": "token", "per": "minute", "value": 2000000 }
]
}
]
}Identity
id is the exact identifier we send when calling your API — never an alias. display_name is what buyers see. A listing is text (chat completions) unless output_modality says "image" or "decisions". A text listing declares streaming: true when your endpoint streams SSE, or false (OpenAI dialect only) when it answers in one JSON body. An image listing is served on POST /v1/images/generations only; it needs no context_length or streaming, and must declare max_output_tokens (the per-image ceiling the reservation is computed from) plus supported_parameters for n and a size or aspect_ratio enum. A decisions listing (a structured decision model that returns typed answers to questions about a state) is served on POST /v1/decisions; it needs a context_length but no streaming, and your endpoint must declare decisions_path, the path we POST the request to. If your API picks the model by its URL, each decisions model declares its own decisions_path instead (Cloudflare Workers AI: /clef and /clef-flash), and then the endpoint needs none; the body still carries the model's upstream_id (or id) as model. The model declares decisions_wire, the question format your API speaks: "systemone" (TypeSafe's System One API, which Jev, the Bespoke Nimble models, Clef, Clef-flash, Decider 2B, Kev 4B and Perplexity Decider v1 27B use; the default when the field is absent) or "gliner" (Fastino's GLiNER schema, sent in an OpenAI chat-completions envelope, so decisions_path can be /chat/completions). Buyers send questions in that format, and the gateway sends them to you in it. A decisions listing is served only on POST /v1/decisions, even when its path is a chat path: chat traffic never reaches it. Two listings of one model id must declare the same wire. decisions_wire on a listing that is not "decisions" is refused.
If your API puts every answer and every error inside a transport envelope of its own, declare it as endpoint.response_envelope, and the gateway opens it before it reads the answer. Only a manifest of decisions models may declare it. The one value today is "cloudflare_v4", Cloudflare's REST envelope: a 200 is {"result": <answer>, "success": true, "errors": [], "messages": []}, and an error is {"errors": [{"code": ..., "message": ...}], "success": false, "result": {}}. A 200 whose envelope does not say success: true is a failed answer, and nothing is billed for it.
If your decisions API asks System One's kinds of questions (a choice, a score, a yes/no) in a body format of its own, declare the format as endpoint.decisions_format. The one value today is "openai": your API speaks OpenAI's POST /v1/decisions format (input, and an ordered questions array of predicate, choice and score questions, answered by an ordered answers array). Your models still declare decisions_wire: "systemone". Buyers still write System One, and the gateway translates both ways: it sends you your format, and it gives the buyer your answers in System One's shape. Only a manifest of System One decisions models may declare it. OpenAI's GPT-6 Luna Decisions uses it.
Two more fields tell the gateway how your System One API reads a body. They are booleans on a decisions model whose decisions_wire is "systemone" (or absent), and are refused on any other listing. Absent means false.
decisions_state_per_question: your API runs one prompt per question, each with the whole state, and bills the state once per question (Perplexity's decider does). The gateway then holds the state once per question when it reserves money and tokens for a request, so a request with many questions is held at what it will cost.decisions_image_parts: your API reads an object whosetypeis"image_url", at any depth of the state or of a question, as an image. A decisions listing takes text and JSON, so the gateway refuses such a body with a typed 400 before any money is reserved, and it never reaches you.
A chat listing may declare input_modalities: what a message to the model may carry. The list always has "text", plus any of "image", "file" (a document such as a PDF), "audio" and "video". Buyers see it on the model page and filter the catalog by it, and the API lists it in /v1/models. The model page shows an input only when every listing of that model declares it. The gateway sends a part only to a listing that declares its kind: a part your listing does not declare never reaches you, and a part that no listing of the model declares is refused with a typed 400. A declared part goes to you as sent when the buyer calls the surface that speaks your dialect; across dialects the gateway refuses it with a typed 400 and does not translate it. Absent means text only. An image or decisions listing takes a text prompt, and the field is refused on it.
A chat listing on an OpenAI-dialect endpoint may declare "chat_api": "responses": the gateway then calls the model on your Responses API (POST /responses under base_url) instead of Chat Completions. Declare it when the model takes function tools only there, as OpenAI's GPT-6 and GPT-5.6 do. Buyers still call either chat surface: the gateway sends the request in the Responses form and reads your answer and its usage back. Parameters with no Responses form on these models (stop, n, seed, logprobs, logit_bias, the penalties) are dropped and recorded, and an output limit under 16 tokens is sent as 16. Absent means Chat Completions.
A chat listing may declare "fixed_sampling": true when the model takes only its default sampling and you refuse temperature (other than 1), top_p and top_k with a 400, as Anthropic does for the Claude 5 models and OpenAI for GPT-6 and GPT-5.6. The gateway then leaves these out on every route of the model and records the drop. Absent means you take them.
Serving a model the marketplace already lists
If you serve a model another provider also lists (the marketplace id claude-sonnet-4-5, say), list it under that id and set upstream_id to the name your endpoint expects:
{ "id": "claude-sonnet-4-5", "upstream_id": "anthropic/claude-sonnet-4.5", "...": "..." }Requests to you carry upstream_id; conformance check C7 expects you to echo it. Prices must be identical to the other providers of that id — the catalog build rejects a mismatch (pass-through pricing means one price per model), unless you set your own prices (see "Changing your prices yourself"), or your listing is opt-in ("routing": "opt_in"): on a model that other providers also list, an opt-in provider is served only to a request that names it (provider.only or provider.order), at its own price. provider.priority orders providers of one model: 0 for a first-party lab, 1 for an aggregator fallback; routing tries lower first.
Endpoint
dialect is "openai" (Chat Completions) or "anthropic" (Messages) — one is enough. Buyers reach your models from both marketplace surfaces regardless; the gateway translates. api_key_env names the secret slot for the key you issue us: it never appears in a manifest, page, or log.
Pricing
Entries are {type, unit, cost_usd_per_million} with costs as decimal strings in USD (never floats). Types: prompt, cached_prompt, cache_write, completion, internal_reasoning; unit is token.
- Declare what you charge. A SKU you omit bills those tokens at your base
prompt/completionrate — a missing SKU never means free, and never means a surprise for either side. - Price changes are append-only. A new effective-dated row, never a rewrite. Every buyer request bills at the price in force when the request arrived at the gateway (see "Changing your prices yourself"), so your statement and their bill can never disagree about history.
- Flat per-token pricing only for now: conditional overrides, time-of-day windows, and per-request SKUs are not yet supported.
Changing your prices yourself
We can enable your listing for self-serve. Then you change its prices, and some of its limits, without waiting for a release. You use the provider key that we issue you (tm_pk_…). One key speaks for one provider. Keep it secret.
The endpoint is https://app.routerplus.com/v1/provider/manifest. Send the key as Authorization: Bearer tm_pk_….
What you can change. These are the editable fields:
provider.privacy_policy_url,provider.terms_of_service_urlandprovider.status_page_url.- For each model you list:
pricing,provider_pricing,enabled,context_lengthandmax_output_tokens.
All other fields are ours. You can leave them out, or send them unchanged. If you change one, we refuse the write and name the field. You cannot add or remove a model. To take a model off our marketplace routes, set "enabled": false: no route that we pay you for reaches it, and our public lists hide it. A buyer who sends their own key for your service (BYOK) is not stopped by it. A limit (context_length, max_output_tokens) can only go down: at most the value we listed, and max_output_tokens at most context_length. For an image or video model, max_output_tokens is ours. For a decisions model, context_length and max_output_tokens are both ours: we refuse a change and name the field (you can send our value unchanged).
GET returns the full state in force. It is our listing with your edits on it. It also returns revision (the latest one), in_force, frozen, price_feed and pending (changes that are scheduled). A pending change with "shadowed": true will never take effect: a later write takes effect first. in_force and each pending change also show pushed_provider_fields: the URLs that you set. The other URLs follow our listing, also when we change them later.
A write changes a URL or a limit only when it sends a different value from the one that the state shows, or null. If you leave a field out of a PATCH, or send the value that the state shows, we keep what you set before. This is also true when our listing hides your value. For example: you lowered context_length to 6000, and then we lowered our listing to 4000. GET shows
- 4000A PATCH of a price only,
PATCH {}, or a POST of your GET body keeps your - 4001When our listing goes back to 8192, your 6000 applies again. To follow our listing again, send
null. Anullthat changes only how we store the field is still a change: itschangesshowpushed_provider_fields(the list before and after) orlowered_context_length/lowered_max_output_tokens(your lowered value before, andnullafter).
GET /v1/provider/manifestPOST replaces your whole editable state, from its effective_from. Send every model you list. If you leave out an editable field, it goes back to our value: no provider_pricing means we pay you the buyer price, no enabled means on, no limit means our limit, no URL means our URL. (When the state already shows our value for a limit or a URL, leaving it out keeps what you set before, as above; send null to follow our listing.) So the safe way is: GET, edit, POST.
A POST replaces every pending change that starts at or after its effective_from. The answer lists them in superseded. To keep a pending change, use PATCH.
POST /v1/provider/manifest
Idempotency-Key: 6f1c…
{ "manifest": { "provider": { "id": "your-id" },
"models": [ { "id": "your-model",
"pricing": [ { "type": "prompt", "unit": "token", "cost_usd_per_million": "0.40" },
{ "type": "completion", "unit": "token", "cost_usd_per_million": "1.60" } ] } ] },
"effective_from": "2026-11-01T00:00:00Z", "expect_revision": 3 }PATCH changes only the fields you name. Set a field to null to go back to our value ("provider_pricing": null means we pay you the buyer price). A field that you do not name keeps its value.
PATCH /v1/provider/manifest
{ "models": [ { "id": "your-model", "context_length": 65536 },
{ "id": "your-old-model", "enabled": false } ] }A PATCH never cancels a change that you (or your price feed) scheduled. It applies to the state in force at its effective_from (the next 5-minute mark if you leave it out; see the rules). It also applies to each pending change after that time. We write each pending change again, at its own time, with your PATCH on it. The answer lists every revision that we write in revisions (each with its effective_from, its diff, its changes and reissues: the pending change that it replaces). superseded lists the old pending changes. head is the latest revision after the write; send it as the next expect_revision.
For example: you scheduled a price increase for tomorrow. Today you PATCH "enabled": false on a model. We write two revisions: the model is off now (a stop applies at once), and tomorrow the increase starts with the model still off. The increase keeps its time and needs no new notice.
We check each revision against what buyers would pay at its time without your write. If we refuse one of them, we write nothing. The 422 names the time of the revision that we refused.
The rules.
- Every change starts at a 5-minute mark. A mark is a UTC time at a whole 5 minutes: 10:00, 10:05, 10:10. A change starts at the first mark at least 2 minutes after your write. Written at 10:02:30, it starts at 10:05. Written at 10:03:30, it starts at 10:10. If you set
effective_from, we move it up to the first mark at or after it: 10:07:10 becomes 10:10, and 11:00 stays 11:00. The answer'seffective_fromsays when the change starts. The 2 minutes let every gateway load the change first, so all of them switch at the same time. - A write cannot change what starts less than 2 minutes after it. Such a pending change starts as scheduled.
- A stop applies at once. A write with no
effective_from(or one that is not in the future) whose only changes turn a model off or lower a limit applies when we write it. The answer says"timing": "at_once". If the same write changes anything else (a price, what we pay you, a URL, turning a model on, or a pending change that a POST replaces), all of it waits for the mark. - Decreases need no notice. Increases need notice. The notice period is the same for every provider: 1 hour, unless we tell you of a change. Set
effective_fromthat far ahead. We check the notice against the time that you set; the change then starts at the first mark at or after it. We compare with what buyers pay at that time, and with every change you already scheduled. - Turning a model off or on, and lowering a limit, need no notice. A model that is off has no price that buyers pay. So when you turn a model on, we compare its price with the price buyers last paid for it (before it went off; if never, the price in our listing). If it is more, that is an increase, and it needs notice. The price that you set while the model is off is not compared.
effective_fromis ISO 8601 with a zone, for example2026-11-01T00:00:00Z. It must be a real time:2026-02-30or hour24is refused. It may not be in the past, or more than 90 days ahead. Leave it out for the next mark.- Margin. On each SKU, your price minus
provider_pricing(what we pay you) may not be below zero. If our listing already accepts a negative margin on a SKU, you may not go below that margin. - Floor. Your key, and your price feed, may not set a price below one tenth of the price in our listing for that SKU. A SKU that we list at 0 has no floor. If you need a lower price, ask us: our operators can set it.
- A SKU we list at 0 may stay 0. To make a priced SKU free, ask us first.
- The margin rule, the floor and the free-price rule apply to a SKU that your write changes: its price, or (for the margin) what we pay you for it. A write also changes a SKU when it replaces a pending change with another price. So you can keep a low or free price that our operators set, but you cannot make it last past the end that they scheduled. And a later change to our listing's margin does not block a write that keeps a SKU as it is.
- At most 20 pending changes (scheduled revisions that will take effect) per listing, and at most 200 scheduled revisions in all, also counting the ones that never take effect (
shadowed). A write that would leave more is a 422too_many_pending. A POST with noeffective_fromends every pending change from its mark on, so it is always possible. - If your write makes a pending change the same as the state just before it, we do not keep that pending change: the write supersedes it.
- At most $1,000 per million tokens per SKU.
- A change of more than 10× needs
"confirm_large_change": true. A change to or from 0 is not under this rule; the free-price rule above applies. - A policy URL must be
https://orhttp://, at most 2,048 characters, with no spaces or control characters. - A price entry has only
type,unitandcost_usd_per_million. Another key is refused with its path. - A body may not contain the character U+0000 (NUL), or a lone UTF-16 surrogate (such as
\ud800without its pair), in any key or string. It may not be nested more than 32 levels deep (the top-level object is level 1). We refuse such a body with a 400. We check it after your key, your request rate, your listing and the query, and before we check the write itself. - Your price is your own. A self-serve listing is outside the identical-price rule above. A buyer pays the price of the provider that served their request.
Answers.
?dry_run=1(or?dry_run=true) checks a POST or PATCH and writes nothing.?dry_run=0and?dry_run=falsewrite. Any other value, and any other query parameter, is a 400: a mistyped dry run never writes.- A write answers 201 with
revision(the first revision written),head,revisions,diff(each price SKU of the first revision:increase,decreaseornew),changes(the other fields) andsuperseded(pending changes it replaces). It also says when the change starts:effective_from(a 5-minute mark, or the time of the write for a stop),requested_effective_from(the time you sent, or the time of the write) andtiming("next_mark"or"at_once"). A dry run answers the same times. - A write that changes nothing answers 200 with
"unchanged": true. We write nothing, but the write counts toward your limit. - The same
Idempotency-Keywith the same body gives the first answer again. With another body it is a 409. It covers the whole write, all its revisions. This is also true when the first write changed nothing, and while we freeze your listing: a retry of a write that we accepted always gets its first answer. While we freeze your listing, a used key with another body is a 423, not a 409. A refused write (any 4xx) does not use up its key, so you can send the fixed write with the same key. GETlists every pending change that will take effect. It also lists the 20 latest pending changes that never will (shadowed: true); when there are more,shadowed_omittedcounts the rest.
Errors. Every error has error.code, error.message and error.errors (each with its field path).
| Status | Code | Why |
|---|---|---|
| 400 | invalid_request | The body is not a write, a field has the wrong type, the body has a NUL or a lone surrogate or is nested too deep, or the query is not dry_run |
| 401 | unauthorized | The key is missing, unknown or revoked |
| 403 | forbidden | The listing is not self-serve |
| 403 | prices_from_feed | We read your prices from your price feed |
| 404 | not_found | We have no listing for your key's provider |
| 405 | method_not_allowed | Use GET, POST or PATCH |
| 409 | conflict | expect_revision is stale, or the Idempotency-Key was used for another body (while we freeze your listing: 423) |
| 413 | payload_too_large | The body is more than 256 KB |
| 422 | manifest_refused | A rule above, or our database refused a value of the write; see below |
| 422 | too_many_pending | The write would leave more than 20 pending changes, or more than 200 scheduled revisions in all |
| 423 | frozen | We froze your listing; writes wait until we unfreeze it |
| 429 | rate_limited | More than 30 writes, or 600 requests, an hour per key |
| 503 | unavailable | The API cannot reach its database; try again after retry-after seconds |
A 422 lists in errors every problem that the checks found, with a note that says this. We check in stages: first the fields you may change, then the manifest rules, then the prices, then the notice and 10× rules. A stage runs only when the stages before it pass. So after you fix the problems in a 422, a new write can show more problems. Use ?dry_run=1 to check a write first. An answer lists at most 100 errors; errors_omitted counts the rest.
Limits. At most 30 writes an hour per key. These answers count as a write:
- 201: we accepted it;
- 200 with
"unchanged": true: it changes nothing; - 403
prices_from_feed, 409conflictand 422 (manifest_refused,too_many_pending): we refused it.
No other answer counts: not a 400, 401, 403 forbidden, 404, 405, 413, 423, 429 or 503, not a dry run, and not the replay of an Idempotency-Key. At most 600 requests an hour per key, of every kind. A body is at most 256 KB.
Price feed. Instead of writing prices, you can publish them. We set the feed up for you in our listing (endpoint.price_feed). Then we read your price list on a schedule, every hour by default. The feed must use https. We do not follow redirects. A read stops after 20 seconds or 4 MiB.
You can use one of two formats:
openai_models: an OpenAI-style model list,{"data": [{"id", "pricing": {"prompt", "completion", "input_cache_read", "input_cache_write", "internal_reasoning"}}]}. Prices are USD per token, as decimal strings. We match a model by the id you serve it under. A"0"on a cache or reasoning price means "not billed separately": that price stays as it is. We pay you the price you publish, so a listing with this format has noprovider_pricing.routerplus:{"models": [{"id", "pricing", "provider_pricing"}]}, in this document's own price form and with our model ids. Noprovider_pricingmeans we pay you the buyer price. A model entry has only these three keys, and a price entry onlytype,unitandcost_usd_per_million. An entry with another key (acurrency, say) is malformed.
We change prices only for the models we list. A model your feed leaves out keeps its price. With openai_models, we change only the prices you publish. With routerplus, an entry is the model's whole price list: a SKU it leaves out is billed like its base (a cached prompt like the prompt, cache write like the prompt, reasoning like the completion). If one entry for our models is malformed, we ignore the whole read. A file that starts with a UTF-8 byte order mark is read as usual.
- A decrease starts at the next 5-minute mark, at least 2 minutes after the read.
- An increase starts at the first 5-minute mark after the notice period plus 5 minutes from the read that first saw it. Until then, the old price stays on the SKUs that rise.
- A later read does not move an increase that is already scheduled. A read with the same prices writes nothing. A decrease on another SKU or model starts at the next mark and does not postpone the increase.
- If your price keeps going up, we keep at most two scheduled increases per price: the first one that we announced, and the newest one. A read with a higher price replaces the increases between them: the earlier price stays until the newest increase starts. So a higher price only ever starts later, and a price that rises on every read never blocks your other changes. We replace only increases that your feed scheduled. An increase that you or our operators scheduled is never replaced, also when a feed read wrote it again (for example, to carry a decrease of another model). An increase that your feed scheduled stays the feed's when you change another field of it with a PATCH.
- If a later read goes back to the price in force, we cancel the increase that an earlier read scheduled: we write the prices in force again, from the next mark. So a price list that goes up and down leaves at most one scheduled increase.
- Your other scheduled changes stay. For example, a model that you scheduled to turn off is still turned off at its time.
- A lower price that our operators scheduled stays until your increase can start (the notice period plus 5 minutes).
- We refuse a change of more than 10x against the price in force, a price of zero (unless we allow free prices for your listing), or a price below the floor (one tenth of our listing's price). Our operators then look at it. The 10x rule is against the price in force only, and only for a price that the read changes. So if a read scheduled a wrong increase, a later read with the right price corrects it, also when the right price is more than 10x below the wrong one. And if our operators scheduled a change of more than 10x and your feed publishes that same price, the read is not refused, also when that change starts after your increase could: the price starts at our operators' time.
- With
openai_models, we also refuse a read while a revision in force or pending pays you your own cost (provider_pricing, from before the feed was set up). Our operators must clear that cost first; we never drop it without a decision. - While we freeze your listing, we do not read the feed. A freeze during a read stops its write.
While a feed is on, your own writes cannot change prices (403 prices_from_feed). You can still change the other fields at any time, also while a fed increase is pending: a PATCH keeps the increase at its time. A POST that replaces a pending fed increase changes prices, so it is a 403.
Gateways read your revisions at least every 30 seconds, also the ones that start later. A change starts at a mark at least 2 minutes after the write, so every gateway has read it before it starts, and all of them switch at the mark. /api/models.json lists it as scheduled within about 2.5 minutes. The public pages show only the price in force, and switch at the mark too. Every request is billed at the price in force when the request arrived at the gateway. A request that arrives just before a change is billed at the price before it, also when the gateway plans it after the change. A stop that applies at once (a model turned off, a limit lowered) reaches every gateway within about 30 seconds.
While we freeze your listing, buyers pay the price in our listing, and we pay you the cost in our listing. A freeze holds back only your prices. It keeps your model states: a model that you turned off stays off, and a limit that you lowered stays lowered. Your key and your price feed cannot write while it lasts. A freeze, and an unfreeze, reach every gateway within about 30 seconds; they are not held to a mark. When we unfreeze it, your revision in force applies again, and so does each pending change that starts within the notice period. If one of them is more than our listing's price, our operators must confirm it before the unfreeze is written. This includes a model that is off in one of them: after the unfreeze, you can turn it on at the price that buyers last paid while it was on, with no notice. If that price is more than our listing's price, our operators confirm it at the unfreeze.
If we turn self-serve off for your listing, the gateways ignore all your revisions: your prices, and also your model states. A model that you turned off is on again at our listing's limits, until self-serve is on again. We also freeze the listing. If we remove your listing, or its file in our repository is not valid, we freeze it too. When self-serve is on again, we unfreeze it with the same check.
Capacity
Entries are {type, unit, per, value} — request, prompt or completion limits per minute/hour/day window (unit: "token" is required for the two token types). Identity is type + window; duplicates are rejected at validation. Absent capacity means undeclared, not zero.
Capacity is information for us, not a setting. Validation checks it, but routing and admission do not read it. The limits that admission enforces for your deployment are a house pool that our operators set by hand from what you declare and what you tell us (see Limits and capacity). Above that pool, your own 429 is the backstop: it pauses the pool for its retry-after (1 to 60 seconds).
Parameters
supported_parameters may be declared per model, and it is enforced on the image route: a value outside a declared enum is a 400 naming the field. An image listing must declare n and a size or aspect_ratio enum, or it fails validation; quality and resolution are optional but must be enums when declared. On chat routes the gateway applies its own per-dialect allowlists, and every parameter it strips is recorded per-request (visible to the buyer in the x-tm-dropped-params header and their audit endpoint). Declaring accurately now means chat enforcement picks it up when it lands.
Data policy
prompt_logging is "none" or "retained", published verbatim on your model pages next to your privacy policy and terms URLs. Say the true thing — buyers filter on it. The marketplace's handling of request data is described separately in the Data policy.
Lifecycle
is_ready: falsestages a model: validated, conformance-testable, listed nowhere, routed never. Flip it when you are ready — a model proves itself before going live, not after.listed: falseaccepts a model by name only: it is routed, priced and billed like any other model, but no list shows it (GET /v1/models, the Models page,/api/models.json, the Playground). Use it for a name that you move from model to model yourself, such as Bespoke Labs'nimble-latest: give it its own id, its ownupstream_idand the price you charge for that name. Two listings of one model id must agree on it.deprecation_date(ISO date): past it, the model leaves the catalog automatically.released(ISO date): the day the model came out. The Models page and the playground list models newest first after a few featured ones, so set it when you add a model: the lab's release date, or the day the model first listed publicly.- Date-pinned requests (
acme-large-1-20260901) resolve to your listed model for routing and billing, and the buyer's original id reaches your API verbatim — you decide whether to serve the snapshot.
The conformance bar
Our bar is published — seven checks, run against every model on both the stream and non-stream paths, at listing time and on every catalog change. We share the report with you.
- C1 — a non-stream completion returns provider-reported usage tokens. Usage is the billable record; no usage, no listing.
- C2 — streamed responses are valid SSE, every frame parseable.
- C3 — streams end with their dialect's terminal marker (
[DONE]/message_stop). - C4 — usage tokens are reported in-stream.
- C5 — an unknown model returns a parseable 4xx JSON error, not a 200 and not HTML.
- C6 — a billed response actually contains assistant content. Usage counters without an answer is the one failure we will never pass.
- C7 — the response reports the model that was asked for. Resolving an undated id to your own dated snapshot is fine; any other equivalence must be declared in
resolves_to. Silently substituting a different model is disqualifying.
An image listing (output_modality: "image") is non-stream only, so C2–C4 do not apply; it runs C5 plus three image checks, each of which buys one real render at the model's top declared quality:
- I1 — a generation returns decodable base64 and provider-reported usage tokens.
- I2 — the tokens billed for that render fit the declared per-image ceiling (
max_output_tokens). An under-declared ceiling is a house loss on every request. - I3 — an undeclared size returns a parseable 4xx JSON error.
- I4 (opt-in,
--image-ceiling) — I2 at every declared size and atsize: auto.
A decisions listing (output_modality: "decisions") runs two checks:
- D1 — one call with three questions comes back as typed answers, with usage. On the
systemonewire: one Choice, one Score and one Noul question, andusage.input_tokens. On theglinerwire: one yes/no task and one three-way task in one schema (sent withstore: false), the message content parsing to one JSON object with a labelled answer under each task name, andprompt_tokensin the usage. D1 calls each model at its owndecisions_pathwhen the model declares one, else at the endpoint's. Withendpoint.response_envelope: "cloudflare_v4", the 200 must saysuccess: true, and D1 reads the answer and the usage insideresult. Withendpoint.decisions_format: "openai", D1 sends the same three questions in OpenAI's form (achoice, ascorewith levels and apredicate), and checks each answer under its name in its own type, withusage.input_tokens. When D1 gets another status, the report shows the first 300 characters of your body. - C5 — an unknown model yields a parseable 4xx error. FastAPI's
{"detail": ...}envelope counts as a described error, and so does Cloudflare's{"errors": [{"code": ..., "message": ...}]}(or one such object in place of the list) when an entry has a message. Withendpoint.decisions_format: "openai", C5 sends the unknown model in OpenAI's form (OpenAI answers 404model_not_found).
For every model, the report also lists any Ratelimit, Ratelimit-Policy or Retry-After header your endpoint sent, or says that it sent none. These headers are reported, never judged.
Wire requirements
- HTTPS endpoint speaking OpenAI Chat Completions or Anthropic Messages, with streaming. An OpenAI-dialect endpoint that answers in one JSON body even when asked to stream declares
streaming: falseon its text models: we ask it for JSON and stream the finished answer to buyers ourselves, so the buyer's first byte waits for the whole answer. - Provider-reported usage tokens on both paths (C1/C4).
- Honest errors: return early 429s at capacity — queueing wrecks your published latency and your buyers' experience; 4xx JSON for bad requests.
- Streaming: response headers within 20 seconds or the attempt fails over; send SSE keep-alive comments through long pre-first-byte gaps, and stream tokens as soon as they exist. Non-stream requests are exempt — they get a long generation budget instead.
- After first output we never switch providers mid-answer — a stall is surfaced to the buyer as yours. Stream steadily.
How uptime is counted
Exact accounting, same numbers routing uses:
uptime = completed ÷ (completed + counted failures), trailing 24h- Counted against you: provider 401/402/403, all 5xx, mid-stream deaths, incomplete streams.
- Never counted: buyer-caused requests — 400 and 413 (bad input), 429 (their rate limit), and client cancellations mid-stream.
- Published only after 100+ requests. Below that the pages show a dash (–) — we never print a fake 100%, yours or anyone's.
- Failures open a per-deployment health circuit: cooldown, then exactly one half-open probe; recovery closes it. While everything for a model cools down we still send a last-resort attempt rather than strand buyers. Content-policy refusals are never retried and never failed over.
Performance
We publish TTFB p50/p95 (from merged t-digests) per model — the same figures the router sees. Two behaviors dominate them: return early 429s instead of queueing, and start streaming immediately.
How you get paid
Pick a settlement mode in your manifest — both directions first-class:
prepaid— we deposit with you ahead of traffic; usage draws it down, and we monitor balance against burn so routing never hits a dry account.arrears— we route first and pay on your interval:half_daily,daily,weekly, ormonthly.
Your statement is generated from the same append-only ledger that bills buyers — token counts and USD per period, auditable to the individual attempt, with per-attempt byte counts recorded as a tokenizer-independent bound for disputes. Statements and conformance reports come by email: there is no provider dashboard yet.
What you get
- Buyer traffic from both wire surfaces the day your models go live.
- A public model page per model: prices, context, your data-policy flag, live honest metrics.
- Per-period settlement statements that reconcile to the token.
- Degraded providers get circuits and time to recover, not silent delisting.
Apply
Email [email protected] with your manifest attached (validate it against the schema first). We run the suite against your endpoint, share the report, stage your models, and flip them live together.
Markdown source for agents: /docs/for-providers.md · index at /llms.txt