How I built an AI data pipeline for a Fortune 500 client — and became the person who decides what's worth building next
Sales sources the lead. I decide whether it's real. Eight chapters, one system — what I actually decided at each stage, what I rejected, and why. The client stays confidential, the reasoning doesn't.
Boots on the Ground
A Fortune 500 mobility & delivery platform wanted real photo coverage of the places people search for on the app — restaurants, shops, venues — not just the road to them. The first pitch wasn't a deck. It was me, a camera, and every business across several blocks in a major NYC commercial district (illustrative area: Midtown, near two major transit hubs).
Shooting ~400 real businesses myself wasn't the plan — it was a deliberately unscalable benchmark. Two different things came out of it:
The client couldn't buy this data from their direct competitor — the incumbent maps/reviews platform — at any price. No competitor sells its core data to a potential rival. That constraint is what made the deal buildable at all.
An MVP That Delivers From Day One
The R&D pipeline: a web-scraping agent chained to a multimodal image-generation model, plus a human check. The interesting decision wasn't the AI — it was what to build the human check out of, at this stage.
L1 · Scraping agent web-native
Finds and evaluates candidate source images for a location from public web sources — filtering for angle, lighting, and address match.
L2 · Generation model image synthesis
Takes the scraped source image and produces a cleaned derivative — removing people, vehicles, clutter, while preserving the actual structure.
Two AI layers that fail differently, chained together — that part was straightforward. The real decision was what to build the review layer out of, before scale existed to justify anything expensive.
Figures shown are rounded and representative of production scale, not exact production numbers.
Illustrative pipeline trace scrape_result · generation_result — synthetic, shape-matched
{
"location": "Marlow & Vine (fictional)",
"candidate_images_found": 4,
"selected_image": { "angle": "street-level", "address_match_confidence": 0.94,
"obstructions": ["parked delivery van, partial storefront"] }
}
{
"location": "Marlow & Vine (fictional)",
"obstruction_removed": true,
"signage_legibility_score": 0.61,
"human_review": "required — every generated image, no exceptions",
"reviewer_action_available": ["accept", "regenerate", "re-source_input", "manual_correction"]
}
There's no second independent source to cross-check a generated image against — unlike the ratings pipeline in Ch.05, where a contradiction between two sources is the actual signal worth escalating on. For imagery, every single output gets a human look before it ships. That's not a gap in the automation — it's the correct design for a task with no independent source to verify against, and it's the reason the tagger workforce in this chapter exists at the scale it does.
Global-Scale Deployment and Operation
A spreadsheet can't process millions of locations. Once volume proved the pilot, I built the platform that replaced it — and the hiring-to-payroll loop that let a small admin team run it without micromanaging.
By this point the requirements were real, not guessed — the spreadsheet phase had already surfaced them. That changed the calculus on build vs. buy.
Tagging platform review surface
Every generated image surfaces for review — no scoring shortcut, because there's no independent source to auto-verify a generated image against. The tagger sees the source image, the generated output, and a fixed set of actions: accept, regenerate, re-source input, or manually correct.
Hiring-to-payroll loop ops system
Auto-graded recruiting test → strong scorers routed to HR for contracting → work/pay tracked live → every payout batch graded → a Slack HR bot routes tickets automatically.
That's one status change. Here's the system underneath it — the part most PMs never build themselves: a hiring-to-payroll loop built for real autonomy and real accountability, not spot-checks.
Step 1 — Hire, without a recruiter in the loop
Step 2 — What the tagger actually sees: full transparency, no black box
This is the actual mechanic behind "autonomy without micromanaging" — a tagger never has to ask a human "how am I doing?" The system already told them, in real time, off the same log that pays them.
Step 3 — The grading queue: urgency computed per-tagger, not a fixed clock
Overdue = work-days accumulated at that tagger's own pace, not a calendar date. A fast tagger hits the 14-day mark sooner and gets flagged sooner — correctly.
| Tagger | 90-day daily pace | Actions since paid | Est. work days | Their avg/day | Status |
|---|---|---|---|---|---|
| Tagger A | 6,140 | 16.8d / 14d | 366 | Overdue | |
| Tagger B | 2,850 | 9.1d / 14d | 313 | Grade soon | |
| Tagger C | 1,190 | 4.2d / 14d | 284 | Building up | |
| Tagger D | 410 | 1.6d / 14d | 259 | Fresh | |
| Tagger E | — | — | 301 | Up to date |
The sample graded per tagger is a random draw from their full history, not cherry-picked, and pay is calculated independently of the grade — volume determines the dollar amount; grading determines whether that person keeps getting work. Once a payout is marked paid, the exact report shown at that moment is locked — a later data correction can never quietly change what someone already got paid for.
Zoomed in — what grading ONE sampled action actually looks like
The table above is the aggregate queue. This is the admin tool underneath it, mid-cycle on Tagger A's overdue batch — one sampled action at a time, not a spreadsheet row.
Grade → Payout → Confirm → Receipt runs once per cycle per tagger. The pass/fail call on this one sampled action feeds directly into the pass-rate tile shown in Step 2 — same event log, no separate summary written later.
Step 4 — Pipeline efficiency: where rework actually comes from
The same admin team also watches which pipeline stage is actually slow — so engineering time goes to the real bottleneck, not a guess.
Avg. time per stage — bars colored by severity, not decoration
The review → regenerate loop is the real bottleneck — not "review" itself. That's the difference this dashboard is built to surface: rework is slow, first-pass review isn't.
The same admin team also watches the machine side of the operation — every batch job, its status, and its real cost, down to which function and which model provider spent it.
Cost breakdown — by function, by provider
| Function | Input | Output | Cached | Calls | Cost |
|---|---|---|---|---|---|
| image_scrape_select | 1.2M | 84K | 310K | 12,480 | $96.10 |
| image_generate | 2.4M | — | — | 12,480 | $268.40 |
| legibility_score | 640K | 22K | 180K | 12,480 | $36.50 |
| Provider | Input | Output | Calls | Cost |
|---|---|---|---|---|
| grounded search model | 1.2M | 84K | 12,480 | $96.10 |
| image-generation model | 2.4M | 22K | 24,960 | $304.90 |
Cost is tracked in three stages, not one number: an initial estimate before the batch runs, a projected figure once real sampling from ≥10% of the batch is in, and a final cost from actual token usage — split by which function spent it and which model provider billed it. That granularity is what makes the auto-cancel row possible: the system caught its own cost curve spiking 3.1× baseline and killed the run before it became an expensive surprise, instead of finding out from the bill.
Could ~5 admins run 100+ workers by just checking in on people? No — spot-checks don't scale past a handful of direct reports. What scales is a system that gives every worker real-time visibility into their own pace and pay, and gives every admin a queue that's already sorted by whose SLA clock is closest to running out. Autonomy and accountability come from the same system, not opposite ends of a trust spectrum.
The Industry's Long Problem: When and Where to Update
The industry-level problem: a city's business landscape can't be fully re-scanned on any reasonable cadence, and by the time a new venue shows up on a maps or reviews platform, competitors already have it. The fix I built finds venues from public-record signals — before they're searchable anywhere at all.
Before the signal: not scanning every block the same way
A naive geographic scan checks every block at the same resolution, which wastes API budget on parks and water while under-sampling dense commercial blocks — the exact blocks worth the most. The scan I built classifies terrain first, into a coarse grid built from one bulk public-map-data query over the whole area — not one query per search tile, which is the actual cost insight: the classification itself has to be cheap, or there's no budget left to spend on the real searches. Each cell is scored by tag priority, checked in this order:
| Priority | Tag signal found | Classification | What happens next |
|---|---|---|---|
| 1st | Commercial / retail / food tags | Dense | Adaptive recursive subdivision |
| 2nd | Water, parks, forest | Skip | Never searched, not even once |
| 3rd | Residential / industrial tags | Sparse | Exactly one search, no recursion |
| fallback | No map-data signal at all | Dense | Treated as priority 1, not skipped |
Recursion depth is driven by the classification, not a fixed zoom level: sparse gets one query and stops; dense subdivides into four quadrants every time a search circle returns the API's max result count — because hitting that cap is the signal there's more there than one query found, not a guess about it.
The genuinely non-obvious call is the fallback row above. An area with zero map-data signal has two plausible readings: nobody's mapped it because there's nothing there, or nobody's mapped it because it's an old, poorly-digitized part of a real city. I bet on the second, on purpose.
Every incumbent maps/reviews platform waits for a venue to exist online, then indexes it. That's a fixed lag no amount of scraping speed fixes. Government records — liquor license applications, health inspections, building permits, change-of-use planning filings — are legally required to exist before a venue opens. That's not a faster scrape; it's a genuinely earlier signal, structurally unavailable to any competitor scraping listings after the fact.
Iterated three times in production, false-positive rate dropping at each step:
One documented real-world run of this system, one major city, surfaced ~90 venues either confirmed new or about to open — before any of them existed on a commercial maps/reviews platform. Figures rounded, representative of a real production run, not the exact count.
A Precision Instrument, Not a Safety Net
After the imagery pipeline proved out, the client asked for a second data type: ratings and reviews. Same research → synthesize → HITL discipline, rebuilt for a different output shape — a written review has to sound real, not just be factually correct.
The waterfall: one expensive call, then cheap reasoning on top of it
Grounded search occasionally returns real data about the wrong business — e.g. a hotel's data instead of the restaurant inside it. L1.5 exists specifically to catch that before it reaches the writing stage: if the description doesn't match the target business, the pipeline discards the result and falls back to a plain search-grounded query instead of writing a confident, wrong review.
The strict rule underneath L3: no training-data priors, ever
The single hardest-won rule in this pipeline: every claim in the written review must come directly from what L1/L2 actually found — never from the model's own memory of that brand. Left unchecked, a model asked about "a Starbucks" or "a Marriott" will confidently describe the generic version of that brand instead of the specific location it was asked about. No web access, no license to fill gaps from memory — only the evidence it was handed.
Three more failure modes this pipeline engineers around
| Failure mode | What breaks without a fix | The fix |
|---|---|---|
| Sampled reviews read as a literal count | A source showing 3 sample reviews gets reported as "3 reviews total" — badly undercounts real popularity | Bucket into an effective-volume scale (few → dozens → ~100 → hundreds → thousands), with a floor so a handful of samples never reads as "1–19 reviews" |
| Merged rating quietly copies the source | Model defaults to returning the primary source's number unchanged — no evidence the cross-check ran | Force the synthesized rating 0.1–0.4 away from the raw score, closing off the copy-through shortcut |
| Review reads like a template | Thousands of reviews start the same way, lean on the same stock phrases | 10 rotating opening angles, plus a named-phrase blocklist ("hidden gem," "worth a visit," "consistently," "beloved by locals") — naming the exact phrase works where "avoid clichés" doesn't |
The real insight: what a hallucination actually looks like
Most HITL designs flag on uncertainty — a low confidence score, a thin data field, anything that "feels" unsure. That's the wrong signal, and it's expensive: a model is uncertain constantly, so uncertainty-triggered review doesn't scale. The actual failure mode worth catching is narrower and more diagnosable — two independent sources actively disagreeing on the same fact. That specific pattern is what a hallucination or a wrong-business match looks like from the outside: not vague doubt, but a concrete conflict a second source would have no reason to produce unless something upstream actually broke. So the classifier is told to ignore missing data and mild uncertainty entirely, and flag only verified, active contradictions between sources — a plain rules layer, not a model, turns that verdict into a delivery status. Getting this distinction right is what makes HITL a precision instrument instead of a blanket safety net: it means the small number of cases that do reach a human are the ones that actually needed one.
| Location (illustrative) | Rating | Status |
|---|---|---|
| Marlow & Vine (fictional) | 4.2 | valid_review |
Same classification, different status spaces, different decision-tree order
This same contradiction classification feeds more than one client's status logic, and not every status decision-tree checks the "flag for human" signal in the same place — that's not an inconsistency to clean up, it's the correct call for each status space's own shape.
There's a second-order safety net underneath this: if the deterministic waterfall dead-ends into "needs human" but the model itself never actually raised a real contradiction, the system treats that as evidence of a pipeline defect — retry the classification — rather than a genuine data conflict to escalate. That distinction is what keeps the human queue tied to real disagreements in the data instead of quietly absorbing the pipeline's own bugs.
The same HITL-verified text also gets indexed to power the client's internal search — one verified dataset, two products.
Scaling as a Data Partner, From One Pilot
active, scaling nowThe commercial signal here matters more than the mechanism: this is the same client, buying a third and fourth capability, without a new sales cycle. That doesn't happen because a pipeline works — it happens because the account trusts the judgment behind it. The newest ask: structured menu data and food imagery for a food-delivery vertical.
The easy version: point the existing image pipeline at food photos and call it done. A menu is a genuinely different extraction problem — semi-structured, no fixed schema across restaurants. Reusing the discipline (research → extract/generate → HITL) instead of the code is what made a new revenue line buildable in weeks, not months.
| Extracted item (illustrative) | Price | Confidence |
|---|---|---|
| Grilled Branzino, seasonal greens | $28 | High |
| House-made tagliatelle, brown butter | $22 | High |
| Market vegetable plate (ask server) | — | Flagged — no price found |
Two more places the same pattern shows up
| Where | What it actually does |
|---|---|
| Food photography prompts | Dish type sets camera angle before anything generates — ramen shoots at 45°, never true overhead (straight-down flattens broth depth and noodle layers). Price level sets a "formality tier": fine dining gets studio-lit language, a neighborhood spot is told to keep the plain plate or tray, not swap in something that doesn't belong. |
| People/car removal | Runs in both directions on purpose — strip people, cars, bikes (including reflections), but also explicitly never introduce new ones. An edit model told to make a scene look "natural" will otherwise add generic background people back on its own. |
One-photo-one-item ownership is enforced by code, not only by prompt instruction — a vision model told not to double-assign a photo will still occasionally do it when two dishes look similar, so a deterministic step resolves any conflict after the model's pass. The same pattern — ask the model to comply, then mechanically check the same rule in code — shows up again in Ch.05's ratings classifier and Ch.03's tagger grading. Same judgment each time: don't trust an LLM's compliance with an instruction when the rule can be checked mechanically instead.
"Expansion revenue without a new pitch is the actual proof that the commercial judgment in Chapter 8 is working, not just the technology. A client doesn't hand you a fourth capability because a pipeline runs — they do it because the last three decisions you made for them were right."
Sharing the Tech Stack Across the Entire Org
active nowThis one isn't about one client anymore. Other internal teams — running entirely different data-acquisition methods on entirely different data — started asking the same question: can we run our tagging work on what you built, instead of Turk, Label Studio, or a shared spreadsheet?
The easy, wrong answer is "yes, use the whole platform." A sourcing/scraping method tuned for one data domain doesn't transparently transfer to another — different teams already use different methods (passive on-device capture, panel data, third-party APIs, web scraping), and assuming otherwise is a real mistake I've had to correct people on directly: someone hearing "it's all on the web" and assuming an unrelated data domain must overlap with this pipeline, when it doesn't.
What transfers ops layer
Recruiting, onboarding, the tagging surface, grading, payout, performance tracking — the entire human-in-the-loop operational loop, reused as-is by teams working on completely different data.
What doesn't sourcing layer
How the data gets found and scraped in the first place — that stays domain-specific, one team's method per data type, not a shared black box.
Knowing exactly where that line sits — and saying so directly when someone assumes otherwise — is as much the job as building the platform was. A platform that quietly overpromises "it does everything" is a worse outcome than one that's honest about its edges.
A platform that spreads because people ask for it is a different kind of proof than one that spreads because a contract requires it. Nobody was told to use this — they asked, because the alternative was a spreadsheet.
How the Entire Org Now Runs Its Sales + Dev Cycle
This isn't a one-time chapter — it's the recurring role I hold on every new deal. Sales sources the lead and runs the first call. By the second call, I'm the one in the room with a working demo and a scaling plan, deciding whether it's real.
The pattern repeats every time a new deal expansion comes up: leadership brings a brief to the table — a new data type, a new market, a new vertical. That's sourcing, not scoping.
What "modeled against real billing, not a projection" actually looks like — a shape-matched, illustrative version of the one-pager I'd actually walk into call 2 with:
| Basis | Cost / record | At proposed volume |
|---|---|---|
| Actual cloud billing, prior quarter (per-record cost, reconciled by hand) | $0.05 | — |
| New capability, demo run against real invoiced rate | $0.07 | ~$14,000 / 200K records |
| Client's stated budget ceiling for this expansion | — | $20,000 |
This is the artifact the go/no-go actually rests on. Figures are illustrative and rounded; the basis (reconciled billing, not an estimate) is real.
No pipeline diagram shows who decided it was worth building in the first place. On this account, that's me — not because of a title, but because I'm the one who shows up to the second call with a working demo and a real number instead of an opinion. The tagging/HITL layer underneath this is no longer one client's tool — other teams now run their own tagging work on it, in place of the third-party labeling tools and spreadsheets they used before. What doesn't generalize as easily is the sourcing/scraping side — different products need different sourcing methods — and knowing that boundary is as much the job as building the platform was.