Case study · RevOps tooling

Every vendor sells you coverage. I built the thing that measures what actually works.

Phone enrichment vendors compete on one number: coverage. None of them publish how much of that coverage survives validation. So I built an open-source scorecard that runs a contact list through several providers, validates every number returned, and prices the difference — per market, per vendor, per waterfall order.

2,000
Contacts tested
4
Providers scored
18.2%
Widest claim gap
33.1%
Cost swing on order alone
01 · What this proves

Four things, in one repository

This is not a dashboard screenshot. It is a working pipeline — data in, validation, scoring, report out — reproducible with one command on any laptop.

The reframe

Coverage × quality

Effective coverage is the only number that maps to conversations booked. I define it, compute it, and show where it inverts the vendor ranking.

Real validation

libphonenumber

Google's rule set, per country: length, prefix, region and mobile-vs-landline. Objective, not anecdotal.

Systems thinking

Overlap & sequencing

Which vendors duplicate each other, and how much the waterfall order alone moves the invoice.

Honesty

Stated limits

Validation proves plausible, not correct. The report says so, and shows how to close that gap with dialer data.

02 · The reframe

What they sell you vs what you get

The dashed outline is what each vendor returned. The solid bar is what passed validation: a real mobile, right country, correct length. The distance between them is what you pay for and cannot use.

BetaReach
28.5% invalid
45.9% valid
claims 64.1%
€0.106
per valid number
AlphaData
11.1% invalid
40.0% valid
claims 45.0%
€0.135
per valid number
DeltaPrime
3.3% invalid
35.1% valid
claims 36.3%
€0.290
per valid number
GammaDACH
10.1% invalid
32.4% valid
claims 36.0%
€0.211
per valid number
BetaReach has the widest gap: a number for 64.1% of contacts, but only 45.9% are valid. On a coverage-only comparison it looks like the strongest vendor in the stack. On cost per valid number, BetaReach wins at €0.106; the cleanest data comes from DeltaPrime at 3.3% invalid.
03 · By market

Where the stack quietly breaks

Effective coverage per country. Colour shows the bad-data rate behind it: clean shaky poor. Vendors do not degrade evenly — a stack that works at home can collapse the week you open a new market.

ProviderATCHDEESFRGB
AlphaData15.3%25.6%18.6%40.7%57.7%39.4%
BetaReach38.0%30.8%40.3%40.0%56.3%41.9%
DeltaPrime32.7%27.3%35.3%27.6%38.0%42.4%
GammaDACH56.7%48.8%58.4%19.7%19.8%19.7%
04 · Overlap

Are you paying twice?

When two vendors return the same number for the same person, one is redundant. Redundancy here is the share of vendor B's hits that A already had and agreed on. This is usually the fastest saving in an enrichment stack, and almost nobody measures it.

PairBothOnly AOnly BAgreementRedundancy
AlphaData + BetaReach62327665955.5%27.0% · keep both
BetaReach + DeltaPrime48180124562.0%41.0% · review
BetaReach + GammaDACH43584728653.8%32.5% · review
AlphaData + DeltaPrime34655338080.9%38.6% · review
AlphaData + GammaDACH27762244465.7%25.2% · keep both
GammaDACH + DeltaPrime27145045582.7%30.9% · review
05 · Sequencing

The order changes the bill

A waterfall stops at the first vendor that returns anything — including junk. So the order you configure changes both your valid coverage and your invoice. Same vendors, same contacts, different sequence.

OrderRaw coverageValidTotal costCost / valid
BetaReach → AlphaData → GammaDACH87.8%66.6%€167.66€0.126
BetaReach → GammaDACH → AlphaData87.8%66.8%€173.82€0.130
BetaReach → AlphaData → DeltaPrime84.7%64.0%€167.84€0.131
AlphaData → BetaReach → GammaDACH87.8%71.4%€196.05€0.137
AlphaData → BetaReach → DeltaPrime84.7%68.8%€196.23€0.143
BetaReach → DeltaPrime → AlphaData84.7%64.8%€185.44€0.143
BetaReach → GammaDACH → DeltaPrime85.5%65.1%€190.74€0.146
AlphaData → GammaDACH → BetaReach87.8%74.9%€223.59€0.149
BetaReach → DeltaPrime → GammaDACH85.5%65.6%€200.10€0.153
AlphaData → DeltaPrime → BetaReach84.7%72.0%€246.33€0.171
AlphaData → GammaDACH → DeltaPrime78.3%71.2%€254.96€0.179
AlphaData → DeltaPrime → GammaDACH78.3%71.5%€269.00€0.188
Best and worst sequence use the same vendors, yet cost per valid number differs by 33.1%. Reordering is free. Renegotiating is not.
The cheapest order is not the widest. AlphaData → GammaDACH → BetaReach reaches 74.9% valid for €223.59. Whether that extra coverage is worth the extra spend is a budget call, not a data call. The scorecard's job is to price the choice, not to make it.
06 · How it works

