---
title: classifier.dev
description: Zero-shot text classification over plain HTTP. No API key, no account.
canonical: https://classifier.dev/
last-updated: 2026-09-22
---

# classifier.dev

Zero-shot classification over plain HTTP. Send text and labels for a label, or images and questions for structured answers. There is no API key or account required for free use, so the example below works the moment you paste it.

Agents: the OpenAPI 3.1 description is at https://classifier.dev/openapi.json and a short index at https://classifier.dev/llms.txt

## Scrape and classify a URL

Send one public HTTP(S) URL instead of input, inputs or items. Any public website is supported; inaccessible, blocked or login-only pages may fail. Use a funded workspace key from https://classifier.dev/app/keys.

```bash
curl https://classifier.dev/v1/classify \
  -H "Authorization: Bearer $CLASSIFIER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: article-001" \
  -d '{
    "url": "https://example.com",
    "labels": ["documentation", "news"],
    "include": ["markdown", "html"]
  }'
```

Context.dev extracts the article once; Jev classifies the Markdown. Results, confidence, scores and usage keep the normal classification shape. article contains url and title. include opts into article.markdown and/or article.html; omit it for compact output. HTML is untrusted page content: sanitize it before rendering. labels, dimensions, instructions and multi work as with text. Model is Jev; long articles require tier fast.

Scraping costs $0.0022 per provider-billed request ($2.20/1,000), plus normal classification. Both formats use the same scrape, at no extra cost. pricing separates scrape_usd, classification_usd and total_usd. Credits are reserved before scraping; a typical URL request temporarily holds $0.0232 and releases the unused amount after settlement. The workspace must have paid credits or an active paid plan and enough balance for the whole hold. Scraping counts toward the default $10 request limit.

A successful scrape remains billable if classification fails. Provider-billed errors (including processed 404s) and uncertain network failures retain the scrape charge; confirmed unbilled failures release it. Read pricing even on errors. No automatic scrape retries. Idempotency-Key rejects repeats with 409 before provider spend; use a new key only for intentional new work.

```
Limits: one URL per request, 8 MB extracted response, 250,000 article tokens,
and existing classification decision limits. No OCR or browser actions.
Article text is never silently truncated. Request source and article content
are not stored in the usage ledger. MCP accepts url and include on
classify_texts, classify_dimensions and classify_multi_label.
```

## Typesafe sdk compatibility

classifier.dev implements TypeSafe's System One wire contract at the same paths as TypeSafe. Point the official JavaScript or Python SDK at this origin; Choice, Noul and Score questions, model listing, usage, request IDs, validation errors and retry headers keep their native shapes.

```bash
npm install @typesafe-ai/sdk
```

```javascript
import { choice, TypeSafeClient } from "@typesafe-ai/sdk";
```

```javascript
const client = new TypeSafeClient({
  apiKey: process.env.CLASSIFIER_API_KEY ?? "unused",
  baseURL: "https://classifier.dev",
});
const result = await client.systemOne({
  state: "I was charged twice. Please fix this today.",
  questions: {
    category: choice("Which team should handle this?", {
      billing: null,
      technical: null,
    }),
  },
});
console.log(result.answers.category.choice);
```

```python
# uv add typesafe-sdk
from typesafe_sdk import Choice, TypeSafeClient
```

```
with TypeSafeClient(api_key="unused", base_url="https://classifier.dev") as client:
    result = client.system_one(
        state="I was charged twice. Please fix this today.",
        questions={"category": Choice(
            instructions="Which team?",
            criteria={"billing": None, "technical": None},
        )},
    )
```

The apiKey selects how classifier.dev accounts for POST /v1/systemone:

```
"unused" or any non-workspace value
  Free anonymous use. The value is ignored and never forwarded. Fast-tier
  quota is counted by questions: 3,000/minute and 20,000/day per IP.
```

```
classifier_agent_...
  A workspace key from https://classifier.dev/app/keys. Requests use the
  workspace's shared quota and credit balance. Free workspaces keep the
  same ceilings; Pro workspaces get 10x limits. Charges use TypeSafe's
  returned token usage and appear in workspace usage history.
```

