Skip to main content
Liorry Herisnor
Status: ShippedData and Analytics2026 — present

Lead Discovery and Enrichment

A discovery and enrichment pipeline that finds publicly published business contacts and enriches records for segmentation — with a mandatory human review step before anything enters a sending list.

At a glance

Two connected services. Discovery searches the public web for published business contact addresses and returns a reviewable result set. Enrichment runs entirely in the background to populate segmentation data. Neither is connected to the sending path — an operator has to explicitly review and create a list, and enrichment is architecturally forbidden from blocking a send.

  • 300K+ contacts / month

    Discovery volume

    Unique, publicly published contact addresses discovered and screened in a single 30-day period — each one checked for a working mail domain, classified, and held for the mandatory human review step.

    Source: Measured from the platform's discovery records, July 2026, deduplicated by address. Raw discovery records for the same period exceed 1.1 million.

  • No auto-import

    Safety model

    Discovery results cannot become a sending list without an operator reviewing filtered results and explicitly creating one.

    Source: Documented operator flow; the service has no path into the sending pipeline.

  • Self-hosted search

    Cost model

    Discovery moved from a paid search API to self-hosted metasearch with an open-source fallback, keeping a legacy paid backend selectable.

    Source: Search backend matrix in the service documentation.

Status
Status: Shipped
Category
Data and Analytics
Dates
2026 — present
My role
Product owner, pipeline architect and delivery lead
Target users
Lead operations staff building prospect lists, Campaign operators segmenting an audience, Compliance reviewers auditing list provenance
Technology
Go, PostgreSQL, Redis, Self-hosted metasearch, MX validation, Queue-based orchestration

What I owned

  • Product vision
  • Discovery
  • Requirements
  • Architecture decisions
  • Delivery coordination
  • Testing
  • Production operations

Areas this project actually involved. Anything not listed, I did not own.

Who and what supported delivery

Operations team members
Lead operations staff who run the review gate — the step the whole design exists to protect.
Business users
Compliance reviewers who need list provenance to be reconstructable after the fact.
AI coding assistants
Implementation assistance under the documented engineering workflow.

The review gate is human by design. Automating it was the obvious optimization and the one I refused.

Related artifacts

The problem

Building a prospect list was manual, slow, and produced data nobody could vouch for.

Discovery was a person with a browser. Searching, opening pages, copying addresses into a spreadsheet, checking whether the domain even accepts mail. Hours of work producing a list whose provenance existed only in that person's memory.

Quality problems surfaced after sending, which is the worst possible time. Addresses that do not resolve, role addresses that should have been filtered, records already on the suppression list. Each one costs sender reputation, and you find out afterwards.

Enrichment had a dangerous shape. The natural implementation — look up enrichment data while generating messages — makes an external data source a dependency of sending. When that source slows down, campaigns stop. The failure is remote, external, and takes the revenue path with it.

Paid search APIs made the cost scale with curiosity. Every exploratory query cost money, which meant people ran fewer of them and worked from worse data.

Nothing was reproducible. Re-running the same discovery produced a different result set, because query generation was ad hoc. There was no way to audit where a list came from.

Previous workflow

Before: manual prospect research

The end state was a spreadsheet whose provenance lived in one person's head.

  1. Human action

    Researcher decides what to search for

    • Problem: Query choice varied by person and by day
    • Problem: No record of what had already been searched
  2. Human action

    Manual web searching

    • Problem: Hours per list
    • Problem: Coverage depended on patience
    • Problem: Paid search API cost per query discouraged exploration
  3. Human action

    Open pages and copy addresses

    • Problem: Transcription errors
    • Problem: Duplicate work across researchers
    • Problem: No deduplication until the spreadsheet stage
  4. Human action

    Spot-check validity

    • Problem: Rarely done thoroughly
    • Problem: Role addresses and non-accepting domains slipped through
  5. Automated software

    Import to a sending list

    • Problem: Quality problems only surfaced after the campaign sent
    • Problem: No provenance record for compliance review
  6. Human action

    Enrichment looked up by hand when needed

    • Problem: Inconsistent coverage
    • Problem: Segmentation limited to what someone had time to fill in
  • Human action
  • Automated software

