Case study · build log

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.

3M+Total Datapoints Processed
$4M+Year-One Deals Closed
80%+Margin at Scale, Not Pilot
<0.1%Escalated — Real Contradictions Only
Disclosure — every place name, ID, and figure below is synthetic — reconstructed to match real production scale and shape, not actual records. The decisions, tradeoffs, and mechanisms are real.
Role: sole builder, ch. 01–03 Infra partners: small eng team, ch. 03 onward Status: live, ch. 06–07 active now
01

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

01
Walked every block, shot every storefront myself
02
Timed the whole effort, block by block
03
Turned time into a headcount/budget baseline
04
Client signed on a proven ceiling, not a promise
Decision — do it manually first, on purpose

Shooting ~400 real businesses myself wasn't the plan — it was a deliberately unscalable benchmark. Two different things came out of it:

An "ideal" for the client. Real photos of real storefronts set the actual quality bar — not a mockup of what AI-generated coverage might look like.
A doomsday-scenario cost model, for us. Timing myself across ~400 locations gives a real per-location time cost — the basis for "if the AI pipeline fails outright, here's the exact headcount, timeline, and budget to do this with people instead." That number is what makes the AI bet defensible internally, not just externally.
Businesses shot solo, several blocks~400
People running the whole benchmark1
Engineers hired before the deal was real0
First call to global production1 yr

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.

02

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.

Fictional location "Marlow & Vine" — where the two layers actually disagree
Scraper found
A usable street-level image, correct address match, but a delivery van obscuring the storefront entrance.
Generator produced
A clean derivative with the van removed — but signage softened past legibility, a known failure mode at this stage.
Decision — how to build the human check

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.

Rejected — 3rd-party labeling tools (Label Studio, Turk-style platforms). Real learning curve to configure for this specific check; no control over the review workforce; gets expensive fast once volume actually scales; and locks the roadmap to whatever features the vendor ships next.
Rejected — building custom review tooling immediately. Overkill for pilot-scale revenue that hadn't been proven yet. Real time and engineering cost, which delays the actual delivery the client is waiting on.
Chosen — a programmed, shared spreadsheet. Lean enough to ship immediately: good documentation, manual work-tracking, and actually running the operation by hand. This is what surfaces the real, tested requirements for the eventual custom build — not a guess at them.
Locations processedmillions
Resolved as edge caseslow single %
Client quality re-request ratesub-0.01%
AI layers chained per location2

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.

03

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.

Decision — build the platform myself, not adopt one

By this point the requirements were real, not guessed — the spreadsheet phase had already surfaced them. That changed the calculus on build vs. buy.

Rejected — Label Studio / Turk-style platforms, again. These are optimized for producing training data — a batch artifact you generate once, not a live operational system that has to ship correct output to a paying client continuously.
Chosen — build the review platform in-house. The AI pipeline sets the baseline, but AI isn't perfect — it's only trustworthy in production when paired with human review, and that review has to be a live operational function, not a dataset-labeling task.

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.

tagging_reviewlocation #48,213
location
Marlow & Vine (fictional)
review_reason
every generated image queues for review
tagger_action
accept regenerate re-source_input manual_correction

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.

01
Apply
Candidate takes an auto-graded onboarding test
02
Score
Test grades itself against a rubric, no human review
03
Route
Top scorers routed to HR for contracting, automatically
04
Onboard
Guidelines shared, first queue item assigned same day
ops.internal / my-progress illustrative UI recreation
Tagger — illustrative viewRole: QA Tagger
214
reviewed this week
$189.40
earned, unpaid
96%
last grading pass rate
Daily pace vs. your own average112%
Days until next grading cycle3 of 14
Every action you take is logged the moment you take it — this page reads the same event log that calculates your pay. Nothing here is a summary written later; it's live.

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.

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.

ops.internal / grading-queue illustrative UI recreation
Overdue Grade soon Building up Fresh Up to date
Tagger90-day daily paceActions since paidEst. work daysTheir avg/dayStatus
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.

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.

ops.internal / grade-cycle / tagger-a illustrative UI recreation
Grade
2Payout
3Confirm
4Receipt
Sampled action
location_review · Marlow & Vine (fictional)
Tagger action taken
re-source_input
Time on task
41s
Grader verdict — sample 7 of 20
Pass
Fail
Note (optional)
Correct call — signage was genuinely illegible after generation, re-sourcing was the right action.
6 passed · 1 failed · 13 remaining

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.

The same admin team also watches which pipeline stage is actually slow — so engineering time goes to the real bottleneck, not a guess.

ops.internal / efficiency illustrative UI recreation
78%
First-pass rate
14%
Regenerated
1.3
Avg trips per location
99.4%
Delivered

Avg. time per stage — bars colored by severity, not decoration

scrape → generate
1.8h
generate → review
1.1d
review → regenerate
2.4d
review → deliver
4.2h

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.

ops.internal / processing illustrative UI recreation
job_8f21c4 · image_batch
Batch 214 — regional refresh
started 09:41 · 12,480 places dispatched
Initial estimate$412
Projected (≥10% sampled)$438
Actual so far$401
running
Cost breakdown — by function, by provider
FunctionInputOutputCachedCallsCost
image_scrape_select1.2M84K310K12,480$96.10
image_generate2.4M12,480$268.40
legibility_score640K22K180K12,480$36.50
ProviderInputOutputCallsCost
grounded search model1.2M84K12,480$96.10
image-generation model2.4M22K24,960$304.90
job_7a10b9 · reviews_batch
Batch 213 — ratings refresh
completed 08:55 · 9,102 places · 0 failed
Initial estimate$295
Projected (≥10% sampled)$301
Final, from real usage$298
completed
job_3c88e2 · image_batch
Batch 212 — priority re-source
auto-cancelled 07:12 · cost spike detected, 3.1× baseline
Initial estimate$180
Actually spent, to cancel$68
Projected, if left running~$558
Avoided by cancelling~$490
auto-cancelled

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.

Workers onboarded & trained100+
Admins running the whole operation~5
Payout visibility per workerreal-time
Batches graded, tied to payevery one

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.

04

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.

Detail worth knowing — classify terrain once, up front, then let density set the recursion

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:

PriorityTag signal foundClassificationWhat happens next
1stCommercial / retail / food tagsDenseAdaptive recursive subdivision
2ndWater, parks, forestSkipNever searched, not even once
3rdResidential / industrial tagsSparseExactly one search, no recursion
fallbackNo map-data signal at allDenseTreated 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.

Decision — when there's no data, assume dense, not empty

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.

Rejected — default unknown areas to skip. Cheapest option, and wrong in exactly the cases that cost the most: map-data gaps cluster in old dense downtown blocks with poor digitization history, not in parks — parks are trivially and completely mapped. Skipping on "no data" would silently drop real commercial blocks, the highest-value terrain in the whole scan.
Rejected — scan everything uniformly, ignore classification. Removes the failure mode but reintroduces the exact cost problem the two-tier grid exists to solve — fine-grained recursive search over parks and water that will never return a business.
Chosen — default a data gap to dense. Trades a handful of wasted searches over a genuinely empty lot against the much worse failure of silently skipping a real commercial block because the map provider never got around to tagging it. The cheaper failure mode is the one to eat.
Decision — public records over waiting for a listing to appear

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:

v1 — one signal. A licensing flag alone: re-inspections, ownership transfers, and rebrands all looked identical to a genuine new venue.
v2 — cross-signal agreement. Require 2 independent gov. systems to agree — independent bureaucracies rarely produce the same false signal by accident.
v3 — add the earliest signal, then filter it. Planning filings give 90–270 days of lead time but are noisy — a 7-stage pre-filter cuts candidate volume ~66% before any AI verification cost is spent.
Independent gov. data sources cross-referenced3
Candidate volume cut before paying for AI verification~66%
Lead time on the earliest signal, before a venue is searchable anywhere90–270 days
Iterations shipped, tested, and corrected in production3

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.

05

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.

L1
Grounded search, gathers raw data
L1.5
Mismatch check — catches the wrong business entirely
L2
Independent second-source gather, cross-checks L1
L3
Writes the final review — no web access, only what L1/L2 already found
Decision — a mismatch check most pipelines skip

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.

Decision — ban the model's own brand knowledge

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.

Failure modeWhat breaks without a fixThe fix
Sampled reviews read as a literal countA source showing 3 sample reviews gets reported as "3 reviews total" — badly undercounts real popularityBucket 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 sourceModel defaults to returning the primary source's number unchanged — no evidence the cross-check ranForce the synthesized rating 0.1–0.4 away from the raw score, closing off the copy-through shortcut
Review reads like a templateThousands of reviews start the same way, lean on the same stock phrases10 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
Decision — a contradiction between sources is the signal, not "low confidence"

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)RatingStatus
Marlow & Vine (fictional)4.2valid_review
Decision — the same signal, checked in a different order for a reason

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.

Where the human-review check can sit in a status tree
A small status space
Check the contradiction flag first, as a hard override before anything else runs. When there are only a few possible outcomes, any verified contradiction is disqualifying no matter what else is true — so checking it first is cheap and correct, not paranoid.
The mobility client's richer status space
Checks the same flag last — only as a catch-all after every deterministic check (closure, address/identity match, business-type edge cases) has already run and failed to resolve the case. A contradiction flag shouldn't preempt a resolution the deterministic logic can already make confidently on its own.

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.

06

Scaling as a Data Partner, From One Pilot

active, scaling now

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

Decision — reuse the discipline, not the code

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)PriceConfidence
Grilled Branzino, seasonal greens$28High
House-made tagliatelle, brown butter$22High
Market vegetable plate (ask server)Flagged — no price found
WhereWhat it actually does
Food photography promptsDish 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 removalRuns 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.
Recurring pattern — prompt instruction, plus a deterministic backstop

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.

Capability sold into the same account, no new sales cycle3rd+
Data types in one delivery: text + image2
Extraction task, not a copy of Ch. 02new
Platform running all eight chapters1

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

07

Sharing the Tech Stack Across the Entire Org

active now

This 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?

Decision — what actually generalizes, and what doesn't

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.

Not this: "just plug your data into our pipeline." The sourcing method is domain-specific — built and tuned for how this specific data behaves on the web. Forcing another team's sourcing problem through it would mean re-solving problems that are already solved correctly elsewhere.
This: the operations layer generalizes, the sourcing layer doesn't. The part that's genuinely domain-agnostic is everything downstream of "a human needs to look at this": the recruiting test, onboarding, the tagging UI itself, grading, payout, performance tracking. That loop doesn't care whether what's being tagged is a location photo, a review, or something from an entirely different data domain — it only needs a queue and a verdict.

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.

Teams now running tagging on this platformmultiple
Third-party tools and spreadsheets it replacedTurk, Label Studio, sheets
Sourcing method shared across those teams0 — by design
Ops layer shared across those teams1

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.

08

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.

Decision — who actually calls the deal

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.

Not evaluated on promise. A lead being "interesting" or "big" isn't the bar — plenty of asks sound good in an email and fall apart on contact with the actual mechanism.
Evaluated on a real demo and a real number. By the second call, I've already built a working proof and modeled the unit economics against actual cloud billing — not a projection. The go/no-go is mine, made on evidence, not enthusiasm.

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:

BasisCost / recordAt 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.