While checking how live DNS actually responds to common email typos, one row stood out. Two typo domains, misspellings of two entirely unrelated mail providers, resolve to the same mail exchanger.
$ dig +short MX gmai.com
10 mail.h-email.net.
$ dig +short MX hotmial.com
10 mail.h-email.net.
Verified against Cloudflare's DNS-over-HTTPS resolver on 2026-08-06, re-checked 2026-08-24. gmai.com is gmail.com missing a letter. hotmial.com is hotmail.com with two letters transposed. Gmail and Hotmail are competitors with nothing in common operationally. Their misspellings share a mail host.
Why that single fact reframes the problem
Detecting typo domains by enumeration is hopeless. The space of plausible misspellings of the top few hundred mail providers is combinatorially large, registration is cheap, and the set changes continuously as domains are registered and expire. Any hand-maintained list of typo domains is out of date the week it ships.
The MX host inverts the shape of the problem. Whoever is collecting mail for these domains has to point them at a mail server, and standing up mail infrastructure has real fixed costs — a host, an IP with tolerable reputation, storage, someone to keep it running. The economically rational move is to register many domains and point them all at one server.
So the domains are a long tail, but the hosts are a short list. You cannot enumerate the tail. You can identify a host, and identifying one host generalises to every domain that operator has already registered and every one they register next week.
That is a much better position. One observation covers an open-ended set of future domains.
The detection stage
nobounce runs typosquat MX matching as stage five, after the MX lookup has returned:
- Syntax.
- Reserved names, per RFC 2606 and RFC 6761.
- Typo correction by Damerau-Levenshtein against curated high-volume domains.
- MX resolution over DNS-over-HTTPS, including null MX and A/AAAA fallback.
- Typosquat MX host match.
- Disposable domains.
- Role accounts.
Stage three catches typos lexically, when the domain is close enough to a candidate for edit distance to fire. Stage five catches the ones stage three missed, using the fact that the domain's mail exchanger is a known collector. The two stages fail in different directions, which is the point of having both.
{
"verdict": "undeliverable",
"reason": "typosquat_mx",
"suggestion": "user@gmail.com",
"confidence": 0.94,
"checked": { "syntax": true, "mx": true, "typo": true, "disposable": true },
"cached": true
}
The distinct reason code matters. typosquat_mx means something quite specific: this domain resolves correctly, publishes healthy MX records, and would pass any conventional deliverability check — and we are rejecting it anyway because of where its mail lands. That is a stronger and more surprising claim than likely_typo, and it deserves to be distinguishable in a caller's logs.
Why the host list lives in a database
The list is a table in D1, seeded with mail.h-email.net, not a constant array in the validator source.
Because the list grows from observation. Adding a newly identified collector should be a data write, not a code change followed by a deploy. A dataset that only improves by shipping code improves at the rate someone remembers to ship code.
There is a moat consequence too, and it is the honest answer to "why not build this myself". A single-tenant validator sees only its own traffic. The observations accumulate across every customer's checks, so the dataset gets better for everyone as volume grows, and a fresh self-hosted implementation starts from the seed list and stays there. The domain-level verdict cache works the same way — shared across all customers, six hours positive and one hour negative, so customer five hundred gets a warm hit on a domain someone else paid to resolve.
The privacy shape that makes it possible
Sharing a cache and a dataset across customers only works if what is shared contains nothing personal.
No plaintext email address is stored anywhere in nobounce. Anything persisted is SHA-256 hashed; the feedback endpoint accepts only a hash and refuses a raw address with a 400. Domains, on the other hand, are kept in clear — a domain is not personal data, and gmail.com is the same string for every user who types it.
That distinction is exactly what lets the shared cache and the typosquat dataset exist. The reusable signal is entirely at the domain layer, which is the layer with no personal data in it. A schema test asserts there is no address-shaped column, so the property is enforced rather than documented.
Trying it
curl -X POST https://nobounce.dev/demo/check \
-H 'Content-Type: application/json' \
-d '{"email":"user@hotmial.com"}'
POST /demo/check needs no key and resolves a frozen fixture corpus, so it shows the real response shape for every verdict and reason without touching live DNS. It is an evaluation endpoint, not a validator you can point production traffic at — live checks require a paid key, from $1/mo.
The broader argument for why MX-only validation gets these domains wrong is in MX Validation Does Not Catch the Typos That Matter.