Install
Requirement: Python 3.8 or newer. The only dependency is Flask, and the launchers install it for you on first run.
macOS
- Download the release ZIP and unzip it somewhere easy to find, e.g. your Desktop.
- Double-click
start.command. - First run only: macOS blocks unidentified developers. Right-click the file, choose Open, then Open again. Needed once, ever.
- Your browser opens automatically at
http://127.0.0.1:5000.
Windows
- Download the release ZIP and unzip it to your Desktop.
- Double-click
start.bat. - If Python isn't installed, get it from python.org and tick "Add python.exe to PATH" on the installer's first screen.
- Your browser opens automatically at
http://127.0.0.1:5000.
start.bat flashes and closes, Python isn't on your PATH. Reinstall Python and tick that checkbox on the first screen.Any platform, from a terminal
$ git clone https://github.com/RippleCheck/Lead-Finder.git
$ cd Lead-Finder
$ pip3 install flask
$ python3 app.py
Leave the launcher window open while you use the app — closing it stops the server.
app.py.Quick start
| # | Action | Detail |
|---|---|---|
| 1 | Pick industry | 108 categories in the dropdown |
| 2 | Pick country | 111 options. India is intentionally excluded |
| 3 | Pick town | Instant for 12 seeded countries; fetched once for others, then cached |
| 4 | Pick mode | No website / dead-bad website / both |
| 5 | Find Businesses | 10–40 seconds. The UI stays responsive throughout |
| 6 | Star the good ones | Shortlisted leads pin to the top |
| 7 | Open a lead | Contact details, verified links, owner name |
| 8 | Copy a message | Social DM, cold email, call script or follow-up |
| 9 | Send it yourself | Manually, from your own account |
| 10 | Mark the stage | New → Contacted → Replied → Won / Lost |
Settings & API keys
Everything is configured in the in-app Settings panel — no terminal, no environment variables, no restart.
| Setting | Effect |
|---|---|
| Your name | Signs every message |
| Portfolio link | Added to cold emails |
| Turnaround | Quoted in messages, e.g. "5 days" |
| AI provider | One-click switch between OpenAI and Anthropic |
| API key | Pasted in, stored in config.json. Never sent back to the browser — the settings endpoint returns only a boolean saying whether a key exists |
Data sources
| Source | Used for | Notes |
|---|---|---|
| Overpass API | Business data | Three public mirrors with automatic failover |
| Nominatim | Geocoding | Primary geocoder |
| Photon (komoot) | Geocoding backup | Fires automatically if Nominatim is rate-limited |
| Bundled seed | Town dropdown | 1,154 towns across 12 countries. Instant, works offline |
| Link checker | Verifying socials | Direct HTTP against the real profile URLs |
All five are free and need no API key.
Why OpenStreetMap and not Google scraping
- Terms of Service. Google's ToS prohibit automated scraping of search results.
- It doesn't survive real use. Google serves a CAPTCHA after a few dozen automated queries and then blocks the IP. A scraper that passes testing dies on day one.
- Unstructured. Search HTML changes constantly, so the parser breaks every few weeks.
OpenStreetMap is free forever, legal to reuse under the ODbL licence, globally covered, and — decisively — structured: website, phone, email, contact:instagram and opening_hours are real fields, so "has no website" is an exact filter rather than a guess.
Search modes
| Mode | Finds | Speed |
|---|---|---|
| No website at all | Businesses with no website tag. Your cleanest pitch | Fast |
| Dead / bad website | Sites that are broken, return 4xx/5xx, have no HTTPS, aren't mobile-friendly, are parked placeholders, or still show a copyright year of 2019 or earlier | ~30s per 60 leads |
| Both | Everything, annotated with why each one qualifies | Slower |
Closed-shop filter
A list full of shut businesses destroys your credibility on the first call. Two independent mechanisms prevent it.
Lifecycle tag rejection
Any element with a lifecycle prefix on any key is dropped: disused, abandoned, demolished, razed, removed, closed, was, old, historic. So disused:shop=bakery never reaches your list.
Explicit closure signals
disused=yes,abandoned=yes,closed=yesshop=vacant(empty retail unit)- any
end_datetag opening_hours=closedoroff
old_name (renamed but trading) or historic=yes (heritage building, still open) is deliberately kept. Only the lifecycle-prefix form and explicit closure flags reject.Lead scoring
The score answers one question: how easily can I reach this person today? It is not a quality or wealth estimate.
| Signal | Points | Why |
|---|---|---|
| Has phone | +40 | You can cold call today |
| Has email | +30 | You can cold email today |
| Has any social handle | +15 | You can at least DM them |
| Verified-live link | +10 | Channel confirmed reachable |
| Opening hours tagged | +10 | Actively maintained listing |
| Data verified 2024-07 or later | +10 | Recently confirmed to exist |
| Data verified 2022-07 to 2024-06 | +5 | Reasonably recent |
| Owner name known | +5 | Open with a real name |
| Street address present | +5 | Real premises, not a ghost listing |
Worked example. A barber with phone, email, address, opening hours, a known operator and a check date of 2026-04 scores 40 + 30 + 5 + 10 + 5 + 10 = 100. A social-only listing scores 15. Both are real leads; the first is worth your morning.
Link verification
Social links in map data are often stale. During a search the top 50 leads' Instagram, Facebook and website links are checked live, twelve at a time.
| Result | Trigger | Meaning |
|---|---|---|
| Live | HTTP 200–399 | Safe to open and DM. Adds +10 score |
| Broken | HTTP 404, 410, 5xx | Dead handle. Don't waste time |
| Unmarked | 401, 403, 429, timeout | Platform blocked the checker. Not evidence it's dead |
When a handle is missing entirely, each lead offers one-click targeted searches: Find their Instagram, Find their Facebook, Google them and Google Maps, plus a link to the source OSM listing so you can audit the data.
Messages
Four types are generated per lead, personalised from that lead's real data, each with a copy button.
| Type | Length | Contains |
|---|---|---|
| Social DM | ~90 words | Platform-neutral wording, so the same text works on Instagram or Facebook |
| Cold email | ~150 words | Subject line, portfolio link, working opt-out |
| Call script | ~350 words | Opener, offer, low-commitment ask, plus six scripted objection responses |
| Follow-up | ~50 words | One gentle nudge with an explicit easy exit |
Objection handling in the call script
| They say | You say |
|---|---|
| "I'm too busy right now" | That's exactly why I do the mockup first — it takes zero time from you. What's the best email? |
| "Facebook works fine for us" | It does, for people already following you. The gap is people googling your trade plus your town — right now they find your competitors. |
| "How much again?" | Flat fee, one payment, no subscription to me. |
| "My nephew was going to do one" | Fair enough. The offer stays open — can I send the free mockup anyway so you've got something to compare? |
| "Send me some information" | Will do — what's the best email? You'll have a mockup in a couple of days, not just a brochure. |
| "We're not interested" | No problem at all, thanks for being straight with me. [Do not push. Mark Lost. Move on.] |
Currency by market
Quoting dollars to a UK barber signals you don't know their market, so prices adapt automatically across 12 zones.
| Market | Price | Market | Price |
|---|---|---|---|
| United Kingdom | £199 | Australia | A$380 |
| Eurozone (IE, DE, FR, ES, IT, NL, PT, AT) | €230 | New Zealand | NZ$400 |
| United States | $250 | Canada | C$340 |
| Everywhere else (fallback) | $250 |
Fifteen niche-specific pitch angles name a concrete pain and a concrete win per trade — hotels get "guests book through sites taking 15–20% commission", barbers get "walk-ins can't check prices before turning up" — so the message never reads generic.
Business brief
One click produces a full brief on the business that ends with a paste-ready prompt for any AI website builder. Find lead → copy brief → paste into an AI → deliver site.
| Mode | Needs a key | What it does |
|---|---|---|
| Quick brief | No | Instant, offline. Structured from all known map data: identity, contact channels, presence gap, and a complete website build prompt with their real details filled in |
| AI brief | Yes | Everything above, plus it fetches their current website's text and has your chosen model write richer copy, infer likely services, and give style direction fitting the trade |
Guardrails in the AI prompt: only stated facts may be used, unknowns must be marked unknown, no claiming to have visited the premises, no invented reviews or awards, no hype vocabulary.
Pipeline & export
- Shortlist. Star leads you intend to message. They pin to the top and are counted in the floating outreach panel.
- Stages. New → Contacted → Replied → Won / Lost, plus private notes per lead.
- Filters. Industry, country, stage, has phone, has email, has Instagram, has Facebook, verified-live social, owner known, fresh data only, shortlist only.
- CSV export. Every field including link-verification status and owner name.
- Persistence. Everything lives in a local
leads.db. Re-running the same search never wipes your pipeline — existing leads are skipped, only new ones are added.
Responsible outreach
These rules protect your business, not just theirs.
| Rule | Why it protects you |
|---|---|
| 20–30 emails/day from a fresh domain | Blasting hundreds on day one gets the domain permanently flagged as spam. Domain reputation cannot be un-burned |
| Use a separate sending domain | Never risk your main address's deliverability |
| Real sender name, reply-to, opt-out | Required by the US CAN-SPAM Act and expected in most markets. The templates include a "reply STOP" line — leave it in |
| Honour opt-outs immediately | One ignored opt-out can cost you a domain |
| Treat Canada separately | CASL generally requires consent before a commercial email, with a narrower B2B exemption than the US, UK or Australia. Start with UK, US and Australia |
| Never push after a clear no | The call script ends politely on purpose. Your time is better spent on the next lead |
API endpoints
The app is a local Flask server. Useful if you want to script against it.
| Method | Endpoint | Purpose |
|---|---|---|
| GET | / | Serves the dashboard |
| GET | /api/health | Liveness probe |
| GET | /api/niches | 108 categories + whether an AI key is set |
| GET | /api/cities | Town list. Returns source: seed | cache | live |
| POST | /api/search | Main search. Body: niche, place, mode, limit |
| GET | /api/leads | Saved leads, stage counts, facets. Filters: niche, country, stage, starred, q |
| PATCH | /api/lead/<id> | Update stage, notes, phone, email, starred |
| DELETE | /api/lead/<id> | Remove a lead |
| POST | /api/message | Generate a message. Body: id, kind, ai |
| POST | /api/brief | Business brief. Body: id, ai |
| GET | /api/settings | Current settings. Never returns raw keys |
| POST | /api/settings | Save settings |
| GET | /api/export.csv | Full CSV export |
The engine also runs standalone from the command line:
$ python3 engine.py "Bakery" "Porto, Portugal"
$ python3 engine.py "Dentist" "Warsaw, Poland"
Files & privacy
| File | Purpose |
|---|---|
app.py | Local server, 13 endpoints, SQLite schema and migrations |
engine.py | Geocoding, Overpass queries, closed-shop filter, link verification, scoring |
messages.py | Message templates, currency zones, business brief, optional AI |
dashboard.html | The entire UI. One file, no build step |
cities_seed.json | 1,154 bundled towns |
leads.db | Created on first run. Your leads and pipeline. Back this up |
config.json | Created on first Settings save. Holds your API key |
leads.db or config.json. The first contains real people's phone numbers and email addresses; the second contains your billable API key. Both are gitignored by default — keep it that way.Privacy. Everything runs locally. The only outbound calls are to OpenStreetMap for business data and, only if you add a key, to your chosen AI provider. No telemetry, no accounts, no analytics.
Troubleshooting
| Symptom | Fix |
|---|---|
| Page won't load on macOS | AirPlay Receiver holds port 5000. Turn it off in System Settings → General → AirDrop & Handoff, or change the port in app.py |
start.bat does nothing | Python isn't on PATH. Reinstall and tick "Add python.exe to PATH" |
| macOS "unidentified developer" | Right-click start.command → Open → Open. Once only |
| "All OpenStreetMap mirrors were busy" | Public mirrors rate-limit at peak times. Wait a minute and retry — the app already tries three |
| "Couldn't find [town]" | Use the dropdown, or type the fuller form: "Leeds, United Kingdom" |
| Zero results | Thin OSM coverage for that trade there. Try a bigger town, a commoner trade, or switch mode to Both |
| Search feels slow | "Dead / bad website" mode makes one live HTTP request per business. ~30s for 60 leads is expected |
| AI rewrite button disabled | No key saved for the selected provider. Settings → paste a key |
License
Ripple Lead Finder is released under the MIT License, Copyright © 2026 Agrajeet Verma. You may use, modify, distribute and sell it, with attribution.
Questions or bugs: open an issue on GitHub or email team@ripplecheck.io.