# crowbot > One OpenAI-compatible API for every direction an AI user wants: frontier models (OpenAI, Moonshot, DeepSeek, Google) > resold at a fraction of their list price, and crowbot's own uncensored CROW models, > behind one key. Billed from a prepaid USD wallet, or from a plan's pass that pays first for > the models it covers; one base URL reaches every model. An account > is a 16-digit number that doubles as the API key — no email, no password, no recovery. Payment > is by crypto or card. Base URL `https://api.crowbot.sh/v1`. Currently live: CROW 2, CROW 1, GPT 6 Astra, GPT 6 Sol, Kimi K3, GPT 5.6 Sol, DeepSeek V4.1 Flash, Gemini 3.8 Flash. ## Connecting crowbot speaks the OpenAI chat-completions dialect. Anything that can talk to OpenAI can talk to crowbot by changing two settings: the base URL and the API key. ``` base URL https://api.crowbot.sh/v1 auth Authorization: Bearer ``` Your account number *is* your API key. There is no separate key to generate. Spaces are accepted, so you can paste it exactly as it was shown to you. ## Quick start ```sh curl https://api.crowbot.sh/v1/chat/completions \ -H "Authorization: Bearer $CROWBOT_KEY" \ -H 'content-type: application/json' \ -d '{ "model": "gpt-6-astra", "messages": [{"role": "user", "content": "Say hello in five words."}] }' ``` With the OpenAI SDK, change two settings and nothing else: ```python from openai import OpenAI client = OpenAI(base_url="https://api.crowbot.sh/v1", api_key="4192083755612094") print(client.chat.completions.create( model="gpt-6-astra", messages=[{"role": "user", "content": "Say hello in five words."}], ).choices[0].message.content) ``` ## Where things live Three origins, each serving only its own paths; a path asked of the wrong one is a plain 404. | origin | serves | |---|---| | `https://api.crowbot.sh` | `/v1/*`, `/api/me` and `/api/plans` — what an agent configures and reads | | `https://chat.crowbot.sh` | the web chat, signup, funding (`/api/funding`, `/checkout`), plans, pairing | | `https://crowbot.sh` | these docs, `/llms.txt`, the installer | `GET https://api.crowbot.sh/api/me` returns `site` and `api`, so a client given one origin can find the others. ## Models Everything below is rendered from the live model table, so it is what the gateway will actually charge and serve right now. | model | id | in | cached in | out | list in | list out | context | max output | status | |---|---|---|---|---|---|---|---|---|---| | CROW 2 | `crow-2` | $6.00 | $0.60 | $10.00 | — | — | 1M | 32.8K | live | | CROW 1 | `crow-1` | $3.00 | $0.30 | $5.00 | — | — | 262K | 32.8K | live | | GPT 6 Astra | `gpt-6-astra` | $0.30 | $0.03 | $1.50 | $10.00 | $50.00 | 1M | 272K | live | | GPT 6 Sol | `gpt-6-sol` | $0.18 | $0.018 | $0.90 | $2.00 | $10.00 | 1M | 128K | live | | Kimi K3 | `kimi-k3` | $0.27 | $0.027 | $1.35 | $3.00 | $15.00 | 1M | 1M | live | | GPT 5.6 Sol | `gpt-5.6-sol` | $0.08 | $0.008 | $0.40 | $4.00 | $20.00 | 1M | 128K | live | | DeepSeek V4.1 Flash | `deepseek-v4.1-flash` | $0.045 | $0.0009 | $0.18 | $0.30 | $1.20 | 1M | 128K | live | | Gemini 3.8 Flash | `gemini-3.8-flash` | $0.0675 | $0.0067 | $0.3375 | $0.75 | $3.75 | 1M | 65.5K | live | Rates are US dollars per million tokens. Input, cached input and output are billed separately at their own rate — there is no single blended price, because a request's cost depends on its shape. **Cached input** bills at the cached rate whenever the upstream reports a hit, at the ratio we get. Not every model reports hits, so budget for the full input rate, and keep the stable part of your prompt first: one changed token near the front invalidates everything after it. ## Limits `context` is the prompt window; `max output` is the ceiling on one response, clamped rather than rejected. Both are editable without a deploy, so treat `/v1/models` as authoritative. ## Why it's this cheap Frontier models at a fraction of list price can look too good to be true. Here's how it works. ### Where the discount comes from We don't run the frontier models ourselves. We buy access from resellers who sell well below the vendors' list prices, and sell it on at a fixed discount off list. The [models page](https://crowbot.sh/docs/models) shows both prices side by side. ### Every upstream is tested first Resellers vary, and some cut corners. Before one gets any of your traffic, we put it through a battery of prompts whose answers we can check. If it gets answers wrong, pads the bill, fakes cache hits, drops your system prompt, breaks tool calls or turns out to be a different vendor's model, we don't use it. ### What we keep For each request: the time, the model, the token counts and the cost, for billing. Not your prompts or replies, and no request log, no name, no email. Web chats stay in your browser. The upstream serving the model does see your request, and we don't control what it keeps. The details are in [Privacy](https://crowbot.sh/privacy). ### Contact admin@crowbot.sh · Discord: https://discord.gg/HKTMW5zsbG ## Endpoints | method | path | origin | auth | what | |---|---|---|---|---| | `POST` | `/v1/chat/completions` | https://api.crowbot.sh | key, paid balance | chat completions — streaming, tool calling, `reasoning_effort` | | `GET` | `/v1/models` | https://api.crowbot.sh | none | live models with limits and per-direction rates | | `POST` | `/v1/tools/codesearch` | https://api.crowbot.sh | key, funded account | run a `codesearch` tool call server-side; see *Code search* | | `GET` | `/api/me` | https://api.crowbot.sh, https://chat.crowbot.sh | key or chat cookie | balance, spend, request count, key hint, `can_spend`, the active `pass` (plan, scope, `ends_at`, `allowance_bp`, `refills`) or null, the balance-fallback switch, and the `site`/`api` origins | | `POST` | `/api/me/balance-fallback` | https://api.crowbot.sh, https://chat.crowbot.sh | key or chat cookie | body `{"on": bool}`: whether the balance pays once a plan's allowance is spent | | `GET` | `/api/me/usage` | https://api.crowbot.sh, https://chat.crowbot.sh | key or chat cookie | this account's own spend, by model and by day, over `days` (default 30, 0 is all time): `{days, models: [{model, label, requests, prompt_tokens, completion_tokens, cached_tokens, spent_usd}], series: [{day, requests, spent_usd}]}` | | `GET` | `/api/me/usage/log` | https://api.crowbot.sh, https://chat.crowbot.sh | key or chat cookie | one row per request, paged 50 at a time. `page`, `sort` (`ts`/`model`/`tokens`/`cost`), `dir` (`asc`/`desc`), `days`. `totals` covers the whole window, never the page | | `GET` | `/api/me/payments` | https://api.crowbot.sh, https://chat.crowbot.sh | key or chat cookie | this account's payments, newest first `{payments: [{at, usd, kind}]}` | | `GET` | `/api/me/keys` | https://api.crowbot.sh, https://chat.crowbot.sh | key or chat cookie | the keys on this account (paired machines) | | `POST` | `/api/me/keys/{ref}/revoke` | https://api.crowbot.sh, https://chat.crowbot.sh | key or chat cookie | revoke one key by its `ref`; the key you are using is refused (409) | | `POST` | `/api/me/delete` | https://api.crowbot.sh, https://chat.crowbot.sh | key or chat cookie | close this account for good; body {account_number}; any balance is forfeited | | `POST` | `/api/pair/start` | https://api.crowbot.sh, https://chat.crowbot.sh | none (per-address cap) | open a pairing: `{device_code, user_code, ttl}`; body `{"hostname"}` optional | | `GET` | `/api/pair/{device_code}` | https://api.crowbot.sh, https://chat.crowbot.sh | none (per-address cap) | poll: 202 `pending`, 200 `{status: ready, api_key}` once, 410 expired | | `GET` | `/pair` | https://chat.crowbot.sh | none | the page a person types the `user_code` into | | `POST` | `/pair` | https://chat.crowbot.sh | chat cookie | the same page, showing Approve for a typed code | | `POST` | `/api/pair/approve` | https://api.crowbot.sh, https://chat.crowbot.sh | chat cookie | approve `{user_code}` (mints a key for that machine) or `{user_code, deny: true}` | | `POST` | `/api/signup` | https://chat.crowbot.sh | none + `X-Pow` (kind `signup`) | mint an account: `{account_number, formatted, api_base}` | | `GET` | `/api/pow` | https://chat.crowbot.sh | none for `?kind=signup`; key or cookie for `chat` | a proof-of-work challenge `{challenge, bits, ttl}` | | `POST` | `/api/chat` | https://chat.crowbot.sh | key or chat cookie + `X-Pow` | the web chat's own streaming door; agents use `/v1/chat/completions` | | `GET` | `/api/models` | https://chat.crowbot.sh | none | the chat picker's model list (same rows as `/v1/models`) | | `POST` | `/api/me/billing-portal` | https://api.crowbot.sh, https://chat.crowbot.sh | key or chat cookie | a session in the processor's portal for the cards it keeps for you `{url}` | | `GET` | `/api/live` | https://chat.crowbot.sh | none | server-sent events: each live topic (`?topics=tokens`) now, then on every change | | `GET` | `/api/live/{topic}` | https://chat.crowbot.sh, https://crowbot.sh | none | a live topic's latest payload; `tokens` is `{total}`, every token served, all time | | `POST` | `/api/tools/codesearch` | https://chat.crowbot.sh | key, funded account | `/v1/tools/codesearch`, reachable from the chat origin | | `GET` | `/api/funding` | https://chat.crowbot.sh | none | what a top-up may be and how to pay for it `{min_usd, max_usd, pay_with, rails}` | | `GET` | `/api/plans` | https://api.crowbot.sh, https://chat.crowbot.sh | none, or key or chat cookie | the plans on sale to this account; `{plans: []}` while none is | | `POST` | `/api/plans/{plan_id}/buy` | https://api.crowbot.sh, https://chat.crowbot.sh | key or chat cookie | buy that plan's pass with credit already in the wallet; `{ok, pass_id}`, 402 when short | | `GET` | `/api/me/subscription` | https://api.crowbot.sh, https://chat.crowbot.sh | key or chat cookie | the renewing agreement this account holds, or `{subscription: null}` | | `POST` | `/api/me/subscription/cancel` | https://api.crowbot.sh, https://chat.crowbot.sh | key or chat cookie | stop renewing; the pass already paid for runs to its end | | `GET` | `/checkout` | https://chat.crowbot.sh | key or chat cookie | `?usd=N` for credit, `?plan=ID` for a pass, `&renew=1` for one that renews → `{checkout_url}` (JSON) or a 303 to the processor (browser) | | `GET` | `/paid` | https://chat.crowbot.sh | none | where the processor sends a person back after paying | | `GET` | `/` | https://chat.crowbot.sh, https://crowbot.sh | none | the landing page on the site origin, the web chat on the chat origin | | `GET` | `/install.sh` | https://crowbot.sh | none | the opencode installer (bash) | | `GET` | `/install.ps1` | https://crowbot.sh | none | the opencode installer (PowerShell, Windows) | | `GET` | `/llms.txt` | https://crowbot.sh | none | this document | | `GET` | `/llms-full.txt` | https://crowbot.sh | none | every docs page inlined | | `GET` | `/docs` | https://crowbot.sh | none | 301 to the first docs page | | `GET` | `/docs/{name}` | https://crowbot.sh | none | one docs page, rendered for people and search engines | | `GET` | `/robots.txt` | https://crowbot.sh | none | what search engines may crawl, and where the sitemap is | | `GET` | `/sitemap.xml` | https://crowbot.sh | none | the public pages, from content/pages.yaml | | `GET` | `/docs/{name}.md` | https://crowbot.sh | none | one docs page as markdown | | `GET` | `/docs/opencode.json` | https://api.crowbot.sh, https://crowbot.sh | none | generated opencode provider config | | `GET` | `/docs/opencode-plugin.js` | https://api.crowbot.sh, https://crowbot.sh | none | the plugin the installer drops in | | `POST` | `/webhooks/payment` | https://crowbot.sh | processor HMAC | the crypto processor's callback | | `POST` | `/webhooks/stripe` | https://crowbot.sh | Stripe signature | the card processor's callback | | `GET` | `/healthz` | every origin | none | liveness, on every host | | `GET` | `/privacy` | https://crowbot.sh | none | the Privacy page | | `GET` | `/terms` | https://crowbot.sh | none | the Terms page | | `GET` | `/refunds` | https://crowbot.sh | none | the Refunds page | | `GET` | `/why` | https://crowbot.sh | none | the Why it's cheap page | `/v1/chat/completions` is a passthrough: **your `system` message is sent to the model unchanged and stays first**, and so are your `tools`, `tool_choice` and `response_format`. Your system prompt is never replaced, edited, reordered or moved behind anything of ours. Some models add a short instruction block of their own **alongside** it — formatting or refusal style. Your prompt keeps its exact bytes and its position; ours goes after it. ## Request ```json { "model": "gpt-6-astra", "messages": [ {"role": "system", "content": "You are a careful assistant."}, {"role": "user", "content": "Hello"} ], "max_tokens": 1024, "stream": true } ``` An **unrecognised** `model` falls back to the default rather than erroring, because agentic clients often send names from other providers. A model id we **do** recognise but are not serving returns `404 model_not_found` and lists the live ids: falling back there would bill you for a model you did not ask for. **Sampling parameters are forwarded** as you send them, including `temperature` and `top_p`. Which of them a model honours is up to its vendor. **`reasoning_effort`** we do interpret. Send `low`, `medium`, `high` or `max`: it overrides the model's configured default, a model without that tier gets its own ceiling (`max` reaches a vendor with three tiers as `high`), and a value none can honour is a `400` naming what the model accepts. Reasoning tokens bill as **output**, so this is a spend control, not a quality slider. ## Streaming Set `stream: true` and read Server-Sent Events. Chunks are `chat.completion.chunk` objects terminated by `data: [DONE]`, exactly as OpenAI emits them. Usage arrives on a final chunk with an empty `choices` array — you do not need to ask for it. Reasoning models put their thinking trace in `choices[].delta.reasoning_content`, alongside the usual `content`. Clients that don't know the field simply ignore it. ## Code search `codesearch` runs on our side: give it a query, get matching code from public repositories. No search account of your own. ``` POST https://api.crowbot.sh/v1/tools/codesearch Authorization: Bearer ``` `query` is required; `lang`, `repo` and `count` are optional. `query` may itself carry search filters (`repo:`, `file:`, `lang:`, `patterntype:regexp`, `patterntype:structural`, …), which compose with the fields; `count` is capped. ```json {"query": "epoll_wait", "lang": "c", "count": 5} ``` The reply is `{"query", "results", "truncated"}`, each result carrying `repo`, `path`, the first matching `line`, a `url` to the source on its own host, and a `snippet` (for a `type:symbol` query, the symbol's kind, name and line): ```json {"results": [{"repo": "github.com/x/y", "path": "loop.c", "line": 42, "url": "https://github.com/x/y/blob//loop.c#L42", "snippet": "int n = epoll_wait(epfd, events, MAX_EVENTS, timeout_ms);"}], "truncated": false} ``` Write queries the way code is written: exact identifiers find code, sentences rarely do. `lang` narrows by language; `repo` as `org/name` is anchored to that one repository, which turns a pile of forks into the canonical file. A backend rate limit comes back as `429 rate_limited` with `Retry-After`. **Wiring it into an agent.** Register the tool below; when the model calls it, POST the arguments to the endpoint above and feed the reply back as the tool result. ```json { "type": "function", "function": { "name": "codesearch", "description": "Search public source code and get back matching snippets with repo, path and line. Use it when a real implementation answers better than a description. Write the query the way code is written, not the way a question is asked: two or three exact identifiers (function, class, option or constant names) find code; sentences almost never do. Set lang when you know the language. Set repo when you know the project - org/name is enough - which turns a pile of forks into the one canonical file. Quote an exact phrase \"like this\" to match it verbatim; add type:symbol to find where a name is defined rather than used (slower, but finds the defining file even when the canonical repo is not indexed); add patterntype:regexp for a pattern. If nothing comes back, or the repository is reported as not found, drop the repo filter or a term and search again.", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "Identifiers and exact phrases, optionally with file:/type:symbol/patterntype:regexp filters" }, "lang": { "type": "string", "description": "Language filter, e.g. python, c++, typescript, go" }, "repo": { "type": "string", "description": "Repository: org/name (anchored to that repo) or a regexp over full names like github.com/org/name" }, "count": { "type": "integer", "description": "Maximum results, capped by the server" } }, "required": [ "query" ] } } } ``` Snippets bill as ordinary input tokens on your next turn; there is no search fee. Calls are rate-limited. This is the same definition the web chat gives the model. ## Errors Errors are `{"error": {"message": …, "type": …}}`. The `type` is the field to branch on — not the status, because a status can be ambiguous. | status | type | meaning | |---|---|---| | 400 | `bad_request` | the body was not a JSON object, or a parameter is invalid | | 400 | `bad_amount` | `/checkout`: not an amount to the cent within `min_usd`..`max_usd` | | 400 | `bad_ref` | `/api/me/keys/{ref}/revoke`: not a key reference | | 400 | `number_mismatch` | `/api/me/delete`: the account number typed is not this account's | | 400 | `cannot_renew` | `/checkout?renew=1`: the chosen payment method cannot hold a card to charge again | | 401 | `invalid_api_key` | account number unknown or revoked | | 403 | `account_disabled` | the account is frozen (a card chargeback or refund); it can read its status but not spend or add funds | | 402 | `insufficient_balance` | out of funds, or this request's input alone costs more than the balance — top up | | 402 | `plan_allowance_spent` | the plan's allowance is spent and this account is set not to fall back on its balance | | 404 | `model_not_found` | a model we know but are not serving; the message lists the live ids | | 404 | `not_found` | no such page, key or route on this host | | 404 | `no_such_plan` | the plan named is not on sale to this account | | 404 | `no_subscription` | `/api/me/subscription/cancel`: this account has no renewing plan | | 409 | `cannot_revoke_current` | that key authenticated this request; revoke it from another | | 409 | `subscription_active` | `/api/me/delete`: a renewing plan is still active; cancel it first | | 410 | `pair_expired` | the pairing code expired or was already used | | 413 | `body_too_large` | over the 16 MiB request-body ceiling | | 428 | `pow_required` | proof of work missing or expired (chat and signup only, never `/v1`) | | 429 | `rate_limited` | back off; honour `Retry-After` when present | | 502 | `tool_error` | a tool (e.g. `codesearch`) failed upstream; the message says how | | 502 | `checkout_failed` | the payment processor did not answer | | 502 | `cancel_failed` | the payment processor did not answer a cancellation; retry shortly | | 404 | `no_customer` | `/api/me/billing-portal`: no card is saved yet; pay by card once | | 502 | `portal_failed` | `/api/me/billing-portal`: the payment processor did not answer; retry shortly | | 503 | `live_full` | `/api/live`: every live stream is taken; poll `/api/live/{topic}` and retry after `Retry-After` | | 503 | `live_warming` | `/api/live/{topic}`: the topic has not been read since the app started; retry shortly | | 503 | `tool_unavailable` | a tool is not configured or is temporarily off | | 503 | `checkout_unconfigured` | payments are not configured on this deployment | | 503 | `no_model` | nothing is live right now | | 503 | `no_provider` | that model's upstream is not configured | | 503 | `no_price` | a feed-priced model with no quote yet — retry shortly | | 503 | `price_fault` | a feed-priced model whose quote moved past our ceiling; off sale until a sane one lands | | 503 | `upstream_unavailable` | our problem, not yours — try another model | | vendor's | `upstream_error` | the upstream's own reason for refusing, with its `Retry-After` if it sent one | Every `/v1/chat/completions` answer, error or not, carries an `X-Request-Id` header. Quote it when reporting a failure: it names the one record we keep of that call. A streamed reply may open with an SSE comment line (`:`) and carry one during any pause: a keepalive, which every SSE client ignores. A failure after the stream has opened arrives as that same `{"error": {"message": …, "type": …}}` envelope in a `data:` frame, followed by `data: [DONE]`. An upstream vendor's own 401/402/403 is never relayed to you as-is. If our account with a vendor lapses you get a `503 upstream_unavailable`: the fault is ours, not your key's or balance's. ## Billing Input, cached input and output bill at separate per-million rates — see [Models](https://crowbot.sh/docs/models.md). Every token sent upstream is billed, and cached input is discounted at the same ratio the upstream gives us. Two things can pay for a request, and they are asked in a fixed order. A **pass** — what buying a plan gives you, see *Plans* — pays first, for any model in its scope. Your **balance** pays for everything else: a model the pass does not cover, or every request when you hold no pass. The two never mix inside one request; whichever said yes at the door settles it. A pass does not bill at the rates above. It carries an allowance, sized from what the plan cost, that requests draw down at what the model costs *us* and that refills continuously at the plan's rate — so it absorbs a burst and comes back over the day. Once the allowance cannot cover a request, that request is the balance's, unless you have said otherwise: `POST /api/me/balance-fallback` with `{"on": false}` caps you at the plan, and a request that would then need the balance is refused with `402 plan_allowance_spent` and a message saying roughly when the allowance is back. `GET /api/me` shows the pass while you hold one: `allowance_bp` is how much of the allowance is left, in basis points of the whole, and `refills` says roughly when it is full again. Non-streamed responses carry `X-Balance-Microdollars` with your remaining paid balance in microdollars (1e-6 USD); a request the pass paid for leaves it where it was. A streamed response does not carry it — headers are sent before the body starts, so the figure would be stale before the first token. Read `/api/me` after a stream, or use the `usage` block on the final chunk. Every request is sized to whatever is paying for it before it is sent upstream: `max_tokens` (or `max_completion_tokens`) is capped to what the pass's remaining allowance or the balance can pay for after the estimated input, and a request whose input alone costs more than that is refused with `402 insufficient_balance` before any vendor is called. Concurrency follows the payer too — a balance gets as many parallel requests as it covers in worst-case requests, a pass at most the slots its plan sold, one at minimum either way — and past that a `429 rate_limited` with `Retry-After`. ## Plans A plan is one price for a fixed number of days. Buying one gives the account a **pass**: an allowance that pays first for every model in the plan's scope, the way *Billing* describes, while the balance goes on paying for everything else. What is on sale is public: ``` GET https://chat.crowbot.sh/api/plans Authorization: Bearer (optional) ``` The reply is `{"plans": [...]}`, one entry per plan you may buy: ```json {"id": "starter", "label": "Starter", "tagline": "Hard working or hardly working?", "price_usd": 15, "days": 30, "concurrency": 2, "scope": ["frontier"], "features": ["Every frontier model", "Refills every day", "Cancel any time"], "badge": null} ``` `price_usd` buys `days` days. `concurrency` is how many requests the pass runs at once; `scope` names the directions on the [Models](https://crowbot.sh/docs/models.md) page it covers (`frontier` is the resold frontier models, `crow` our own). `features` and `badge` are the card's own words. A pass fixes its allowance in money when it is bought, so nothing on a plan moves when a vendor reprices. The list is what *your* account may buy. Signed out, it is whatever is on general sale; while plans are in a closed beta it is empty unless your account is in it. `{"plans": []}` means nothing is on sale to you, not that the call failed. ## Funding, programmatically The chat UI does all of this with buttons; an agent can do it with four calls, all on `https://chat.crowbot.sh` except the last. 1. **Create an account.** `GET https://chat.crowbot.sh/api/pow?kind=signup` returns `{"challenge", "bits", "ttl"}`. Find a `nonce` (≤ 40 chars) such that `sha256(":")` has at least `bits` leading zero bits, then `POST https://chat.crowbot.sh/api/signup` with header `X-Pow: .`. The reply is `{"account_number", "formatted", "api_base"}` — the only time the number is ever shown. Each signup from an address raises the next one's `bits` for a day, and signups are capped per address; a `428` means the work grew meanwhile: fetch again. 2. **Pick an amount.** `GET https://chat.crowbot.sh/api/funding` → `{"min_usd", "max_usd", "pay_with", "rails"}` (`rails`: `[{"name", "label", "renews"}]`). Any amount in range is accepted, to the cent (`usd=3.50`), credited 1:1. 3. **Get an invoice.** `GET https://chat.crowbot.sh/checkout?usd=` with `Authorization: Bearer ` returns `{"checkout_url"}` (a browser `Accept: text/html` request is redirected there instead). Payment is by crypto or card; crypto settles in stablecoin. 4. **Confirm.** There is no payment-status endpoint. The processor's callback credits the wallet; poll `GET https://api.crowbot.sh/api/me` until `balance_microdollars` rises. Balances never expire. ## Device pairing and keys A machine can hold its own key, so a laptop can be revoked later without changing the account number. This is what the installer does; the raw flow: 1. `POST https://chat.crowbot.sh/api/pair/start` (optional body `{"hostname": "…"}`) → `{"device_code", "user_code", "ttl"}`. Unauthenticated, capped per address. 2. Show the person the `user_code` and send them to `https://chat.crowbot.sh/pair`, where they type it and press Approve while signed in (programmatically: `POST https://chat.crowbot.sh/api/pair/approve` with the chat cookie and `{"user_code"}`, or `{"user_code", "deny": true}`). 3. Poll `GET https://chat.crowbot.sh/api/pair/`: `202 {"status": "pending"}` while waiting, then `200 {"status": "ready", "api_key"}` exactly once, then `410 pair_expired`. Codes last 15 minutes. `GET /api/me/keys` lists the account's live keys with a `ref`, `name`, `hint`, `created_at` and whether each is the one making the request; `POST /api/me/keys//revoke` revokes one. The key that authenticated the request cannot be revoked (`409 cannot_revoke_current`): there is no recovery, so that click would be a permanent lockout. ## Clients See [opencode](https://crowbot.sh/docs/opencode.md) for a one-line installer. Any agent that speaks chat-completions needs only the base URL and the key. **Codex** (the OpenAI CLI): add a provider to `~/.codex/config.toml` and select it. ```toml model_provider = "crowbot" model = "gpt-6-astra" [model_providers.crowbot] name = "crowbot" base_url = "https://api.crowbot.sh/v1" env_key = "CROWBOT_API_KEY" wire_api = "chat" ``` **Claude Code** speaks the Anthropic Messages API, which crowbot does not serve: pointing `ANTHROPIC_BASE_URL` here gets a 404. Use a translating proxy (LiteLLM), opencode, or Codex. ## Docs - [Quickstart](https://crowbot.sh/docs/quickstart.md): Create an account, fund it, make the first call - [API](https://crowbot.sh/docs/api.md): Endpoints, auth, streaming, tool calling, error types - [Models](https://crowbot.sh/docs/models.md): Live lineup with per-direction rates and limits - [opencode](https://crowbot.sh/docs/opencode.md): Paste-ready opencode provider config ## Machine-readable - [https://api.crowbot.sh/v1/models](https://api.crowbot.sh/v1/models): live models with limits and per-direction rates - [https://api.crowbot.sh/docs/opencode.json](https://api.crowbot.sh/docs/opencode.json): generated opencode provider config ## Optional - [https://crowbot.sh/llms-full.txt](https://crowbot.sh/llms-full.txt): every page inlined, one fetch