Do not put a real TypeSafe API key here: classifier.dev never forwards caller credentials. GET /v1/models is public and does not spend quota or credits. The corresponding HTTP resources are POST /v1/systemone and GET /v1/models.

Images go through the same route. A runnable Node.js example is at https://classifier.dev/developers (IMAGE CLASSIFICATION). Set model to "dgemma" and add an images array of data URLs (image/png, image/jpeg, image/webp or image/gif, base64). At most 4 images and 900,000 data URL characters total must fit in the 1 MB body. The questions keep the Choice, Noul and Score shapes and are answered about the images and the state together:

```json
{"model": "dgemma",
 "state": {"note": "Look at the attached image."},
 "images": ["data:image/png;base64,iVBORw0KGgo..."],
 "questions": {"red": {"type": "noul",
                       "instructions": "Does the image contain a red square?"}}}
```

"dgemma" is DiffusionGemma 26B-A4B in vLLM's structured-read mode: one denoise step over a seeded answer template, read as a calibrated distribution per question, re-read a few times when the first read is uncertain. Text-only bodies may name it too. A choice question offers at most 26 options and a request at most 64 questions. Jev never sees these requests: when the model is down the answer is 503 dgemma_unavailable, not a text-only guess. A body it refuses is 400 dgemma_input with its reason, a saturated model is 429 dgemma_busy with Retry-After, and images sent under another model are 400 images_unsupported. The output is structured answers, not generated image data. The TypeSafe JavaScript SDK 0.6.0 forwards images at runtime but its TypeScript request type omits that field; direct HTTP exposes the documented JSON contract.

## Laya and kev

Calls with neither model nor processing use Jev. Two other models are available, both hosted by Beam on shared inference endpoints:

```
model: "laya"   ModernBERT-large with a trained decision head. Answers
                every question in one batched forward pass. State plus one
                question must fit 512 tokens.
model: "kev"    Qwen2.5-0.5B with released LoRA weights and a pointer
                head. Encodes state once and scores question branches
                together in one 8,192-token packed sequence.
```

Omit processing to automatically choose fast for one decision (up to four multi-label questions), or bulk for larger work. Supplying processing without model implies Laya. Explicit lanes are honored. With explicit model: "jev", processing is accepted but has no effect. Each result is labelled jev/laya or jev/kev; neither model reports a checkpoint.

```json
{"model":"laya","processing":"fast","input":"Please refund this charge",
 "labels":["billing","technical"]}
```

Fast: one decision per call, 60 questions/minute and 2,000/day per caller. Bulk: up to 1,000 questions per call, 1,000/minute and 20,000/day. Large calls are chunked internally and results retain input order. A single-label decision is one question; multi-label uses one question per label. Fast accepts at most four questions. Multiple dimensions normally need bulk. Trial limits also apply to paid and operator keys; existing Smart quotas still apply. Shared capacity can return 429 even with quota remaining. Quotas count attempted questions, including failed inference.

Both models accept short text: at most 2,000 characters, 2–16 labels of at most 100 characters each, and instructions up to 400 characters. The text, question and labels must also fit the model's context — 512 tokens for Laya, 8,192 for Kev — and oversized content is rejected, not silently shortened. Upstream caps a request at 32 questions, so a large batch is split into several requests and reassembled in input order.

Jev's published accuracy and calibration measurements do not describe either model. We have not fitted temperatures on classifier.dev traffic: treat scores and Smart's confidence-triggered reviews as experimental, not a quality guarantee.

Neither model has a warm pool to start, so there is no cold-start 503. On 429, respect Retry-After and use bounded retries with backoff. A refused request is not retried upstream: the refusal is returned as it happened. Shared endpoints are not a replica in every region or a latency promise. Accepted work is held only in memory; there is no durable batch-job service. Overload never silently switches the model or processing lane.

Laya and Kev inference has no retail charge during this trial. Optional tier: "smart" reviews remain separately priced as before and can be slower.