Goals and non-goals

Goals

  1. Discovery runs as a repeatable job with recorded inputs and generated queries.
  2. Results are filtered and reviewed before anything can become a sending list.
  3. Retrying a job reproduces the same query set rather than a new random one.
  4. Enrichment never sits in the sending path, under any configuration.
  5. Discovery cost is not proportional to how many questions someone asks.
  6. Repeated work is cached rather than repaid.

Non-goals

  • No automatic import. Explicitly out of scope. A human reviews and creates the list.
  • Not a contact database product. It supports the platform's own list building.
  • No scraping behind authentication. Publicly published business contact information only.
  • No enrichment guarantee. Records may have no enrichment data, and every consumer must handle that.

My role

  • Discovery — established that the expensive part was not searching but reviewing, which is why the review screen got the design attention rather than the crawler.
  • Requirements — wrote the enrichment service specification, including its hard constraints: background-only, additive schema, no changes to the sending core, and tolerance of missing data everywhere.
  • Safety design — made the no-auto-import rule a structural property rather than a policy: the service has no path into the sending pipeline at all.
  • Pipeline architecture — the database-driven queue, its claim semantics, idempotent expansion and stale-lock reclamation.
  • Cost architecture — moved discovery from a paid search API to self-hosted metasearch with an open-source fallback, keeping the paid backend selectable for comparison.
  • Reproducibility — required query generation to be seeded by job identity, so a retry produces the same queries.
  • Test planning and UAT — with the lead operations staff who use the review screen daily.
  • Operational tuning — bandwidth budgeting across competing crawl consumers.

Product solution

Discovery. An operator creates a job from keywords, optional manual search queries, seed domains, or direct URLs. For keywords, the service expands contact-focused query templates, optionally scoping some of the budget to specific public platforms. It runs those searches, fetches page content, extracts candidate addresses, and validates and classifies each one — does the domain accept mail, is this a role address, is it already suppressed.

The operator then reviews. The results screen filters by mail-exchange validity, confidence, role type and suppression status. From there they either create a list or export the data. There is no third option, and no automatic path.

Enrichment. A separate background service that owns three tables and nothing else. Jobs expand into a queue with set-based SQL and conflict handling that makes re-running a job a no-op rather than a duplicate. Workers claim rows with skip-locked semantics, heartbeat while working, and a reaper reclaims locks left behind by a worker that died. The service populates segmentation data and is architecturally incapable of delaying a send.

Workflow

Discovery — job to reviewed list

  1. Human action

    Operator creates a job

    Keywords, manual queries, seed domains or direct URLs.

  2. Automated software

    Query expansion

    Contact-focused templates expanded and shuffled, seeded by job identity so a retry reproduces the same set.

  3. Data store

    Repeat-query cache checked

    Recently seen queries are served from cache with a bounded lifetime.

  4. External service

    Self-hosted metasearch

    Decision point: Falls back to an open-source harvester when metasearch returns nothing usable.

  5. Automated software

    Page fetch through a metered proxy pool

    Bandwidth drawn from a shared monthly budget split across competing consumers.

  6. Automated software

    Address extraction and classification

    Mail-exchange validation, role-address detection and suppression cross-check.

  7. Approval gate

    Operator review

    Decision point: Filter by validity, confidence, role type and suppression. Create a list, or export — there is no automatic import.

