The fit check for visitors: paid for by a proof of work and a daily budget, kept by no one

Once the owner turns it on, visitors check their own postings behind a rate limit, a proof of work and a daily budget; nothing is kept, and a claim nobody can vouch for is withheld.

Status
Accepted 4 October 2026
The record
0018-fit-check-for-visitors.md

Context

The fit check is the owner's (ADR 0017): a person reviews every page before anyone reads it. A hiring manager with a posting in hand would rather not wait for that, and the examples show the pipeline, not their own role. Opening it to visitors changes four things. Every run costs money, a few cents of model calls, so a public form is an invitation to spend the owner's. Programs fill in forms. A stranger's text goes to a model provider. And a result reaches its reader without anyone having looked at it first.

Decision

  • Off until the owner turns it on (app.fit.public.enabled, FIT_CHECK_PUBLIC in the deploy). A visitor's own posting then runs only while the model has a key and a price, a database holds the budget, and the day's budget lasts; GET /api/fit/public says which of these is missing, in words the page shows as they are. The examples run for anyone, on or off: they replay the replies kept for them, and cost nothing.
  • Guards in order, the cheapest first. The rate limit (three runs an hour from an address; ten challenges and twenty example runs a minute); the posting's length (6,000 characters); whether it is on, and the budget; and only then the proof of work, which spends the challenge. A challenge is refused while a run could not follow it, so no browser works for nothing.
  • The proof of work leaves the introductions for a package of its own, proof, which knows no feature (an architecture rule holds it to that). A challenge now names the purpose it was issued for inside its signed salt (?expires=…&purpose=fit-check&), and is taken for that purpose alone: one solved for an introduction cannot pay for a fit check, which asks up to a million tries to an introduction's hundred thousand. Its key is app.proof-of-work.key now, and the store of spent challenges sweeps itself once a day.
  • A daily budget, as a Decorator on the model port. Before each call BudgetedLanguageModel asks a ledger what the day has spent on its purpose, and refuses once that reaches the budget, a dollar by default, with BUDGET_SPENT and a Retry-After until midnight UTC; after the call it adds the call's metered cost, and a call without a price counts as the whole budget. The ledger is a table (site.model_spend, migration V8: a day, a purpose, a sum and a count), so that a cold start does not give the day its budget back.
  • A Bulkhead outside the budget. Between asking what the day has spent and adding a call's cost lies the call, up to a minute and a half, and any number of calls could ask meanwhile. BulkheadLanguageModel lets two of visitors' calls run at once (app.fit.public.concurrent-calls); the next waits up to a minute for a place, then is turned away with MODEL_BUSY. A call asks the budget only once it holds a place, so the day's budget is overshot by two calls at most, however many visitors come at once.
  • Answered, then forgotten. The result goes back in the response, no-store, and nowhere else: nothing of the posting or of the result is kept or logged, and it has no address to share. The day's spend is the only trace.
  • Withheld where nobody reviewed it. The claim check runs as for the owner, but a claim naming what its evidence does not show (UNSUPPORTED_CLAIM), which would stop the owner publishing, is withheld from a visitor: the line shows the requirement alone, counts as no match, and the result says how many it withheld. The examples' attack shows it.
  • A page of its own, /fit-check/, prerendered, in the sitemap and linked from the contact section: the status; where it is on, a form that says before anything is sent that the posting goes to Anthropic's API; the examples; and the result where the run was started. The proof of work is solved in the worker the introductions use. A published page and a visitor's result share one view.
  • In the public reference, four operations. A visitor's own run is not recorded, as that would cost a call and show someone's posting: an example's run, which answers in the same shape, stands in for it, and says so.

Consequences

  • A posting a visitor checks goes to Anthropic, which deletes what its API is sent within 30 days, save the exceptions it lists, and may not train models on it. The page says so before anything is sent, and so does /privacy/ (ADR 0007).
  • What visitors can spend is bounded whatever they do: the day's budget and two calls, cents. A spending limit on the key in Anthropic's console bounds it again, whatever the code does.
  • A run that the budget runs out on between its two calls is refused its second: the visitor is told the budget has just run out, and the first call was paid for nothing. It happens to a run or two a day at most.
  • Spending the day's budget turns visitors' own runs off until midnight UTC: whoever comes after sees why, and still has the examples. That is the price of a bound on cost. The proof of work costs a browser seconds and native code far less, so it keeps out scripts that fill in forms, not a determined spender: the rate limit makes one need many addresses, and the budget caps what they can do with them.
  • A visitor reads a result nobody reviewed. The claim check catches an invented technology, not every overstatement (ADR 0017), so the result says nobody reviewed it.
  • A cold start is unchanged: the service and what it loads are made with the first call that needs them.