Lead Data Cleaner, Deduper & Scorer
Pricing
from $1.50 / 1,000 cleaned lead records
Lead Data Cleaner, Deduper & Scorer
Turn messy scraped leads into a clean, deduplicated, scored list. Chain after any Google Maps, LinkedIn or directory scraper. No scraping, no email sending, nothing stored.
Pricing
from $1.50 / 1,000 cleaned lead records
Rating
0.0
(0)
Developer
Logan Bishop
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
Turn messy scraped leads into a clean, deduplicated list, and see exactly why each lead scored what it did.
Point this Actor at any Apify dataset (Google Maps, LinkedIn, directory or custom scrapers) or paste a few records. It normalizes emails, phones and websites, merges duplicate businesses, flags disposable and role-based emails, and gives every lead a 0–100 quality score with the reasons spelled out. It never scrapes, sends email or connects to mail servers, and it keeps nothing after the run.
What it does
- Cleans the mess: trims and collapses whitespace, repairs garbled accents (
CafébecomesCafé), fixes ALL-CAPS and all-lowercase business names, strips tracking parameters (utm_*,fbclid,gclid) from websites, and upgrades links tohttps. - Validates emails and phone numbers: checks email syntax, flags disposable domains and role accounts (
info@,sales@), suggests fixes for common domain typos (gmial.combecomesgmail.com), and formats phones to international E.164 (+15125550134). - Removes duplicate leads: merges records that share a website, phone number, business email or exact name + city, and optionally catches near-identical names in the same city. The merged record keeps the most complete data.
- Scores every lead 0–100: weighted by completeness and validity, with a plain-English list of reasons next to every score.
- Checks email domains (optional): a DNS lookup confirms the domain can receive mail. No SMTP probing, ever.
How to use it
- Get your records. Run any scraper and copy its dataset ID, or paste records as JSON.
- Run the Actor. Set Dataset ID (or Records), pick a Scoring profile, and click Start. Field names such as
title,email,phone,websiteandaddressare detected automatically. - Use the clean list. Download it as CSV, Excel or JSON from the Dataset tab, or read it through the API.
Chain it after any scraper
JavaScript (apify-client):
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });// 1. Run your scraper (replace with the Actor and input you use)const scraperRun = await client.actor('<your-scraper>').call({ /* scraper input */ });// 2. Clean its datasetconst cleanRun = await client.actor('hiredhandai/lead-data-cleaner').call({datasetId: scraperRun.defaultDatasetId,scoreProfile: 'local-business',});// 3. Read the cleaned, scored leadsconst { items } = await client.dataset(cleanRun.defaultDatasetId).listItems();console.log(items.filter((lead) => lead.score >= 70));
Python (apify-client):
import osfrom apify_client import ApifyClientclient = ApifyClient(os.environ["APIFY_TOKEN"])clean_run = client.actor("hiredhandai/lead-data-cleaner").call(run_input={"datasetId": "YOUR_DATASET_ID", "defaultCountry": "GB"})for lead in client.dataset(clean_run["defaultDatasetId"]).iterate_items():if lead["score"] >= 70:print(lead["name"], lead["email"], lead["score"])
It also works as a step in Make, n8n, Zapier and Google Sheets flows: run your scraper, then pass its defaultDatasetId to this Actor with the Apify integration of your tool.
Input
| Field | Type | Default | Description |
|---|---|---|---|
datasetId | string | Apify dataset to clean. Pick it in the Console, or pass its ID through the API. Use this or records. | |
records | array | Up to 10,000 inline records (small jobs, API use). | |
fieldMap | object | auto-detect | Map your field names to name, email, phone, website, address, city, country, category. Only needed for unusual names, e.g. {"Company": "name"}. |
defaultCountry | string | US | Two-letter code used to read phone numbers written without a country prefix. |
dedupe | fuzzy / exact / off | fuzzy | How aggressively to merge duplicates. |
checkMx | boolean | true | DNS check that each email's domain can receive mail. |
scoreProfile | general / b2b-outreach / local-business | general | Which signals matter most for your use case. |
dropInvalid | boolean | false | Remove records scoring below minScore instead of just labelling them. |
minScore | integer 0–100 | 0 | Used with dropInvalid. |
maxRecords | integer | Optional cap on how many input records are read. |
Output
One item per surviving record in the default dataset. Real output from the prefilled example input (three messy records, two of them the same business):
{"name": "Acme Plumbing LLC","email": "info@acmeplumbing.com","emailStatus": "valid_syntax","emailIsRole": true,"emailIsDisposable": false,"phone": "+15125550134","phoneStatus": "valid","website": "https://www.acmeplumbing.com","domain": "acmeplumbing.com","city": "Austin","country": "US","score": 60,"scoreReasons": ["+10 name", "+25 valid email", "+20 valid phone", "+15 website", "-10 role email"],"dupeGroupId": "g_1","isPrimaryOfGroup": true,"dupeCount": 1,"sourceIndex": 0}
| Field | Meaning |
|---|---|
emailStatus | valid_syntax, valid_syntax_mx_ok (domain can receive mail), valid_syntax_no_mx, invalid_syntax or missing |
emailSuggestion | Corrected address when the domain looks like a typo (joe@gmial.com gives joe@gmail.com). The original is left unchanged. |
phoneStatus | valid, possible (plausible length, unverified), invalid or missing |
score, scoreReasons | 0–100 and every contribution behind it |
dupeGroupId, dupeCount | The duplicate group and how many other records were merged into this one |
sourceIndex | Position of the kept record in your input |
A run summary is saved to the key-value store as OUTPUT: records in and out, duplicates merged, invalid emails and phones, average score.
How scoring works
The score is deterministic and open, so you can trust and explain it. For the General profile:
| Signal | Points |
|---|---|
| Business name present | +10 |
| Valid email | +25 (+5 more if the domain accepts mail) |
| Valid phone | +20 (+8 if only "possible") |
| Website | +15 |
| Address | +10 |
| Category | +5 |
| Name, email, phone, website and address all present and valid | +10 |
Role email (info@, sales@) | −10 |
| Disposable email | −30 |
| Email domain has no mail server | −10 |
B2B outreach weights email and a verified mail domain most and penalizes role and disposable emails harder. Local business weights phone and address most and is relaxed about role emails. Scores are capped to 0–100.
Pricing
Pay per event, no subscription:
- $1.50 per 1,000 cleaned records (
record-cleaned, $0.0015 each). You are charged for records returned, so duplicates that were merged away are free. - A tiny start fee charged by Apify on every run (about $0.00005).
| Job | Records returned | Cost |
|---|---|---|
| 500 scraped leads with typical duplicates (about 400 unique) | 400 | about $0.60 |
| 10,000 unique leads | 10,000 | about $15 |
| 100,000 unique leads | 100,000 | about $150 |
You can set a maximum cost per run in the run options; the Actor stops cleanly and tells you how many records were not returned. Plan prices and platform usage are covered in the Apify pricing.
Limits
- Up to 100,000 records per run (10,000 inline). Records larger than 100 KB are skipped and counted in the run summary.
- Speed measured on Apify (1 GB): 500 records in about 3 seconds; 10,000 records with live email-domain checks in about a minute (turn off
checkMxfor seconds). Results depend on how fast the email domains answer DNS.
FAQ
Does it send emails? No. It never sends anything.
Does it check whether an inbox exists (SMTP)? No. It only does a DNS lookup of the email's domain. A valid-looking address can still bounce, and the score says "valid email", not "deliverable guaranteed".
What happens to my data? It is processed during the run and written to your own run dataset. The Actor stores nothing else and sends your data nowhere. The only network traffic is DNS lookups of email domain names (not addresses) when checkMx is on.
Does it enrich or look up missing data? No. It only cleans, merges and scores what you give it. No external data sources are used.
How does it decide two records are duplicates? Same website domain, same phone number, same business email (free-mail and disposable addresses are ignored), or the same name and city. In fuzzy mode it also merges near-identical names in the same city, but never when each name has a word the other lacks ("Red Rock Bakery" and "Red Rock Realty") or when numbers differ ("Store 1000" and "Store 1001").
Can I clean a CSV? Upload it as a dataset in Apify, or convert it to JSON and paste it into records.
Which countries work? Any country supported by the phone library. Set defaultCountry so national numbers parse correctly.
Support
Questions, bugs or feature requests: use the Issues tab on this Actor, or email leadcleaner@lbishop.app. I answer within 48 hours.
If this saved you time, a rating on the Actor page helps others find it.