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.
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.
Full files: harvest.py · fingerprint.py · score_leads.py · brief_top_leads.py
Overpass query over OpenStreetMap · amenity=restaurant, website + phone tags present · one Manhattan bounding box
| name | website | phone |
|---|---|---|
| Sam Sunny | samsunnynyc.com | +1-212-837-1044 |
| Little Alley | littlealley.nyc | +1-646-998-3976 |
| The Musket Room | musketroom.com | +1-212-219-0764 |
| Hard Rock Cafe | hardrockcafe.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.
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
| category | platform | sites |
|---|---|---|
| reservations | resy | 26 |
| ordering | toast | 13 |
| ordering | bentobox | 10 |
| reservations | opentable | 9 |
| delivery marketplace | grubhub | 9 |
| reservations | sevenrooms | 4 |
| delivery marketplace | ubereats | 4 |
| voice-AI-adjacent | popmenu (platform-level only) | 2 |
| ordering | square | 2 |
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.
A bare-word grep for "tock" during early manual scoping produced two real false positives on this exact dataset:
"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.
+40 phone-only/no ordering · +25 OpenTable (live Maple integration) · +15 delivery marketplace w/ no direct ordering · −50 voice-AI-adjacent already detected
| restaurant | score | why |
|---|---|---|
| Hard Rock Cafe | 80 | phone-only + OpenTable + Grubhub |
| The Laurels | 80 | phone-only + OpenTable + Grubhub |
| Senza Gluten | 80 | phone-only + OpenTable + Grubhub |
| Tick Tock Diner | 65 | phone-only + OpenTable |
| Pera | 65 | phone-only + OpenTable |
| Tipsy Parson | 65 | phone-only + OpenTable |
| Jack's Wife Freda | 55 | phone-only + Grubhub, no direct ordering |
| Sam Sunny | 40 | phone-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.
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.
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
| chain | locations | evidence |
|---|---|---|
| 5 Napkin Burger | 2 | Upper West Side vs. Hell's Kitchen — different phone, different location-specific URL |
| Pio Pio | 2 | Upper West Side vs. Upper East Side |
| IHOP | 2 | Manhattan vs. Brooklyn |
| Carmine's | 2 | Upper West Side vs. Times Square |
| Dallas BBQ | 2 | Chelsea 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.
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.
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
| metric | result |
|---|---|
| leads with a changed absolute score | 6 / 65 |
| top-10 overlap between Model A and Model B | 10 / 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.
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.
Or write directly: ai.adityachouhan@gmail.com