Code generation: a model at 1/271st the price of the best scorer, at 97.8% of its quality.

Docs

Potion speaks the OpenAI chat-completions protocol. If you already have an OpenAI client, you change one line and keep everything else — the request body, the response shape, streaming and tool calls are unchanged.

You are reading this signed out, so the base URL below is the generic one and the policy section shows the four shapes rather than yours. Sign in and this page fills in with your own endpoint and bound policy.

Ask the docs

A question about anything on this page, answered from this page, served through Potion as rag-answer — with the receipt it got.

ask the docs · answered from this page only · served through potion

the receipt — x-frontier-trace, forwarded as x-potion-receipt — appears here

Quickstart

  1. 1. Create a serving key on API keys.
  2. 2. Point your client at the base URL below.
  3. 3. Send a request. Watch it appear on Connect with the routing decision it got.
Base URL
https://api.withpotion.com/v1
curl
curl https://api.withpotion.com/v1/chat/completions \
  -H "Authorization: Bearer $POTION_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"potion-auto","messages":[{"role":"user","content":"Write a python function that reverses a string"}]}'
Node.js (openai SDK)
import OpenAI from 'openai';

const client = new OpenAI({
  baseURL: 'https://api.withpotion.com/v1',
  apiKey: process.env.POTION_API_KEY,
});

const res = await client.chat.completions.create({
  model: 'potion-auto', // any label; Potion routes by prompt + policy
  messages: [{ role: 'user', content: 'Write a python function that reverses a string' }],
});
console.log(res.choices[0].message.content);
Python (openai SDK)
from openai import OpenAI

client = OpenAI(
    base_url="https://api.withpotion.com/v1",
    api_key=os.environ["POTION_API_KEY"],
)

res = client.chat.completions.create(
    model="potion-auto",  # any label; Potion routes by prompt + policy
    messages=[{"role": "user", "content": "Write a python function that reverses a string"}],
)
print(res.choices[0].message.content)

Hand it to your agent

If Claude Code, Cursor, Codex or another coding agent does your integration, give it one of these. Each block is written for the agent: what to change, what to keep, how to verify, and what never to do. Your key is not in the block; the agent reads it from the environment.

hand this to your agent · claude code, cursor, codex, anything
# Route this app's AI requests through Potion

