Quote over the session
An agent publishes prices over a WebSocket session. A quote is valid only while the session is open and the quote has not expired.
- Connect and log in.
- Read the
listedframe. It names the deployments that you can quote. - Send a
quotefor each one. Read thequote_ackorquote_reject. - Keep the session alive. Quote again before each quote expires.
All frames are JSON objects with a type field. OpenAPI names them ProviderMessage and ExchangeMessage.
Connect
wss://router.inference.ai.exchange/v1/provider/ws
The exchange closes the socket if login is more than five seconds late, or if any frame is 1 MiB or larger.
Log in
The first frame must be login. For any other frame, the exchange sends login_reject and closes the connection.
{
"type": "login",
"slug": "acme",
"api_key": "sk-liquid-...",
"dispatch_url": "https://provider.example.com/dispatch",
"dispatch_token": "a-long-random-secret",
"dispatch_dialect": "liquid",
"capabilities": []
}
| Field | Description |
|---|---|
slug |
Your current handle. The exchange refuses a handle that you gave up. |
api_key |
A provider key. See Provider credentials. |
dispatch_url |
Where the exchange sends awarded jobs. Omit it with the byok dialect. The scheme, host and port must match the registered declared_endpoint. The path is free. |
dispatch_token |
Optional. A secret that you choose. See Dispatch token. |
dispatch_dialect |
Optional. liquid (default), openai, anthropic or byok. See Dispatch dialect. |
capabilities |
Optional. handover and notice_ack. The exchange ignores unknown values. |
Dispatch token
The exchange sends the token as Authorization: Bearer <token> on each call to dispatch_url and to its /content route. Refuse a call without it, so that only the exchange can send you work.
The exchange keeps the token in memory for this session only. A new login replaces it. The reference agent reads it from LIQUID_DISPATCH_TOKEN.
Dispatch dialect
| Value | Effect |
|---|---|
liquid |
The default. The exchange sends the native delivery. |
openai |
dispatch_url is the base URL of an OpenAI-compatible API. dispatch_token is its key. See OpenAI-compatible. |
anthropic |
The same, for an Anthropic Messages API. See Anthropic. |
byok |
For a provider that sells only BYOK deployments. Give no dispatch_url. |
Capabilities
handover: your agent can move to a new exchange process with no gap. See Move to a new exchange process.notice_ack: your agent acknowledges each notice. See Acknowledge notices.
Login result
After a successful login, the exchange sends login_ack and then listed:
{ "type": "login_ack", "provider_id": "7c9e…", "slug": "acme", "server_time": "2026-09-21T10:00:00Z" }
{ "type": "listed", "models": [ … ] }
login_ack lists each accepted capability in capabilities.
A failed login gets login_reject with a reason, and the connection closes. For example, the key is unknown, the handle does not match the provider, or the dispatch_url differs from the declared endpoint.
Two logins for one provider
A new login for the same provider replaces the current session and removes its offers. Run one agent for each provider, unless the agents use handover.
A login with handover keeps the offers of the current session instead:
- The older session keeps each offer, and gets its awards, until the newer session quotes it. Then the exchange sends
supersededand closes the older session.
Listed deployments
The exchange sends listed after login. Quote only the deployments in the newest listed frame. It has each LISTED deployment and each UNLISTED deployment ("unverified": true). It has no SUSPENDED or paused deployment. Only your workspace can buy an unverified offer.
model is the dispatch handle. canonical_model is the public name. See Register deployments for the two names.
{
"type": "listed",
"models": [
{
"model_id": "3f0c…",
"unverified": false,
"deployment_name": "gpt-oss-120b",
"model": "gpt-oss-120b",
"max_input_tokens": 128000,
"max_output_tokens": 4096,
"tokenizer_id": "o200k_base",
"markup_bps": 1500
}
]
}
markup_bps is the margin from the console. It is absent when the console has no margin.
Send a quote
Send one quote frame for each listed deployment. Send it again each time the price changes.
{
"type": "quote",
"target": { "kind": "registered", "model_id": "3f0c…" },
"pricing": [
{ "billable": "input_text", "unit": "mtok", "usd": 0.12 },
{ "billable": "cache_read", "unit": "mtok", "usd": 0.03 },
{ "billable": "output_text", "unit": "mtok", "usd": 0.48 }
],
"fixed_fee": 0,
"service_class": { "ttft_ms": 1500, "completion_ms": 30000 },
"measured_p95_ttft_ms": 620,
"supported_features": ["text", "tools", "input.image", "sampling.top_p"],
"router_memberships": ["liquid.auto"],
"retention_policy_supported": ["none"],
"warm_prefixes": [],
"synthetic": false,
"revision": 7
}
Prices are JSON numbers in USD.
| Field | Type | Req. | Description |
|---|---|---|---|
target |
object | Yes | { "kind": "registered", "model_id": "…" }. |
pricing |
array | No | The prices of the quote, one line for each billable. Each line has billable, unit, usd and an optional variant. See The price table. Omit a billable if you do not charge for it. |
fixed_fee |
number | No | USD for each job. Default 0. |
service_class |
object | Yes | ttft_ms and completion_ms: the deadlines that you agree to. If you miss one, the buyer still pays and the exchange marks your deployment slow. |
measured_p95_ttft_ms |
integer | No | Your p95 time to first token. The exchange ranks on its own data. |
supported_features |
string[] | No | The request parameters that the offer supports. See Supported features. |
router_memberships |
string[] | No | The router models that this offer competes under. |
workload_profile_targets |
string[] | No | The workload profiles that this offer targets. |
retention_policy_supported |
string[] | Yes | none, allowed: the policies that you can obey. An offer with none also serves buyers who request allowed. A buyer who sets no rule needs none. The exchange rejects a quote that declares neither. |
warm_prefixes |
string[] | No | Prefix digests that you agree to serve at the cache_read price, also if the cache removes the entry. |
media_token_cost |
object | No | Input tokens for each attachment: image_base, image_per_tile, image_unmeasured, audio_per_second, pdf_per_page, video_per_second. If absent, the exchange uses its own high table. |
synthetic |
boolean | No | true when a local stand-in serves. A production exchange refuses it. |
valid_for_ms |
integer | No | How long the quote is valid, in milliseconds. Default: ten minutes. No upper limit. Zero is refused. quote_ack gives the accepted value. |
revision |
integer | Yes | Must increase in a session. The exchange rejects a revision equal to or lower than the current one. A newer session with handover takes over an offer at any revision. |
The price table
A quote states its prices as a table in pricing. Each line is one price.
"pricing": [
{"billable": "input_text", "unit": "mtok", "usd": 0.25},
{"billable": "output_text", "unit": "mtok", "usd": 1.0},
{"billable": "output_video", "unit": "second", "usd": 0.08},
{"billable": "output_video", "unit": "second", "variant": {"resolution": "720p", "audio": false}, "usd": 0.03}
]
billableis what the price is for. Token prices:input_text(input that no cache served or kept),cache_read,cache_write_short(five minutes),cache_write_long(one hour),output_textandoutput_audio(audio output tokens, withoutput.audio). Counted prices:output_image,output_speech,output_video,input_audio(audio to transcribe),input_reference_image,input_reference_videoandinput_reference_audio.unitis the singular unit of the billable:mtok(a million tokens),image,characterorsecond. A line with another unit is refused.usdis USD for oneunit. Formtokit is USD per million tokens. A speech price of $15.00 per million characters is0.000015.variantholdsresolution,audio,qualityandsize. A variant line holds for a request that states every option the variant names. A line with no variant is the price for a request that no variant line holds for. If two variant lines hold, the line that names more options wins.output_videoand the threeinput_reference_*billables takeresolutionandaudio. See Sound, resolution and price.output_imagetakesqualityandsize. The buyer states them asqualityandsizeof the image request. For example,{"quality": "hd"}is the price of an image that the buyer asks for inhd.output_speechtakesquality. The buyer states it asqualityof the speech request.
Each billable has one line with no variant. The exchange refuses these quotes with invalid_terms on the field pricing: a line with a variant on a billable that takes none, a line whose unit is not the unit of its billable, and two lines with the same billable and the same variant (also when the prices are equal). Do not send a second line with no variant. For token billables, output_image and output_speech the exchange refuses it. For output_video and the input_reference_* billables it keeps the later line, so send only one.
A quote changes only prices, deadlines and declared capabilities. The registration sets everything else.
media_token_cost sets the cap and the ranking. Settlement charges the counts that your upstream reports. If your value is too low, you pay the difference.
Quote result
The exchange answers each quote with one quote_ack or quote_reject.
{
"type": "quote_ack",
"offer_id": "3f0c…",
"revision": 7,
"activation_seq": 118,
"valid_for_ms": 600000,
"notes": [
{
"code": "defaulted",
"field": "valid_for_ms",
"detail": "absent: the offer rests for the default lifetime"
}
]
}
{
"type": "quote_reject",
"model_id": "3f0c…",
"reason": "…",
"code": "paused"
}
valid_for_ms on the ack is a duration from now. Quote again well before it ends.
Notes
notes is absent when the offer rests as you quoted it and agrees with your claims. Each note is one change by the exchange, or one conflict:
defaultedonvalid_for_ms: you omitted the lifetime, so the offer rests for ten minutes.defaultedonsupported_features: the exchange addedtextto a chat quote.ignored: the exchange does not know the field or value. It has no effect.withheld: the exchange removed a feature for a time, because requests that needed it failed.overridden: an operator set the features of this deployment. They replace yours.conflictonretention_policy_supported: the quote disagrees with the retention claim that you state on Data handling. The exchange changes nothing and applies the quote. You statezero_retention, but the quote has nonone: buyers who set no retention rule cannot buy the offer. Or you stateretention_days, but the quote hasnone. Correct the quote or the claim.
Rejection codes
Read code and field of quote_reject. Do not parse reason. field is absent when no single field is the cause.
| Code | Repair |
|---|---|
malformed |
The frame does not parse. reason names the field. Send the quote again. |
no_registration |
Quote a deployment from the listed frame. |
paused |
Resume the deployment in the provider console, then quote again. |
suspended |
An operator must reinstate the deployment. |
catalogue_unavailable |
Quote again later. |
wrong_target |
Quote with { "kind": "registered", "model_id" }. |
synthetic_on_production |
Quote the real model, or use a development exchange. |
invalid_terms |
A value cannot rest. Repair the field in field. |
stale_revision |
Send a revision higher than the one that rests. |
superseded |
A newer session quotes this deployment. This session can re-mark only offers that it still holds. |
contract_price |
A reservation contract is linked to the deployment. Quote its input and output prices, no fixed fee. |
other |
A code this build does not know. Read reason. |
The exchange keeps the last refusal of each deployment until a quote rests. GET /v1/provider/models returns it as quote_reject, with reason, first_rejected_at, last_rejected_at and count. The Models page shows it too.
Supported features
supported_features uses a fixed list of names. Declare only what your upstream supports. The exchange sends a parameter only to offers that declared it. The registered input_modalities also limit the media features (Register deployments).
| Feature | Request parameter |
|---|---|
text |
Plain text. Each request includes it. |
tools |
tools |
response_format.json_object |
response_format: {"type": "json_object"} |
response_format.json_schema |
response_format: {"type": "json_schema", …} |
input.image |
image_url content parts, or Anthropic image blocks |
input.audio |
input_audio content parts |
input.file |
file content parts, or Anthropic document blocks |
input.video |
video_url content parts. Declare it only if the model reads video |
sampling.<name> |
top_p, top_k, min_p, repetition_penalty, stop, seed, logprobs, logit_bias, frequency_penalty or presence_penalty |
reasoning.effort |
OpenAI reasoning_effort |
reasoning.budget_tokens |
Anthropic thinking.budget_tokens |
output.audio |
modalities with "audio", and audio. Needs an output_audio line |
output.image |
modalities with "image". Needs an output_text line for image tokens |
image.seed |
seed of an image job |
image.background |
background of an image job: transparent or opaque |
image.output_compression |
output_compression of an image job |
image.stream |
stream: true of an image job: you send draft images before the final image |
speech.instructions |
instructions of a speech job, for the clip or for a turn |
speech.turns |
input of a speech job as more than one turn, each with its own voice |
transcription.diarize |
diarize of a transcription job: each segment names its speaker |
transcription.keyterms |
keyterms of a transcription job |
embedding.input_type |
input_type of an embeddings job |
video.seed |
seed of a video job, a whole number of 0 or more |
video.creativity |
creativity of a video job, a number from 0 to 1 |
video.upscale_factor |
upscale_factor of a video job, a whole number of 2 or more |
video.continue |
previous_job_id of a video job: the video continues an earlier job |
- Media controls. Declare a media control only if your agent sends it to the upstream and the upstream honors it. A job with the control reaches only an offer that declared it. Quote a media model has the rules.
- Image output. An offer has one
output_textprice. If image output tokens cost more, quote the higher price withoutput.image. Text-only requests then pay it too. - Reasoning levels. If your upstream accepts only some levels, list them in
reasoning_efforts, for example["low", "high", "max"]. The exchange sends the nearest listed level, and a tie goes to the higher one. With no list, it sends the buyer's level unchanged. The order isnone,minimal,low,medium,high,xhigh,max. If the upstream refuses a level and its error names the levels it accepts, the exchange remembers them for one hour and tries your offer again at the nearest level. If the error names none, or the upstream takes no level, the job goes to the next offer. If no other offer takes the job, the exchange tries your offer again with no level, and the receipt shows this. - Log probabilities.
sampling.logprobsmeans you must send them. See Log probabilities.
Withdraw a quote
{ "type": "withdraw_quote", "target": { "kind": "registered", "model_id": "3f0c…" } }
{ "type": "mass_withdraw" }
withdraw_quote removes one offer. mass_withdraw removes each offer of the provider, and the exchange confirms with a risk_notice. Neither frame cancels a job that the exchange already awarded. A quote has no capacity field. To stop awards when you are full, withdraw the offer or let it expire, and quote it again when you have room.
Keep the session alive
If a session sends no frame for five seconds, the exchange closes it and removes its offers. Send a heartbeat when you have nothing else to send. The exchange replies with a heartbeat.
A heartbeat does not extend a quote. A quote ends after its valid_for_ms, even on an open session. A newer revision restores the offer.
When the session closes, the exchange removes its offers. Jobs already dispatched continue and settle.
Reconnect
After you connect again, log in and send each quote again. Revisions start again in a new session.
After the exchange closes a session, connect again at once. After a failed connection, wait 0.25 seconds, then double the wait up to 5 seconds. After superseded with no other session, do not connect again.
The close frame has a code:
| Code | Cause | Next step |
|---|---|---|
| 1012 | The exchange process stops. | Connect again at once. |
| 4000 | A login without handover replaced this session. |
Make sure that only one agent runs, then connect. |
| 4001 | A newer session holds all offers of this session. | Do as for superseded. |
| 4002 | The exchange received no frame for five seconds. | Connect again at once. |
| 4003 | The agent did not read its frames and the exchange queue filled. | Read the socket continuously, then connect. |
| 4004 to 4006 | An operator revoked the key (4004), the provider is not admitted (4005), or its jurisdiction cannot trade (4006). | Wait, as after a failed connection. |
With no close frame, a network fault or crash ended the session. Connect again at once.
Acknowledge notices
The exchange sends a settlement_notice to the session that holds the offer. If that session closed, it goes to the current session. If no session is open, the notice is lost, unless you use notice_ack. Send notice_ack in capabilities. When login_ack has it, acknowledge each award_notice and settlement_notice:
{ "type": "notice_ack", "award_id": "aw_…", "notice": "award" }
Set notice to award or settlement. After each login with notice_ack, the exchange sends the kept notices again, oldest first.
- A notice can arrive twice, or on another session. The award id and kind identify it.
- The exchange keeps a notice for 15 minutes, up to 4,096 for each provider, in memory. A crash deletes them. For a job with no notice, read the jobs feed.
- A login without
notice_ackdeletes the kept notices.
Move to a new exchange process
Before the old process of a release stops, the exchange sends { "type": "handover" } to each session that logged in with handover. Then do these steps:
- Keep this session open and quote on it.
- Open a second session to the same address and log in with
handover. It reaches the new exchange process. - Quote each deployment on the new session.
- When the new session has a
quote_ackorquote_rejectfor each quote, send{ "type": "handover_ready" }on the old session. - Serve jobs from both sessions. The old process closes the old session when it stops. Then use only the new session.
Without handover, your offers leave the book when the old process stops, until you connect and quote again.
A new version of your agent uses the same takeover. See Upgrade the agent.
Requirements for the handover capability
Send ["handover"] only when your agent does all of these:
- Hold two sessions at once and share the award state between them.
- Send a
heartbeaton each session. Countrevisionfrom the start of each session. - On
handover, follow the steps above. Never open a third session. Serve the jobs of the old session until the exchange closes it. - On
quote_rejectwithsuperseded, stop quoting that deployment on that session, but keep the session alive. If it gets a newlistedframe, it is current again. Quote each deployment on it. - On
superseded, let the session close. If the agent has no other session, do not log in again. Finish the current dispatches, then exit. Another process holds the offers, and a new login takes them back. - On any other close with no other session, reconnect. On shutdown, send
mass_withdrawon each session, then finish the current dispatches.
If login_ack lacks the capability, the session works without handover. The reference provider does all of this. Run your agent has tests.
Quote your cache prices
A buyer pays for input in up to four parts, each at its own rate (see the fields).
input_text: no price means the offer has no input price.cache_read: without it, settlement refuses a job that reports a cache read and you are not paid. The ranking also assumes no cache saving. If your rate card has no cache-read price, quote the input rate here.cache_write_shortandcache_write_long: without them, the exchange bills a written token as uncached input.
Quote a write rate only if your upstream charges for a write (Anthropic does, and OpenAI from GPT-5.6). Otherwise you carry any extra charge.
The cap covers a write only up to a cache breakpoint of the buyer. With no breakpoint, the exchange bills written tokens as uncached input, also if you quoted a write rate.
Win sessions with your cache
In an agentic session, the buyer sends the same conversation on each turn. If you served the previous turn and quote a cache_read price, the exchange ranks your offer as if your cache serves part of the next turn. A higher list price can still win.
- The exchange measures your hit rate from settled jobs. Until then it assumes one half.
- The price cap is the cold worst case. To lower it, list the prefix digests that you guarantee in
warm_prefixes. The cap then uses thecache_readprice for them. You carry the risk if your upstream drops the entry. - Keep one deployment for each model. A session stays with one deployment, so a second one does not share its cache.
- A session lasts five minutes after your last turn, or one hour with a one-hour breakpoint.