Public Norwood checker
How it works, in a paragraph. A visitor arrives with no account. Two short safety questions come first; if either answer suggests something other than ordinary male-pattern loss, the flow stops there and no photo is ever sent anywhere. Otherwise they upload two photos, which are checked in the browser and again on the server, converted to JPEG, and re-encoded — the re-encode is what strips EXIF, and with it the GPS coordinates phones write by default. A daily allowance is reserved before any money is spent. The estimate returns a stage or declines to give one, that conclusion is stored behind a hashed token, and the visitor gets a link to it that expires. The photos are never stored.
The estimate is bound from a flag, and the binding is chosen once when the service starts rather than checked per request. That is the point: with the flag off, the code that can reach the provider is never loaded, so there is no path — no missed guard, no stray branch — by which a request reaches a paid API. Turning it on takes a restart, which is the cheaper side of that trade on a public endpoint that spends money. See running the checker for the operational side.
Responsibilities
| Concern | Where it lives |
|---|---|
| The safety gate, capture, all sixteen tool states | This app — see the tool state machine |
| Converting HEIC before upload | This app — see photo handling |
| The daily allowance, image hardening, the estimate, storage | The Densy API |
Routing, basePath, link previews | This app — see deployment |
The API side of this feature lives in the Densy backend, which is not public. This section documents its behaviour and contract — what it accepts, what it refuses, what it keeps — rather than its code. That is the level a reader of this site needs anyway, but it is a deliberate boundary rather than an omission.
Why it abstains, and why that is the safe answer
The scale has three possible conclusions and they are not equally weighted. Declining to read a photo costs the visitor one retry. Returning a wrong stage tells someone they are losing more or less hair than they are, and they may act on it. So the design prefers the first, everywhere:
- Either gate question answered anything other than “no” abstains before the provider is ever called. That path spends no money and no quota.
- A pattern that does not look like ordinary male-pattern loss returns no stage and refers to a doctor, with no Densy call to action attached — a suspected exclusion is not a sales opportunity.
- Stage VII returns the stage but refers to a specialist, also with no call to action.
- Low confidence does not produce a stage. It produces an abstention. The response shape has no representation for “a stage I am unsure about”, so the combination cannot be expressed rather than being avoided by convention.
Which failures cost the visitor a check
The allowance is reserved first and released on the paths that must not count. The alternative — run the check, then increment — is the race that lets two simultaneous requests both spend at the provider.
| Outcome | Consumes a check |
|---|---|
| Bad file, wrong type, too small, HEIC that could not be converted | No — refused before a reservation exists |
| The tool is off, or the provider errored | No — released. Our failure, not theirs |
| A completed conclusion, including an abstention | Yes |
| A retry of a submission already answered | No — one check total, however many attempts |
That last row is why a submission carries an idempotency key: the point is that retrying the same submission does not spend a second check, which a key minted per attempt could never achieve.
What is stored, and for how long
Only what the result page needs, and nothing that identifies anyone:
- the outcome, and the stage when there is one
- a SHA-256 hash of the result token — never the token itself, so the table cannot be enumerated and a copy of it reconstructs no working link
- the model, token counts and cost, so AI spend stays visible
- a day-scoped HMAC of the visitor’s IP address, used only to count the daily allowance
No photo, no reference to a photo, no account, no raw IP address. An hourly job deletes expired results rather than marking them, and the quota key is deleted with its day — two days of the same address produce unrelated keys, so the rows cannot be joined into one person’s history.
Scope, and the safety boundary
This tool is an acquisition surface. Using it without registering is the point, and it is not a step inside the member product.
Suspected scarring alopecia has to be blocked wherever Densy could act on it. What ships here is triage and abstention only — a genuine part of that requirement and not the whole of it, because this surface has no checkout and no routine to lock.