Skip to content

Repository files navigation

LeadPolish

Clean and qualify a lead list without uploading it anywhere.

Deduplicate, verify emails and phone numbers, fix the capitalisation, then grade every record on whether the person behind it is worth someone's afternoon — all in memory, with no database, no account, and no API key required.

Quickstart · What it does · Lead qualification · Measured results · AI-optional · CLI · Contributing

License: MIT Python 3.10+ No database Everything free

LeadPolish reviewing proposed changes

The problem

You buy or export a lead list. It arrives with the same person in it four times, half the phone numbers missing their country code, RAJESH KUMAR next to rajesh kumar, a column of N/A and asdf, and a hundred email addresses at domains that stopped accepting mail two years ago. Cleaning it by hand takes a day. Uploading it to a SaaS tool means handing your client's contact list to a third party.

LeadPolish is the third option: it runs on your machine, shows you every change before it makes one, and forgets your file the moment it sends the results back.

Quickstart

git clone https://github.com/devvaham/leadpolish && cd leadpolish
pip install -e ".[server]"
npm --prefix web install && npm --prefix web run build
uvicorn leadpolish.api:app

Open http://127.0.0.1:8000 and click Try the deliberately messy sample — 41 rows of duplicates, dead domains and broken phone numbers, ready to clean.

No account, no key, no config file. If you only want the command line, the second and third commands are optional:

pip install -e .
leadpolish clean your-leads.csv -o cleaned.csv --report audit.pdf

# or try it on the bundled mess first
leadpolish clean leadpolish/data/messy_leads.csv --dry-run

What it actually does

Everything below is deterministic. No API key, no network calls beyond an optional DNS lookup you can switch off.

Deduplication Exact matches on email and phone first, then fuzzy matching on name, company and contact details together. Every group is scored, explained, and yours to approve or reject one at a time. Blank fields on the record you keep are filled in from the copies removed.
Email verification RFC-correct syntax, then a live DNS check that the domain publishes a mail server. Disposable providers, shared mailboxes (info@, sales@), placeholder domains and misspelt ones (gmial.com) are flagged separately. No mailbox is ever probed — see honesty.
Phone verification Google's libphonenumber, driven by candidate generation rather than a single parse: the cell as typed, 00 swapped for +, as a bare national number, as an international number that lost its +. Output is E.164, with region, line type (mobile / landline / toll-free / VoIP), carrier and time zone.
Names & companies shaun mcdonaldShaun McDonald, SIOBHAN O'BRIENSiobhan O'Brien, MARY-ANNE VAN DER BERGMary-Anne van der Berg. Pvt Ltd / Private Limited / Pvt. Ltd. all converge, so they finally deduplicate.
Field standardisation usa, U.S.A. and United States of America become one thing. So do KA and karnataka, 12 church st and 12 Church Street, www.acme.co.in and https://acme.co.in.
Blank & junk detection N/A, asdf, xxx, -, test, keyboard mashing, repeated characters, placeholder emails and empty critical fields — flagged against the row, never silently dropped.
Lead qualification Every delivered record is graded A–D on whether it is worth contacting: can you reach them, can they say yes, is this a real business contact, and do they match the buyer you described. See below.
Audit report A client-ready PDF that opens with a plain-English paragraph, shows real before/after evidence, and pairs every issue with what to do about it.

The flow

1. Upload — drag in a CSV, TSV or Excel file. Bad encodings, semicolon delimiters, BOMs, blank spacer rows and ragged lines are all handled.

2. Map columns — LeadPolish guesses from the headers and the cell contents, so a column called Column3 full of things with an @ in it is still the email column. Correct anything it got wrong. Unmapped columns pass through untouched.

Column mapping

3. Review — nothing is applied until you have seen it. Every proposed change as before → after, every duplicate group with its score and reasoning, every issue with the row it came from.

Reviewing duplicate groups

4. Download the cleaned CSV or Excel file, and the audit report.

The client-ready audit report

Lead qualification

Cleaning tells you the data is correct. It does not tell you the list is any good — a spotless file can still be four hundred interns at companies you do not sell to. Every delivered record is scored from four angles:

Reachability Is there a verified way to reach this person at all
Authority Seniority read from the job title across eight levels, so a Founder outranks a Manager and "Senior Vice President" is not mistaken for "President"
Business identity Does the email domain agree with the website and the company name. minh.nguyen@vnexpress.net filed under "Minh Corp" is flagged — the address works, it just belongs to a newspaper
ICP fit Do they match the buyer you described

Out comes a 0–100 score, an A–D grade, a next step (call / email / nurture / do not work) and the reasoning, all written into the export as LP Lead Score, LP Grade, LP Action, LP Seniority, LP Department, LP Why and LP ICP Match.

Describe your buyer — seniority, function, country, company email required — and the same file reorders itself around it. Records that break a rule are still scored and ranked and marked, never silently dropped.

Lead quality tab

Measured results

benchmarks/ holds 200 hand-labelled leads and a runner. The labels were written from what a person would say after ten seconds on each row, before any of the scoring existed.

python benchmarks/make_dataset.py    # regenerate the file
python benchmarks/run.py             # measure against it
Planted duplicates found 20 / 20 (100% recall)
Merges of records that contradict each other 0
Grades agreeing with the human label 163 / 163 (100%)
Junk records reaching grade A or B 0
Genuine decision makers buried in C or D 0
Throughput 50,000 rows cleaned, deduplicated and graded in ~42s

The benchmark is synthetic. It shows the tool does what it claims on data whose right answer is known; it cannot prove behaviour on your list. Run it on your own export and read the preview — that is the real test.

AI-optional is the point