Enrichment — background only

  1. Automated software

    Job created with a scope

    A whole list, or an explicit set of records.

  2. Automated software

    Set-based expansion into the queue

    Conflict handling makes re-running the same job idempotent rather than duplicative.

  3. Automated software

    Worker claims a batch

    Skip-locked claim semantics so parallel workers never contend for the same rows.

  4. External service

    External enrichment sources

    Decision point: Failure marks the row for retry and never propagates toward sending.

  5. Data store

    Enrichment record written

    Provider, geography, risk signals and extensible attributes.

  6. Monitoring / feedback

    Heartbeat and stale-lock reaper

    Locks held by a dead worker are reclaimed on a fixed cycle.

  7. Automated software

    Available for segmentation

    Consumers must tolerate a missing record. Absence is normal, not an error.

Architecture

Discovery and enrichment — sanitized view

Two independent services. Neither has a path into the sending pipeline.

Operator surface

  • Job creation

    Human

    Keywords, manual queries, seed domains, direct URLs, platform scoping.

  • Results review screen

    Approval gate

    Filter by mail-exchange validity, confidence, role type and suppression. The only route from discovery to a list.

Discovery service

  • Query expander

    Automated

    Template-based generation seeded by job identity, so retries reproduce the query set.

  • Query cache

    Data store

    Bounded-lifetime cache so repeated research does not repay the same cost.

  • Self-hosted metasearch

    External service

    Primary backend, bound to the local interface only. Open-source harvester as fallback; the legacy paid backend remains selectable.

  • Fetch and extract

    Automated

    Page retrieval through a metered proxy pool, then address extraction, validation and classification.

Enrichment service

Owns exactly three tables and touches nothing in the sending core.

  • Job expander

    Automated

    Set-based SQL expansion with conflict handling for idempotency.

  • Queue table

    Data store

    Queued, processing, done, error and dead states with claim ownership and timestamps.

  • Batch worker

    Automated

    Skip-locked claiming, batched record fetches, explicit timestamp updates — no database triggers.

  • Heartbeat and reaper

    Monitoring

    Stale locks reclaimed on a fixed cycle so a dead worker cannot strand its batch.

  • Enrichment records

    Data store

    Provider, geography, risk signals and extensible attributes for segmentation.

Supporting services

Each replaced a paid dependency or split a concern out of the pipeline.

  • AdFetch

    Automated

    A self-hosted fetch and scrape service this pipeline calls for every page fetch, implementing the same request and response shapes as the paid API it replaced so callers did not change. A separately run service also used by the AdSpy pipeline. See the separate case study.

  • Layered verification

    Automated

    Syntax, then mail exchange, then an SMTP handshake, then catch-all detection, then disposable-provider and role-address detection. One concern per stage, so a rejection names the layer that produced it.

  • Domain trap scoring

    Automated

    A separate service scoring trap and parked domains, consumed by the enrichment stages rather than embedded in them.

Boundary

  • No automatic path to sending

    Approval gate

    Discovery results require explicit operator list creation. Enrichment is read-only from the sending path's perspective and may be entirely absent.

Diagram is sanitized for publication: it shows responsibilities and data flow, not hostnames, addresses or credentials.

Where AI is used — and where it is not

AI / LLM responsibilities

  • Suggesting keyword and niche themes for an operator to consider before a job is created.
  • Summarizing a result set so a reviewer knows what they are about to look at.
  • Implementation assistance and code review under the engineering workflow.
  • Drafting the service specification, reviewed and corrected by me before it became the contract.

Deterministic responsibilities

  • Query generation — template-based and seeded, so it is reproducible.
  • Address extraction from page content.
  • Mail-exchange validation and role-address classification.
  • Suppression cross-checking.
  • Queue claim semantics, idempotent expansion and stale-lock reclamation.
  • Bandwidth budgeting and crawl throttling.
  • Every count and confidence value shown on the review screen.

Decisions and tradeoffs

Mandatory human review before anything becomes a sending list

