Purchasing rules

If you send only model, the exchange buys the offer with the lowest cost for that model. The cost is the estimated charge plus the value of the predicted wait. See Price and wait. Add an exchange object to the body to change that. It sets which models compete, how the exchange ranks them, and which limits an offer must meet. It is one optional root field, so an unchanged OpenAI or Anthropic SDK can send it.

How the exchange buys

Providers post offers on the book. An offer is a standing price for one model, with the deadlines that the provider promises. For each request:

  1. The exchange drops each offer that fails a limit of your request.
  2. It ranks the rest by cost. A strategy can change the ranking.
  3. The best offer wins. Its price cap is the most that the job can cost: the worst case, where the answer reaches its output limit and no cache saving counts.
  4. The exchange holds the cap on your balance before the first token.
  5. You pay the charge: the tokens that the provider really used. The rest of the hold returns to your balance. The charge never exceeds the cap.

Each settled job has a receipt. See Receipts.

If the winner fails before it writes any output, the exchange can try the next offer. After the first output, it never switches provider.

The controls you use most

Each field is optional.

response = client.chat.completions.create(
    model="liquid.auto",
    messages=[{"role": "user", "content": "Summarise this."}],
    extra_body={
        "exchange": {
            "cap_usd": 0.05,
            "limits": {"ttft_ms": 1500},
            "candidates": {"providers": {"exclude": ["px_slow"]}},
        }
    },
)

If no offer meets the limits, the exchange returns an error and charges nothing. See Refusals. Every other field is in the Reference.

Send rules

The exchange object has six groups, in the order that you decide them:

cap_usd stays at the top of the object, and you can also send it in limits.

curl https://router.inference.ai.exchange/v1/chat/completions \
  -H "Authorization: Bearer sk-liquid-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "architect.basket",
    "messages": [{"role": "user", "content": "Say hello."}],
    "exchange": {
      "candidates": {"models": {"allow": ["gpt-oss-120b", "qwen3-*"]}},
      "rank": {"strategy": {"kind": "price", "price_band_bps": 100}},
      "cap_usd": 0.02
    }
  }'

The Anthropic SDK takes the same extra_body. An unknown field in exchange returns 400 invalid_request. So does allowed_models.

The groups

Each rule has a place in one of six groups.

Group Rules
candidates models.allow, models.exclude, creators.allow, providers.allow, providers.exclude, providers.prefer
rank strategy, cost_tier, fast, failure_risk_ranking, latency_nanos_per_ms, workload_profile, allow_single_supplier
limits cap_usd, cap_nanos, price.input_usd_per_mtok, price.output_usd_per_mtok, price.cache_read_usd_per_mtok, price.cache_write_usd_per_mtok, price.unit_usd, ttft_ms, completion_ms, min_tps
privacy retention, regions, requirements
cache mode, expected_hit_rate, response, response_ttl_seconds
session sticky, pin, margin
{
  "exchange": {
    "candidates": { "providers": { "exclude": ["px_slow"] } },
    "limits": { "cap_nanos": 50000000, "ttft_ms": 1500 }
  }
}

Models

model sets which models compete, and whether exchange.candidates.models.allow and exchange.rank.strategy can change that.

model Models that compete exchange.candidates.models.allow exchange.rank.strategy
A model, such as gpt-oss-120b That model Refused: address_takes_no_basket price or fastest. Default price.
A router model, such as liquid.auto The candidates of the router model Limits the candidates price, fastest or smartest; auto if the router model has a cap. Default: its own ranking.
architect.basket The models in the basket Required. Without it: basket_requires_models price, fastest, smartest or auto. Default price.
A preset The saved models of the preset Replaces the saved models Replaces the saved strategy. See Presets.

A strategy that model does not accept returns 400 strategy_conflicts_with_address. smartest for one model returns 400 smartest_needs_several_models. To rank by speed or score through liquid.auto, use a model-name suffix.

The basket

exchange.candidates.models.allow is a list. Each entry is one of:

[] admits each listed model, including a model listed later. If no entry matches a listed model, no offer is admitted. The exchange removes repeated entries, then refuses a blank entry (400 invalid_basket) and a router model or architect.basket as an entry (400 basket_names_router). To choose the ranking, use exchange.rank.strategy.

{
  "model": "architect.basket",
  "messages": [{ "role": "user", "content": "Say hello." }],
  "exchange": {
    "candidates": { "models": { "allow": ["gpt-oss-120b", "gpt-oss-20b"] } },
    "rank": { "strategy": { "kind": "auto", "preference": "balanced" } },
    "cap_usd": 0.02
  }
}

Strategy

exchange.rank.strategy ranks the offers that meet all limits.

