DEV Community

Axel Freeman
Axel Freeman

Posted on Originally published at axelfreeman.com

The reject file: every row my B2B list threw away, and the check that killed it

The reject file: every row my B2B list threw away, and the check that killed it

A list is usually sold by what is in it. This is a post about what came out of mine, and why — because the count that
survives the checks is the only number you can audit, while "100% verified" is a number you can only believe.

I keep one working file for outbound segments. As of today it holds 7 728 company domains collected from
49 public sources, of which 1 381 publish an address of their own (18%), and 1 350 of the 1 358 domains behind
those addresses answer an MX lookup right now. Everything below is counted from that file, not estimated.

The rule that decides everything: where the domain comes from

Before any technical check, one sourcing rule does most of the work. A domain enters the file only if it sits in a field
the owner filled in about itself:

  • a company directory's website field,
  • a package's projectUrl (NuGet) or homepage_uri (RubyGems), homepage (npm, crates.io),
  • a repository's homepage field,
  • a launch post's canonical URL.

A domain dug out of the body of a job ad or a sentence in a post is inferred, and inference is where invented
addresses come from. Job feeds are the clearest example: the vacancy mentions the employer, a customer, an ATS and a
cloud host in the same paragraph, and half the candidate domains have to be thrown away by hand. Hiring threads yield
addresses about 11% of the time. Package registries, the same idea one ecosystem over, yield 15%. Agency and
SaaS catalogues yield 60%.

The six checks, in this order

  1. Domain from a structured field. As above. No field, no row.
  2. Fold to the root, then de-duplicate. blog., app., shop. and demo. subdomains collapse into one company, otherwise a single vendor arrives as four rows. After folding: 7 728 rows, 7 728 unique domains, zero rows without a domain.
  3. Fetch the host live. Not answering 200 on HTTPS and then HTTP means it is dropped — parked domains and dead projects included. Cheapest filter, biggest pile of removed weight.
  4. Does the site behave like a product company? At least two of: pricing or plans, sign-up or free trial, a "book a demo" or "contact sales" path, integrations or API documentation. This is what keeps blogs, documentation sites and academic pages out of a B2B segment.
  5. Does the address belong to the domain it sits on? Addresses are kept only when the mail domain matches the site they were found on, or a subdomain of it. Agency catalogue pages publish third-party addresses and placeholders like hello@world.com often enough that this check is not optional — a row with someone else's address is a complaint scheduled for later.
  6. Does the domain answer for its mail? An MX lookup. Of the 1 358 domains behind published addresses, 1 350 answer and 8 do not; those rows are marked and kept out of batches rather than counted as contacts.

What each family yields, measured instead of claimed

Family of sources Rows Published an address Yield
Agency and SaaS directories 876 522 60%
Company directories (YC dump) 548 265 48%
Package registries 328 50 15%
Launch feeds and product posts 745 88 12%
Hiring threads and job feeds 4 012 432 11%
Repository homepage fields 553 59 11%

Two things only show up because the table is published.

First, the ecosystems are not comparable. Directories built for selling arrive with the contact already on the page;
hiring feeds are written to attract candidates, not to be harvested.

Second, a structured field is a guarantee about the domain, not about the contact. Repository homepages come from a
field its owner filled in and still yield 11% — a package registry's projectUrl yields 15%, an agency directory 60%.
If you buy rows by the thousand, the family they came from is a better predictor of what reaches a human than the word
"verified" on the invoice.

The reject file

What fails is not deleted. It is written out with a reason column, so the count can be audited instead of trusted, and
it comes in three parts:

  • Dropped at sourcing — the domain was not in a structured field, or the host never answered.
  • Dropped at the product test — the site behaves like a blog, a docs site or another directory rather than a vendor.
  • Dropped at the mail test — no MX, or an address belonging to a different domain than the page it was found on.

The reject file is handed over with the segment. A batch is built from the working file, and a row that is not in the
file is not in the batch — no purchased lists are mixed in, because a purchased row has no source column to audit.

What I do with this

I build outbound segments for SaaS teams, agencies and local B2B companies, and the reject file is part of the
deliverable. The checks above, the yield table, and the three reject lists are published on
the verification page,
together with the current numbers of the working file. If you want the same six checks run against a list you already
own, that is the fastest way to find out whether it is worth sending at all — and the
scopes and prices
are published the same way: $900 for the fourteen-day pilot, $1 900/month for the ongoing engine, $2 900 for the full
build.

One line to take away, whether or not you hire anyone: measure a list by what it throws out. The proportion that
survives six checks is a fact about the file. "Verified" is a fact about the seller's confidence.

Top comments (0)