# Classifiers

Your own labels, your own endpoint. Paste examples in the dashboard,
publish, and send text to a classify endpoint that answers with one of
your labels, verbatim.

## The short version

1. Create a classifier in the dashboard and declare its labels.
2. Add at least 4 examples per label. Training needs that floor.
3. Train. The version moves draft, validating, training, then ready.
4. Publish. The endpoint answers at `POST /v1/classify/{classifier_id}`.
5. Roll back or retrain at any time. Publish is always explicit;
   traffic never retrains a version.

## Auth

Classify calls use the same bearer keys as the model API:

```http
Authorization: Bearer <api key>
```

The dashboard (create, train, publish) uses your session, not a key.
There is no MCP tool for classify in v1. The surface is REST only.

## Classify

```http
POST /v1/classify/{classifier_id}
```

```json
{"text": "card hasn't arrived after 3 weeks"}
```

`text` is required and must be a non-empty string. The limit is 64 KiB
UTF-8 bytes, tighter than the model surface, because classifier inputs
are short-form.

A confident call answers:

```json
{"label": "card_delivery_estimate", "confidence": 0.83,
 "status": "ok", "version": 3, "classifier_id": "cls_..."}
```

Three laws hold on every answer:

- `label` is always one of your declared label strings, verbatim, or
  null. Never a near-miss rewrite.
- `confidence` is the model's score for the answer. The low-confidence
  threshold ships as a 0.80 default. Your publish bar overlays it on
  that version.
- `version` tells you which trained version answered, so you can pin
  or detect a rollout.

Training accuracy is honest about what it can measure. At 100 total
examples or fewer it is a leave-one-out self-check: every example is
scored by a head trained without it. Above 100 it is a held-out
estimate. A null accuracy means "not measurable - add examples".
Near-duplicate examples inflate both numbers.

## Low confidence

When confidence lands under the version's bar, the endpoint refuses
to guess. With no bar set, that bar is the shipped 0.80 default:

```json
{"label": null, "confidence": 0.34, "status": "low_confidence",
 "version": 3, "candidates": ["card_delivery_estimate", "card_arrival"]}
```

`candidates` lists the labels that were in play. Your code decides
what a low-confidence call means: a fallback queue, a human, or a
plain `text` record. Publish accepts an optional `confidence_bar`
between 0.60 and 0.95. The bar overlays the 0.80 default on that
version and is stored with its metrics. It is your choice. Nothing
fits it on training data.

## Versions

Each training run opens a new version: draft, validating, training,
ready, or failed. Publish moves the endpoint pointer; rollback moves
it back. Old versions stay, so rollback is instant and never
retrains. A failed training leaves the previous endpoint live.

## Caps

| cap | free | paid |
|---|---|---|
| active endpoints | 1 | 25 (owner-tunable) |
| labels per classifier | 25 | 50 |
| examples per label | 50 | 50 |
| included calls / month | 1,000 | 5,000,000 |
| example chars | 2,000 | 2,000 |
| classify text chars | 64,000 | 64,000 |

Over a cap is an explicit refusal, never a silent truncation. The
paid endpoint cap is a platform setting the owner can tune; the free
plan always holds one endpoint.

Calls draw the tenant pool, not a per-endpoint bucket. Every call,
confident or low-confidence, consumes one included call; past the
pool the prepaid wallet pays per call like every other mivia call.
When the pool is spent and the balance is zero, the call answers 429:

```json
{"detail": {"error": "balance empty", "upgrade_url": "/billing"}}
```

Free-farming stays bounded: signup waves pause behind the
registration toggle, and per-tenant rate limits bound how fast any
free tenant can burn its pool. See [pricing](/docs/pricing).

## Records link

The records create call accepts an optional `classifier_id`. When set,
your classifier answers instead of the built-in types, and the record
stores your label as its type. A low-confidence answer stores a
fallback row with `unclassified: true` in its metadata. See
[records](/docs/records).

## Starter packs

Three seed templates ship with the app, each with labels and enough
examples to train past the floor: support triage, community
moderation, and agent gating. Copy one into the wizard, adjust the
labels to your traffic, and train.

- [support triage](/classifier-packs/support-triage.json)
- [community moderation](/classifier-packs/community-moderation.json)
- [agent gating](/classifier-packs/agent-gating.json)

## Errors

Every classify failure is an explicit status. See
[errors](/docs/errors) for the full table: 404 unknown classifier,
410 archived, 409 not published, 422 bad text,
413 over 64 KiB, 429 rate or balance, and 502 when the model service
is down (that one is refunded, never billed).
