# Small Paper > 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 Small Paper's own uncensored abliterated 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 20-digit number that doubles as the API key — no email, no password, no recovery. Payment > is by crypto. Base URL `https://api.smallpaper.io/v1`. Currently live: Kimi K3.1 Pro, GLM 5.3, GLM 5.3 [Cyber], GPT 6.1 Sol, Gemini 4 Carbon, Gemini 4 Argon, DeepSeek V4.1 Flash 1017, Kimi K3.1 Pro, GLM 5.3 Default, Zeus [CHEAP], DeepSeek V4.1 Flash 1017, GLM 5.3 Flash, Zeus. --- # Quickstart ## 1. Get an account number Open [https://chat.smallpaper.io](https://chat.smallpaper.io) and press **Create a new account**. You get a 20-digit number like `4192 0837 5561 2094 7381`. That number is the whole account: no email, no password, **no recovery**. It is also your API key. Write it down — we store only a hash, so it cannot be shown again. ## 2. Fund it A new account starts empty, so the chat and the API both answer `402 insufficient_balance` until you fund it. Press **Top up** in the chat UI and name an amount. Payment is by crypto, and balances never expire. Without a browser: `GET https://chat.smallpaper.io/api/funding` for the allowed range, then `GET https://chat.smallpaper.io/checkout?usd=25` with your key as the Bearer returns `{"checkout_url"}`; pay it, and poll `GET https://api.smallpaper.io/api/me` until the balance rises. The [API](https://smallpaper.io/docs/api.md) page has the whole flow, signup included. ## 3. Call it ```sh curl https://api.smallpaper.io/v1/chat/completions \ -H "Authorization: Bearer $SMALLPAPER_API_KEY" \ -H 'content-type: application/json' \ -d '{ "model": "glm-5.3-flash-abliterated", "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.smallpaper.io/v1", api_key="41920837556120947381") print(client.chat.completions.create( model="glm-5.3-flash-abliterated", messages=[{"role": "user", "content": "Say hello in five words."}], ).choices[0].message.content) ``` ## 4. Point a coding agent at it See [opencode](https://smallpaper.io/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 = "smallpaper" model = "glm-5.3-flash-abliterated" [model_providers.smallpaper] name = "Small Paper" base_url = "https://api.smallpaper.io/v1" env_key = "SMALLPAPER_API_KEY" wire_api = "chat" ``` **Claude Code** speaks the Anthropic Messages API, which Small Paper does not serve: pointing `ANTHROPIC_BASE_URL` here gets a 404. Use a translating proxy (LiteLLM), opencode, or Codex. ## Checking your balance ```sh curl https://api.smallpaper.io/api/me -H "Authorization: Bearer $SMALLPAPER_API_KEY" ``` Returns the balance, total spent and request count. A non-streamed chat-completions response also carries `X-Balance-Microdollars`; a streamed one cannot, its headers go first. --- # API reference Small Paper speaks the OpenAI chat-completions dialect. Anything that can talk to OpenAI can talk to Small Paper by changing two settings: the base URL and the API key. ``` base URL https://api.smallpaper.io/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. ## 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.smallpaper.io` | `/v1/*`, `/api/me` and `/api/plans` — what an agent configures and reads | | `https://chat.smallpaper.io` | the web chat, signup, funding (`/api/funding`, `/checkout`), plans, pairing | | `https://smallpaper.io` | these docs, `/llms.txt`, the installer | `GET https://api.smallpaper.io/api/me` returns `site` and `api`, so a client given one origin can find the others. ## Endpoints | method | path | origin | auth | what | |---|---|---|---|---| | `POST` | `/v1/chat/completions` | https://api.smallpaper.io | key, paid balance | chat completions — streaming, tool calling, `reasoning_effort` | | `GET` | `/v1/models` | https://api.smallpaper.io | none | live models with limits and per-direction rates | | `GET` | `/api/me` | https://api.smallpaper.io, https://chat.smallpaper.io | 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.smallpaper.io, https://chat.smallpaper.io | key or chat cookie | body `{"on": bool}`: whether the balance pays once a plan's allowance is spent | | `GET` | `/api/me/usage` | https://api.smallpaper.io, https://chat.smallpaper.io | 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}]}`, a day without usage as zeros | | `GET` | `/api/me/usage/log` | https://api.smallpaper.io, https://chat.smallpaper.io | 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.smallpaper.io, https://chat.smallpaper.io | key or chat cookie | this account's payments, newest first, paged 50 at a time (`page`): `{payments: [{at, usd, kind, plan_id, credited}], page, pages, total, totals: {credited, credited_usd}}`; `plan_id` names the plan a settled payment bought, else null; `totals` covers every payment, never the page | | `GET` | `/api/me/keys` | https://api.smallpaper.io, https://chat.smallpaper.io | key or chat cookie | the keys on this account (paired machines) | | `POST` | `/api/me/keys/{ref}/revoke` | https://api.smallpaper.io, https://chat.smallpaper.io | key or chat cookie | revoke one key by its `ref`; the key you are using is refused (409) | | `POST` | `/api/me/delete` | https://api.smallpaper.io, https://chat.smallpaper.io | key or chat cookie | close this account for good; body {account_number}; any balance is forfeited | | `POST` | `/api/pair/start` | https://api.smallpaper.io, https://chat.smallpaper.io | none (per-address cap) | open a pairing: `{device_code, user_code, ttl, interval}`; body `{"hostname", "client"}` optional | | `GET` | `/api/pair/{device_code}` | https://api.smallpaper.io, https://chat.smallpaper.io | none (per-address cap) | poll: 202 `pending`, 200 `{status: ready, api_key}` once, 410 expired | | `GET` | `/pair` | https://chat.smallpaper.io | none | the page a person types the `user_code` into | | `POST` | `/pair` | https://chat.smallpaper.io | chat cookie | the same page, showing Approve for a typed code | | `POST` | `/api/pair/approve` | https://api.smallpaper.io, https://chat.smallpaper.io | chat cookie | approve `{user_code}` (mints a key for that machine) or `{user_code, deny: true}` | | `POST` | `/api/signup` | https://chat.smallpaper.io | none + `X-Pow` (kind `signup`) | mint an account: `{account_number, formatted, api_base}` | | `GET` | `/api/pow` | https://chat.smallpaper.io | none for `?kind=signup`; key or cookie for `chat` | a proof-of-work challenge `{challenge, bits, ttl}` | | `POST` | `/api/chat` | https://chat.smallpaper.io | key or chat cookie + `X-Pow` | the web chat's own streaming door; agents use `/v1/chat/completions` | | `GET` | `/api/models` | https://chat.smallpaper.io | none | the chat picker's rows, coming-soon ones included with their `status` (`/v1/models` is live only): `{models, default, efforts, lines, pieces, uploads: {per_message, file_max_bytes, max_px, picture_max_bytes}, vision: {images_max, step, max_px}}`; `default` is the default model's id, `efforts` the `reasoning_effort` rungs, `lines` each line's label by id, `pieces` each media tool's caps and its `failed` words by tool name | | `POST` | `/api/me/billing-portal` | https://api.smallpaper.io, https://chat.smallpaper.io | key or chat cookie | a session in the processor's portal for the cards it keeps for you `{url}` | | `GET` | `/api/live` | https://chat.smallpaper.io | none | server-sent events: each topic named (`?topics=tokens,news`, required) now, then on every change | | `GET` | `/api/live/{topic}` | https://chat.smallpaper.io, https://smallpaper.io | none | a live topic's latest payload; `tokens` is `{total, rate}`: every token served, all time, and tokens a second over the last minute | | `POST` | `/api/tools/generate_image` | https://chat.smallpaper.io | key + `X-Pow` | the web chat's image tool: queues `{job, price_micros, retry_ms}`, billed per picture made when done; not offered on `/v1` | | `GET` | `/api/tools/generate_image/{job}` | https://chat.smallpaper.io | key | a picture request's `{status, retry_ms}`, with `charged_microdollars` and `made` once done or `message` once failed | | `GET` | `/api/tools/generate_image/{job}/image/{k}` | https://chat.smallpaper.io | key | the k-th finished picture (from 0), fetched for each download and never stored; `410 media_gone` once the vendor has dropped it | | `POST` | `/api/tools/generate_video/quote` | https://chat.smallpaper.io | key | the web chat's video tool, priced: `{price_micros, funded_by, payable, seconds, aspect, resolution}` for the submit's body; nothing is held | | `POST` | `/api/tools/generate_video` | https://chat.smallpaper.io | key + `X-Pow` | the web chat's video tool: a submit with `expect_micros` queues `{job}`, billed by the second when done; not offered on `/v1` | | `GET` | `/api/tools/generate_video/{job}` | https://chat.smallpaper.io | key | a clip's `{status, retry_ms}`, with `charged_microdollars` once done or `message` once failed: submitting, queued, rendering, done, failed or expired | | `GET` | `/api/tools/generate_video/{job}/video` | https://chat.smallpaper.io | key | a finished clip's mp4, fetched for each download and never stored; `410 media_gone` once the vendor has dropped it | | `GET` | `/api/funding` | https://chat.smallpaper.io | none | what a top-up may be and how to pay for it `{min_usd, max_usd, pay_with, rails, wallet}`; `wallet` (`{name, kind}`) is the row a plan the balance covers is paid from | | `GET` | `/api/plans` | https://api.smallpaper.io, https://chat.smallpaper.io | 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.smallpaper.io, https://chat.smallpaper.io | key or chat cookie | buy that plan's pass with credit already in the wallet; `{ok, pass_id}`, 402 when short | | `GET` | `/checkout` | https://chat.smallpaper.io | key or chat cookie | `?usd=N` for credit, `?plan=ID` for a pass → `{checkout_url}` (JSON) or a 303 to the processor (browser) | | `GET` | `/paid` | https://chat.smallpaper.io | none | where the processor sends a person back after paying | | `GET` | `/` | https://chat.smallpaper.io, https://smallpaper.io | none | the landing page on the site origin, the web chat on the chat origin | | `GET` | `/docs` | https://smallpaper.io | none | 301 to the first docs page | | `GET` | `/docs/{name}` | https://smallpaper.io | none | one docs page, rendered for people and search engines | | `GET` | `/robots.txt` | https://smallpaper.io | none | what search engines may crawl, and where the sitemap is | | `GET` | `/sitemap.xml` | https://smallpaper.io | none | the public pages, from content/site/pages.yaml | | `GET` | `/docs/{name}.md` | https://smallpaper.io | none | one docs page as markdown | | `GET` | `/docs/opencode.json` | https://api.smallpaper.io, https://smallpaper.io | none | generated opencode provider config | | `POST` | `/webhooks/{hook}` | https://smallpaper.io | the rail's own signature | a payment rail's callback, at the tail it declares | | `GET` | `/healthz` | every origin | none | liveness, on every host | | `GET` | `/privacy` | https://smallpaper.io | none | the Privacy page | | `GET` | `/terms` | https://smallpaper.io | none | the Terms page | | `GET` | `/acceptable-use` | https://smallpaper.io | none | the Acceptable use page | | `GET` | `/refunds` | https://smallpaper.io | none | the Refunds page | | `GET` | `/why` | https://smallpaper.io | none | the Why it's cheap page | | `GET` | `/claims` | https://smallpaper.io | none | the Claims page | | `GET` | `/install.sh` | https://smallpaper.io | none | the opencode installer (bash) | | `GET` | `/install.ps1` | https://smallpaper.io | none | the opencode installer (PowerShell, Windows) | | `GET` | `/llms.txt` | https://smallpaper.io | none | this document | | `GET` | `/llms-full.txt` | https://smallpaper.io | none | every docs page inlined | | `GET` | `/docs/opencode-plugin.js` | https://api.smallpaper.io, https://smallpaper.io | none | the plugin the installer drops in | `/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": "glm-5.3-flash-abliterated", "messages": [ {"role": "system", "content": "You are a careful assistant."}, {"role": "user", "content": "Hello"} ], "max_tokens": 1024, "stream": true } ``` Omitting `model` selects the default model. Any unrecognised or non-live `model` id returns `404 model_not_found` and lists the live ids (`503` while `off_sale`), so you are never billed 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`, `xhigh` or `max`: it overrides the model's configured default, any model takes any of them, and one without that tier gets its highest below it (`xhigh` and `max` reach a vendor with three tiers as `high`). Any other value is a `400` naming these. 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. ## Tool calling Standard OpenAI shape in both directions: ```json { "tools": [{"type": "function", "function": { "name": "bash", "description": "Run a shell command", "parameters": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]} }}], "tool_choice": "auto" } ``` Replies carry `message.tool_calls[]` with `function.arguments` as a JSON **string**, and `finish_reason: "tool_calls"`. Return results as `{"role": "tool", "tool_call_id": …, "content": …}` messages — one per call, as OpenAI expects. Parallel calls work; send every result before the next turn. Streaming tool calls arrive as `delta.tool_calls[]` fragments. Concatenate `function.arguments` per `index` to rebuild the JSON. ## Errors Errors are `{"error": {"message": …, "type": …}}`. The `type` is the field to branch on — not the status, because a status can be ambiguous. A type worth sending again unchanged adds `"retry": true`. | 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 | `number_mismatch` | `/api/me/delete`: the account number typed is not this account's | | 400 | `rail_not_accepted` | `/checkout?plan=` or `/api/plans/{id}/buy`: this plan does not take that way of paying (its `payment_kinds`) | | 401 | `invalid_api_key` | account number unknown or revoked | | 403 | `account_disabled` | the account is frozen (for example after an abuse violation 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 | | 405 | `method_not_allowed` | the path takes another method; `Allow` names it | | 409 | `cannot_revoke_current` | that key authenticated this request; revoke it from another | | 409 | `price_changed` | the chat's video tool: the price moved since the quote it was sent with; quote it again | | 410 | `pair_expired` | the pairing code expired or was already used | | 410 | `media_gone` | a finished clip the vendor no longer holds; nothing was kept here to send instead | | 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 | `checkout_failed` | the payment processor did not answer | | 404 | `no_customer` | `/api/me/billing-portal`: no saved billing profile is attached to this account | | 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 refused: its own reason when your request is what it refused (a 4xx), our sentence for a throttle or a fault; 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://smallpaper.io/docs/models.md). Every token sent upstream is billed, and cached input is discounted at the same ratio the upstream gives us. A **pass** (see *Plans*) pays first for models in its scope; your **balance** pays for the rest. One request is paid by one of them. 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. Streamed responses don't (headers precede the body): read `/api/me` or the final chunk's `usage`. 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: each request in flight holds what it can cost (its input and its capped output), and together they may hold what the balance covers, or a pass's remaining allowance and a little past it, up to the parallel requests its plan sold. Past that comes a `429 rate_limited` with `Retry-After`, which names the limit that was reached. ## 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.smallpaper.io/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", "One-time payment"], "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://smallpaper.io/docs/models.md) page it covers (`frontier` is the resold frontier models, `abliterated` 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.smallpaper.io` except the last. 1. **Create an account.** `GET https://chat.smallpaper.io/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.smallpaper.io/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.smallpaper.io/api/funding` → `{"min_usd", "max_usd", "pay_with", "rails"}` (`rails`: `[{"name", "label"}]`). Any amount in range is accepted, to the cent (`usd=3.50`), credited 1:1. 3. **Get an invoice.** `GET https://chat.smallpaper.io/checkout?usd=` with `Authorization: Bearer ` returns `{"checkout_url"}` (a browser `Accept: text/html` request is redirected there instead). Payment is by crypto. 4. **Confirm.** There is no payment-status endpoint. The processor's callback credits the wallet; poll `GET https://api.smallpaper.io/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.smallpaper.io/api/pair/start` (optional body `{"hostname": "…", "client": "…"}`; the key is named ` @ `) → `{"device_code", "user_code", "ttl", "interval"}`. Unauthenticated, capped per address. 2. Show the person the `user_code` and send them to `https://chat.smallpaper.io/pair`, where they type it and press Approve while signed in (programmatically: `POST https://chat.smallpaper.io/api/pair/approve` with the chat cookie and `{"user_code"}`, or `{"user_code", "deny": true}`). 3. Poll `GET https://chat.smallpaper.io/api/pair/` every `interval` seconds: `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. --- # Models Rendered from the live model table: what is served and charged now. | model | id | in | cached in | out | list in | list out | context | max output | status | |---|---|---|---|---|---|---|---|---|---| | Kimi K3.1 Pro | `kimi-k3.1-pro-abliterated` | $8.50 | $0.85 | $20.50 | — | — | 1M | 32.8K | live | | GLM 5.3 | `glm-5.3-abliterated` | $4.725 | $0.75 | $7.50 | — | — | 1M | 32.8K | live | | GLM 5.3 [Cyber] | `glm-5.3-cyber` | $1.90 | $0.35 | $5.60 | — | — | 1M | 131.1K | live | | GPT 6.1 Sol | `gpt-6.1-sol` | $0.32 | $0.018 | $1.12 | $2.00 | $10.00 | 1M | 128K | live | | Gemini 4 Carbon | `gemini-4-carbon` | $0.45 | $0.03 | $2.10 | $2.00 | $10.00 | 1M | 65.5K | live | | Gemini 4 Argon | `gemini-4-argon` | $0.48 | $0.03 | $2.30 | $2.00 | $10.00 | 1M | 65.5K | live | | DeepSeek V4.1 Flash 1017 | `deepseek-v4.1-flash` | $0.06 | $0.0015 | $0.24 | $0.15 | $0.60 | 1M | 128K | live | | Kimi K3.1 Pro | `kimi-k3.1-pro` | $0.35 | $0.04 | $1.45 | $2.00 | $10.00 | 1M | 128K | live | | GLM 5.3 Default | `glm-5.3-default` | $0.15 | $0.036 | $0.45 | $1.40 | $4.40 | 1M | 128K | live | | Zeus [CHEAP] | `zeus-cheap` | $0.10 | $0.01 | $0.45 | — | — | 1M | 128K | live | | DeepSeek V4.1 Flash 1017 | `deepseek-v4.1-flash-1017-abliterated` | $0.50 | $0.035 | $1.90 | — | — | 1M | 32.8K | live | | Claude Opus 5.5 | `claude-opus-5-5` | $0.95 | $0.054 | $4.75 | $4.00 | $20.00 | 1M | 128K | off_sale | | GLM 5.3 Flash (default) | `glm-5.3-flash-abliterated` | $0.45 | $0.20 | $0.95 | — | — | 1M | 32.8K | live | | Zeus | `zeus` | $0.85 | $0.032 | $2.50 | — | — | 1M | 32.8K | live | USD per million tokens; input, cached input and output each bill at their own rate. **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. ## Directions One key reaches every direction below. **Frontier models, resold.** The frontier models are sold at a fixed discount off the vendor's own published price, so your price moves only when theirs does. `list in`/`list out` show their price; a row without them is not resold. How the resellers are tested, and what we keep: [why it's this cheap](https://smallpaper.io/why). **Uncensored models, our own.** The abliterated line runs abliterated weights with no policy layer at the model level, at fixed rates. Same endpoint, same key, same wallet. There is no cached *output* rate, here or at any vendor. Caching stores the state of a prompt prefix; output tokens are generated fresh on every call, so there is nothing to reuse. ## Statuses | status | in the chat picker | callable via `/v1` | |---|---|---| | `live` | selectable | yes | | `off_sale` | shown, greyed out | no: a `503` until it is back on sale | | `coming_soon` | shown, greyed out | no | | `deprecated` | not shown | no | A retired model keeps its row, so past usage stays attributable. Naming an unrecognised or non-live model over `/v1` is a `404 model_not_found` listing what is live (`503` while `off_sale`); omitting `model` selects the default. ## Choosing - **Kimi K3.1 Pro** (`kimi-k3.1-pro-abliterated`) — Kimi K3.1 Abliterated MXFP4 $8.50 in / $20.50 out per 1M, 1M context. - **GLM 5.3** (`glm-5.3-abliterated`) — GLM 5.3 Abliterated FP8 $4.725 in / $7.50 out per 1M, 1M context. - **GLM 5.3 [Cyber]** (`glm-5.3-cyber`) — Experimental: may refuse but cheaper $1.90 in / $5.60 out per 1M, 1M context. - **GPT 6.1 Sol** (`gpt-6.1-sol`) — OpenAI's fast frontier release $0.32 in / $1.12 out per 1M, 1M context (list price $2.00 in / $10.00 out). - **Gemini 4 Carbon** (`gemini-4-carbon`) — Google's heavyweight. Slower to start, built for the hard problems $0.45 in / $2.10 out per 1M, 1M context (list price $2.00 in / $10.00 out). - **Gemini 4 Argon** (`gemini-4-argon`) — Google's heavyweight reasoning model $0.48 in / $2.30 out per 1M, 1M context (list price $2.00 in / $10.00 out). - **DeepSeek V4.1 Flash 1017** (`deepseek-v4.1-flash`) — DeepSeek's fast one. Cheap enough to leave running $0.06 in / $0.24 out per 1M, 1M context (list price $0.15 in / $0.60 out). - **Kimi K3.1 Pro** (`kimi-k3.1-pro`) — Moonshot frontier model $0.35 in / $1.45 out per 1M, 1M context (list price $2.00 in / $10.00 out). - **GLM 5.3 Default** (`glm-5.3-default`) — Balanced uncensored GLM 5.3 release $0.15 in / $0.45 out per 1M, 1M context (list price $1.40 in / $4.40 out). - **Zeus [CHEAP]** (`zeus-cheap`) — Fast uncensored reasoning model $0.10 in / $0.45 out per 1M, 1M context. - **DeepSeek V4.1 Flash 1017** (`deepseek-v4.1-flash-1017-abliterated`) — The abliterated variant $0.50 in / $1.90 out per 1M, 1M context. - **GLM 5.3 Flash** (`glm-5.3-flash-abliterated`, the default) — GLM 5.3 Flash Abliterated $0.45 in / $0.95 out per 1M, 1M context. - **Zeus** (`zeus`) — 500 tokens/s beast (experimental: may refuse) $0.85 in / $2.50 out per 1M, 1M context. Every live model streams. Reasoning models return their thinking trace in `reasoning_content`; `/v1/models` says per model which of tool calling, reasoning, temperature and prompt caching it has, under `capabilities`. Pick on price; the lineup above says which is the default, and omitting `model` gets 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. --- # Using Small Paper with opencode [opencode](https://opencode.ai) reaches Small Paper through the AI SDK's OpenAI-compatible provider: a config block. Works with opencode 1 and 2. The installer adds a plugin that refreshes the otherwise frozen model list at startup. ## Install ```sh curl -fsSL https://smallpaper.io/install.sh | bash ``` On Windows, in PowerShell (5.1 or 7): ```powershell irm https://smallpaper.io/install.ps1 | iex ``` It asks whether to configure opencode globally or for one project, then takes your account number or pairs through the browser. Pairing creates a **separate key for that machine**, so a laptop is revocable without changing your account number. Non-interactive: ```sh curl -fsSL https://smallpaper.io/install.sh | bash -s -- --global --key 41920837556120947381 curl -fsSL https://smallpaper.io/install.sh | bash -s -- --local --pair curl -fsSL https://smallpaper.io/install.sh | bash -s -- --global --pair --dry-run # print, write nothing ``` The PowerShell installer takes the same flags as `-Global`, `-Local`, `-Key`, `-Pair` and `-DryRun`; `iex` cannot pass them, so use the script-block form: ```powershell & ([scriptblock]::Create((irm https://smallpaper.io/install.ps1))) -Global -Key 41920837556120947381 & ([scriptblock]::Create((irm https://smallpaper.io/install.ps1))) -Local -Pair ``` The installer **merges** into an existing config and keeps a timestamped backup. It only ever writes Small Paper's provider entry, and it refuses to touch a config it cannot parse. An opencode 2 style `providers.smallpaper` entry is replaced, because opencode 2 would prefer it to the one the installer writes. Then run `/models` inside opencode and pick a Small Paper model. ## Keeping the model list current Two files get written: | | | |---|---| | `opencode.json` | the provider block, including a snapshot of the models | | `plugin/smallpaper.js` | refreshes that model list at startup | The plugin loads from that directory: nothing to configure, no duplicate on re-run. It replaces only the model list from `https://api.smallpaper.io/docs/opencode.json`, never your key; unreachable, it gives up within ~2 s. Delete `plugin/smallpaper.js` if you would rather pin the model list. Re-run the installer when it needs updating. The plugin needs opencode 1.3.4 or newer; on anything older the installer skips it. On opencode 2 it adds models we start serving after you installed, but it cannot retire or reprice the ones already in your `opencode.json`, because opencode 2 applies the file's own entries last. Re-run the installer for those. **Moved to opencode 2?** Re-run the installer. A plugin from an earlier install only speaks opencode 1's plugin API, so opencode 2 lists it as failed under `/plugins`. A 404 naming a model you thought you had means your config predates a rename; the error says what replaced it, and re-running the installer repoints you. ## Config, by hand The generated block with current models, limits and prices is always at **[https://api.smallpaper.io/docs/opencode.json](https://api.smallpaper.io/docs/opencode.json)**. > **Do not `curl -o` over your global config.** `~/.config/opencode/opencode.json` holds > your other providers, agents, keybinds and MCP servers, and opencode merges settings > across levels rather than replacing them. Redirecting the generated file over it > deletes all of that. Paste the `provider` block into your existing file instead, or let > the installer merge it. ```json { "$schema": "https://opencode.ai/config.json", "provider": { "smallpaper": { "models": { "deepseek-v4.1-flash": { "attachment": true, "cost": { "cache_read": 0.0015, "input": 0.06, "output": 0.24 }, "interleaved": "reasoning_content", "limit": { "context": 1048576, "output": 128000 }, "modalities": { "input": [ "text", "image" ], "output": [ "text" ] }, "name": "DeepSeek V4.1 Flash 1017", "reasoning": true, "temperature": true, "tool_call": true }, "deepseek-v4.1-flash-1017-abliterated": { "attachment": true, "cost": { "cache_read": 0.035, "input": 0.5, "output": 1.9 }, "interleaved": "reasoning_content", "limit": { "context": 1000000, "output": 32768 }, "modalities": { "input": [ "text", "image" ], "output": [ "text" ] }, "name": "DeepSeek V4.1 Flash 1017 [UNCENSORED]", "reasoning": true, "temperature": true, "tool_call": true }, "gemini-4-argon": { "attachment": true, "cost": { "cache_read": 0.03, "input": 0.48, "output": 2.3 }, "interleaved": "reasoning_content", "limit": { "context": 1048576, "output": 65536 }, "modalities": { "input": [ "text", "image" ], "output": [ "text" ] }, "name": "Gemini 4 Argon", "reasoning": true, "temperature": true, "tool_call": true }, "gemini-4-carbon": { "attachment": true, "cost": { "cache_read": 0.03, "input": 0.45, "output": 2.1 }, "interleaved": "reasoning_content", "limit": { "context": 1048576, "output": 65536 }, "modalities": { "input": [ "text", "image" ], "output": [ "text" ] }, "name": "Gemini 4 Carbon", "reasoning": true, "temperature": true, "tool_call": true }, "glm-5.3-abliterated": { "attachment": false, "cost": { "cache_read": 0.75, "input": 4.725, "output": 7.5 }, "interleaved": "reasoning_content", "limit": { "context": 1000000, "output": 32768 }, "modalities": { "input": [ "text" ], "output": [ "text" ] }, "name": "GLM 5.3 [UNCENSORED]", "reasoning": true, "temperature": true, "tool_call": true }, "glm-5.3-cyber": { "attachment": false, "cost": { "cache_read": 0.35, "input": 1.9, "output": 5.6 }, "interleaved": "reasoning_content", "limit": { "context": 1000000, "output": 131072 }, "modalities": { "input": [ "text" ], "output": [ "text" ] }, "name": "GLM 5.3 [Cyber] [UNCENSORED]", "reasoning": true, "temperature": true, "tool_call": true }, "glm-5.3-default": { "attachment": false, "cost": { "cache_read": 0.036, "input": 0.15, "output": 0.45 }, "interleaved": "reasoning_content", "limit": { "context": 1048576, "output": 128000 }, "modalities": { "input": [ "text" ], "output": [ "text" ] }, "name": "GLM 5.3 Default", "reasoning": true, "temperature": true, "tool_call": true }, "glm-5.3-flash-abliterated": { "attachment": false, "cost": { "cache_read": 0.2, "input": 0.45, "output": 0.95 }, "interleaved": "reasoning_content", "limit": { "context": 1048576, "output": 32768 }, "modalities": { "input": [ "text" ], "output": [ "text" ] }, "name": "GLM 5.3 Flash [UNCENSORED]", "reasoning": true, "temperature": true, "tool_call": true }, "gpt-6.1-sol": { "attachment": true, "cost": { "cache_read": 0.018, "input": 0.32, "output": 1.12 }, "interleaved": "reasoning_content", "limit": { "context": 1048576, "output": 128000 }, "modalities": { "input": [ "text", "image" ], "output": [ "text" ] }, "name": "GPT 6.1 Sol", "reasoning": true, "temperature": true, "tool_call": true }, "kimi-k3.1-pro": { "attachment": false, "cost": { "cache_read": 0.04, "input": 0.35, "output": 1.45 }, "interleaved": "reasoning_content", "limit": { "context": 1048576, "output": 128000 }, "modalities": { "input": [ "text" ], "output": [ "text" ] }, "name": "Kimi K3.1 Pro", "reasoning": true, "temperature": true, "tool_call": true }, "kimi-k3.1-pro-abliterated": { "attachment": true, "cost": { "cache_read": 0.85, "input": 8.5, "output": 20.5 }, "interleaved": "reasoning_content", "limit": { "context": 1048576, "output": 32768 }, "modalities": { "input": [ "text", "image" ], "output": [ "text" ] }, "name": "Kimi K3.1 Pro [UNCENSORED]", "reasoning": true, "temperature": true, "tool_call": true }, "zeus": { "attachment": true, "cost": { "cache_read": 0.032, "input": 0.85, "output": 2.5 }, "interleaved": "reasoning_content", "limit": { "context": 1048576, "output": 32768 }, "modalities": { "input": [ "text", "image" ], "output": [ "text" ] }, "name": "Zeus [UNCENSORED]", "reasoning": true, "temperature": true, "tool_call": true }, "zeus-cheap": { "attachment": true, "cost": { "cache_read": 0.01, "input": 0.1, "output": 0.45 }, "interleaved": "reasoning_content", "limit": { "context": 1048576, "output": 128000 }, "modalities": { "input": [ "text", "image" ], "output": [ "text" ] }, "name": "Zeus [CHEAP]", "reasoning": true, "temperature": true, "tool_call": true } }, "name": "Small Paper", "npm": "@ai-sdk/openai-compatible", "options": { "apiKey": "{env:SMALLPAPER_API_KEY}", "baseURL": "https://api.smallpaper.io/v1" } } } } ``` Set the key the config expects: ```sh export SMALLPAPER_API_KEY=41920837556120947381 # your account number, no spaces ``` ## What each field is doing | field | why it matters | |---|---| | `npm` | `@ai-sdk/openai-compatible` is the right driver: we serve `/v1/chat/completions`, not `/v1/responses` | | `apiKey` | `{env:SMALLPAPER_API_KEY}` keeps your account number out of the file | | `tool_call` | true where the model calls tools; we translate them in both directions, so agent mode works | | `reasoning` + `interleaved` | `"reasoning_content"` is the field our SSE actually emits, so opencode renders the thinking trace instead of discarding it | | `temperature` | true where the model takes a sampling temperature; sampling parameters are forwarded as you send them | | `attachment` | true where the model reads images, so opencode lets you attach them | | `modalities` | image in only where the model reads images, text out; without it opencode 2 offers image input to every model | | `cost` | **our** rates, not the upstream vendor's — so opencode's spend display matches what you are actually charged | | `limit` | real context and output ceilings, so opencode truncates before we have to | ## Notes - **Fund the account first.** A new account is empty, so opencode gets an `insufficient_balance` 402 until you top up. See [Quickstart](https://smallpaper.io/docs/quickstart.md). - **Your system prompt is honoured.** opencode's agent definition reaches the model unchanged and stays first. See [API](https://smallpaper.io/docs/api.md). - **No proof-of-work on the API.** The chat requires one per message; `/v1` never does. - If a model shows up but every call fails, check `/api/me` for your balance first. --- # The Small Paper CLI A terminal coding agent for Small Paper, in one file. It installs itself, signs in by pairing the machine, and keeps itself up to date. The source archive is available at [smallpaper.io/cli](https://smallpaper.io/cli), licensed MIT or Apache-2.0. ## Download | Platform | File | Size | |---|---|---| | Windows (x64) | [`smallpaper-x86_64-pc-windows-msvc.exe`](https://smallpaper.io/cli/download/smallpaper-x86_64-pc-windows-msvc.exe) | 16 MB | | macOS (Apple silicon) | [`smallpaper-aarch64-apple-darwin`](https://smallpaper.io/cli/download/smallpaper-aarch64-apple-darwin) | 13 MB | | Linux (x86_64, any distro) | [`smallpaper-x86_64-unknown-linux-musl`](https://smallpaper.io/cli/download/smallpaper-x86_64-unknown-linux-musl) | 17 MB | These downloads are for version v0.2.0-12. Checksums are in [`SHA256SUMS`](https://smallpaper.io/cli/download/SHA256SUMS). There is no build yet for Intel Macs, arm64 Linux or Windows on ARM. ## Install One line installs Small Paper for you: it downloads the file for your machine, checks it against [`SHA256SUMS`](https://smallpaper.io/cli/download/SHA256SUMS) and runs `smallpaper install`. ### macOS and Linux ```sh curl -fsSL https://smallpaper.io/cli/download/install.sh | sh ``` ### Windows (PowerShell) ```powershell irm https://smallpaper.io/cli/download/install.ps1 | iex ``` The rest of this section installs by hand, from the downloads above. ### Windows 1. Download the `.exe` and double-click it. 2. It asks: *Install Small Paper for this user, so `smallpaper` works in any terminal? [Y/n]*. Press Enter. 3. It installs to `%LOCALAPPDATA%\Programs\smallpaper`, adds that folder to your user PATH and adds an entry in Apps & Features. No admin rights are needed. 4. Open a **new** terminal and type `smallpaper`. The file is unsigned, so SmartScreen warns the first time: click **More info**, then **Run anyway**. From a terminal, `smallpaper-x86_64-pc-windows-msvc.exe install` does the same as the double-click. ### macOS (Apple silicon) ```sh curl -fsSL -o smallpaper https://smallpaper.io/cli/download/smallpaper-aarch64-apple-darwin && chmod +x smallpaper && ./smallpaper install ``` Use this line rather than a browser download. A file curl saves is not quarantined, so Gatekeeper lets it run. A file a browser saves is blocked until you allow it under **System Settings → Privacy & Security → Open Anyway**; right-click → Open no longer works since macOS 15. ### Linux (x86_64) ```sh curl -fsSL -o smallpaper https://smallpaper.io/cli/download/smallpaper-x86_64-unknown-linux-musl && chmod +x smallpaper && ./smallpaper install ``` The binary is fully static, so it runs on any distribution. On macOS and Linux, `smallpaper install` copies the binary to `~/.local/bin/smallpaper`. If that folder is not on your PATH yet, it prints the line to add to your shell profile. ## First run | Command | What it does | |---|---| | `smallpaper` | opens the interactive session | | `smallpaper login` | pairs this machine: it shows a code; open [https://chat.smallpaper.io/pair](https://chat.smallpaper.io/pair), sign in and enter it | | `smallpaper login --key ` | signs in with your account number instead | | `smallpaper signup` | creates an account from the terminal: no email, no password, no recovery | | `smallpaper -p "explain this repo"` | runs one prompt without the interactive session | | `smallpaper --version` | prints the version, e.g. `smallpaper 0.2.0 (v0.2.0-12)` | ## Models The CLI fetches active models, context windows and rates live from `https://api.smallpaper.io/v1/models` — the same lineup managed in Small Paper's model catalog. ```sh smallpaper models # list active models and current default smallpaper models --refresh # fetch latest /v1/models immediately smallpaper --model glm-5.3-flash-abliterated ``` Inside an interactive session, run `/models` to switch models or `/effort` to adjust `reasoning_effort`. ## Updates An interactive session checks daily and installs a newer release once its signed checksums match; the next start says so. `smallpaper update` updates now. To turn updates off, set `auto_update = false` in `~/.smallpaper/config.toml`, or set `SMALLPAPER_NO_UPDATE=1`. ## Uninstall `smallpaper uninstall` removes the program, the PATH entry and, on Windows, the Apps & Features entry. It keeps `~/.smallpaper`, where your keys and sessions are; delete that folder by hand to remove them too. ## Check a download In the folder you downloaded to: ```sh sha256sum -c --ignore-missing SHA256SUMS # Linux shasum -a 256 -c --ignore-missing SHA256SUMS # macOS ```