{ "kind": "price", "price_band_bps": 0 }
{ "kind": "auto", "preference": "balanced", "version": "2", "price_band_bps": 0 }
{ "kind": "smartest" }
{ "kind": "fastest" }

Fields:

Fewest redos

Set exchange.rank.failure_risk_ranking to true to rank each offer at its estimated charge divided by the chance of a good answer.

{ "exchange": { "rank": { "failure_risk_ranking": true } } }

The chance comes from the rates of the deployment over the last 7 days: failures that the provider caused, answers that came after the deadline, bad tool calls, and bad structured output. A late answer counts by how late it was, and the late rate has half the weight of a failure, because you got and paid for the answer. A deployment with no rate uses the rate of its model.

Preferred providers

Set exchange.candidates.providers.prefer to try providers in order, then use the rest of the eligible market. See Preferred providers.

Rank by one word

Add :smartest, :fastest or :cheapest to the model name to choose the ranking. exchange.rank.strategy accepts the same words. See Axis routing.

Presets

A preset saves models, a strategy and limits under a name. Create one on the Presets page. Then send the name as model:

{ "model": "fast", "messages": [ … ] }

A value in the request replaces the saved value:

A preset belongs to one workspace. Its name cannot be architect.basket. Its models cannot include a router model, architect.basket or another preset.

A preset with strategy auto needs a price bound. The bound can come from the request, the x-liquid-cap-usd header, the preset or the key. The editor sets the cost tier high if you set no bound.

The Presets page previews a sample request of 4,000 input and 1,000 output tokens. It buys nothing.

A preset buys one job kind: chat (default), image_generation, audio_speech or video_generation. Set kind when you save. If a save omits it, the kind stays. Auto and token-based price or speed rules apply only to chat, so the exchange refuses to save them on another kind. See Modalities.

A preset from a receipt

On a receipt, select Save as preset. The editor starts from what the request asked for, not from the winning offer. A preset that copies the winner has one model at one provider, so each request fails when that provider pauses. Four stops set how much of the job the preset keeps:

The preset keeps a limit that a router model added, such as the default cap of liquid.auto.

A preset linked to a key

A key linked to a preset limits each request with that key, for any model. The request cannot widen those limits.

Headers

A client that cannot change the body can send rules as headers. A header that cannot be read returns 400 invalid_header. No header sets models or ranking. To set them from headers only, use a preset.

Header or body

Each purchasing rule has a field in the exchange block. Some rules also have a header. When both set the same rule, one of them wins, as the table shows. In general the body wins: a header fills only a rule that the body and the preset leave unset.

Rule Body field Header Which wins
Price cap exchange.cap_usd x-liquid-cap-usd The body.
Time to first token exchange.limits.ttft_ms x-liquid-ttft-ms The body.
Workload profile exchange.rank.workload_profile x-liquid-profile The body.
Retention exchange.privacy.retention x-liquid-retention The body. A preset linked to the key can make it stricter, never weaker.
Expected cache hit rate exchange.cache.expected_hit_rate x-liquid-cache-hit-rate The body.
Response cache on or off exchange.cache.response x-liquid-cache The body. response: false turns the cache off even if the header is true.
Response cache lifetime exchange.cache.response_ttl_seconds x-liquid-cache-ttl The header. A header lifetime outside 1 to 86400 seconds is refused. A body lifetime outside it is moved into the range.
Clear the stored answer None x-liquid-cache-clear Only the header sets it.
Any rule of the exchange block The field of the rule x-liquid-rules The body, the preset and the single-rule headers, one rule at a time. A list in the header applies only if the body did not send that list.
Auto preference exchange.rank.strategy.preference x-liquid-policy The body. The header fills a preference that no other field set. A price ranking ignores it.
Candidates, models, ranking The fields of the groups None Only the body or a preset sets them.
Pin a job to one offer None x-liquid-pin-offer Only the header sets it. A key that cannot pin offers gets 403 pin_not_permitted.
Cost tag None x-liquid-tag Only the header sets it. See Cost tags.
Routing detail in the response None x-liquid-explain Only the header sets it. See Explain the choice.
Repeat a request safely idempotency_key, on routes that move money idempotency-key The header and the body must agree, or the route refuses the request.

A preset counts as part of the body for this table. A key's preset and the guardrails of the workspace limit both. See Presets.

The headers that need more words:

Rules in one header

x-liquid-rules holds the exchange block as JSON. Use it for rules that have no header of their own:

x-liquid-rules: {"privacy":{"regions":["eu"]},"candidates":{"providers":{"exclude":["acme"]}}}

Cost tags