Options considered
Auto-import above a confidence threshold · Auto-import with post-hoc audit · Mandatory review, no automatic path
Chosen
Mandatory review, with no code path from discovery into sending
Reason
A confidence threshold is a number someone will raise under deadline pressure. Sending to a badly sourced list damages reputation for weeks and is not undoable.
Cost and risk accepted
Discovery cannot scale beyond the review capacity of the people doing it. That is a real throughput ceiling.
Result
Accepted deliberately. The review screen got the design investment instead, because review is the actual bottleneck.

Reimplement the paid scraping API rather than wrap or replace it

Options considered
Keep paying per request · Wrap a different vendor · Build a service exposing the same API shape
Chosen
Build AdFetch — a self-hosted service implementing the original API's request and response shapes
Reason
Callers already spoke that API. Keeping the contract identical meant the swap touched no calling code and could be reverted by changing one address.
Cost and risk accepted
A scraping service to operate — browser rendering, queuing, proxy egress and storage are all now ours to run.
Result
The paid dependency was removed without a migration. The same service now backs page fetching for discovery.

Self-hosted metasearch instead of a paid search API

Options considered
Keep the paid API · Self-host metasearch · Open-source harvester only
Chosen
Self-hosted metasearch primary, harvester fallback, paid backend still selectable
Reason
Per-query cost was suppressing exploration. People ran fewer searches and worked from worse data because each query had a price.
Cost and risk accepted
A service to run and keep working, and result quality that varies more than a commercial API's.
Result
Query cost stopped constraining research. The fallback chain covers the quality variance.

Enrichment background-only, never in the sending path

Options considered
Inline during message generation · On demand with a timeout · Background-only with tolerated absence
Chosen
Background-only, with every consumer required to handle a missing record
Reason
Inline enrichment makes an external data source a dependency of revenue. A remote slowdown becomes a sending outage.
Cost and risk accepted
Segmentation may run on incomplete data, and null-handling is spread across every consumer.
Result
An external provider cannot take sending down. Written as a hard constraint in the specification, not a preference.

Seed query generation by job identity

Options considered
Fresh randomization each run · Fixed query order · Randomized but seeded by job identity
Chosen
Seeded by job identity
Reason
Randomization improves coverage; unrepeatable randomization makes a retry a different job and makes provenance unauditable.
Cost and risk accepted
A retried job cannot discover anything the original could not.
Result
Retries are genuine retries. Generated queries are stored on the job for audit.

A database-driven queue rather than a message broker

Options considered
Dedicated message broker · In-memory queue · Database table with skip-locked claiming
Chosen
Database table with skip-locked claiming and a stale-lock reaper
Reason
The workload is low-rate and batch-shaped. A broker adds a component to operate and a second place for state to disagree with the database.
Cost and risk accepted
Claim polling load on the database, and locking semantics that have to be got right rather than inherited.
Result
One fewer system to run. Job state and queue state cannot diverge because they are the same store.

Challenges

The first queue implementation could strand work. A worker that died while holding claimed rows left them marked as processing forever. Nothing errored — the job simply never completed, and its counters stopped moving. Fixed with heartbeats and a reaper that reclaims locks older than a threshold, but the lesson generalizes: any claim mechanism needs an answer for the claimant dying, and that answer has to be designed rather than assumed.

Idempotency was harder than expected. Re-running a job initially duplicated queue rows. The correct fix was conflict handling scoped to rows already queued or processing — which required being precise about what "already in flight" means, rather than deduplicating on identity alone.

Bandwidth is a shared, finite budget. Discovery competes with other crawling consumers for a metered monthly allowance. Raising concurrency to make discovery faster silently starves the others. This became an explicit split with a safe burn rate rather than a per-service tuning decision.

Result quality varies more without a commercial API. Self-hosted metasearch sometimes returns little for a query the paid backend handled well. The fallback chain covers most of it, and the honest position is that we traded some consistency for the ability to explore freely — which was the right trade for how the tool is actually used.

Review is still the bottleneck, by design. Discovery can produce candidates faster than people can review them. Every proposal to relax that has been a proposal to auto-import under a different name.