```
From this repository's CLI:
  node cli/classify.js billing,technical --model laya "Please refund this"
  node cli/classify.js billing,technical --model kev --processing bulk < tickets.txt
```

## Long documents

With the default model or model: "jev", an input over 32,000 characters automatically uses Jev long-context processing. Use POST /v1/classify with a workspace key backed by a paid balance or active paid subscription. Anonymous access and free signup credit do not qualify. Only tier: "fast" is supported; tier: "smart" is refused with bad_tier.

Synchronous limits: 250,000 original context tokens in total, counted with cl100k_base across all inputs; 20 documents; 32 decisions (documents times dimensions, or documents times labels in multi-label mode); and a 1 MB request body. Instructions allow 4,000 characters and labels 200 each. Each continuous whitespace or non-whitespace run may contain at most 8,192 UTF-16 code units. Longer runs return 400 long_context_input before document tokenization or chunking, to bound tokenizer work. The byte limit can be reached before the token limit. Requests at or under 32,000 characters per input keep the ordinary Jev path. Existing dedicated enterprise/operator access remains a trusted operational exception to the public funding requirement.

Chonkie's RecursiveChunker splits each document into 600-token chunks using cl100k_base. Parallel Jev calls screen for relevant or uncertain evidence, including opposing evidence and exceptions. Eligible whole chunks are packed in source order for final Jev classification within a 20,000-token cl100k_base budget and a conservative provider context estimate.

The final call may omit eligible evidence when the budget is full. This is evidence selection, not a promise of reading every passage in the final call or of universal accuracy. usage.long_context discloses context_tokens, documents, chunks, screened_chunks, eligible_chunks, selected_chunks, omitted_chunks, screening_input_tokens, final_input_tokens, screening_calls, final_calls, screening_ms, final_ms and tokenizer. If no evidence qualifies, or no eligible chunk fits for any document, the request returns 422 long_context_no_evidence with no charge.

context_tokens and documents count the original inputs once. Chunk counts, selection counts and phase usage accumulate across dimension passes; they are not necessarily unique passages. screening_calls and final_calls count answered provider calls, not attempted calls. Unknown provider token usage remains null rather than being reported as zero.

