# 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](https://inference.ai.exchange/docs/price-and-wait.md).
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](#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](https://inference.ai.exchange/docs/receipts.md).

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.

- `cap_usd`: an offer with a higher price cap does not compete.
- `limits.ttft_ms`: an offer that promises a slower first token does not compete.
- `candidates.providers.exclude`: a listed provider does not compete.
- `privacy.retention`: `none` limits offers to providers that do not keep your prompt. See
  [Data handling](https://inference.ai.exchange/docs/data-handling.md).
- `rank.strategy`, or a model suffix: `price`, `fastest`, `smartest` or `auto`. See [Strategy](#strategy).

```python
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](#refusals). Every other field is in the [Reference](#reference).

## Send rules

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

- **Candidates**: `exchange.candidates`. Which models, creators and providers compete.
- **Rank**: `exchange.rank`. How the exchange ranks the offers that meet all limits.
- **Limits**: `exchange.limits`. An offer must meet each limit to compete.
- **Privacy**, **cache** and **session**: data rules, caching rules and session rules.

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

```bash
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`                                                                                                                                                                                   |

```json
{
  "exchange": {
    "candidates": { "providers": { "exclude": ["px_slow"] } },
    "limits": { "cap_nanos": 50000000, "ttft_ms": 1500 }
  }
}
```

- `cap_nanos` is the cap as an integer of nano-USD. `50000000` equals `cap_usd` `0.05`. You can send
  it at the top of the object or in `limits`.
- If you set one rule twice with different values, the exchange returns `400 conflicting_parameters`,
  and the message names both fields. It never picks one.
- `session.sticky` and `session.pin` are two settings: `sticky` applies to ranking by price, and `pin`
  applies to Auto.
- An unknown field in a group returns `400 invalid_request`.

## 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](#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](#rank-by-one-word).

### The basket

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

- A served model name: `gpt-oss-120b`.
- A deployment symbol: `acme/gpt-oss-120b`. It admits only that deployment.
- A pattern with `*`: `gpt-*`, `*-mini`. It admits each match when the request arrives.

`[]` 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`.

```json
{
  "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.

- `price`: the lowest cost wins. See [Price and wait](https://inference.ai.exchange/docs/price-and-wait.md).
- `auto`: the best published score for the estimated charge wins. It needs a price bound: a cap, a cost
  tier other than `any`, or an output-rate ceiling. See [Auto Router](https://inference.ai.exchange/docs/auto.md).
- `smartest`: the highest published score wins, whatever the charge. It is Auto version 4 and needs a
  price bound.
- `fastest`: the highest median output speed that the exchange measured wins. An offer with no
  measurement ranks last. The estimated charge breaks a tie. See
  [How the exchange measures performance](https://inference.ai.exchange/docs/performance-measurement.md).

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

Fields:

- `kind`: required. `smartest` and `fastest` take no other field.
- `price_band_bps`: `price` and `auto` only. Offers within this many basis points of the lowest cost
  count as one cost. 0 to 10000, default 0. A larger value returns `400 invalid_price_band`.
- `preference`: `auto` only. `economy`, `balanced` or `premium`.
- `version`: `auto` only. `"1"` to `"8"`, default `"8"`. Version 4 is `smartest`.

### 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.

```json
{ "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.

- The rule changes the order of the offers and nothing else. The price cap, the hold and the charge do
  not change.
- The chance of a redo counts at most 50%, so an offer ranks at no more than twice its charge.
- It applies to ranking by price and to [Auto](https://inference.ai.exchange/docs/auto.md). The request value wins over the preset.
- A request with `response_format` of `json_object` or `json_schema` uses the rule by default. Send
  `false` to rank by charge alone.

### Preferred providers

Set `exchange.candidates.providers.prefer` to try providers in order, then use the rest of the eligible market.
See [Preferred providers](https://inference.ai.exchange/docs/provider-preferences.md).

### 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](https://inference.ai.exchange/docs/axis-routing.md).

## Presets

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

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

A value in the request replaces the saved value:

- `exchange.candidates.models.allow` replaces the saved models.
- `exchange.rank.strategy` replaces the whole saved strategy. A field that it omits gets its default.
- Each limit that the request sends replaces the saved limit.

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](https://inference.ai.exchange/docs/modalities.md).

### 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:

- **Exact replay**: only the winning model and provider.
- **Same model**: the winning model, any provider.
- **Your order**: what the request asked for. This is the default.
- **Open**: models and strategy, no limits.

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.

- **Models.** Only the preset's models can serve. A request that names some of them is limited to those.
  A request that names none returns `403 key_policy_violation`. An empty list sets no limit.
- **Limits.** If both set a limit, the stricter applies: the lower `cap_usd`,
  `limits.ttft_ms`, `limits.completion_ms`, `limits.price.*` and `rank.cost_tier`; the higher
  `limits.min_tps`; and `privacy.retention: "none"`, `rank.fast: true` and
  `rank.allow_single_supplier: false`. Excluded models and providers combine. An offer must
  pass `exchange.candidates.providers.allow`, `exchange.candidates.creators.allow` and `exchange.privacy.regions` of both. A different
  `exchange.rank.workload_profile` returns `403 key_policy_violation`.
- **Strategy.** The key does not enforce it. A request that names the preset and sends no strategy uses
  the saved one.

## 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](#presets).

### 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](#cost-tags).                                                                                       |
| Routing detail in the response   | None                                         | `x-liquid-explain`        | Only the header sets it. See [Explain the choice](#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](#presets).

The headers that need more words:

- `x-liquid-retention`: `none` or `allowed`.
- `x-liquid-cache-hit-rate`: 0 to 1. It is not `x-liquid-cache`, which switches the response cache.
- `x-liquid-rules`: a whole `exchange` block as JSON. See [Rules in one header](#rules-in-one-header).
- `idempotency-key`: a repeated request gets the first response. No second job runs.

### Rules in one header

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

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

- It is the lowest layer. The body, the preset and the single-rule headers win over it, one rule at a
  time. A list in the header applies only if the body did not send that list and the preset left it
  empty.
- It cannot set `candidates.models.allow` or `rank.strategy`. Use a preset.
- A block that is not valid JSON, or has an unknown rule, returns `400 invalid_header`. The header holds
  ASCII only.

### 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](#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.

```json
{
  "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](https://inference.ai.exchange/docs/errors.md).

## Reference

### Cost

- `cap_usd`: maximum charge for the job. An offer with a higher worst-case charge is not eligible.
- `exchange.limits.price.input_usd_per_mtok`, `exchange.limits.price.output_usd_per_mtok`: maximum published input and generation rates, USD
  per million tokens.
- `exchange.limits.price.cache_read_usd_per_mtok`, `exchange.limits.price.cache_write_usd_per_mtok`: the same for cache rates. An offer with
  no such rate passes.
- `rank.cost_tier`: `any`, `low`, `medium`, `high`, `xhigh` or `max`. It overrides the tier of the router
  model. `any` removes the limit.

### 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](https://inference.ai.exchange/docs/fees.md).

- The floor of a kind is at most 1 cent per million tokens (0.1 cent for a cache read).
- An offer that charges nothing for a kind pays 5% of the model's list price or mean rate, when one
  exists, and at most that amount.
- A job on a free offer, such as `liquid.free`, pays the floor. Its cap and hold are above zero.
- A job priced by image, character or second pays no floor.

### Service

- `limits.ttft_ms`: maximum time to first token. The deadline adds 1 second per 10,000 input tokens, for the
  prompt read.
- `limits.completion_ms`: maximum time to completion.
- `exchange.limits.min_tps`: minimum generated tokens per second that the offer's deadline must give.
- `rank.fast`: a boolean for a stricter service threshold, with no numeric value.

### Eligibility

- `exchange.candidates.models.exclude`: names or patterns (`gpt-*`, `*-mini`) that must not serve the job. It wins over the
  basket.
- `exchange.candidates.providers.allow`: handles or patterns that can compete. `[]` and `["*"]` admit all.
- `exchange.candidates.providers.prefer`: handles or patterns to try first, in this order. It is not a limit. See
  [Preferred providers](#preferred-providers).
- `exchange.candidates.providers.exclude`: handles that must not compete.
- `exchange.candidates.creators.allow`: creators whose models can compete. Case and known aliases match (`meta-llama` =
  `meta`, `qwen` = `alibaba`).
- `exchange.privacy.regions`: regions where the deployment can be.
- `exchange.privacy.retention`: `none` or `allowed`. An offer that cannot obey `none` is not eligible.
- `exchange.privacy.requirements`: `{ "require": [claim, ...], "checked_only": bool }`. The provider must hold
  every claim. With `checked_only`, a declared claim does not pass. A lapsed claim never passes. See
  [Provider claims](https://inference.ai.exchange/docs/provider-claims.md).
- `exchange.rank.workload_profile`: a named profile. A saved workload has the profile
  `<workspace>.<workload>@v<revision>`. A profile of another workspace gets `400 invalid_request`.
- `exchange.rank.allow_single_supplier`: accept an award when only one independent supplier is eligible. Default
  `true`.

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

### Prompt caching

- `exchange.cache.mode`: `auto` (default), `buyer` or `off`. Who places prompt-cache breakpoints. See
  [Prompt caching](https://inference.ai.exchange/docs/caching.md).
- `exchange.cache.expected_hit_rate`: 0 to 1. The share of input tokens that you expect to be billed at the cache
  read rate.

#### 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.

- The rule changes the order of the offers and nothing else. The cap, the hold and the charge do not
  change.
- An offer with no cache read rate, or with a cache read that costs no less than uncached input, gets no
  credit.
- `0` turns the credit off. A value outside 0 to 1 returns a `400` that names the field.
- The request value wins over the preset. It applies to ranking by price and to [Auto](https://inference.ai.exchange/docs/auto.md).
- A rate higher than the share that the provider really reads can make you pay more than the best offer
  would have cost.

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`:

- `expected`: the share of input that ranking billed at the cache read rate.
- `source`: `buyer` or `measured`.
- `credit_usd`: what the credit took off `charge.estimated_usd`, the ranking estimate.
- `real`: `usage.cached_tokens` divided by `usage.prompt_tokens`, once the job has usage. A `real` rate far
  below `expected` means that your rate is too high, or that the provider did not read its cache.

### Response fields

[Receipts](https://inference.ai.exchange/docs/receipts.md) describes each stored field, including `charge.uncached_usd`, `created_at`, `charge.cache_usd`, `finish_reason` and the media quantities. [Fees](https://inference.ai.exchange/docs/fees.md) explains `charge.fee_usd`.

- `job_id`, `provider_slug`, `offer_id`: the job, the serving provider and the winning offer.
- `status`: `pending` until usage settles, then `settled` or `credited`.
- `charge.cap_usd`: the price cap, locked before the first token.
- `charge.total_usd`: supplier charge plus fee, minus credits. `charge.fee_usd` and `charge.credits_usd`
  are parts of it.
- `charge.cap_held_usd`: what the cap took off the metered charge, fee included. Present only when the
  job used more than its cap. The provider carries it.
- `usage.prompt_tokens`, `cached_tokens`, `cache_write_tokens`, `completion_tokens`: metered usage in the
  provider's tokenizer. `prompt_tokens` includes cache reads and writes.
- `usage.reasoning_tokens`: hidden reasoning tokens, billed as output. Present only when the provider
  reports them.
- `timing.ttft_ms`, `timing.completion_ms`: measured time to first token and to completion. TTFT starts
  at request arrival, so it includes the auction.
- `timing.ttft_limit_ms`: your TTFT limit. Absent for image jobs and old receipts.
- `timing.ttft_deadline_ms`: `ttft_limit_ms` plus 1 second per 10,000 input tokens. The job is `met` or
  `missed` against this deadline, not against the provider's promise.
- `timing.promised_ttft_ms`: the time that the winning offer promised. A `met` job can be slower.
- `timing.service`: `met`, `missed`, `failed`, `pending`, `unknown`, `not_started` or
  `single_supplier_refused`. A late job is charged like any other.
- `addressing`: the requested name, how it resolved, and the model and precision that served it.
  `addressing.auto` records why Auto chose. See [Auto Router](https://inference.ai.exchange/docs/auto.md).
- `answer_checks.structured_output`: `result` and, for `schema_mismatch`, the failed `keyword`. Present
  only with `json_object` or `json_schema`. See [Structured output](https://inference.ai.exchange/docs/errors.md#structured-output).
- `answer_checks.tool_calls`, `tool_call_errors`, `tool_call_faults`, `tool_markup`: the tool calls, the
  bad ones, the bad ones by kind, and `1` when the text has tool-call markup but the answer has no tool
  call. Present only with `tools`. See [Tool calls](https://inference.ai.exchange/docs/errors.md#tool-calls).
- `buyer.client`: the program that sent the request. See [Client names](#client-names).
- `routing`: present only with `x-liquid-explain: true`. See [Explain the choice](#explain-the-choice).

### 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](https://inference.ai.exchange/docs/auto.md).

### Older fields

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

- `exchange.policy`: use `exchange.rank.strategy.preference`.
- `exchange.approved_models`: use `exchange.candidates.models.allow` with `model` set to `architect.basket`.
- `exchange.price_band_bps`: use `exchange.rank.strategy.price_band_bps`.

`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](https://inference.ai.exchange/docs/move-from-openrouter.md).

- `max_price.prompt`, `max_price.completion`: `exchange.limits.price.input_usd_per_mtok` and
  `exchange.limits.price.output_usd_per_mtok`. A number or numeric string.
- `only`: `exchange.candidates.providers.allow`.
- `order`: `exchange.candidates.providers.prefer`. With `allow_fallbacks: false`, it is also
  `exchange.candidates.providers.allow`, so only those providers compete.
- `ignore`: `exchange.candidates.providers.exclude`. A name must be a provider handle, or a pattern with `*`.
- `data_collection: "deny"`: `exchange.privacy.retention` becomes `none`. `"allow"` changes nothing.
- `zdr: true`: requires the `zero_retention` claim in `exchange.privacy.requirements`.
- `sort: "price"`: `exchange.rank.strategy` becomes `price`. Refused when the strategy is another kind.
- `require_parameters`, `allow_fallbacks: true`: accepted. The exchange already matches on the features
  that the request needs.

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`.