Testing and validation

Testing layers and their evidence
LayerWhat it coversEvidence
Acceptance criteriaReproducible query generation, idempotent expansion, no path from discovery to sending.Written before implementation; each maps to a test.
UnitQuery expansion determinism, address extraction, role classification, validation.Package test suites.
IntegrationJob creation through to a reviewable result set against fixture pages.Fixture-backed integration tests.
ConcurrencyParallel workers claiming from the same queue without overlap; stale-lock reclamation.Tests that kill a worker mid-claim and assert reclamation.
IdempotencyRe-running a job produces no duplicate queue rows.Regression test written when the duplication defect was found.
UATLead operations staff running real research on the review screen.Operator feedback drove the filter set.
MonitoringJob progress, queue depth, error and dead-row counts, bandwidth burn rate.Counters exported; bandwidth tracked against the monthly split.
Safety checkFeature flag defaults off, and there is no code path from discovery into the sending pipeline.Verified by inspection at review.

Results

Before and after comparison for each measured outcome
MeasureBeforeAfterHow it is measured
Discovery effortHours of manual searching and transcription per list.A job definition, then review of a filtered, validated result set.The manual work that remains is review, which is the part that requires judgment.
ProvenanceExisted only in the researcher's memory.Job inputs and generated queries stored on the job record.A list can be traced to the queries that produced it.
ReproducibilityRe-running produced a different result set.Query generation seeded by job identity; retries reproduce the query set.Verified by determinism tests.
Discovery costPer-query charges on a paid search API discouraged exploration.Self-hosted metasearch primary, with fallback and the paid backend still selectable.Cost no longer scales with how many questions someone asks.
Sending-path riskInline enrichment would have made an external source a sending dependency.Background-only, with absence tolerated by every consumer.Written as a hard constraint in the service specification.

No conversion or list-performance figures are published here. Those belong to specific campaigns and clients, and I do not have permission to share them.

Evidence

  • Discovery job creation

    Keywords, manual queries, seed domains and platform scoping options.

    Placeholder — evidence pending sanitization review before publication.
  • Results review screen

    Filtering by validity, confidence, role type and suppression before list creation.

    Placeholder — evidence pending sanitization review before publication.
  • Enrichment queue state

    Job progress with claimed, done, error and dead row counts.

    Placeholder — evidence pending sanitization review before publication.
  • Service specification excerpt

    The hard constraints section, including background-only and tolerated absence.

    Placeholder — evidence pending sanitization review before publication.

Lessons

What worked. Making the safety property structural. "No auto-import" as a policy is a sentence someone can decide to make an exception to. "There is no code path from discovery into sending" is not.

What failed. My first queue design had no answer for a worker dying mid-claim. It failed silently — the job simply never finished. Any claim mechanism needs its failure case designed, not assumed.

What changed. The review screen became the product. I started out thinking discovery was the hard part; the operators made it clear within a week that finding candidates was easy and deciding about them was the work.

What I would do differently. I would have made query generation seeded from the start. Retrofitting reproducibility meant reworking the expander and accepting that earlier jobs are not auditable.

What this demonstrates about AI-product leadership. This is a system where the obvious AI application — "use a model to find and qualify leads" — is worse than the deterministic implementation on accuracy, cost and speed. The genuinely valuable judgment was recognizing that, keeping the model on the assistive side, and spending the design effort on the human review step that actually gates quality.

Next milestone

  • Scheduled refresh of stale enrichment records. The job type is specified but not implemented; records currently age without a refresh cycle.
  • Segment-scoped enrichment jobs, so a job can target a segment rather than a whole list or an explicit record set.
  • Review-assist ranking. Ordering the review queue so the most likely decisions surface first — assisting the reviewer, explicitly not replacing the review gate.

Want the detail behind this?

I am happy to walk through the architecture, the decisions and what I would change.