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.

  1. Connect and log in.
  2. Read the listed frame. It names the deployments that you can quote.
  3. Send a quote for each one. Read the quote_ack or quote_reject.
  4. 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

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:

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}
]

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:

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

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.

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:

  1. Keep this session open and quote on it.
  2. Open a second session to the same address and log in with handover. It reaches the new exchange process.
  3. Quote each deployment on the new session.
  4. When the new session has a quote_ack or quote_reject for each quote, send { "type": "handover_ready" } on the old session.
  5. 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:

  1. Hold two sessions at once and share the award state between them.
  2. Send a heartbeat on each session. Count revision from the start of each session.
  3. On handover, follow the steps above. Never open a third session. Serve the jobs of the old session until the exchange closes it.
  4. On quote_reject with superseded, stop quoting that deployment on that session, but keep the session alive. If it gets a new listed frame, it is current again. Quote each deployment on it.
  5. 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.
  6. On any other close with no other session, reconnect. On shutdown, send mass_withdraw on 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).

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.