Most tools in this space put the cleaning behind an API key. LeadPolish does the opposite: the deterministic engine does all of the cleaning, and the AI layer only picks up what the rules deliberately refused to guess at.

Paste in your own provider key (Anthropic, OpenAI, or any OpenAI-compatible endpoint) and the AI gets:

  • the borderline duplicate pairs — the ones that scored just under your threshold, where the matcher would rather ask than guess
  • inconsistent job titles and industries — as a list of distinct values, with no row attached
  • free-text addresses that need splitting into city, state and postcode

That is all. It cannot override a decision the rules already made, and it can only fill a blank, never overwrite what your file already said.

If the key is missing, wrong, rate limited, or the provider is down, those steps are skipped and the rest of the run is untouched — byte-identical to a no-AI run, with a note saying what was skipped. This is tested, not aspirational.

Decisions made during the preview are replayed on download, so the file you get is the file you reviewed rather than a second, different answer.

Privacy

  • Nothing is stored. No database, no session, no cache, no temp files. The upload is held in memory for the duration of one request and dropped when the response is sent. Every response says so in a header.
  • Nothing leaves the machine except two things you control: an optional DNS lookup to check whether an email domain accepts mail (togglable), and the optional AI calls (off unless you paste in a key).
  • The AI never sees the file. It sees two records when adjudicating a duplicate, or a column's distinct values with no row attached.
  • No webfonts, no analytics, no CDN. The UI uses system fonts on purpose — a webfont request would hand every visitor's IP address to a third party.
  • Your API key is never persisted. It lives in the browser tab, is sent with the request so the server can call your provider, and is discarded with it.

Because there is no storage, there is also nothing to delete, no data-retention policy to write and no breach surface. Closing the tab is the entire cleanup.

What this does not claim

An audit report that overstates its confidence is worse than no report:

  • A domain that accepts mail is not proof a mailbox exists. LeadPolish checks MX records. It never sends a test message or probes an address — that gets your IP blocklisted and lies to you on catch-all domains anyway.
  • Line type comes from published numbering plans, not a live carrier lookup. A number ported between operators still reports its original allocation, and in countries where mobile and fixed ranges overlap you get "mobile or landline".
  • Fuzzy matching is a judgement call. LeadPolish shows its score and its reasoning and lets you overrule it. It will never merge two records that have different email addresses and different phone numbers on a similar name alone.

Command line

leadpolish inspect leads.csv          # what did it detect?
leadpolish clean leads.csv \
    -o cleaned.xlsx \                 # .csv or .xlsx
    --report audit.pdf \              # .pdf or .html
    --changes changes.csv \           # every before/after
    --audit audit.json \              # machine-readable summary
    --region IN \                     # default country for bare phone numbers
    --threshold 88 \                  # duplicate sensitivity, 50-100
    --map email="Work Email" \        # override a column guess
    --skip dedupe \                   # turn an operation off
    --no-mx                           # fully offline

--dry-run reports without writing the cleaned file.

Putting it on a URL

docker build -t leadpolish . && docker run -p 8000:8000 leadpolish

There is no database to provision, so the free tier of anything is enough. A render.yaml blueprint is included for one-click deploys; Fly, Hugging Face Spaces and plain Docker are covered in docs/DEPLOY.md, along with the settings worth tightening once the URL is public.

Note that LeadPolish has no accounts by design — anyone with the URL can use it. That is fine for a tool that stores nothing, but put a password in front of it before sharing the link widely.

Configuration

Everything is optional. Copy .env.example if you want to change the defaults.

Variable Default What it does
LEADPOLISH_MAX_UPLOAD_MB 50 Largest file the server accepts
LEADPOLISH_MAX_ROWS 100000 Rows read from a single file
LEADPOLISH_PREVIEW_ROWS 500 Rows in the preview table
LEADPOLISH_MAX_CHANGES 5000 Changes returned to the browser
LEADPOLISH_ALLOW_ORIGINS * CORS origins

There is deliberately no API key setting: keys are supplied per-request by the person using the tool, never stored on the server.

Development

pip install -e ".[server,dev]"
pytest                                   # 245 tests
npm --prefix web run dev                 # UI on :5173, proxying to :8000
npm --prefix web run e2e                 # browser walk-through of the whole flow
npm --prefix web run e2e:ai              # the same, with the AI layer on

Both browser walk-throughs expect a server on :8137 (uvicorn leadpolish.api:app --port 8137) and Chromium; set CHROMIUM_PATH if Playwright cannot find it. e2e:ai additionally expects a stand-in provider on :8199python web/mock_provider.py 8199 speaks both the Anthropic and the OpenAI wire format, so the AI path is testable end to end without spending anything or handing a test file to a real provider.

How it is put together

leadpolish/
├── tables.py      file in, DataFrame out: encodings, delimiters, Excel
├── mapping.py     which column is which
├── reference.py   the lookup tables — countries, suffixes, junk tokens
├── text.py        names, companies, titles, addresses, locations
├── emails.py      syntax, MX, disposable, role, typo detection
├── phones.py      libphonenumber with candidate generation
├── junk.py        placeholder and keyboard-mash detection
├── dedupe.py      exact pass, then blocked fuzzy scoring
├── pipeline.py    orchestration, change log, audit
├── report.py      the client-ready HTML/PDF report
├── api.py         stateless FastAPI endpoints
├── cli.py         inspect and clean
└── ai/            the optional layer: one provider interface, three tasks
web/               React + Tailwind, built into leadpolish/static

Adding a cleaning rule usually means editing a literal in reference.py. See CONTRIBUTING.md.

Licence

MIT. Use it, sell what you make with it, no strings.

About

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages