Email List Pre-Flight: dedupe, syntax, MX, disposable screen avatar

Email List Pre-Flight: dedupe, syntax, MX, disposable screen

Pricing

from $0.30 / 1,000 email address screeneds

Go to Apify Store
Email List Pre-Flight: dedupe, syntax, MX, disposable screen

Email List Pre-Flight: dedupe, syntax, MX, disposable screen

Screen a large email list before you pay a per-address verifier: normalise, de-duplicate, syntax-check, classify role/disposable/freemail addresses and flag domains with no MX. No SMTP probe, so it is priced as a pre-filter and never claims mailbox-level deliverability.

Pricing

from $0.30 / 1,000 email address screeneds

Rating

0.0

(0)

Developer

Michael Campanelli

Michael Campanelli

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

7 days ago

Last modified

Categories

Share

Email List Pre-Flight

Screen a large email list before you pay a per-address verifier. Deduplicate, normalise, syntax-check, classify (role / disposable / freemail) and flag domains that cannot receive mail — at a fraction of the cost of a full mailbox-level verification pass.

Why a pre-filter and not another "email verifier"

The Apify Store already has strong per-address SMTP verifiers at roughly $0.00065–$0.00089 per address (observed posted prices, 2026-10-08). They are worth it, but they are priced per address, so every address that was obviously dead before you started — a typo, a duplicate, a mailinator.com throwaway, a domain with no MX record at all — is money spent on nothing.

This actor does that first pass deterministically and cheaply, and it never claims more than it measures. Its output separates definite problems (invalid_syntax, duplicate, no_mx, disposable, role_account) from unknowns (mxFound: null when a domain was not resolved). Nothing is ever reported as "deliverable": stats.smtpChecked is false in every run.

What it checks

CheckWhat it meansNeeds DNS
normalisestrips display names, mailto:, quotes, trailing domain dots; lowercasesno
dedupeone row per unique address; later copies get verdict: duplicate and duplicateOfno
syntaxRFC-lite dot-atom local part, length limits (64 local / 254 total / 63 per label), valid label and TLD shapeno
MXdoes the domain publish any MX record at allyes
disposablethrowaway providers (short, auditable exact-domain list)no
roleshared mailboxes (info@, sales@, noreply@, …) — deliverable, but usually not the person you wantno
freemailconsumer providers (gmail, outlook, …) — a flag, never a failure verdictno
typo suggestiongmial.com → gmail.com, example.con → example.comno

Input

{
"addresses": ["Dana <dana@acme.example>", "sales@acme.example"],
"emailsText": "optional pasted blob, newline/comma/space separated",
"checks": { "mx": true, "disposable": true, "role": true, "freemail": true },
"concurrency": 8,
"resolverTimeoutMs": 2500
}

checks.mx: false makes a run completely DNS-free (pure CPU), which is the cheapest possible shape: no proxies, no external services, no browser.

Output

One dataset row per input entry, in input order:

{
"input": "Dana <dana@acme.example>",
"normalized": "dana@acme.example",
"domain": "acme.example",
"verdict": "ok",
"duplicateOf": null,
"flags": { "syntaxOk": true, "mxFound": true, "isDisposable": false, "isRole": false, "isFreemail": false },
"mxHosts": ["mx1.acme.example", "mx2.acme.example"],
"suggestedCorrection": null,
"screened": true
}

screened marks the rows that were actually screened (and therefore billed); duplicate rows are reported with screened: false and are never charged.

Plus a SUMMARY key-value record with the run totals (input, unique, screened, duplicates, per-verdict counts, resolver errors).

Declared outputs

The Actor ships a machine-readable output schema (.actor/output_schema.json), so the run's Output tab, the output property of the API's run response, and MCP/agent clients can find the results without reading this file:

OutputLink
Screened addresses (JSON){{links.apiDefaultDatasetUrl}}/items
Screened addresses (CSV){{links.apiDefaultDatasetUrl}}/items?format=csv
Run summary{{links.apiDefaultKeyValueStoreUrl}}/records/SUMMARY

The CSV link is the one to use if you want the cleaned list straight in a mail tool; the JSON link keeps the flags object per row.

verdict is one of ok, invalid_syntax, no_mx, disposable, role_account, duplicate. A duplicate row is reported but not billed.

Billing model

Pay-per-event: one address-screened event per unique address evaluated (duplicates are never charged twice). stats.screened is the number of events the run requests; SUMMARY.chargedEvents is the number the platform actually confirmed, so a buyer can audit the bill from the dataset alone. The two differ only when a run hits the buyer's maxTotalChargeUsd budget, and then the shortfall is logged and recorded in chargeLimitReached/chargeError instead of being hidden.

Limits, stated plainly

  • No SMTP probe. Mailbox existence, catch-all domains, full mailboxes and greylisting are out of scope by design — run a verifier after this pass.
  • The disposable and freemail lists are deliberately short, exact-domain lists rather than a scrape of a public blocklist: they are auditable and stable, and they will not catch every throwaway provider in existence.
  • Local-part case is normalised for deduplication (RFC 5321 allows case-sensitive local parts; in practice none of the large providers use that).
  • DNS answers are point-in-time and are reported as measured, never cached across runs.

Development

node test/screen.test.js && node test/input.test.js && node test/pipeline.test.js

run.js replays the whole pipeline offline from a JSON input that carries a captured mx map, so the deterministic behaviour (and the charged-event count) can be reproduced without any network access.

Offline replay (run.js)

PENNY_INPUT_FILE=/path/to/input.json node run.js

run.js calls the same lib/pipeline.js the actor calls, so its output is byte-deterministic. If PENNY_INPUT_FILE is unset, unreadable or does not carry addresses[]/emailsText, it falls back to an embedded demo fixture and says so on stderr; the emitted JSON always carries inputSource (file:<path> or demo), so a verifier can never mistake the fallback for their own input.