You are integrating Potion, a measured model router with an OpenAI-compatible API.
Base URL: https://api.withpotion.com/v1
Key: read `POTION_API_KEY` from the environment. Do not ask for it, do not echo it; if it is unset at runtime, fail with a clear message BEFORE constructing a client. This matters: the OpenAI SDKs silently fall back to `OPENAI_API_KEY` and `OPENAI_BASE_URL` when given undefined, so an app migrating off OpenAI with those still set would quietly keep sending traffic to OpenAI while believing it is on Potion. Always pass both `apiKey` and `baseURL` explicitly.
Retries: the SDK's default retries (on 408/409/429/5xx and connection errors) are safe to keep — those responses are refused before any model runs, so nothing is billed twice. A 200 is billed once.
Model: `potion-auto` (or the org's NAMED router id `potion/<org-slug>` — shown on the dashboard's Router page; both route identically). Routing is decided by the rule bound to the key, never by this field; the field is echoed back and recorded with the request as a label, so an existing per-tier label (e.g. `fast`, `best`) may be passed through unchanged as a free tag. The receipt (below) shows what answered.
Base URL: your Potion origin plus `/v1` — put it in `POTION_BASE_URL` (default `https://api.withpotion.com/v1`) so it can be changed without a code edit (inside a private network the origin differs).
## The receipt
Every Potion response carries the header `x-frontier-trace`. Its exact format:
`cluster=classification;strategy=ffe46bc9;frontier=v4;policy=min_cost;fallback=0;provenance=live`
- `cluster` — the kind of work Potion detected (one of: classification, extraction, code-gen, code-review, rag-answer, summarization, rewrite-edit, creative, multi-step-reasoning, agentic-tool-use)
- `strategy` — the first 8 characters of the hash of what answered (a model, or a measured combination)
- `frontier` — the version of the measured frontier it was picked from
- `policy` — the rule in force: min_cost, max_quality or latency_bound
- `fallback` — 0 when the rule picked a measured point; 1 when nothing measured qualified and the default served
- `provenance` — `live` means served by a real provider on live evidence; anything else is not a real answer
Optional extras: `policy_override=<name>` when a per-request rule was used; `upgraded=0|1` for combinations.
The model that answered is in a second header, `x-potion-model` (a model name, or `combination:<type>` for a measured combination). The response body's `model` field only echoes the label you sent.
Streaming: `stream: true` streams for a single model; when Potion serves a measured combination that cannot stream, it returns a complete 200 JSON body instead, with the header `x-latency-contract: non-streamed`. Streaming clients must handle a non-streamed JSON response.
Discover policies with GET /v1/policies (ids, names, and which one the key is bound to); never guess policy names — omit `x-potion-policy` entirely to use the key's bound policy (that IS the convention). Every JSON answer carries a `potion` object: requested vs resolved cluster/policy, the model that answered, and `fallback` + `fallback_reason` (`policy_infeasible` = no measured point met the bar; the best point served) — read it instead of parsing the trace.
Tool loops work as in OpenAI (assistant `tool_calls` turn, then `role: "tool"` results). Forwarded parameters: temperature, top_p, stop, seed, user, response_format (JSON mode / JSON schema), parallel_tool_calls, max_tokens; `n` must be 1. Content-part arrays are accepted for text; image parts return 400 `unsupported_content` — do not send vision traffic through Potion yet.
Errors are OpenAI-shaped JSON: `{"error":{"message","type","code"}}`; `error.code` is the stable field (e.g. `budget_exceeded`, `rate_limit_exceeded`, `cluster_not_found`, `no_policy_bound`). Error responses do not carry the receipt.

Optional request headers: `x-potion-cluster: <cluster>` to state the kind of work yourself (one of the ten above; unknown values return 400 cluster_not_found); `x-potion-policy: <policy name or id>` to use a different rule for this one request.

## Steps
1. Find every place an OpenAI client is constructed (`new OpenAI(...)` in JS/TS, `OpenAI(...)` in Python, or equivalent), and every raw `fetch`/HTTP call to an OpenAI-shaped endpoint. There may be more than one. Leave Potion's own internal provider abstractions alone if you happen to be inside Potion's repo.
2. Set the base URL to `https://api.withpotion.com/v1` and the API key to `process.env.POTION_API_KEY` / `os.environ["POTION_API_KEY"]`. Keep every other option. Do this in the package that makes the calls (in a monorepo, not the root).
3. Set `model` to `"potion-auto"` on chat-completions calls. Leave messages, temperature, streaming, tools, tool_choice, max_tokens untouched.
3b. To read the receipt: JS/TS `const { data, response } = await client.chat.completions.create({...}).withResponse(); response.headers.get('x-frontier-trace')` (openai >= 5). Python `raw = client.chat.completions.with_raw_response.create(...); raw.headers['x-frontier-trace']; raw.parse()` (openai >= 1.0).
4. If the code branches on the provider model name in responses (e.g. parsing `response.model`), make it tolerant: Potion returns the label you sent.
5. Run the existing test suite. Preserve behaviour: if you moved from raw HTTP to the SDK, note that the SDK throws on non-2xx instead of returning a failed response and only JSON-decodes JSON content types — keep the app's observable behaviour the same.

## Verify
1. Send one real request through the app. If `POTION_API_KEY` is not set, fail with a clear message before calling anything.
2. Read the response headers `x-frontier-trace` and `x-potion-model` and print them. Their absence means the request did not go through Potion. Reading a header is not "changing response handling".
3. Confirm in code that the header's `provenance` is `live` and `fallback` is `0` on a normal request. (The human can see the same request on the dashboard under Home → Your first receipt; an agent cannot, so do not try.)

