Live demo · real restaurants, real websites, no API keys

A lead-sourcing pipeline for a voice-AI-for-restaurants GTM team

Harvest real restaurants, fingerprint their real websites for the ordering/reservation/delivery/voice-AI stack they already run, score them for fit, and generate rep-ready pre-call briefs — end to end, on public data. Built around the exact top-of-funnel problem a company like Maple (voice AI for restaurants) names in its own GTM Engineer job posting.

restaurant-lead-engine— stdlib Python, 4 stages, no keys required
Scale note

80 restaurants from one Manhattan bounding box, homepage-only fingerprinting. This demonstrates pipeline architecture and detection discipline — not a production-scale account universe. Google Places/Yelp enrichment (review counts, hours, categories) is the obvious next upgrade; it needs a billed API key and isn't included here.

The GTM problem underneath the engineering problem

A voice-AI phone agent for restaurants sells against a specific, quantified pain: restaurants miss an estimated 21–43% of incoming calls during peak hours, costing roughly $100–600 per location per day in unrealized orders. Selling that story at scale — a company like Maple has stated a target of 40,000 customers, up from roughly 2,500 today — means the sales team (Maple is concurrently hiring SMB AEs and BDRs, per public job listings) needs a constant, prioritized feed of accounts, not a cold list bought once. That's the pipe this project builds: the "pipes that feed leads in" line from a GTM Engineer job description, made real.

Maple also runs live integrations through POS/reservation partners — OpenTable, and POS platforms including Shift4/SkyTab, Quantic, and TRAY, per its own announced partnerships. A restaurant already running one of those partner platforms has less activation friction for a Maple-style agent than one running nothing at all — that distinction is built directly into the scoring model below.

Four stages, real data end to end

Full files: harvest.py · fingerprint.py · score_leads.py · brief_top_leads.py

1. harvest — Overpass API (OSM), no key 2. fingerprint — fetch real homepages, detect stack 3. score — disclosed Maple-fit weights 4. brief — rep pre-call brief, evidence-cited 5. dedupe — collapse real chains to 1 account 6. dashboard — a page an AE actually opens 7. analysis — compare a scoring hypothesis offline
stage 1 — harvest: 80 real NYC restaurants

Overpass query over OpenStreetMap · amenity=restaurant, website + phone tags present · one Manhattan bounding box

namewebsitephone
Sam Sunnysamsunnynyc.com+1-212-837-1044
Little Alleylittlealley.nyc+1-646-998-3976
The Musket Roommusketroom.com+1-212-219-0764
Hard Rock Cafehardrockcafe.com+1 212-343-3355

80 restaurants harvested, 65 homepages successfully fetched in stage 2, 15 fetch-failed and excluded from scoring rather than dropped silently — reported in data/leads_scored.json under excluded.

stage 2 — fingerprint: what's actually detected across 65 real sites

Raw-HTML domain-fragment matching — every hit stores the literal fragment, but a hit can also come from comments or inactive references and requires review

categoryplatformsites
reservationsresy26
orderingtoast13
orderingbentobox10
reservationsopentable9
delivery marketplacegrubhub9
reservationssevenrooms4
delivery marketplaceubereats4
voice-AI-adjacentpopmenu (platform-level only)2
orderingsquare2

22 of the 65 fetched sites had zero detections at all — no ordering, no reservation platform, nothing but a phone number. Those are the highest-priority accounts in stage 3, and the pipeline says so because the join says so, not because I asserted it.

the precision lesson — found twice while building this, not hypothetical

A bare-word grep for "tock" during early manual scoping produced two real false positives on this exact dataset:

  • Tick Tock Diner matched because the restaurant's own name contains "tock" — it actually runs OpenTable, not Tock.
  • samsunnynyc.com also matched "tock" — from a JS config string, "stores.EnableOutOfStockAlignment", nothing to do with reservations at all.

fingerprint.py requires the real domain fragment (exploretock.com, not the word "tock") specifically because of these two catches — both verified clean in the current output. Small example, but it's the actual argument for evidence-based detection over keyword matching.

stage 3 — score: top 10 of 65, by disclosed Maple-fit weight

+40 phone-only/no ordering · +25 OpenTable (live Maple integration) · +15 delivery marketplace w/ no direct ordering · −50 voice-AI-adjacent already detected

