Skip to content

Honest verdicts

For operators · scanning

For embedders · Rust

Explanation

Adler reports three verdicts per probe — Found, NotFound, and Uncertain(reason) — instead of the binary Found / NotFound model most username-search tools use. This page exists because that single choice colours every other design decision in the engine. If you understand the principle here, the rest of the docs make sense by default.

A username-search tool exists to answer one question: does an account with this name exist on this site? The honest answer to that question is three-valued, not two-valued. Some sites refuse to tell us; the right answer for those is “we don’t know — and here’s why,” not “the account doesn’t exist.”

That’s it. Everything below follows.

Each probe lands in exactly one bucket:

StateWhat it meansThe site’s response was
FoundThe account exists.A working response that confirms it.
NotFoundThe account does not exist.A working response that confirms it.
Uncertain(reason)We can’t tell, and here’s why.A response that isn’t a working signal — a CDN block, a rate-limit, a login wall, a captcha, an interstitial.

The cleavage between NotFound and Uncertain is the entire principle. A NotFound from Adler means the site told us this account doesn’t exist. An Uncertain(cloudflare_challenge) means the site was blocked by Cloudflare’s bot edge before we could ask. Those are categorically different facts, and treating them as the same fact is the bug we’re trying not to ship.

Look at the Sherlock / Maigret model. They scan ~3,000 sites, return Found or NotFound for each, and produce a report. Run that against a fresh datacenter IP — Hetzner, Linode, an AWS box — and ~30% of the “NotFound” rows are actually walls.

The operator sees a clean-looking list of ~2,100 NotFounds and ~900 Founds. They reason: “the target doesn’t have accounts on those 2,100 sites.” That reasoning is wrong on roughly 600 of them, because those 600 sites did not return a verdict — they returned a wall. The tool guessed NotFound. The operator inherited the guess.

In a red-team / OSINT engagement, that’s a load-bearing wrong belief. You might pivot away from an account that does exist on Reddit because your tool quietly told you it didn’t. You might write a report that says “no Patreon presence” because your tool quietly couldn’t reach Patreon from your IP.

Adler’s Uncertain makes the guess impossible. The row reads Uncertain(cloudflare_challenge) and the operator has three choices:

  1. Accept the uncertainty (the data point is genuinely missing).
  2. Resolve it with the access engine — proxy, browser, session, escalation.
  3. Investigate manually for that specific site.

All three are honest. Option (1) is something the operator opts into deliberately, not something the tool decides for them.

Same scan, same target (blue), same network (a US-residential proxy). Tools side by side:

SiteSherlockMaigretAdler
GitHubFoundFoundFound
RedditNotFoundNotFoundUncertain(cloudflare_challenge)
PatreonNotFoundNotFoundUncertain(cloudflare_challenge)
InstagramNotFoundNotFoundUncertain(browser_required)
GitLabFoundFoundFound
TwitchNotFoundNotFoundFound (after escalation to browser)

Sherlock and Maigret report blue as absent from Reddit, Patreon, Instagram, and Twitch. Adler reports four pieces of information — three Uncertains with reasons, plus one Found that came back after the automatic escalation tried the browser path on a Cloudflare wall.

The operator now knows:

  • blue on Reddit / Patreon: needs a residential IP or a browser fetch to resolve.
  • blue on Instagram: bot-protected, needs the browser backend.
  • blue on Twitch: account exists — the escalation just found it.

None of that information is recoverable from the binary-verdict versions.

Uncertain is never bare — it always carries a reason from a closed, documented set:

ReasonWhat happenedOperator remedy
rate_limitedHTTP 429 or 503 with Retry-After.Slow down (--max-rps), rotate IP.
cloudflare_challengeCloudflare interstitial / “Just a moment…”.Browser backend, residential IP.
captchaCaptcha gate.Manual; Adler does not solve.
robots_disallowedOperator opted into --respect-robots.Drop the flag if your engagement allows.
deadlinePer-scan timeout elapsed.Raise --deadline-secs or narrow the filter.
network(detail)TCP refused, DNS failure, TLS handshake error.Network / DNS investigation.
body_read(detail)The site returned headers but the body never finished.Likely transient; retry.
browser_budget--browser-budget exhausted before this site got its turn.Raise the budget or narrow the scan.
username_not_allowedSite’s regex_check says the username isn’t a legal pattern on this site.None — Adler short-circuited before any request.
browser_failed(detail)Browser backend itself errored.Driver / version mismatch; see Browser backend.
geo_unavailableSite requires a country-specific egress not in the pool.Add the egress to --proxy-pool or accept the Uncertain.
session_requiredSite’s access policy names a session not in --sessions.Supply the named session or accept the Uncertain.
other(detail)Anything else (e.g. doctor pre-flight skip).Read the detail.

The full enum lives in UncertainReason on docs.rs.

As an operator. Treat Uncertain as a signal to take an action, not a result to ignore. The access engine exists to convert Uncertains into binary verdicts; the cases that survive escalation genuinely have no verdict, and you can either accept that or investigate manually. Either way, you’re not pivoting away from an account that actually exists.

As an embedder. When you wrap CheckOutcome in your own pipeline, preserve the Uncertain branch. Don’t silently downgrade Uncertain(geo_unavailable) to NotFound for downstream consumers — that re-introduces the bug Adler exists to avoid. If your downstream must be binary, surface the Uncertain reasons in your output and let the next layer decide.

As a benchmarker. When you compare Adler’s recall to Sherlock or Maigret, separate three buckets: confirmed-Found, confirmed-NotFound, and Uncertain(*). The headline recall of binary-verdict tools is inflated by counting walls as NotFound; Adler’s “lower” recall is the same number on a more honest denominator. The bench harness in the main repo isolates these.

MatchKind and UncertainReason are the type-system enforcement. MatchKind is the three-variant enum (Found, NotFound, Uncertain); UncertainReason is the closed enum carried by the third variant. Both are public in adler-core.

The router in Client::probe_once never returns a bare NotFound for a probe that was blocked at the network or CDN edge — every Uncertain-producing path in the engine explicitly attaches a reason before the outcome leaves the function. The bot-detection heuristics codify what counts as a “we got blocked” response so the rest of the engine doesn’t have to reason about it.

When someone proposes a change that would let a NotFound escape from a blocked response, the right thing to say is “no, we don’t ship that even to look better in a comparison table.” That position is what this project is, more than any specific feature.