## Never
- Never print, log, or commit the Potion key. Read it from the environment variable `POTION_API_KEY`.
- Never alter the OpenAI request or response shapes, or the headers Potion returns. Streaming and tool calls work unchanged.
- Never rely on the model field to choose a model: it is a label. The rule on the key decides; `x-potion-model` shows what answered.

Paste it into your agent with your key already in the environment. The block never contains the key.

Authentication

Every call carries a serving key as a bearer token: Authorization: Bearer $POTION_API_KEY. Keys come in two scopes. A serve key sends traffic and reads its own state; serve+admin is additionally allowed to provision — mint keys, move budgets, rebind policy.

A key is shown once, at creation, and stored only as a SHA-256 hash. Potion cannot show it to you again and will not pretend otherwise — if it is lost, revoke it and mint another. Each key carries its own policy binding, so separate keys are how you run different trade-offs side by side.

You do not bring provider keys. Potion serves every request from its own, across providers — which is also what lets the router reach the whole catalogue rather than the one account you happened to have.

The model field means what it says

Your router has a name: potion/<your-org> — shown on your Router page and first in GET /v1/models. It is the model id to put in your code: Potion compiles that router from your workload, quality bar, and the measured frontiers, versions it as the evidence moves, and your receipts name the version each request rode. potion-auto is the plain alias — the two route identically.

Three cases, no surprises. potion/<your-org> (or potion-auto) routes: Potion classifies the request and serves the measured pick under your policy. A known model name pins: exactly that model answers, the trace says policy=pinned, and nothing overrides your explicit choice. An unknown name is an error (400 unknown_model) — never a silent reroute.

Migrating an app whose model strings you cannot change yet? Flip migration mode in Settings · Controls (or PUT /api/org-settings) and every label routes like potion-auto — an explicit, org-level choice. The receipt always names what actually answered.

The decision header

Every response carries x-frontier-trace, which is the routing decision in full. A router you cannot audit is a router you cannot trust, so this ships on every request rather than behind a debug flag.

x-frontier-trace
cluster=code-gen;strategy=6efe8a56;frontier=v2;policy=min_cost;fallback=0;provenance=live
clusterThe workload type the prompt was classified into.
strategyFirst 8 characters of the selected strategy hash — the exact configuration served, resolvable in Frontiers.
frontierWhich published frontier version the choice came from. It increments when new evidence republishes.
policyThe rule that selected the point: min_cost, max_quality, latency_bound, compound — or pinned, when you named the model yourself.
fallback0 means a measured frontier existed and your policy selected a point on it. 1 means it did not, and the request rode the default strategy — the honest signal that Potion has nothing measured for this work yet.
provenancelive means the evidence behind the choice came from real provider runs. Anything else means it did not, and should not be treated as a measurement.
x-potion-modelA sibling header naming the model that actually answered — also stamped onto your request log as served_model, so the ledger never guesses.
constrainedPresent only as constrained=tools, when the request carried tools and your policy's optimum was a prompt-transforming strategy. Selection narrowed to single-model points, which the tool contract requires. Your policy's bound still held — a quality floor, cost ceiling or latency bound is never breached by narrowing, only its optimum is — so fallback stays 0.

Receipts and the kept line

Every token, accounted for. Each served request becomes a row on Receipts: when it ran, the kind of work, the model that answered, what it cost — and what your named baseline would have cost, recorded at serve time from real token counts, never reconstructed later. The difference is the kept line, and the month’s kept lines sum to the savings figure on Today — the same number, all the way up.

Programmatic access: GET /api/routing-activity returns the rows (servedModel, costUsd, baselineCostUsd, the parsed trace, and a summary that counts only requests which carried a routing decision).

Outcomes — tell it what good means

