Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

What judging costs, and how to cap it

What a judged request adds to the bill, how the call multiplier scales it, and the daily cap that stops the spending.

ExplanationLast updated

On this page

Turn a judge on and a second model call goes out behind every request it looks at. That call is on your provider bill, and the first thing anybody holding a budget asks is how much it adds.

The answer is a multiplication, and the number doing the multiplying belongs to the judging method rather than to the model you named. Grading one answer once is one call. Asking the same question in both orderings is two. Seating three graders is three.

The second thing they ask is what stops it. That answer is one line in a config file, checked before each call and settled after it. This build has two keys spelled max_usd_per_day, and leaving one out is stopped at the door while leaving the other out turns a whole feature off in silence.

How a judge call happens is where the extra call sits relative to the person waiting for a response, and what the measurement itself costs is the wider surface this page is one slice of. There is no procedure here; the panel you would configure is run a cross-family panel.

The call multiplier

The reason to start here rather than at the price of a token is that the method decides how many calls one graded row makes, and a method change can double a bill without changing a model. The table below covers every judging setup that reaches production in this build, one row per multiplier.

The setup Grading calls per graded row Where it composes
pointwise-bool 1 evals.evaluators[].judge, and it is what an absent method: reads as
panel with N seats N, at three seats or more evals.evaluators[].judge.method: panel
shadow duplication, pointwise 2 × self_consistency_k shadow.duplicate
shadow duplication, pairwise co-run 2 more on top shadow.duplicate.pairwise

The shadow rows read higher than the others for a reason worth stating once: that plane grades both arms of a comparison, the answer your router chose and the answer the duplicate produced, so its multiplier starts at two before any option is added. The duplicate request itself is a further upstream call on top, and it is billed like any other.

The arithmetic is two lines, and both are checkable against your own ledger:

grading calls  =  sampled rows × calls per graded row
judging spend  =  grading calls × price per grading call

Here it is instantiated. On a ledger this documentation harness produced, one pointwise-bool grading cost $0.000172, priced against claude-haiku-4-5 from the litellm@2026-07-14#179affb snapshot. Eighty rows were graded, so eighty calls were made, and they sum to $0.013760. Those token counts came from a loopback stub standing in for the judge model, which makes the dollar arithmetic you can follow rather than anybody’s bill; your own price per call is your provider’s.

Now hold the traffic still and change only the method. The same eighty rows through the three-seat panel is 240 calls, which is $0.041280 at the same price per call. Nothing about the traffic moved. The multiplier is the entire difference, which is why it belongs in the sizing decision rather than in the surprise at the end of the month.

Where the dollars turn up

Four mechanisms in this build can call a grading model, and they do not all report to the same place. Knowing which meter carries which is what stops a reader concluding that judging is free because the surface they happened to open shows nothing.

The judge Where its dollars are reported Its cap
the evals-plane judge one eval:<id> line on the metacost surface evals.evaluators[].max_usd_per_day, required whenever a judge: block is present
the shadow-duplication judge the shadow line, beside the duplicate’s own dollars shadow.duplicate.max_usd_per_day
the cascade judge inside the served request’s row, broken out as judge_cost none of its own
the guardrail judge on the guardrail disclosure for that request guardrails.judge.max_usd_per_request, which is required and declared rather than enforced

The evals plane is the one this section is about, and the metacost surface is where its bill lands. This is that surface on a running instance whose ledger holds the eighty graded rows above:

{
  "sources": [
    {
      "name": "eval:annotation-queue",
      "spend_usd": { "usd": 0, "estimated": false, "unpriced": false },
      "period": "day"
    },
    {
      "name": "eval:judge-task-completion",
      "spend_usd": { "usd": 0.01376, "estimated": false, "unpriced": false },
      "cap_usd": 20,
      "period": "day",
      "burn_pct": 0.0688
    }
  ],
  "provenance": {
    "pricing_snapshots": [],
    "rows": 0,
    "estimated_rows": 0,
    "unpriced_rows": 0,
    "demo_rows": 0,
    "truncated": false,
    "source": "scan",
    "indexed_through": { "file": "decisions.jsonl", "offset": 214190, "ts": "2026-08-24T18:05:05.557Z" },
    "window": { "since": "2026-08-24T00:00:00.000Z", "until": "2026-08-25T00:00:00.000Z", "tz": "UTC" }
  }
}

