Most usage-based APIs I have paid for charge by length. Tokens, calls, rows, seconds of video. It is a proxy for value, and it quietly pays a model to pad: the same question answered in four sentences earns more than the one answered in one.
I wanted to see what happens if the price is decided after the work instead, by something that reads the result.
The payment layer already allowed the shape of this: authorize a ceiling before the work starts, settle less after it. What it could not do β what nothing can do β is say how much less. That number has to come from somewhere, fast enough to sit inside a request, and it cannot come from the thing that produced the work.
Here is what I built, in the order I got it wrong.
The judge is Jev, a System One model: you send state and typed questions, it answers with probabilities and a confidence, no prose. I asked it one question β how much work does this answer represent, on a three-level scale β and mapped the answer to a tier.
That was the mistake. score: 0.99 on a scale whose levels are lookup / synthesis / investigation means "essentially a synthesis". I floored it to 0 and charged for a lookup.
The distribution is the product. The moment you round it into a bucket you have thrown away the only thing that distinguishes a calibrated model from a general one asked for JSON. The fix is to charge the mean:
const expected = LEVELS.reduce((total, level, i) => total + level.micros * p[i], 0);
12% a synthesis and 88% an investigation, at $0.025 and $0.05, is $0.047 β not a tier. And it is unbiased in a specific, limited sense: if the probabilities are calibrated, what you charge converges on the expected value of the tier prices β the value function I chose, not some true worth of the work. Rounding to a tier throws that information away and makes the price jump at an arbitrary line; whether the jump favours you or the buyer depends on where the line sits.
$0.0235 is not a display value, by the way. The ledger holds micros, so that is the amount that settles β which is what lets a price be a mean in the first place.
Uncertainty also stops needing a hand-tuned rule. A judge torn between two levels charges between them, automatically.
The second question I ask is whether the answer is grounded β supported by what the service actually read, rather than asserted. Every call came back between 0.07 and 0.54, and I spent a while believing the model was bad at it.
It was not. I was sending the question and the answer, and asking "is this supported by the sources?" β without sending the sources. With them in the state, grounded answers score 0.98 and ungrounded ones stay low.
What I do with that number is a pricing decision, not a mathematical consequence: I multiply the expected price by the grounding probability, which treats a wholly ungrounded answer as worth nothing and a half-grounded one as worth half. Someone else could reasonably floor it, or refuse to bill at all below 0.5. Whatever you choose, put it in the receipt.
Obvious in hindsight. A verification question with nothing to verify against is a vibe check, and it will happily return numbers that look like data.
Every path through the judge falls back to a rule-based one: no key, timeout, error, unrecognised shape. A trial key that expires should change your prices, not stop your server.
Then I set a real key and everything kept working β priced by rules. The key was a Vercel AI Gateway key (vck_β¦), I was sending it to TypeSafe's own endpoint, the answer was 401, and my fallback swallowed it into a reason string nobody was reading.
A fallback in the path of money must be loud. It is now in the receipt: every response says which judge decided and why, so "the rules priced it because the judge answered 401" is visible to the buyer, not just to the logs.
Text is the easy case. The sharper one is detection: "find every solar panel in this photo" has no price until it runs β there might be forty, there might be none β and the model that answers also scores its own answers.
I assumed a segmentation model would sidestep that, because the SAM I remembered was class-agnostic: you clicked, it gave you a mask, it had no idea what the mask was. SAM 3 is not that model. It takes a noun phrase, returns every instance of it, and scores each one itself. The self-marking problem does not disappear with segmentation; it arrives with it.
SAM 3 found the three cars in that photo in 2.0 seconds, scoring them 0.93β0.97. It is equally confident about a crop that is not a car, which is the point.
So a second opinion decides the money: a vision model says what each crop shows, in one sentence, and Jev says whether that sentence is the thing the buyer asked for. Neither of them chose the crop. The price is $0.01 per accepted detection, so three cars surviving at p β₯ 0.9 is $0.03 of a $0.20 ceiling.
Ask for a bicycle and SAM returns nothing, so the charge is $0.00. The empty photo is the case a flat fee gets wrong, and the one a caller remembers.
That zero is not an error path I wrote, either. The handler answers 422 and the gate releases the hold β the authorization goes back to the caller, unspent, to use elsewhere. Charging nothing has to be as ordinary as charging something, or the seller quietly rounds up.
I would rather have asked the vision model directly and read the probability from its logprobs. Two things stopped me, and I diagnosed the first one wrong:
logprobs: true with an image returned 400. I wrote "the gateway refuses logprobs alongside images" in a comment and moved on. The actual error was max_output_tokens below the minimum of 16 β my max_tokens: 1. Read the error body.
Hence: describe, then decide.
That is measurable, so I measured it. Seven crops of the same photograph β the three cars, plus a front wheel, a traffic light, a palm tree and bare road β judged three ways.
All three score 6 / 7, and all three are wrong about the same crop: a front wheel, described as "a car wheel and part of the vehicle's body".
Equal on accuracy. Not equal on what you can do next. Only the graded answer was unsure β 0.85, against a confident yes from the other two β and the real cars sit at 0.98 and above. On this tiny sample, 0.9 happens to separate the wheel from the cars, so moving the threshold there makes that column 7 / 7 with nothing else lost. That is a threshold picked by looking at the same seven crops it is scored on, which is not evidence of anything except that the knob exists. A yes/no has no knob at all.
That is the whole argument for the extra call, and it rests on seven crops of one photograph. It is an anecdote, not a benchmark. But the shape holds: if your price is graded, your judge has to be.
Not the judging. Ten questions judged in parallel came back in about 600 ms for a fraction of a cent, because a System One model bills input tokens and emits no prose.
What costs you is the rate limit. Three image crops took 38 seconds, because the gateway team I was on allows five vision calls a minute. Plan the queue, not the tokens.
And the failure that would actually keep me up is neither. It is the provider going quiet between charge and charged, leaving a charge that is neither β which is the part I did not have to solve. The ledger records the reservation and the settlement separately, and reconciliation asks the provider afterwards what really happened. Once, never twice.
The judge never sees the image. It sees a sentence a vision model wrote about the image, so its calibration applies to "is this sentence a car" and not to "is this region a car". If the description is wrong, the judge is confidently wrong. The visual-to-language hop is unmeasured, and it is the weakest link in the chain.
Two ways to close it, neither done: take two descriptions per crop and let disagreement cost the seller, or find a route where the vision model itself returns a distribution.
Outcome pricing is not new β support tools have billed per resolution for years. What I have not seen is the buyer being handed the arithmetic: the distribution the price was averaged over, the grounding score that discounted it, the confidence, the model version that decided, and the threshold it was measured against.
"pricing": {
"charged": "$0.01739", "authorized": "$0.05",
"judgedBy": "typesafe-ai/jev", "confidence": 0.82,
"reason": "depth 0.00 / 0.12 / 0.88 β $0.047 expected, Γ 0.37 grounded"
}
A price nobody can check is a price nobody trusts. And it is what a refund argument is about later, so it had better be written down at the time.
The whole route is twelve lines, and three of them are the payment layer:
app.post('/detect', tollstile(toll.price(upTo('$0.20'))), async (c) => {
const found = await sam3(image, concept); // proposes, and scores itself
const kept = await secondOpinion(image, concept, found); // someone else's opinion
const payment = c.get('payment');
if (kept.length === 0) return c.json({ charged: '$0.00' }, 422); // the hold is released
await payment.fulfill({ amount: price(kept) }); // settle what survived
return c.json({ detections: kept, pricing: explain(kept) });
});
The pricing does not care which compatible rail moves the money: x402's upto scheme settles the computed amount here, and any other variable-amount rail plugs into the same handler. Fixed-amount rails β MPP's card charges among them β are excluded from a route priced this way, and the gate says so at definition time rather than at settlement. The judge never learns which rail paid, and should not.
Everything above runs, and you can poke it without an account or a wallet: demo.tollstile.com/jev prices questions this way, and the detection example is on GitHub.
If you try it on your own images, measure the thing I could not: how often the judge agrees with a person, over enough cases to mean something. Then publish that number next to your price.