restaurantscorewhy
Hard Rock Cafe80phone-only + OpenTable + Grubhub
The Laurels80phone-only + OpenTable + Grubhub
Senza Gluten80phone-only + OpenTable + Grubhub
Tick Tock Diner65phone-only + OpenTable
Pera65phone-only + OpenTable
Tipsy Parson65phone-only + OpenTable
Jack's Wife Freda55phone-only + Grubhub, no direct ordering
Sam Sunny40phone-only, no platforms detected at all

The two accounts with a detected voice-AI-adjacent platform (Zoob Zib Thai, Chote Nawab) score −10 net — still phone-dependent, but flagged as a likely displacement conversation rather than a greenfield one. Scored down, not hidden.

stage 4 — brief: a real rep pre-call brief, generated from stage 2/3 output

No Claude/OpenAI API call — no key available in this environment. Template-composed from real detected facts, disclosed here exactly as it is in the code, not presented as a live model call.

Pre-call brief — Hard Rock Cafe
Website: hardrockcafe.com/location/new-york/
Phone: +1 212-343-3355
Maple-fit score: 80

Why: +40 phone-only, no online ordering · +25 OpenTable detected
     (live Maple integration) · +15 Grubhub, no direct ordering

Detected stack:
- online ordering: no online ordering detected on the site
- reservations: opentable (evidence: opentable.com)
- delivery marketplace: grubhub (evidence: grubhub.com)
- voice-AI product: none detected

Talking point: High call-dependency, no direct ordering channel
of their own — the exact profile of a restaurant losing orders
to missed calls during peak hours.
stage 5 — dedupe: real multi-location chains, not a hypothetical

A separate, wider citywide Overpass query (400 restaurants) — the 80-restaurant demo sample was too small to reliably surface chains, so this stage documents and runs a bigger one on purpose

chainlocationsevidence
5 Napkin Burger2Upper West Side vs. Hell's Kitchen — different phone, different location-specific URL
Pio Pio2Upper West Side vs. Upper East Side
IHOP2Manhattan vs. Brooklyn
Carmine's2Upper West Side vs. Times Square
Dallas BBQ2Chelsea vs. Times Square

Without this stage, a sales team sourcing from raw map data would call two separate "leads" that are actually one HQ decision-maker — a real, disclosed data-hygiene problem, not a hypothetical one, found by widening the query and checking rather than assumed. The matching heuristic (normalized base-name collision) is disclosed as imprecise in dedupe_chains.py: it will both over-merge coincidental name collisions and under-merge chains with inconsistent naming — the actual tradeoff real GTM data hygiene runs into, not hidden behind a clean demo number.

stage 6 — dashboard: the rep tooling layer, not just a script

build_dashboard.py generates rep_dashboard.html — one self-contained file, sortable by score, searchable by name, click a row to expand its full brief

Every earlier stage in this pipeline produces a file a GTM engineer can read — JSON, markdown. None of them are something an AE would actually open between calls. This closes that gap directly: no server, no build step, no framework — an AE double-clicks one HTML file and gets a working, sortable lead list with pre-call context inline. That's the literal ask in a GTM Engineer job description that names "a rep tooling layer that makes an AE genuinely faster" as a first-class responsibility, not an afterthought.

Open the live rep dashboard →

stage 7 — offline scoring sensitivity comparison

Hypothesis: delivery-marketplace-only accounts deserve a stronger fit bonus (+30 instead of the shipped +15) because 15–30% commission pressure is as real a pain point as call-dependency

metricresult
leads with a changed absolute score6 / 65
top-10 overlap between Model A and Model B10 / 10
Spearman rank correlation (full ranked list)1.000

The decision, written from the numbers rather than a feeling: keep Model A as-is. Six leads' scores moved, but the shift was uniform within that group and never crossed another lead's score band, so who-gets-called-first remained unchanged. The delivery-commission hypothesis might still be true, but this offline comparison cannot establish conversion lift. A real A/B test would require prospective randomized assignment and observed commercial outcomes.

Why I built this

The other four demos in this portfolio (signal-engine-demo, account-brief-demo, gtm-warehouse-demo, the mem0 audit) are built on public dev-tool signals. This one is built around a specific company's stated GTM problem — reading a job description for what it's actually asking for, then building the pipe, not a generic portfolio piece.

Let's talk