That is the response bytes of GET /tt/api/v1/metacost, pretty-printed with no value changed, captured on 2026-08-24 from a source build serving the harness ledger described above. Two consecutive requests returned identical bytes. The window is the current UTC day and moves; the offset and ts under indexed_through belong to that ledger file. The dollars are the same loopback-stub arithmetic, so quote the shape rather than the figure.

Three readings, in the order they matter. eval:judge-task-completion carries the judging bill for the day with the cap and the burn against it on the same object, and 0.01376 against a cap of 20 is the 0.0688 percent shown. eval:annotation-queue reports $0 and is not flagged unpriced, which is a measured zero: collecting human labels makes no model call, and free is a measurement. And the provenance envelope underneath counts shadow-duplication rows rather than evaluator outcomes, so its zeroes describe a population this ledger has none of. An evaluator line’s own exclusions ride on that line, which the next section shows.

The four planes also meter differently in a way a cost report can miss. The cascade judge runs on the serving path, so its dollars sit inside a served request’s row, and the row’s top-level cost prices the accepted attempt alone.

A one-stage cascade with a judge and final fallbackThe cascade calls the cheap tier, runs the optional inline judge, and checks accept_when. If the condition holds, that first response goes to the client. Otherwise TokenTriage discards it and calls the final frontier fallback, whose response goes to the client without another acceptance check. Every model attempt and judge call that runs is billed.stage: try-cheapmodel call, billedjudge?another billed callaccept_whenchecks the outcomeThe client's responseone returned attemptacceptescalatefinal: frontierno acceptance checkreturn fallbackEvery call that runs is billed. Top-level cost covers the returned attempt; discarded attempts are recorded per stage.

Read the ladder from the bottom. Every rung is an upstream call the provider bills, the inline judge beside each rung is another, and only the accepted attempt is priced into the row’s headline figure. Two consequences follow for anyone summing costs: a report that adds up cost.usd across rows understates a cascade, and a governance budget, which burns that same top-level figure, does not see the cascade judge’s dollars either. Escalate when the answer falls short has the break-even arithmetic and the per-stage disclosure that carries the missing money.

The null that must not become a zero

Every figure above came from a row-level record. The per-subject figure is a different story, and it is the one that gets misread, because the field is present and its value looks like a number that got lost.

A judges reading serves spend as a present null on every row this build serves. Here is one, lifted from a subject that has been through the whole validation protocol and been promoted:

      "state": "promoted",
      "kappa": 0.6761023407729995,
      "validated_n": 105,
      "iaa": 0.8397435897435899,
      "gold_revision": "93f8396",

      … the floors_applied and floors blocks, elided …

      "spend": null,
      "selective_risk": null,
      "anchor": null,
      "last_run": "2026-08-24T18:08:58.333Z"

A fragment of one subject out of GET /tt/api/v1/evals/judges, captured from a booted instance on 2026-08-24; the elision is marked and the fragment is therefore not parseable on its own. The judge verdicts behind that κ came from a loopback stub answering a canned score, and the two annotators’ labels were designed by the harness. So κ, validated_n, iaa and gold_revision are what a promoted subject looks like rather than what any judge scores. The assembler, the κ arithmetic, the IAA, the floors and the registry are the binary’s, and the key set, state and the three nulls are not fixture values.

The field is a pointer with no omitempty, so the key is served rather than dropped, and a measured zero and an unmeasured figure cannot serialize the same way. The binary states the distinction itself, in a note it serves beside the reading and prints in the CLI’s text output. It reads: “spend is NOT MEASURED and is served null on every row, which is not the same claim as $0. … The JUDGING spend is real money and is recorded per ROW rather than summed per subject.”

Promotion does not change it. The capture above is a promoted subject with a measured κ, a measured inter-annotator agreement and a null spend, because the protocol that promotes a judge measures agreement and does not add up dollars.

So read the dollars where something adds them up. The metacost line in the previous section totals them per evaluator per period. Underneath it, each judge-written eval outcome carries its own cost object, and on the shadow plane a grade row carries judge_cost beside the judge id, the metric, the judge model and the prompt sha. All three are checkable. What no surface answers is “what did this subject cost”, and filling that in with $0.00 would publish a confident free over calls that were billed.

A call that could not be priced is not a free call

The price table does not cover every model, and a judge pointed at one it cannot price produces a dollar figure nobody can compute. The rule is the same one the rest of this product’s cost surfaces follow: the figure is withheld, the row is counted, and the total says how many it left out.

