v1.0.0 · Updated 30 July 2026

Documentation

Everything about installing, running and getting results from Ripple Lead Finder. Every number here was read from the shipped source.

Quick start

Requirement: Python 3.8 or newer. Everything else installs itself.

macOSWindows 10 & 11Linuxone command
  1. Download the release ZIP and unzip it somewhere easy to find — your Desktop is fine.
  2. Open Terminal in that folder.
  3. Run:
    $ ./start.sh
  4. Wait for:
    Ripple Lead Finder is READY  →  http://127.0.0.1:5000
  5. 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.

macOS, first run only. macOS blocks unidentified developers. Right-click 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 toRun
Stop it./stop.sh — or Ctrl-C in the window
Restart it./stop.sh && ./start.sh
Refresh the templates./sync.sh
Check what's installedpython3 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.

macOS port clash. AirPlay Receiver holds port 5000 on every modern Mac. The app finds a free port by itself and prints the real address — use that one. To pin a port: PORT=8080 ./start.sh.

Quick start

#ActionDetail
1Pick industry258 categories in the dropdown, grouped into 17 segments
2Pick country111 options, India included
3Pick townInstant for 12 seeded countries; fetched once for others, then cached
4Pick modeNo website / dead-bad website / both
5Find Businesses10–40 seconds. The UI stays responsive throughout
6Star the good onesShortlisted leads pin to the top
7Open a leadContact details, verified links, owner name
8Copy a messageSocial DM, cold email, call script or follow-up
9Send it yourselfManually, from your own account
10Mark the stageNew → Contacted → Replied → Won / Lost
Practical tip that beats any feature. Smaller towns convert better than big cities: less competition, and the owner usually answers the phone themselves rather than a receptionist. Try Otley or Ilkley before trying London.

Settings & API keys

Everything is configured in the in-app Settings panel — no terminal, no environment variables, no restart.

SettingEffect
Your nameSigns every message
Portfolio linkAdded to cold emails
TurnaroundQuoted in messages, e.g. "5 days"
AI providerOne-click switch between OpenAI and Anthropic
API keyPasted in, stored in config.json. Never sent back to the browser — the settings endpoint returns only a boolean saying whether a key exists
AI is strictly optional. Every feature works with no key at all. A key only makes each message uniquely worded. If an API call ever fails, the app falls back to the template — you never get an empty box. Leaving a key field blank preserves the previously saved key.

Data sources

SourceUsed forNotes
Overpass APIBusiness dataThree public mirrors with automatic failover
NominatimGeocodingPrimary geocoder
Photon (komoot)Geocoding backupFires automatically if Nominatim is rate-limited
Bundled seedTown dropdown1,154 towns across 12 countries. Instant, works offline
Link checkerVerifying socialsDirect 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.

Honest limitation. OSM coverage is thinner than Google's in some regions, and a few businesses have a website that simply isn't tagged. That's why every lead carries a one-click "Google them" link — verify before you pitch.

Search modes

ModeFindsSpeed
No website at allBusinesses with no website tag. Your cleanest pitchFast
Dead / bad websiteSites 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
BothEverything, annotated with why each one qualifiesSlower

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=yes
  • shop=vacant (empty retail unit)
  • any end_date tag
  • opening_hours=closed or off
Tested edge case. A business tagged 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.

SignalPointsWhy
Has phone+40You can cold call today
Has email+30You can cold email today
Has any social handle+15You can at least DM them
Verified-live link+10Channel confirmed reachable
Opening hours tagged+10Actively maintained listing
Data verified 2024-07 or later+10Recently confirmed to exist
Data verified 2022-07 to 2024-06+5Reasonably recent
Owner name known+5Open with a real name
Street address present+5Real 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.

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.

ResultTriggerMeaning
LiveHTTP 200–399Safe to open and DM. Adds +10 score
BrokenHTTP 404, 410, 5xxDead handle. Don't waste time
Unmarked401, 403, 429, timeoutPlatform blocked the checker. Not evidence it's dead
Why the third state exists. Instagram and Facebook aggressively bot-block automated requests. Treating "blocked" as "broken" would mark perfectly good leads as dead and quietly waste the best half of your list. When in doubt, the app says nothing rather than something false.

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.

