# AI-Trader > An order book for LLM inference. Providers post asks from idle GPUs, buyers > post bids with a price ceiling, and the book matches on price. The API is > OpenAI-compatible, so an existing integration moves with a base URL change > plus one required field. This file is written for machines. It is complete enough to write a working integration without fetching anything else. Human docs: https://ai-trader.dev/docs ## Base URL and auth https://api.ai-trader.dev/v1 Send `Authorization: Bearer `. Buyer keys look like `sk_live_...`, provider (node) keys like `pk_live_...`. Register at https://ai-trader.dev/register. ## The one required parameter Every completion carries `max_price_per_mtok`: a hard ceiling in USD per million **output** tokens. - It is required. Omitting it returns `400 missing_price_ceiling`. - Nothing ever fills above it. If no node can serve at or under your number, you get `409 no_fill` instead of a more expensive fill. That makes the ceiling a spend cap you control from your own code. The most a request can cost is the number you wrote. ## Quickstart ```python import os from openai import OpenAI client = OpenAI(base_url="https://api.ai-trader.dev/v1", api_key=os.environ["AITRADER_KEY"]) stream = client.chat.completions.create( model="llama-3.1-8b-instruct", messages=[{"role": "user", "content": "Explain a limit order book."}], stream=True, extra_body={"max_price_per_mtok": 0.40}, ) for chunk in stream: print(chunk.choices[0].delta.content or "", end="") ``` ```bash curl https://api.ai-trader.dev/v1/chat/completions \ -H "Authorization: Bearer $AITRADER_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "llama-3.1-8b-instruct", "messages": [{"role": "user", "content": "hello"}], "max_price_per_mtok": 0.40 }' ``` ## Recommended integration: price-capped fallback The lowest-risk way to adopt this. Set the ceiling to the rate you already pay elsewhere, and fall through to your current provider on `409`. You take the fill only when it is cheaper, and you cannot overpay, because the ceiling is yours. ```python from openai import OpenAI, APIStatusError market = OpenAI(base_url="https://api.ai-trader.dev/v1", api_key=os.environ["AITRADER_KEY"]) incumbent = OpenAI() # Your current contracted output rate, trimmed so a fill still beats it once # the buyer fee (below) is added on top. CEILING = 0.86 def complete(**kwargs): try: return market.chat.completions.create( **kwargs, extra_body={"max_price_per_mtok": CEILING} ) except APIStatusError as err: if err.status_code != 409: raise return incumbent.chat.completions.create(**kwargs) ``` ## Endpoints | method | path | purpose | | --- | --- | --- | | POST | `/v1/chat/completions` | OpenAI-compatible. Spends a fill you own, else takes the best ask under your ceiling. | | GET | `/v1/models` | Tradeable contracts, mid price, live capacity. | | GET | `/v1/book?model=&depth=<1-50>` | Order book snapshot for one model. | | POST | `/v1/orders` | Post a resting bid, or an ask if you run a node. | | GET | `/v1/orders` | Your orders, newest first. Open by default; `?status=` reads history. | | DELETE | `/v1/orders/{id}` | Cancel and release unfilled escrow. | | GET | `/v1/jobs` | Every job served to you, newest first. | | GET | `/v1/jobs/{id}` | Metered tokens, cost, verification state. | | SSE | `/v1/stream/book` | Book snapshots, pushed on change. | ## Order types All are `POST /v1/orders` with `side`, `model`, `limit_per_mtok`, and a size. Send `Idempotency-Key` to make retries safe; reusing a key with a different body returns `422 idempotency_key_reuse`. - **Immediate-or-cancel** — `immediate_or_cancel: true`. Crosses now, cancels any remainder, never rests. `409 no_fill` if nothing is inside the limit. - **Resting limit bid** — the default. Rests and fills as cheap asks arrive. Each fill is prepaid capacity; a later completion spends it before touching the book. Unspent fills release after 30 minutes. - **Good-til-time** — add `expires_in` (seconds). Withdraws itself and refunds unfilled escrow at the deadline. - **Batch bid** — send `commands: [{prompt, max_tokens}, ...]` instead of `size_tokens`. Size is summed from the commands. The venue drains prompts as fills land; collect from the returned `results_url`. A batch always rests, so it cannot be immediate-or-cancel. Composes with `expires_in`. - **Growing expense** — add `growing_expense: {step_per_mtok, every_seconds, max_per_mtok}` to a resting bid and it climbs its own price on a timer until it fills or hits the cap. `max_per_mtok` must exceed `limit_per_mtok` or you get `400 invalid_escalation`. Providers use `auto_undercut: {step_per_mtok, every_seconds, floor_per_mtok}` on an ask, which descends instead. Sending the wrong block for your side is `400 invalid_escalation`. ```http POST /v1/orders Authorization: Bearer sk_live_... { "side": "bid", "model": "llama-3.1-8b-instruct", "limit_per_mtok": 0.18, "expires_in": 28800, "commands": [ { "prompt": "Label the sentiment of this review: ...", "max_tokens": 8 }, { "prompt": "Extract every line item as JSON: ...", "max_tokens": 512 } ] } ``` ## Billing - **Output tokens only.** Prompt/input tokens are not billed at all. For long-input, short-output work (classification, extraction, summarisation) this is usually the largest difference against per-input-token pricing. - Prices are quoted per million tokens; every size on the wire is a whole number of tokens. - The venue meters by re-tokenising the response. The node's own report is recorded but never billed from. - A bid escrows `size × limit` plus the buyer fee. Escrow is a hold, not a charge; cancelling or partially filling releases the remainder. - A bid fills at the **ask**, not at its own limit. The spread goes to the buyer. - Aborted, truncated, and node-dropped streams bill only the tokens that reached you. - Fees: 2% buyer, 3% provider. The buyer fee sits on top of `limit_per_mtok`, so set your ceiling slightly under any rate you are comparing against. ## Errors worth handling | status | code | meaning | | --- | --- | --- | | 400 | `missing_price_ceiling` | `max_price_per_mtok` was absent. | | 400 | `unsupported_parameter` | `stream` was not true. See below. | | 400 | `invalid_sampling` | A sampling value fell outside its range. See below. | | 400 | `invalid_escalation` | Malformed or wrong-side escalation block. | | 402 | `escrow_insufficient` | Bid needs more funded balance than you hold. | | 409 | `no_fill` | Nothing available at or under your ceiling. | | 422 | `idempotency_key_reuse` | Key replayed with a different body. | Errors are always `{"error": {"code": ..., "message": ...}}`. Branch on `code`, never on `message`. ## Supported and refused parameters Required: `stream: true`. Completions are served only as an SSE stream — a buffered response sends nothing until the answer is done, so your client waits the whole job for headers and most give up at 300s. Jobs here run longer. A node that vanishes mid-answer cannot arrive as a status code, because you already had a 200 when the stream opened. It comes as a `data: {"error": {...}}` frame after whatever text made it through; partial output is billed, the rest refunded, and a retry is free. Supported: `messages` (system/user/assistant roles preserved), `max_tokens` (default 4096, capped at 32768, larger values clamped rather than rejected), `response_format: {"type":"json_object"}`, `stop` (up to 4 sequences). Sampling: `temperature` (0-2), `top_p` (0-1), `top_k`, `seed`, `frequency_penalty` and `presence_penalty` (-2 to 2) are passed to the backend as given. Anything you omit keeps the default the model ships with, which samples — output is not reproducible unless you ask for `temperature` 0. A value outside those ranges is `400 invalid_sampling`, refused before any escrow is taken. `seed` repeats an answer only on the same node running the same weights — the venue does not route on it. Batch commands take the same fields, each one independently, so a single bid can mix a greedy extraction with a warmer piece of prose. The `400` names the position of the command that was wrong. Read `finish_reason`: `stop` (finished), `length` (hit a token ceiling — send a follow-up for the rest), `null` (node never said; treat as unknown). Responses carry an `x-aitrader-job` header for `GET /v1/jobs/{id}`. ## History Nothing is discarded. `GET /v1/orders` defaults to `status=open` — the resting ones — which is why an immediate-or-cancel client can see an empty list and conclude the venue keeps no record. It does: pass `?status=filled|cancelled|expired|all` to read it back. `GET /v1/jobs` enumerates every job served to you, including ones you kept no id for, with our metered token count, the node that served it, and the fee charged on top — the rows to reconcile your own records against. Both page with `page` and `page_size` (default 50, clamped to 200) and answer with `page`, `page_size`, `page_count`, `total`, `data`. Both are scoped to the key that authenticated; there is no parameter that widens them to another account. An unknown `status` is `400 invalid_status`. ## When this is a poor fit - Latency-critical interactive traffic — depth varies with who is online, and a `409` in front of a waiting user is worse than a slightly higher bill. - Frontier closed models — contracts here are open models at a named quantisation. - Workloads needing reserved or contracted throughput. - Work that must be bit-identical run to run. Sampling is yours to set, but the venue does not route on a seed, and two honest nodes at a different quantisation diverge at the first near-tie. ## Live market Generated 2026-09-12T03:59:52.450Z. Do not hard-code these; call `GET /v1/models`. _No two-sided markets right now. Call `GET /v1/models` for the current list._ ## More - https://ai-trader.dev/use-cases — integration patterns and which workloads fit - https://ai-trader.dev/docs/api — API reference - https://ai-trader.dev/docs/orders — order types in full - https://ai-trader.dev/docs/pricing — fees, escrow, settlement - https://ai-trader.dev/docs/verification — how a node's output is checked - https://ai-trader.dev/docs/host-api — for selling GPU capacity