Send x-liquid-tag to label what a call is for, such as a team or a project. A report can group the spend by the label. The exchange never sends the tag to a provider. A tag has 1 to 63 characters, starts with a letter or a digit, and holds letters, digits, dots, dashes and underscores. The exchange changes upper case to lower case. A tag that does not fit returns 400 invalid_header, and the call does not run.

Response

Each response has an exchange object. A stream sends it in the last chunk. It holds the job_id (also the x-liquid-job-id header), the provider_slug, the price cap in charge.cap_usd, what you paid in charge.total_usd, the metered usage, and timing.service: met if the provider kept its deadlines, else missed. You still pay for a late job. Response fields has every field.

Explain the choice

Send x-liquid-explain: true to add a routing object to the exchange block. This example is shortened.

{
  "exchange": {
    "job_id": "job_01k9x2",
    "provider_slug": "acme",
    "status": "settled",
    "charge": { "cap_usd": 0.0134, "fee_usd": 0.000038, "total_usd": 0.000804 },
    "timing": { "ttft_ms": 120, "completion_ms": 900, "service": "met" },
    "routing": {
      "clearing_rule": "first_price",
      "offers_considered": 9,
      "offers_eligible": 2,
      "single_supplier": false,
      "eligibility": {
        "excluded": [
          { "provider": "beta", "reason": "service_too_slow", "count": 4 },
          { "provider": "gamma", "reason": "region_policy", "count": 3 }
        ]
      }
    }
  }
}

Read a job later

GET /v1/jobs/{job_id} returns the same block for one job, with routing always included. A key with read scope or higher can call it. A job of another workspace returns 404, the same as a job that does not exist. The exchange writes the receipt just after it answers, so a call at once can return 404. Try again after a moment.

Refusals

No eligible offer returns 503 no_eligible_offer. If each offer only lacks a parameter of the request, the error is 400 unsupported_parameter, and param names the parameter. eligibility.excluded has one entry per considered offer with its reason. Reason codes are in Errors.

Reference

Cost

Exchange fee

The fee of a job is the larger of 5% of the supplier charge and a floor for each metered token. charge.fee_usd shows it. See Fees.

Service

Eligibility

An empty list is a value. "candidates": {"providers": {"allow": []}} on a request clears the preset's rule.

Prompt caching

Expected cache hit rate

By default the exchange ranks offers as if no input were cached. For a job that reads most of its input from a cache, an offer with a cheap input rate and an expensive cache read rate can rank above an offer that costs you less. Set exchange.cache.expected_hit_rate, such as 0.99, and the exchange ranks each offer with a cache read rate on the input that it expects to bill at that rate.

If you set no rate, the exchange can use a measured share, when its operator turns the setting on. It is the share of your workspace's input that repeated in the last 7 days, and it needs 50 settled jobs. Your rule wins over it.

When a rate applied, the exchange block holds cache_hit_rate:

Response fields

Receipts describes each stored field, including charge.uncached_usd, created_at, charge.cache_usd, finish_reason and the media quantities. Fees explains charge.fee_usd.

Client names

A receipt shows buyer.client only for a client that the exchange lists, such as claude-code or codex. The exchange reads the user agent and X-Title to find it. It never shows the user agent, Origin, Referer or an unlisted name, because that text can name a project or an internal host.

Router models

A request for liquid.auto admits each listed model in its cost tier, and no other router model. Router models have no versions, so liquid.auto@3 is not one. GET /v1/exchange/routers lists each router model with its rules, its ranking (price or Auto) and the models it admits now. exchange.candidates.models.allow limits the candidates to the basket. The router model's rules still set each limit that the request omits. See Auto Router.

Older fields

These fields still work. Sending one with the field that replaced it returns 400 invalid_request.

exchange.allowed_models is removed and returns 400 invalid_request, because its patterns could admit a deployment that a model list cannot name. Use exchange.candidates.models.allow.

OpenRouter provider

A client that targets OpenRouter sends a provider object. The exchange applies each setting that it can apply exactly, and refuses every other setting. It never ignores one. A provider that is null or {} asks for nothing. To move a whole integration, see Move from OpenRouter.

The exchange refuses these with 400 unsupported_parameter, and param is provider: quantizations, preferred_min_throughput, preferred_max_latency, max_price.request, max_price.image, allow_fallbacks: false without order, any other sort, and any unknown setting. One answer lists every refused setting. It also refuses a negative or non-numeric price and an ignore name that no provider has.

provider can only narrow the limits. The exchange applies it after exchange and keeps the stricter value of each field. It refuses the request if only, order with allow_fallbacks: false and exchange.candidates.providers.allow share no provider, or if order and exchange.candidates.providers.prefer list different providers. The preset of the key and the guardrails of the workspace limit provider as they limit exchange. A field in provider replaces the value of a preset that you name as model.