TypeLengthContains
Social DM~90 wordsPlatform-neutral wording, so the same text works on Instagram or Facebook
Cold email~150 wordsSubject line, portfolio link, working opt-out
Call script~350 wordsOpener, offer, low-commitment ask, plus six scripted objection responses
Follow-up~50 wordsOne gentle nudge with an explicit easy exit

Objection handling in the call script

They sayYou 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.

MarketPriceMarketPrice
United Kingdom£199AustraliaA$380
Eurozone (IE, DE, FR, ES, IT, NL, PT, AT)€230New ZealandNZ$400
United States$250CanadaC$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.

ModeNeeds a keyWhat it does
Quick briefNoInstant, 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 briefYesEverything 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 parametersWhat goes wrong
SpacesBusiness 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 scriptsHindi, Arabic and accented names percent-encode into long unreadable strings that several clients mangle further.
Link previewsA 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.

CategoryWhat it measures
Turning visitors into customersWhether 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 businessReviews, map and directions, social profiles, FAQ, favicon, a findable email — the signals that separate a business from a scam
Being found on GoogleHTTPS, page title, meta description, single H1, structured data, canonical, social preview tags, sitemap, robots.txt
Working on a phoneMobile viewport, responsive layout, declared language
Loading fastPage 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

LinkDetail
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.

RuleWhy it protects you
20–30 emails/day from a fresh domainBlasting hundreds on day one gets the domain permanently flagged as spam. Domain reputation cannot be un-burned
Use a separate sending domainNever risk your main address's deliverability
Real sender name, reply-to, opt-outRequired by the US CAN-SPAM Act and expected in most markets. The templates include a "reply STOP" line — leave it in
Honour opt-outs immediatelyOne ignored opt-out can cost you a domain
Treat Canada separatelyCASL 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 noThe 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.

MethodEndpointPurpose
GET/Serves the dashboard
GET/api/healthLiveness probe
GET/api/niches258 categories, their segments, and whether an AI key is set
POST/api/manual-linkFoundry links for a hand-typed business. Body: business, niche, city, phone. 400 on empty business or unknown trade
POST/api/audit31-check site audit. Body: url, business, optional lead. Always 200 — read the ok flag
POST/api/proposal-linkBuild a /proposal/ or /present/ deep link
GET/api/foundry-statusCatalogue freshness, slug drift, audit engine health
POST/api/local-variantsOllama message variants; empty list when Ollama is absent
GET/api/citiesTown list. Returns source: seed | cache | live
POST/api/searchMain search. Body: niche, place, mode, limit
GET/api/leadsSaved 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/messageGenerate a message. Body: id, kind, ai
POST/api/briefBusiness brief. Body: id, ai
GET/api/settingsCurrent settings. Never returns raw keys
POST/api/settingsSave settings
GET/api/export.csvFull 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

FilePurpose
app.pyLocal server, 13 endpoints, SQLite schema and migrations
engine.pyGeocoding, Overpass queries, closed-shop filter, link verification, scoring
messages.pyMessage templates, currency zones, business brief, optional AI
dashboard.htmlThe entire UI. One file, no build step
cities_seed.json1,154 bundled towns
leads.dbCreated on first run. Your leads and pipeline. Back this up
config.jsonCreated on first Settings save. Holds your API key
If you fork this repo, never commit 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

SymptomFix
Page won't load on macOSAirPlay Receiver holds port 5000. Turn it off in System Settings → General → AirDrop & Handoff, or change the port in app.py
start.bat does nothingPython 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 resultsThin 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 disabledNo 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.

Two licences apply. MIT covers the code. The business data comes from OpenStreetMap under the ODbL licence, which legally requires attribution to OpenStreetMap contributors. If you fork or redistribute this, keep that credit — it's an obligation, not a courtesy.

Questions or bugs: open an issue on GitHub or email team@ripplecheck.io.