01
Decode, without double-firing
Camera or gallery. The scanner guards against the same code being processed twice while a scan is already in flight — an unglamorous fix that is documented as the first critical one in the architecture notes.
scanner_screen.dart
02
Walk the redirect chain
Up to ten hops, following each one manually rather than letting the HTTP client do it, and recording the source, destination and status of every hop. The app renders the whole chain rather than just the endpoint.
redirectResolver.js
03
Resolve it three times, as three devices
The same URL is resolved in parallel under desktop, iPhone and iPad user-agents. If the registrable domains disagree, the destination is cloaking — showing one thing to scanners and another to phones. A device that dead-ends is recorded as exactly that, never quietly rounded up to 'consistent'.
domainDiverged / deadEndDevice
04
Reputation on every hop, not just the last
Google Web Risk runs against the final URL and each intermediate hop in parallel. A lookup that fails is recorded as unverified — it is never coerced into a pass.
05
Check the certificate that actually served it
Issuer, common name, hostname and SAN match, and chain trust for the final hop — the one you would actually have landed on.
certClient.js
06
Read the destination's intent
Password fields, payment fields, meta-refresh targets, off-site link domains, and Android package download intent. Most importantly: the registrable domain that each form actually submits to, which is the thing a screenshot can never tell you.
contentIntent.js
07
Correlate brand without a logo database
A login form on one domain posting credentials to an unrelated one is a strong mismatch. A certificate for a different registrable domain is a weak one. Payment processors and identity providers are allow-listed, because legitimately posting off-site is exactly what they do.
brandCorrelation.js
08
Interrogate the domain itself
Punycode and mixed-script homographs, raw-IP hosts, shorteners, and around twenty reputation-laundering hosts where anyone can get a subdomain on a trusted parent. Domain age is fetched over RDAP and cached — and only when a form is present, because that is the only time it changes the answer.
domainIntel.js
09
Look at it, if you're entitled to
For premium accounts: real screenshots at three viewports, text and logo detection, then a Gemini pass with the screenshots attached. Its system prompt casts it explicitly as a corroborating voice — deterministic checks run alongside it, and it is told not to invent risk.
"a CORROBORATING voice"
10
Gate the verdict on corroboration
Dangerous requires more than one thing agreeing: an app download from a non-store domain, or a brand mismatch together with a credential form, or device divergence together with a form. A clean shortener that lands on a valid, reputable domain is not flagged — because crying wolf is how a security app teaches people to ignore it.
deriveBackendStatus()
11
Show your working
The explainer returns the final status, the named branch that decided it, every input and every gate boolean. It was built to diagnose false positives rather than merely to ship verdicts.
RiskGatingPolicy.explain()