A pipeline, each step with a job

Five stages, each isolated so any one can be swapped without touching the others. That separation is the whole point: replacing simulated vendors with live APIs changes exactly one file.

generate_data
Input
Builds a contact list across six markets. Every phone is verified valid at creation, so ground truth is trustworthy.
providers
Fetch
Four vendors with different coverage, error and pricing profiles. This is the file you swap for real API calls.
validate
Judge
libphonenumber checks parse, validity, region and line type, and returns one verdict per number.
scorecard
Measure
Coverage, bad-data rate, effective coverage, cost per valid, overlap matrix and waterfall simulations.
report
Deliver
Renders this page: one HTML file, no dependencies, sendable to someone who will never run Python.
07 · Under the hood

What "valid" actually means, in code

The entire argument rests on one function. If validation is sloppy, every number in this report is noise. So the rule lives in one place, written in business terms, where anyone can challenge it.

# validate.py — three checks, in order
parsed = phonenumbers.parse(phone, expected_country)

# 1. A real, dialable number in that plan?
is_valid = phonenumbers.is_valid_number(parsed)

# 2. Does it belong to the market we expected?
region = phonenumbers.region_code_for_number(parsed)
country_matches = region == expected_country

# 3. A mobile, not a switchboard?
is_mobile = phonenumbers.number_type(parsed) in (
    PhoneNumberType.MOBILE,
    PhoneNumberType.FIXED_LINE_OR_MOBILE,
)

# The business rule, in one obvious place
def is_usable(r):
    return r.is_valid and r.country_matches and r.is_mobile
Why check 3 matters

Switchboards pass naive checks

A vendor returning the company reception number for every contact scores 100% on a length-and-format check and burns an SDR's entire week. Line type is the check that catches it.

Why the rule is extracted

"Usable" is a business decision

If your team happily dials landlines, delete one condition and rerun. Writing the definition down settles the argument instead of burying it inside a query.

A bug I found in my own build. The first synthetic generator produced "true" numbers that libphonenumber rejected: unassigned prefixes, and UK where the ISO code is GB. That inflated every vendor's bad-data rate by several points. Ground truth now generates and re-verifies until each number is genuinely valid. A measurement tool that cannot measure itself is worth nothing.
08 · Going live

Where this plugs in to your stack

Everything above runs on synthetic data, so it is safe to publish and free to run. Four changes take it to production, in roughly this order.

Step 1 · CRM
Export a real contact list. Any CSV with contact_id, full_name, company, country, segment. Country must be an ISO code — GB, not UK.
Step 2 · Vendors
Replace the mocks with API calls. Only providers.py changes. Every provider returns the same shape: ProviderResult(contact_id, phone, provider). Nothing downstream cares where the number came from, which is what makes an A/B of a new vendor an afternoon's work.
Step 3 · Schedule
Run it monthly. A cron job, or an n8n / Make schedule, writing each run to Postgres. Vendor quality drifts; one audit is a snapshot, a monthly one is a trend you can take into a renewal.
Step 4 · Close the loop
Feed dialer outcomes back. Push connect and wrong-number results from Aircall or Ringover into the scorecard. That upgrades "valid" into "actually reached this person" — the only true accuracy measure, and the one no offline check can give you.
09 · Surface area

One project, three job descriptions

Revenue OpsVendor & spend decisions

  • Vendor evaluation on evidence rather than reputation
  • Cost per valid record as a renewal argument
  • Redundancy analysis across an enrichment stack
  • Waterfall sequencing as a lever on spend

Data qualityMeasurement design

  • Separating coverage, validity and accuracy as distinct metrics
  • Ground-truth design and self-verification
  • Segmented analysis that exposes market-level failure
  • Explicit, editable business rules over buried logic

EngineeringBuild & ship

  • Python pipeline with a swappable provider interface
  • libphonenumber integration for objective validation
  • Reproducible synthetic fixtures, no credentials required
  • Zero-dependency HTML deliverable for non-technical readers
10 · The read

What a hiring team should take from this

01

I question the metric before I chart it

Coverage was never the right number. The work was defining effective coverage, not visualising what everyone already tracked.

02

I ship the pipeline, not just the finding

One command reproduces every number here. A conclusion nobody can rerun is an opinion with a chart attached.

03

I make the rules arguable

The definition of "usable" is six lines in one file. Anyone can disagree, change it and rerun — which is how ops decisions should work.

04

I state what I cannot prove

Validation is a floor, not a guarantee. Saying so out loud is what makes the rest of the report trustworthy.