Quick start
Requirement: Python 3.8 or newer. Everything else installs itself.
- Download the release ZIP and unzip it somewhere easy to find — your Desktop is fine.
- Open Terminal in that folder.
- Run:
$ ./start.sh - Wait for:
Ripple Lead Finder is READY → http://127.0.0.1:5000 - Open the address it printed. Your browser usually opens it for you.
Rather not touch Terminal? Double-click start.command on a Mac or
start.bat on Windows. Exactly the same thing.
start.command, choose Open, then Open again. Once, ever.What the first run does
Seconds, not minutes — the only dependency is Flask. Running it twice never starts a second copy.
- Finds Python and pip
- Installs Flask if missing
- Creates the databases and folders
- Downloads the Foundry template list
- Runs a health check and prints the result
- Starts the app and opens your browser
The status table
Every start prints this. It is the honest state of your machine, not a splash screen.
Ripple Lead Finder
────────────────────────────
App READY
Database READY
Foundry CONNECTED
Catalogue 198 industries
Audit READY
Ollama OPTIONAL / OFF
────────────────────────────
Ripple Lead Finder is READY → http://127.0.0.1:5000
If something is missing it says which feature you lose, not which package failed:
Audit OPTIONAL / OFF request failed
! Website audits are off — the Foundry audit engine didn't answer. Everything else works.
The app still starts. Finding leads, writing messages and building Foundry links carry on working.
Stopping, restarting, syncing
| You want to | Run |
|---|---|
| Stop it | ./stop.sh — or Ctrl-C in the window |
| Restart it | ./stop.sh && ./start.sh |
| Refresh the templates | ./sync.sh |
| Check what's installed | python3 bootstrap.py check |
./stop.sh stops only the process this app started, found by the id it
recorded and confirmed against the port it is listening on. It will not go hunting for
anything called "python" and kill it.
Windows: double-click start.bat, or run python bootstrap.py setup
then python app.py. The .sh scripts are macOS and Linux only — we would
rather say that than pretend.
PORT=8080 ./start.sh.Quick start
| # | Action | Detail |
|---|---|---|
| 1 | Pick industry | 258 categories in the dropdown, grouped into 17 segments |
| 2 | Pick country | 111 options, India included |
| 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.
Foundry links
Every lead gets two personalised demo links — a website and a matching admin dashboard — carrying that business's own name, phone, town, trade and your agency name. Each has its own copy button on the lead card, so you never paste the wrong one.
How the link is built
The five fields are joined with a pipe, UTF-8 encoded, then base64url encoded into a single
?d= parameter:
BusinessName|Phone|City|TradeSlug|AgencyName
↓ utf-8, then base64url (no padding)
https://foundry.ripplecheck.io/demo/barber/?d=VGhvcm50b24ncyBCYXJiZXJzfCs0NDc3MDA5MDAxMjN8TGVlZHN8YmFyYmVy
| Why not plain parameters | What goes wrong |
|---|---|
| Spaces | Business names contain them. WhatsApp, Instagram and SMS terminate a link at the first space, so the recipient taps half a URL and gets nothing. |
| Non-Latin scripts | Hindi, Arabic and accented names percent-encode into long unreadable strings that several clients mangle further. |
| Link previews | A broken URL means no Open Graph fetch, so the message shows a bare blue link instead of a card with their name on it. That card is what earns the tap. |
| Ampersands & apostrophes | & inside a name silently truncates the query string; ' breaks naive parsers on the receiving page. |
This is encoding, not encryption. The token is trivially decodable and is meant to be — it contains the business's own public name, town and phone. Its job is to survive being pasted into any messenger on earth, not to keep anything secret. Do not put anything private in it.
Manual link builder
For prospects you found outside the app — a walk-in, a referral, a DM — press Manual link in the header. Type the business name, trade, town and phone and both links build live as you type, with their own copy buttons and a Copy both. The output is byte-identical to what a real lead produces.
The same thing from a terminal:
python3 make_link.py "Cric8InNet" cricket-academy \
--city Delhi --phone "+91-9999-30-5050" --kind both
Deploy order
The links only personalise once FOUNDRY-decoder.js is live on the Foundry site —
that is the script that reads ?d= and writes the details into the page. Upload it there
before sending any links, or prospects will see the generic template.
The free audit
Foundry's audit engine, wired into the lead panel. Nothing to install — it runs on their server, so the button works the moment the app opens.
Thirty-one deterministic rules across five categories. No AI and no third-party service, which is why the same site audited twice returns the same score. Nothing is stored: the report is generated live from the public page and disappears when the tab closes.
| Category | What it measures |
|---|---|
| Turning visitors into customers | Whether someone landing on the page can actually contact, book or buy — call to action, booking, contact form, tap-to-call, WhatsApp, prices, opening hours |
| Looking like a real business | Reviews, map and directions, social profiles, FAQ, favicon, a findable email — the signals that separate a business from a scam |
| Being found on Google | HTTPS, page title, meta description, single H1, structured data, canonical, social preview tags, sitemap, robots.txt |
| Working on a phone | Mobile viewport, responsive layout, declared language |
| Loading fast | Page weight, response time, image alt text, lazy loading, script count, analytics |
Scoring
Each check carries a weight based on how directly it costs money. A missing tap-to-call button outweighs a missing favicon, because one loses phone calls and the other loses nothing measurable. Failures are sorted by weight, so the top of the list is the thing to fix first — not the easiest thing, and not the thing that would sell the most work.
What the panel shows
- Score out of 100 and a letter grade, with a lead temperature — hot, warm or cold.
- Five category bars, each scored separately.
- The failing checks, worst first, each with why it costs them enquiries and what fixing it involves.
- An opening line written from their biggest problem, with a copy button.
- Two links to send — the read-only report and the priced proposal.
The two links
| Link | Detail |
|---|---|
| Audit report | /audit/?u=<their site>&b=<business> — the same report as a read-only page you can send. Carries noindex, so a client's score never appears in search results. |
| Proposal | /proposal/?b=&c=&w=&ind=&cur=&plan=&add=&disc=&score= — scope, timeline, itemised cost and payment terms on one page, printable to PDF. Pure front-end: the link is the quote, so nothing can change after you send it. |
Currencies: USD INR GBP EUR AED.
Plans: starter business software platform.
Add-ons: care,content,photo,seo,ads.
Batch auditing
Shortlist, top 5 or top 10 from the toolbar. Requests are spaced roughly three seconds apart because Foundry runs on shared PHP hosting and a burst gets throttled or 508s. A failed audit never stops the run; that lead is simply left unscored.
Failure handling
The endpoint always returns HTTP 200 with an ok flag — the response
body is read as text and parsed in a try/catch rather than assuming JSON, because a shared
host under load returns an HTML error page, not JSON. Whatever comes back, the panel shows
the reason and offers a retry.
A site that times out is real information, not a bug: a site that doesn't answer the audit engine isn't answering some of their customers either. That is a fair thing to open a conversation with.
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 | 258 categories, their segments, and whether an AI key is set |
| POST | /api/manual-link | Foundry links for a hand-typed business. Body: business, niche, city, phone. 400 on empty business or unknown trade |
| POST | /api/audit | 31-check site audit. Body: url, business, optional lead. Always 200 — read the ok flag |
| POST | /api/proposal-link | Build a /proposal/ or /present/ deep link |
| GET | /api/foundry-status | Catalogue freshness, slug drift, audit engine health |
| POST | /api/local-variants | Ollama message variants; empty list when Ollama is absent |
| 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.