Here is the same metacost surface over a ledger whose judge outcomes are unpriced:

    {
      "name": "eval:judge-task-completion",
      "spend_usd": { "usd": null, "estimated": false, "unpriced": true },
      "cap_usd": 20,
      "period": "day",
      "disclosures": [
        "all 80 eval outcome(s) in this window are UNPRICED and excluded — the spend is UNKNOWN, not $0.00",
        "spend is UNPRICED, so no burn percentage against this cap is computable"
      ]
    }

One source line out of the same endpoint, pretty-printed. The rendering is the shipped server’s; the input is not. Every judge model within reach of this capture is one the price table covers, so the run used a copy of the harness ledger with the cost object on its eighty judge outcomes rewritten to the unpriced shape. The count 80, the cap and the two sentences are the server’s own.

Four things in that line are the pattern to recognise. usd is null rather than 0. unpriced is true, which is what separates an unknown from a measured free. burn_pct is absent, because a percentage of an unknown is not a number. And the disclosure rides on the same object as the figure it qualifies, carrying the count it excluded rather than leaving it to a footnote. A CLI rendering the same money prints the literal UNPRICED where a dollar would go. How to read a cost figure is the rule in general.

The cap, and what happens when it is spent

max_usd_per_day on an evaluator entry is required whenever that entry carries a judge: block, and the message says why: a judge spends money on every sampled request, and a metered evaluator without a declared daily cap is stopped at tokentriage validate. That is the cap doing its job before anything boots.

At runtime it measures after rather than predicting before. A call is admitted while the day’s spend is under the cap, and the call that crosses it is allowed to finish, so realized daily spend can pass the figure in the file by at most one in-flight evaluation per worker. The number in the file is a stopping point rather than a ceiling, and the product says so in the text it prints rather than leaving you to discover it.

The value you write What it does
a positive figure admits calls while the UTC day’s settled spend is below it
0 a stricter setting, not an absent one: no call is admitted and settled spend stays at zero
absent, beside a judge: block stopped at tokentriage validate, by name
.inf stopped, at the config layer and again at the gate

Every stop is counted under a named reason rather than passing quietly, and the vocabulary separates two situations an operator has to tell apart: a cap that is declared and spent, and an evaluator for which no cap was declared at all. Both surface on what the measurement itself costs, which carries a line per configured evaluator with its counted refusals beside its spend.

A panel spends against that one cap and re-reads it before each seat. A panel stopped at its third seat has already made two calls, and those dollars leave your account with no row to show for them, because a panel votes with all its seats or it does not vote. Whether they also reach the counter the cap is checked against is the next section, and the answer is not the one the arithmetic so far would lead you to.

A panel seat that reports no usage stops the cap counting

A panel folds its seats into one figure for the invocation, and the fold follows the house rule for an unknown: if any part could not be priced, the whole is unpriced, because summing the rest would claim the unpriceable part was free. An unpriced settlement is then counted and excluded rather than added, so the day’s accumulator does not move.

On a single-model judge that is the right answer, because there is no priced part to keep. On a panel there is one, and it goes with the rest. A seat that reports no token usage, which includes any seat whose call failed, makes the whole invocation unpriced and takes its siblings’ priced dollars down with it.

The size of that hole is measured in the product’s own suite, and pinned by a test so the figure is a contract rather than a paragraph:

The measured gap: $0.0100 spent while the cap counter read $0.0000
$ go test ./internal/evaluator/ -run TestPanelPricedSpendBeforeAFailingSeatIsDisclosedNotSummed -v
=== RUN   TestPanelPricedSpendBeforeAFailingSeatIsDisclosedNotSummed
    desc_panel_test.go:767: MEASURED AMPLIFICATION: 50 rows x 2 PRICED calls at $0.0001 = $0.0100 genuinely spent; budget.SpentToday = $0.0000; declared cap $0.05 never moved. UNPRICED contagion + Settle's early return on an unpriced fold. A seat that fails on every row is an unbounded spender under a declared cap — escalated, NOT fixed here (a house-wide convention change).
--- PASS: TestPanelPricedSpendBeforeAFailingSeatIsDisclosedNotSummed (0.00s)
PASS
ok  	github.com/tokentriage/platform/internal/evaluator	0.021s