Price: $0.084 per million original context tokens (2 × Jev's $0.042 rate). Each input is counted once; dimensions do not multiply the context price. Actual screening and final-call usage do not change this retail charge.

```json
{"input": "<a 300,000-character contract>",
 "dimensions": {"kind": ["lease", "employment", "supply"],
                "renews": ["automatically", "on notice", "never"]}}
```

A 402 long_context_payment_required requires a funded workspace; 400 long_context_too_large means the original context token cap was exceeded; too_many_inputs and too_many_decisions identify the document/decision caps. 400 long_context_input means invalid long-context input; 503 long_context_unavailable means processing is unavailable.

## Ten-million-token jobs

Send the whole document once to POST /v1/classify. A funded workspace can upload up to 10,000,000 original cl100k_base tokens in a 100 MB request. Large single-document requests automatically return 202 with a status_url; add Prefer: respond-async to use that flow for a smaller document too. Splitting, screening, retries and final judgment happen on the server.

```
POST /v1/classify
Authorization: Bearer classifier_agent_...
Content-Type: application/json
Prefer: respond-async
```

```json
{"input":"<the entire document>","labels":["renewing","not-renewing"]}
```

Or upload a UTF-8 text file directly:

```bash
curl 'https://classifier.dev/v1/classify?labels=renewing,not-renewing' \
  -H "Authorization: Bearer $CLASSIFY_API_KEY" \
  -H 'Content-Type: text/plain' --data-binary @document.txt
```

JSON accepts one input string (or a one-element inputs/items array), labels, optional instructions and multi, and fast/jev only. Multi-label jobs allow up to 32 labels. Raw text accepts repeated label query parameters or comma-separated labels, plus optional instructions and multi=true. Labels and instructions may appear anywhere in the JSON object.

Poll the returned status_url with the same workspace key. Status progresses from queued to processing to finished; finished includes result with the normal results, usage and pricing fields. A failed job includes an error and refunds its reservation. Network/provider failures retry automatically. Use an optional UUID Idempotency-Key to safely retry a lost upload response.

The repository CLI uploads and waits in one command:

```
node cli/classify.js renewing,not-renewing --document document.txt --json
```

Tokens are counted exactly as in the original whole document, independent of network boundaries. The workspace reserves that actual token price after upload and settles it once on success: $0.084/M, or $0.84 for 10M tokens. Source text is temporarily stored privately while queued, then deleted as it is screened. Final Jev reads up to 20,000 selected evidence tokens; usage.long_context discloses any omitted eligible chunks.

```
Cancel unfinished work and refund its hold with
POST /v1/long-context/jobs/{id}/cancel.
```

Jobs expire after 24 hours. Source and selected evidence are deleted on completion, cancellation, failure or expiry; results remain available until expiry. Signup credit alone does not enable this feature.

## Legacy chunklaya

Explicit model: "chunklaya" keeps the legacy opt-in service, Laya behind a chunk-and-index harness. It is not selected automatically. When configured, it accepts up to 4,000,000 characters per input and 20 documents per request, subject to the 1 MB request body limit. Results use chunklaya/multilingual. It has no retail charge during the trial. tier: "smart" is refused with bad_tier. A document with more passages than the service scores in one request (256 paragraphs), or a request with more questions than fit, is refused with chunklaya_input rather than answered from part of the text. A busy service answers 429 chunklaya_busy with Retry-After; an unreachable one 503 chunklaya_unavailable. There is no fallback to another model.

Accuracy on long documents has not been measured against Jev on classifier.dev traffic; the harness's own results are in its repository, github.com/myxamediyar/chunklaya. No retail charge during this trial.

## Against the model it runs on

The model behind this service is Jev; the smart tier re-asks what Jev was unsure about. Same public test sets, 400 items each, measured live on 2026-09-18:

```
                                        AG News             emotion
                                     all   unsure(49)    all  unsure(122)
-------------------------------------------------------------------------
jev alone = classifier.dev fast    87.5%        65.3%  61.8%        36.9%
classifier.dev smart               90.0%        83.7%  62.7%        41.8%
```

The fast tier is Jev, so one row serves both. Smart re-asked 43 / 126 of 400 items. Gaps under about 5 points are noise. The full table, with latency and cost, is at https://classifier.dev/benchmark

## When this is worth a network call

A language model can classify anything it can see. The question is whether you want the text in your context at all. Call this when reading the input is the expensive part:

Filtering before reading. Forty search results, six worth opening. Judging them yourself pulls all forty into context first. One call returns forty labels and you read only the survivors.

Cascade pre-filtering. Drop the obvious no's cheaply, then spend real reasoning on what is left.

Streams nobody reads line by line. Log lines, error buckets, inbound tickets, the changed files in a large diff.

Deterministic routing. A pipeline branch that must take the same path for the same input on every run, instead of drifting with a model's reasoning.

All four are the same move: classify ten thousand things without reading them. A thousand inputs go in one request and come back in about a second. Below five or so items, skip it: you have already paid the context cost.

## CLI

The same API from the shell, one line per input, in input order:

```bash
npm i -g classifier-dev
classify bug,feature,praise < feedback.txt
classify relevant,"not relevant" --review 0.7 < snippets.txt   # the unsure ones
classify db,web,ml --count < titles.txt                        # a histogram
```

Plain lines, JSON or NDJSON in; label, confidence and text out. Batches of a thousand per request, four at a time, and rows stream as they land, so piping to head returns at once on a large file. Retries rate limits and upstream failures on its own. classify --help has the rest. Source in cli/ at https://github.com/mrmps/classifier-dev

## MCP

The same tools inside Claude, ChatGPT, Codex, Cursor or any MCP client, over Streamable HTTP with no key:

```
https://classifier.dev/mcp          classify_texts, classify_dimensions, classify_multi_label, count_labels, review_uncertain
https://classifier.dev/mcp/docs     list_docs, read_doc, search_docs
```

```bash
claude mcp add --transport http classifier https://classifier.dev/mcp
codex mcp add classifier --url https://classifier.dev/mcp
```

Listed in the official MCP registry as dev.classifier/classifier and dev.classifier/docs: https://registry.modelcontextprotocol.io/v0/servers?search=dev.classifier

Claude.ai: Customize > Connectors > Add custom connector > paste the URL. ChatGPT: Settings > Security and login > Developer mode, then create an app with the URL and "No Authentication". Step by step for every client, plus what each tool does: https://classifier.dev/mcp-setup

## Agent skill

Install this as a skill and your agent will remember to reach for it:

```bash
npx skills add https://classifier.dev
```

Served from this domain over RFC 8615 well-known discovery, with no repository in between:

```
/.well-known/agent-skills/index.json   the discovery document
/skill.md                              the skill itself, readable as-is
```

An agent without a skills runtime can fetch /skill.md and follow it.

## Agent feedback

If classifier.dev itself returns a wrong response shape, contradicts its documentation or creates repeated integration friction, report it through the agent-feedback protocol instead of classifying the report as input.

```
GET  https://classifier.dev/.well-known/agent-feedback.json   discovery and accepted fields
POST https://classifier.dev/api/v1/feedback                   full report with optional evidence
POST https://classifier.dev/api/v1/observations               lightweight category + summary
GET  https://classifier.dev/api/v1/receipts/{id}              confirm that it landed
```

No key is needed. Read the discovery document before submitting, and never include credentials, private input text or unrelated user data in a report. If classifier.dev materially helped your work, agents are encouraged to send a testimonial too: use category "testimonial" and include reporter.agent_type plus a short reporter.agent_description explaining what kind of agent you are and what the service helped you do.

## Spending limits

Free inference allows up to $0.01 of provider cost per Fast request or $0.10 per Smart request, $0.50 per IP per UTC day, and $100 across everyone per UTC day. IPv6 addresses share a /64 allowance. At most four free requests run concurrently per IP. If Smart reviews reach the request ceiling, the fast answers remain and usage.escalation_failed counts missed reviews. Longer prompts or expensive batches need a funded workspace API key. Reservations include in-flight work, retries and fallback models. Anonymous proxy traffic requires a funded key. Unfunded workspace keys share the free limits. Funded work uses its workspace balance, outside the shared free budget, with a default $10 maximum request allowance. Synchronous request bodies are limited to 1 MB; whole-document jobs allow 100 MB (see TEN-MILLION-TOKEN JOBS). A supplied Idempotency-Key prevents re-execution: synchronous repeated keys receive 409, not a cached response; whole-document UUID keys return the same job. Free keys are scoped to the IP and UTC day; workspace keys are scoped to the account.

## Usage

```
GET  https://classifier.dev/{labels}/{text}
GET  https://classifier.dev/?labels={a,b}&text={text}
POST https://classifier.dev  {"input":"...","labels":["...","..."]}
POST https://classifier.dev  {"inputs":["...", ...up to 1000],"labels":[...]}
```

## Examples

```bash
curl https://classifier.dev/spam,not+spam/Win+a+free+iPhone+now
spam
```

```bash
curl classifier.dev -d '{
  "input": "the checkout button does nothing",
  "labels": ["bug", "feature", "praise"]
}'
{
  "tier": "fast", "model": "jev-1.13.0", "modelsUsed": ["jev-1.13.0"],
  "results": [{
    "label": "bug", "confidence": 1,
    "scores": {"bug": 1, "feature": 0, "praise": 0},
    "ms": 260, "model": "jev-1.13.0"
  }],
  "usage": {"classifications": 1, "escalated": 0, "ms": 260}
}
```

```bash
curl "classifier.dev/entailment,neutral,contradiction/Only+12+of+40+sites+were+inspected.+Every+site+was+inspected."
contradiction
```

Spaces can be written as + or %20, and labels are separated by commas.

The same request as query parameters, for code that builds URLs:

```bash
curl "https://classifier.dev/?labels=spam,not+spam&text=Win+a+free+iPhone+now"
spam
```

input, q, classes and categories are read as text and labels too, and the two forms mix: /spam,not+spam?text=... is the same call. Every option below works on both. A malformed GET answers with a URL that would have worked.

```
In the path form a raw comma, slash or plus sign is a separator. A label
that contains one is written percent-encoded, %2C, %2F or %2B, so
/C%2B%2B,python/... reads the label C++. In the query form + is a space and
%2B a plus sign, and a comma inside a label is written %252C.
```

## Multiple dimensions

Make several independent decisions about every input in one request:

```bash
curl https://classifier.dev/v1/classify -H 'content-type: application/json' -d '{"items":["Checkout charges me twice"],"dimensions":{"team":["billing","identity","platform"],"urgency":["immediate","normal","low"],"kind":["bug","request","question"]}}'
```

results[i].dimensions[name] contains that field's label, confidence, scores, model and ms. Results stay in input order. inputs or input work too; items is an alias for inputs in this mode. Use only one input spelling.

To define a dimension's criteria, replace its array with an object:

```
"urgency": {"labels":["immediate","normal","low"],"instructions":"Active financial harm is immediate; minor inconvenience is low."}
```

Up to 20 dimensions, 2-100 distinct labels per dimension, and 1,000 decisions (items times dimensions) per request. Names cap at 64 characters, labels at 200, instructions at 4,000, and dimension definitions at 16,000 combined. Each decision counts toward the existing quota; public smart requests cap at 200 decisions. Do not combine dimensions with labels, multi or max_labels. Large batches are packed into multiple upstream calls. An individual input and question that cannot fit the model context returns dimension_context_too_large.

Jev answers the dimensions independently against the same input. Confidence is derived from the option distribution; it is not a literal probability of correctness. Add an unknown category when evidence may be insufficient. Smart escalation happens per field. Escalated fields have null confidence and scores, since the original distribution no longer describes that answer. Confidence and scores can also be null when the provider returns no score.

usage reports items, dimensions, classifications (decisions), escalated, fallback, and ms. If Jev is unavailable, fallback processes the batch with bounded concurrency inside the same request spending allowance. A failed field fails the whole request rather than returning an incomplete matrix.

## Parameters

```
labels        Two to one hundred categories. Required unless dimensions is supplied.
dimensions    Named label sets for independent decisions; see MULTIPLE DIMENSIONS.
input         Text to classify. Above 32,000 characters, default/jev uses
              paid Jev long-context processing; see LONG DOCUMENTS.
inputs        Up to one thousand strings; long-context requests allow 20
              documents and 250,000 original context tokens in total.
tier          Either fast (the default) or smart, in any case. Anything
              else is a 400 with code bad_tier, never a silent fast.
instructions  Extra criteria, such as "judge the reviewer's overall verdict".
verbose       On GET requests, ?verbose=1 returns JSON instead of a bare label.
              Sending Accept: application/json does the same.
multi         Return every category that applies instead of just one.
max_labels    Cap how many multi-label answers come back.
```

Results come back in input order. Single-label results carry label, confidence, scores and model. Confidence and scores can be null; check for null before comparing thresholds. POST multi-label results instead carry labels (an array), scores and model, with no singular label or confidence.

Batch responses also carry modelsUsed. The top-level model is "mixed" when different results were answered by different models, such as a smart-tier batch where only some inputs were escalated, or a large batch whose chunks reached Jev through different transports (jev@vercel and jev-1.x are the same model asked two ways).

## Confidence

The model behind this is a decision model, not a language model prompted to classify. It returns a calibrated probability for every label, so the confidence is a real forecast of whether the label is right. Measured:

```
six-way emotion, 400 items      confidence >= 0.9   right 82% of the time
                                confidence <  0.5   right 29% of the time
four-way news topic, 400 items  confidence >= 0.9   right 92% of the time
                                confidence <  0.5   right 64% of the time
```

Use it. Act on high-confidence answers, and route the rest to a person, a reasoning model, or the smart tier, which does exactly that for you.

Route null confidence to review: the provider returned no score, or a smart answer replaced the scored answer. The first model's probabilities never describe a reasoning model's answer. Neither tier guarantees identical labels or scores across calls.

Confidence and scores may be rounded to two decimals depending on which transport answered, so do not read meaning into the third digit.

Confidence does not measure category fit. It says which of your labels fits best, not whether any of them fit. "The weather is nice today" against bug / feature / praise comes back "praise", with a confidence that looks like any other answer's. If none-of-the-above is a real outcome, add it as a label: the same text against those three plus "none of these" picks "none of these". That works; hoping for a low score does not.

Scores likewise express the model's choice among the labels you supplied; they do not validate the input or prove that the label is correct. Supply labels that cover the inputs your caller may send.

Acronyms, identifiers, and non-language inputs retain the scores the model returns.

## Multi-label

One article, fifty tags, the ones that fit:

```bash
curl classifier.dev -d '{
  "input": "...",
  "labels": ["ml", "databases", "... up to 100 ..."],
  "multi": true, "max_labels": 10
}'
{"results": [{
  "labels": ["databases", "serverless", "rust", "caching", ...],
  "scores": {"databases": 0.98, "serverless": 0.98, ..., "gaming": 0.01}
}]}
```

On GET, add ?multi=1 and the labels come back one per line.

Every label is judged independently as a yes/no probability, and the answer lists those at or above 0.7, most likely first. The full score map is returned so you can set your own threshold: on a seven-task set, 0.7 gave recall 0.99 and precision 0.81 (F1 0.887); 0.5 gave recall 1.00 and precision 0.74. max_labels keeps the top N. One request, about 200ms.

## Tiers

```
fast     Every answer comes from the decision model, in one round trip.
         Measured: four-way news topic 87.7%, six-way emotion 60.5%, which is
         the same accuracy as a 3.4-second reasoning model on the news topics
         at two milliseconds per item.
```

```
smart    Same first pass, then every single-label answer below 0.7 confidence
         is re-asked of a fast reasoning model and replaced. Measured:
         emotion 61.8% to 63.7%, news topics 87.5% to 90.0%, by re-asking
         30% and 12% of the items. Those results carry escalated: true and
         the reasoning model's name. Confidence and scores are null, with
         an unscored explanation: the new answer has no comparable probabilities.
         usage.escalated counts them. A few seconds per escalated item, so
         a batch on smart is slower in proportion to how uncertain it is.
         If the reasoning model cannot be reached, the fast answer stands
         without escalated, and usage.escalation_failed says how many.
```

```
Multi-label answers ignore the tier: the reasoning model was
measured re-judging them and made them worse.
```

The models are not fixed. They are benchmarked as candidates appear and swapped when a measurement, not a launch post, says to. If the decision model is unavailable, requests fall back within their spending allowance to a chain of language models on different providers; JSON responses always report which model actually answered.

## Limits

Free limits are counted per IP address in classifications, not requests, so a batch of a thousand inputs spends a thousand of them. The fast tier allows 3,000 per minute and 20,000 per day; the smart tier 200 per minute and 2,000 per day. A batch must fit the remaining quota in full. Public smart requests accept at most 200 inputs; larger batches return 400 so callers can split them. Anonymous traffic also shares a 5,000/minute and 50,000/day allowance across every caller using the same label set. Rotating IPs does not reset it; workspace, operator and partner keys bypass it. REST, MCP, dimensions and TypeSafe choice questions share these counters. Each dimension debits its labels by the item count; each SDK choice question debits its labels once. These count attempts admitted by this gate, including attempts subsequently refused by another quota or a provider. Pro workspaces allow 30,000/minute and 200,000/day on fast, 2,000/minute and 20,000/day on smart, shared across keys and agents. Pro, operator and partner keys have a 1,000-input ceiling. Workspace keys use the workspace credit balance and share workspace quotas. Free workspaces have the same ceilings as public access. Current plans are at https://classifier.dev/pricing. Send Authorization: Bearer classifier_agent_... on REST or MCP requests.

Every classification response carries RateLimit-Limit and RateLimit-Policy, plus RateLimit-Remaining once the limiter has been consulted (every 200 and 429; a 400 never reached it). Over the limit is a 429 with Retry-After; nothing is slowed down or silently dropped.

## Errors

```
POST errors are JSON. Classification errors include a stable code:
```

```json
{"error": "Provide at least 2 labels; got 1 ("spam").", "code": "too_few_labels"}
```

The GET forms answer plain text instead, an error: line and, on a 400, the usage: and try: lines with a URL that would have worked; add ?verbose=1 or send Accept: application/json for the JSON object, which then carries usage and try as fields.

```
400   bad_json, no_input, too_many_inputs, too_few_labels, too_many_labels,
      empty_label, duplicate_labels, empty_input, input_too_long, bad_tier,
      chunklaya_input, dgemma_input, images_unsupported, long_context_input,
      long_context_too_large
401   invalid_api_key for unsupported credentials. Workspace authentication
      also rejects invalid, paused or revoked keys with an error message;
      workspace errors do not include a code.
402   insufficient workspace balance for inference;
      long_context_payment_required for long context without paid funding
403   the key is inactive or the workspace cannot authorize usage
404   not_found
422   long_context_no_evidence; no charge
429   rate_limit_minute, rate_limit_day, label_set_limit, chunklaya_busy, dgemma_busy,
      with Retry-After; on the free tier the body also carries upgrade, the
      URL of the plan that lifts the limit (https://classifier.dev/pricing)
502   typesafe or typesafe_<status> when the decision model failed;
      openrouter_<status>, chain_exhausted or timeout when the fallback
      chain did; upstream_other. Retry with backoff.
503   long_context_unavailable, chunklaya_unavailable, dgemma_unavailable;
      label_set_unavailable when label admission cannot be checked;
      no inference starts. Respect Retry-After and retry with backoff.
402   request_spending_limit: send fewer or shorter inputs, or use a funded
      workspace key. Do not repeatedly retry an unchanged over-budget request.
```

The full list, in the shape a client can validate against, is components.schemas.Error in https://classifier.dev/openapi.json

For higher limits, book a short call: https://cal.com/michaelsf/coffee

## On request

The public service is one shared deployment. By arrangement, for teams whose data or latency budget cannot go through it: a dedicated deployment in your own cloud (AWS, GCP or another), private end-to-end encrypted inference, and higher accuracy or lower latency, from a model tuned to your labels and served on capacity that is yours. Email contact@classifier.dev or book a call at https://cal.com/michaelsf/coffee; the numbers are measured on your own data before you commit. Terms on https://classifier.dev/pricing.

## Updates

The free tier is the whole service today. What is being built on top of it:

```
Private inference       your text never reaches a shared provider
Faster inference        lower response time for latency-sensitive paths
Dedicated endpoints     capacity that is yours, at your own latency
Trained endpoints       tuned on your labelled data, not just your labels
A self-serve API        keys and higher limits without booking a call
Better classification   accuracy work, measured the way /benchmark is
```

Subscribe with a request, naming what you would use first:

```bash
curl -X POST https://classifier.dev/subscribe \
  -H "content-type: application/json" \
  -d '{"email":"you@example.com","wants":["private","trained"]}'
```

```
"wants" is optional. It takes any of:
  private, faster, dedicated, trained, api, accuracy
```

When "wants" includes faster, also send desired_latency_ms (1–60000).

Confirm from the email before updates start; an agent can POST the emailed token as {"token":"..."} to /subscribe/confirm instead. Links last 24 hours, and repeat requests within the hour send one email.

One mail when something on that list ships, nothing in between. Your address and what you ticked are kept apart from API traffic. Unsubscribe by replying.

## Privacy

The text you send is never stored or logged. It goes to the model provider for the classification and nowhere else. Per-request analytics record a keyed fingerprint of the label set, plus the tier, model, latency, status and coarse country. Successful simple and multi-label classifier names are also kept for 90 days in a separate aggregate registry with no caller identity or source text. Its shared fingerprint lets operators associate label names with pseudonymous usage records. The same collection applies to TypeSafe choice requests with one distinct label set.

Built by @michael_chomsky — https://x.com/michael_chomsky

