Skip to Content
OperationsNorwood stage checker

Running the Norwood stage checker

Everything on this page is an operational dial for the free checker at densy.club/norwood-stage. All of it is set through the environment, so changing a number does not need new code — but the estimate itself needs a restart, on purpose.

How the tool works end to end is the technical page. This page is what you may change, and what breaks if you do.

Who can change these, and which name to change

Changing any value here means changing the API’s environment and restarting it. That is a deploy-level action, not something in an admin screen.

VariableControls
NORWOOD_PER_VISITOR_DAILY_LIMITChecks one visitor may run per UTC day
NORWOOD_GLOBAL_DAILY_LIMITChecks the whole tool may run per UTC day — the cost ceiling
NORWOOD_RESULT_TTL_DAYSHow long a result link keeps working before it is deleted
NORWOOD_TRUSTED_PROXY_HOPSHow many proxies sit in front of the API. See the failure modes below
NORWOOD_CLASSIFIER_ENABLEDWhether the estimate runs at all. Compared to the exact string true
NORWOOD_MODELWhich model produces the estimate
NORWOOD_RESULT_BASE_URLThe prefix the emailed result link is built from
NORWOOD_QUOTA_HMAC_SECRETKeys the per-visitor counter. The API refuses to start without it

This page names the variables and never states their values. Every one of them has a fallback in code and a real value in the deployment environment, so a number written here would be a second answer to the same question — wrong from the moment someone turns a dial, and wrong silently, because nothing checks a doc against an environment. Read the current numbers where they are set.

The last row is the one to get right before anything else. NORWOOD_QUOTA_HMAC_SECRET is read in a constructor that throws when it is absent, and the API resolves its providers at startup — so a missing secret does not disable the checker, it stops the whole service from booting. That is deliberate: a guessable quota key is a limit anyone can walk past, and the alternative failure is invisible, because the tool would work perfectly and simply not be limited.

The two daily limits

There are two, and they answer different questions.

LimitBoundsVisitor sees, when reached
Per visitor, per dayCasual repetition“That is your checks for today”
Across the whole tool, per dayCost and abuse“The checker is at capacity today”

Both reset on the UTC day rather than a local one, so the allowance is one rule everywhere rather than a window that moves with wherever the server happens to be running.

The global limit is the real cost ceiling. A per-visitor limit is only ever as strong as the cost of getting another address, which for anyone determined is close to zero. Set the per-visitor number for fairness; set the global number for the bill.

When the global limit is reached the tool does not offer a waiting list. It stops offering estimates and offers the guided baseline scan instead — a visitor turned away with something to do next rather than a queue to join.

Answering “I am out of checks already”

A completed conclusion costs one check, including an abstention — the visitor got an answer, even if the answer was “we cannot read these”. Our own failures cost nothing, and retrying the same submission costs one in total rather than one per attempt.

So that question is usually the full allowance of completed conclusions, at least one of which was an abstention that the visitor did not read as an answer. Which failures cost the visitor a check is the full table.

Turning the estimate on

The estimate is enabled by a flag, and it defaults to off so that a fresh environment — a preview, a new region, a restored staging copy — never starts spending money at an AI provider by simply existing. Two things are confirmed before it is switched on anywhere that faces the public:

A labelled validation set has been run, with its false-negative rate recorded

Not “we tried it and it looked right”. The rate at which it gives a stage to a photo it should have declined is the number that matters, because that is the failure that reaches a person as a confident wrong answer.

Someone is accountable for the medical-safety claims

A named person, not a team — whoever signs off that the wording shown to a visitor is safe to show them.

With the flag off the tool still runs and still protects itself: the safety gate works, photos are validated and re-encoded, both limits apply, and the endpoint reports that the estimate is unavailable — without spending one of the visitor’s checks. That is the correct state for any environment that has not had the two confirmations above.

The value must be the exact string true — it is compared, not tested for truthiness, because the string false is itself truthy. Any other value leaves the estimate off while the tool keeps working, so a mistake here is silent rather than loud.

Turning it on takes a restart, and that is a design decision rather than a limitation: the implementation that can reach the provider is chosen once when the service starts, so while the flag is off that code is never loaded and no request can reach it. A per-request check would be one forgotten branch away from spending money.

Confirming it took effect

The service says which way the flag went at startup, in one line — whether the estimate is live, with the model and both limits, or off. Read that line rather than submitting photos to find out. The dangerous direction is the silent one.

What cannot be undone

A result is reachable only by its link, the link is the only key, and links expire. An hourly job deletes the stored result rather than hiding it, so nothing about a result is recoverable afterwards — there is no account it belongs to and no copy to restore.

An expired link and a link that never existed return the same answer. If someone reports a link “not working”, there is no way to distinguish an expiry from a typo, and that is intentional — do not go looking for one. What is stored, and for how long.

Never run the development schema-sync command against a database holding these tables. Two separate reasons, and the second is worse than it sounds. It reconciles the database to the entity list, which means dropping anything the entities do not mention. And it creates tables without recording a migration — so the next deploy runs the migration anyway, hits type already exists on the first statement, and because migrations run while the database connection is being established, the service does not start at all. Not degraded: down.

If that has already happened, the two tables will exist with no matching row in the migrations table. Drop them — they hold nothing worth keeping at that point — and let the next deploy create them properly. The migration is written without IF NOT EXISTS guards deliberately: a collision should stop the deploy loudly rather than leave a half-known schema.

When AI spend looks wrong

Every completed check records the model, its token counts and its cost. Two things to check before assuming a leak:

  1. The global daily limit. Spend is bounded by it, so spend above the bound means the limit is not what you think it is.
  2. The trusted proxy-hop count. The symptom is a whole day’s allowance consumed by very few people — the per-visitor limit is being bypassed while the global one still holds. The values that must be right before launch has the setting and both directions it fails in.
Last updated on