Run from a checkout of the product on 2026-08-24. Nothing in it is machine-specific; the three dollar figures and the row count are the test’s own constants, and it is the failure mode’s own alarm: the day the fold is repaired, this test goes red and somebody has to come back and rewrite it.

Fifty rows, two priced seats each at $0.0001, one seat failing every row. $0.0100 of priced spend was measured, SpentToday read $0.0000, and the declared $0.05 cap did not move. It errs towards admitting, which is the direction that costs you money: a panel with one consistently broken seat spends without a bound under a cap that looks untouched. A failing seat is also hard to see from outside, because a dead seat, a panicking seat, a mid-fan-out budget stop and a malformed vote vector all reach an operator as one skip named evaluator_error.

The settlement is counted even where the dollars are not, and that count is the thread to pull. The metacost line for that evaluator grows a disclosure reading N settlement(s) UNPRICED: … this cap is not advancing, ranked ahead of everything else on the line so a reader who stops after one disclosure stops on that one. What the count cannot tell you is how much of the spend behind it was priced before the fold zeroed it.

Until the fold changes, read that disclosure as the working cap for a panel, and repair the failing seat rather than lowering the figure in the file. It is escalated in the product’s source rather than fixed, and the test above is what holds the escalation to a number.

The cap that turns the feature off instead

The second key spelled max_usd_per_day sits under shadow.duplicate, and it acts in the opposite direction from the first. Absent or 0, it turns the whole of shadow duplication off: no refusal, no warning, no log line, and no rows.

Two configs establish it, and they differ by one line:

   duplicate:
     enabled: true
     sample_rate: 0.05
-    max_usd_per_day: 5.0
     pairwise: true

With the cap, the validator prints the plane’s egress warning and its own call arithmetic:

$ tokentriage validate --config with-cap.yaml
warning: shadow.duplicate: sampled shadow duplication PHONES OUT — the duplicate runs against the configured upstream and the grader makes 4 Anthropic Messages call(s) per sampled request under this config (2 pointwise grades × self_consistency_k, plus 2 for the pairwise co-run when shadow.duplicate.pairwise is on) with the model named by classifier.oracle.model (falling back to the built-in cheap judge model) and the key named by classifier.oracle.api_key_env. The single-binary air-gap posture is broken for this path. BOTH the duplicate's and the grader's measured dollars are capped POST-HOC by shadow.duplicate.max_usd_per_day (measured, not predicted), and a judge call whose price cannot be resolved is EXCLUDED from that cap and counted — never folded in as $0. Unlike evals.evaluators[].judge this leaf carries no external: allow gate — it shipped before the gate existed and adding requiredness here would refuse configs that are valid today
OK: configuration is valid

Take the max_usd_per_day line out and leave enabled: true, sample_rate: 0.05 and pairwise: true where they are:

$ tokentriage validate --config no-cap.yaml
OK: configuration is valid

Boot the second file and the log carries no shadow line of any kind. The sampler is not composed at all, so the ledger collects no duplicate rows and no grades, for as long as the config stands. Both probes were scratch copies of the shipped examples/evals/journey-2-validated-judge.yaml example with the shadow.duplicate block above appended, run on 2026-08-24 against a source build on Windows. The first block shows one copy of a warning this command prints twice, and the boot runs had their listeners on port 0 and replica.name pinned so no hostname reached the capture.

Two keys spell the same words and take opposite actions on the same missing value. An operator who carries the evaluator key’s behaviour across to this one collects nothing for weeks and then finds the routing claim it was funding has no evidence under it.

What this page does not cover

Sizing shadow duplication against what it buys. The spend-versus-saving ratio, the healthy spike-and-decline shape and the two figures called metacost that must not be subtracted are what the measurement itself costs, which is the surface this page reads from.

Writing the block the cap sits in. The cap is one leaf on a block that has several, and the steps that get the whole of it past validate and past boot are configure a judge. Every optional leaf beside it, with the default the code applies, is Evals & calibration configuration.

Latency. On the evals plane there is none your client can observe, because the grading call is made after the response has been written. The cascade judge is the exception, and its wall time is inside the served row’s total_ms.

Rehearsing a judge without paying for it. The evals-plane judge takes endpoint_env and can be pointed at a loopback listener. The shadow-duplication judge has no endpoint leaf at all: it dials a compiled-in provider URL, so shadow.duplicate.pairwise needs outbound traffic and real credentials. Air-gap judging, and the known limits is that story.