Email verification tools mostly do one thing under the hood: they open an SMTP connection to the domain's mail server and ask whether a mailbox exists, then hang up before sending anything. Here is how that works, in about 60 lines of Node, and what happened when I ran it against 499 Y Combinator companies.
Disclosure: I built MailFerret, a pay-per-call email finder for AI agents that uses this technique. The code below is the core idea, free to copy.
The SMTP conversation
Every domain that receives email publishes MX records. Connect to the first one on port 25 and you get a short dialogue:
S: 220 mx.google.com ESMTP
C: EHLO probe.example.com
S: 250-mx.google.com at your service
C: MAIL FROM:<verify@example.com>
S: 250 2.1.0 OK
C: RCPT TO:<paul@supabase.com>
S: 250 2.1.5 OK <- mailbox exists
C: RCPT TO:<qzxv81kd@supabase.com>
S: 550 5.1.1 The email account that you tried to reach does not exist
C: QUIT
250 on RCPT TO means "I'll accept mail for this address". 550 5.1.1 means "no such user". You never send DATA, so no email is delivered.
Minimal Node implementation
import net from "node:net";
import { promises as dns } from "node:dns";
async function rcptCheck(domain, addresses) {
const mx = (await dns.resolveMx(domain)).sort((a, b) => a.priority - b.priority)[0].exchange;
return new Promise((resolve) => {
const sock = net.createConnection({ host: mx, port: 25 });
const out = {}; let step = 0, i = 0, buf = "";
const send = (l) => sock.write(l + "\r\n");
sock.setTimeout(12000, () => sock.destroy());
sock.on("close", () => resolve(out));
sock.on("data", (d) => {
buf += d;
const last = buf.trimEnd().split("\r\n").pop();
if (!/^\d{3} /.test(last)) return; // wait for the final line of a multi-line reply
const code = +last.slice(0, 3); buf = "";
if (step === 0) { step = 1; return send("EHLO probe.example.com"); }
if (step === 1) { step = 2; return send("MAIL FROM:<verify@example.com>"); }
if (step === 2) { step = 3; return send(`RCPT TO:<${addresses[0]}>`); }
if (step === 4) return; // reply to QUIT
out[addresses[i]] = code;
if (++i < addresses.length) return send(`RCPT TO:<${addresses[i]}>`);
step = 4; send("QUIT");
});
});
}
// First address is random: if the server accepts it, the domain is catch-all.
console.log(await rcptCheck("resend.com", ["qzxv81kd3j@resend.com", "zeno.rocha@resend.com"]));
// { 'qzxv81kd3j@resend.com': 550, 'zeno.rocha@resend.com': 250 }
To find an address rather than check one, generate the usual patterns (first.last, first, flast, firstlast, f.last...) and RCPT them in order in one session. The first 250 on a domain that rejected the random address is your answer.
The catch: catch-all domains
Some domains accept mail for any address. Send RCPT TO:<anything@domain> and you get 250. On those domains SMTP verification can't tell real mailboxes from fake ones. That's why the snippet probes a random address first.
How common is that? I took 499 random active companies from the public yc-oss YC list and checked each website domain:
| Count | Share | |
|---|---|---|
| Google Workspace | 409 | 82% |
| Microsoft 365 | 23 | 5% |
| No MX on the website domain | 43 | 9% |
| Catch-all (of 456 with mail) | 254 | 56% |
| Verifiable (random address rejected) | 177 | 39% |
| Refused to answer (Proofpoint, Cloudflare routing, SES inbound...) | 25 | 5% |
On Google Workspace alone, 60% of these startups had catch-all routing on. So for founders' addresses, more than half the "valid" results you get from any SMTP-based verifier are really "unknown". Full numbers and method: mailferret.appin60.com/study.
Things that bite in production
- Port 25 is blocked on most cloud hosts (AWS, GCP, Azure, Vercel, Workers). You need a box where outbound 25 is open.
-
IP reputation matters. Cloudflare Email Routing answered our residential IP with
550in the banner; Proofpoint and some others just time out. Treat those as "unknown", not "invalid". -
Greylisting returns
4xxthe first time. Retry later or report unknown. One useful exception: Google's450 4.2.1 The user you are trying to contact is receiving mail at a rate...is only sent for mailboxes that exist. - Don't hammer one server. Batch all candidates into one session and cache catch-all results per domain.
- Large TXT records (SPF/DMARC) can time out through some local stub resolvers; querying 1.1.1.1 / 8.8.8.8 directly fixed it for me.
-
Policy rejections aren't "user unknown". Parse the text:
5.1.1/ "does not exist" means no mailbox; "blocked", "Spamhaus", "policy" means they refused you.
If you'd rather not run it yourself
MailFerret wraps this as an HTTP API that AI agents pay per call with x402 (USDC on Base, no API key):
-
POST /find{first_name, last_name, domain}: $0.01, charged only when the mail server confirms the mailbox. Catch-all and not-found results are free. -
POST /verify{email}: $0.003. -
POST /domain{domain}: $0.005 for mail provider, catch-all status, SaaS tools revealed by SPF, DMARC, public emails and the likely pattern.
From Claude Code or Codex with AgentCash: npx agentcash fetch https://mailferret.appin60.com/find -m POST -b '{"first_name":"Zeno","last_name":"Rocha","domain":"resend.com"}'. There are 5 free lookups a day on the homepage.
Cleaning a whole list instead? The homepage has a bulk CSV option: $4 per 1,000 addresses in USDC, quoted before you pay.
Use it for legitimate B2B outreach, and follow CAN-SPAM/GDPR when you email anyone.
Top comments (0)