Everything above measures quality with a judge — a model scoring another model against a rubric. That is the best anyone can do without you. But your application already knows the truth: the SQL ran or it didn’t, the validator passed, the person accepted the draft or rewrote it. Send that back and your own traffic becomes the measurement instrument.

request_id is the id every completion already carries. Potion looks up the request you were served, copies its cluster, strategy and router version onto the outcome at ingest, and the evidence shows up on your router as your app’s verdicts — on its own scale, never averaged into judge scores.

Report an outcome
curl -X POST https://api.withpotion.com/v1/outcomes \
  -H "Authorization: Bearer $POTION_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"request_id":"chatcmpl-abc123","success":true,"validator":"sql_executed"}'
TypeScript
const resp = await client.chat.completions.create({ /* … */ });

// later, where your app already knows the answer was good:
await client.outcome(resp.id, { success: true, validator: 'sql_executed' });
Python
resp = client.chat.completions.create(...)

# later, when the generated SQL has actually run:
client.outcome(resp.id, success=True, validator="sql_executed")

Any one of these counts as a signal:

successBoolean — did this answer do its job?
scoreYour own number in [0, 1]. Bounded so the scale stays honest.
humanaccepted, edited, rejected or regenerated — what a person actually did with it.
validatorName of the check that ran, e.g. sql_executed, tests_passed, schema_valid.
labelA free-text label from your own taxonomy.
failure_reasonWhat went wrong, when something did.

Append-only: send the validator result now and the human verdict an hour later as a second call — the latest signal of each kind wins, so a correction is one more POST and never an edit. Unknown fields are rejected rather than silently dropped, and an outcome for a request Potion did not serve is a 404.

Why it is worth the ten minutes
Outcomes are the only signal that can move routing on evidence a judge cannot produce. Wire one validator you already run and the router starts optimising for the thing you actually care about, instead of the thing a rubric can see.

Policies

A policy is the rule Potion optimises under. It is bound per key, and applies to every request that key sends.

  • min_cost — cheapest option holding a quality floor.
  • max_quality — best measured quality under a cost ceiling.
  • latency_bound — best quality inside a p95 latency budget.
  • compound — a quality floor and a latency bound, cheapest of the survivors.

Cost ceilings are expressed per 1,000 requests, not per 1,000 tokens. Latency bounds are p95 in milliseconds.

Rebind this key's policy
curl -X POST https://api.withpotion.com/v1/policies \
  -H "Authorization: Bearer $POTION_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"min_cost","qualityFloor":0.8}'

The first two weeks

Your router works on day one — v1 routes every request on Potion’s own live measurements, under your quality bar. Then, over your first week or two of traffic, Potion reads a small, redacted, capped sample of your requests (an explicit setting, off with one switch) and measures models on your actual work. When coverage fills for a kind of work, Potion proposes your own quality bar for it — you accept or ignore; nothing changes silently. Each accepted bar recompiles your router as a new version with the change written on it. Starting from scratch, with no incumbent to measure against, the arc is identical — your work is measured against a strong default bar instead. The Router page shows live where you are in this arc.

Your bar, your floor, your pins

Name what you use today (PUT /api/incumbents) and your quality bar becomes a measurement against your own model on your own traffic rather than a number we picked. Potion samples consented requests (capped per kind of work, personal data redacted), measures, and then proposes your bar — the proposal appears on Today and applies in one click (POST /api/learning/proposals/:id/apply).

The floor (PUT /api/floor) sets the org-wide quality minimum and rebinds every live key. Pins (GET/PUT/DELETE /api/pins) freeze the exact frontier version serving a workload — useful while you run your own comparisons — and GET /api/frontier-changelog narrates every movement in plain English, including what a pin is holding back.

Workload types

Every prompt is classified into one of these before anything is selected. Each has its own frontier, because the best strategy for extraction is not the best strategy for multi-step reasoning — that difference is the entire reason routing pays.

code-genWriting code to a specification
code-reviewFinding defects in code and explaining their impact
extractionPulling structured fields out of unstructured documents
summarizationCondensing a document while preserving what matters
classificationAssigning a label from a fixed set
multi-step-reasoningProblems needing several dependent steps
creativeOpen-ended writing where there is no single right answer
rewrite-editRevising text to a brief without changing its meaning
rag-answerAnswering from supplied source passages
agentic-tool-usePlanning and sequencing tool calls

A prompt that matches none of them confidently is served on the default strategy, and the trace says fallback=1 rather than guessing.

Streaming and compatibility

Set stream: true and you get standard server-sent events terminated by data: [DONE], the same as any OpenAI-compatible client expects. The routing decision is chosen before the first token, so x-frontier-trace is present on the response headers even while the body is still streaming.

  • Tools and function calling pass through to the selected model unchanged. Because tool semantics cannot survive a strategy that rewrites or fans out the prompt, a request carrying tools is served from a single-model point; if that is not your policy's optimum, the trace says constrained=tools.
  • Token usage and cost come back on the response, taken from the provider’s own reported figures where it reports them rather than from a modelled estimate.
  • Agentic loops work as in OpenAI: an assistant turn carrying tool_calls and the role: "tool" result turns are accepted and forwarded to the model verbatim.
  • Request parameters forwarded to the selected model: temperature, top_p, stop, seed, user, response_format (JSON mode and JSON schema), parallel_tool_calls, max_tokens. response_format and stop, like tools, are served only by single-model points. In JSON mode a model that fences its object in ```json has the fence removed on non-streaming responses when the inside parses, so JSON.parse works as it does on OpenAI. n must be 1.
  • Content-part arrays are accepted; text parts are joined. Image parts are refused with 400 unsupported_content until a vision frontier is measured — never silently dropped.
  • Typed routing on the response: every non-streaming answer carries a top-level potion object — requested vs resolved cluster and policy, policy_source (key_default when you sent no override — omitting x-potion-policy is the convention for "use the policy bound to this key"), the model that answered, and fallback with a fallback_reason (policy_infeasible means no measured point met your bar and the best point served). The x-frontier-trace header stays the source record.
  • Policy discovery: GET /v1/policies lists your org's policies with ids and names and marks the one bound to the calling key — populate a selector from it instead of guessing identifiers. An unknown x-potion-policy returns policy_not_found with the available policies and the safe default in the error.
  • x-latency-contract appears only when a measured combination cannot token-stream: the value non-streamed means the answer arrives as one JSON body despite stream: true.
  • Legacy completions are shimmed at /v1/completions.

Errors

Errors use the OpenAI envelope — { error: { message, type, param, code } } — so existing client error handling keeps working.

400 invalid_request_errorThe body did not validate — a missing messages array, a malformed policy.
400 unsupported_contentThe messages carry image parts; Potion routes text only for now. The message says how many.
401 authentication_requiredNo bearer token was supplied.
401 invalid_api_keyThe key is unknown, revoked or expired.
403The key is valid but its scope does not cover this call — provisioning with a serve key rather than serve+admin.
413The request body exceeds the accepted size.
400 unknown_modelThe model value is neither potion-auto nor a model Potion serves. Name one from /v1/models, or enable migration mode.
400 cluster_not_foundAn explicit X-Potion-Cluster hint named a cluster that does not exist.
403 insufficient_roleThe call needs a higher role — minting keys, rebinding policies, applying proposals and flipping org settings are admin actions.
429 rate_limit_exceededToo many requests. Back off and retry.
429 budget_exceededYour spend cap would be crossed by this call. Refused BEFORE the provider is called, so it costs nothing.
503 service_unavailableNo upstream could serve the request.

Limits and budgets

A budget is a cap on spend with an optional hard stop. The check runs before the upstream call, so a refused request costs nothing — a cap that only notices after the money is gone is not a cap. Set it on Usage or through /api/budgets.

Rate limits are enforced per key. A limited response carries the standard retry hints; treat 429 as backpressure rather than failure.

API reference

Everything this dashboard does is an HTTP call you can make yourself with a Bearer token. A serve key covers the serving surface and its own reads; provisioning needs serve+admin.

  • POST/v1/chat/completionsServe a request. OpenAI-compatible; streaming supported.
  • POST/v1/completionsLegacy completions shim.
  • POST/v1/embeddingsPlatform embedder.
  • GET/v1/modelsThe catalogue, with potion.measured marking what is actually routable.
  • GET / POST/v1/policiesRead or rebind the calling key's own policy.
  • POST/v1/outcomesReport what actually happened after an answer — your app's verdict becomes routing evidence.
  • POST/api/planDescribe what you're building → workload type + measured options. member+
  • GET/api/connectionBase URL, bound policy, keys, per-cluster routing readiness.
  • GET/api/routing-activityRecent requests with the routing decision each one got.
  • GET / POST/api/api-keysList or mint keys. Minting requires serve+admin.
  • GET / PUT/api/budgetsSpending cap and hard stop. admin
  • GET/api/usageRequests, tokens and spend; /api/usage/current for the live day; /api/usage/invoice for the period invoice.
  • PUT/api/incumbentsName what you use today; starts consented measurement. admin
  • GET/api/learningSampling progress and bar proposals; POST /api/learning/proposals/:id/apply accepts one. admin to apply
  • PUT/api/floorOrg-wide quality floor; rebinds every live key. admin
  • GET / PUT / DELETE/api/pins/:clusterIdFreeze or release the frontier version serving a workload. admin to change
  • GET/api/frontier-changelogEvery frontier movement, narrated.
  • GET/api/certificationsSuite certifications — what is vouched for, and what was refused.
  • GET / PUT/api/org-settingsModel-field semantics: migration mode on or off. admin to change
  • POST/v1/tracesAgent spans in; priced, loop-flagged, clustered into agent-* workloads.
  • GET / PUT/api/traces/retentionSpan retention in days; 0 keeps metadata only. admin to change
  • GET/api/auditKey custody, sign-ins and incidents, one chronology; /api/audit/export.jsonl for a window. admin
  • GET / POST/api/alertsWebhook or Slack notifications. Subscribe to evidence_ready to hear when a challenger or a workload is measured and waiting on your decision. admin

Pricing: aligned by construction

Model costs pass through at cost. Potion’s revenue is a share of the savings your receipts verify — the same serve-time counterfactual described above, summed per period. If Potion saves you nothing, it earns nothing above cost. The invoice (GET /api/usage/invoice, Settings · Billing) itemises model cost, verified savings, and the share — the bill and the proof are the same numbers.

Agent journeys

Agent workloads send spans to POST /v1/traces with the same bearer key. Potion prices every span, flags tool-call loops, and clusters redacted sessions into agent-* workloads that grow their own frontiers. Pin a request to one explicitly with the X-Potion-Cluster header. Retention is yours:PUT /api/traces/retention (0 = metadata only; prompts and attributes are redacted on purge).

Things worth knowing

  • A catalogue is not a frontier. Potion knows about more models than it will route to. Only points it has measured for your kind of work are ever selected automatically — an unmeasured model is reachable, never auto-chosen.
  • Quality numbers carry intervals. Every measured quality ships with the confidence interval its evidence supports. Where two strategies overlap inside that interval, Potion reports them as tied rather than inventing a ranking — and your policy decides on cost or latency instead.
  • Latency numbers start provisional. Before you have traffic, p95 comes from evaluation runs (the model call only). Potion switches to serving-grade latency, measured end to end on your own requests, once there is enough of it.
  • Your first requests route on platform evidence. Measurements of the workload type, not of you. As your traffic accumulates, the numbers become yours.