SaaS Media

How To Qualify Companies With AI Before You Buy Contact Data

Paying for contact data before you know which companies deserve it? Read each company’s own pages with AI first, then enrich only what fits.

Author:
Vlad Shvets
Contributors
Vlad Shvets
Date:
October 31, 2026

The steps below decide which companies on your list fit before you pay for anyone’s contact data. Contact data is the one line of an outbound budget spent before anyone decides who deserves a call: a list goes to an enrichment tool, the tool returns people, and which companies were worth a person comes up afterwards, usually from sales.

Empact Partners, as a consultancy selling go-to-market work to B2B software companies, finds its own next partners in this order: read each company’s pages, judge them against criteria written down first, buy contacts only for the companies that pass, and check each address again in the week it is sent.

We ran it on 25 companies on 31 October 2026, then ran the opposite order on the same list. You finish with your list split into pass and fail, the line behind every call, verified addresses for the companies that passed, and the cost of each step. On our list the two orders cost about the same. What differed was who the addresses belonged to.

Accounts. Firecrawl, whose free plan reads 1,000 pages a month (we used 52); an Anthropic API key with a few dollars on it; and an Anymailfinder plan, the smallest of which is $29 a month for 400 credits (we used 35).
Your machine. A terminal with Python 3. On a Mac, typing python3 the first time offers to install it. The three scripts use only the standard library.
From someone else. The three API keys, if your company’s tool accounts sit with ops or engineering.
Time. About an hour for 25 companies. The scripts ran for about ten minutes; the rest is reading what they found.

This starts from a list you already have and stops at addresses ready to send. Finding the companies and writing the email are other jobs. If you do not write code, give the three files below to Claude Code and ask it to run them in order, with the same three keys.

Qualify Every Company From Its Own Pages Before You Buy Anything

Step 1: Write your criteria and your list

In an empty folder, save the text below as criteria.md. Change the first line to what you sell and the three conditions to what your buyers have in common.

Keep each condition to something a company’s own pages can show: a pricing page, a signup, who the product is for. Revenue and headcount rarely sit on a homepage, and a condition no page can answer turns every company into a question.

# Fit criteria

We sell go-to-market work to software companies that sell to other businesses.
A company passes only when its own pages show all three conditions.

## Conditions
1. It is an operating company with its own product, and it takes new customers:
   a signup, a free trial, a demo request, a pricing page or a contact-sales route.
2. It sells software, priced by subscription or by usage. Hardware, physical goods,
   a one-off license, or hours of service do not count.
3. Its buyers are businesses. A product sold mainly to consumers does not count,
   even when a business plan sits beside it.

## Disqualifier
- It sells services by the hour or by the project: an agency, a consultancy, a studio.

## When the page does not say
A condition the page neither shows nor contradicts is not said, which is never a fail.

The last line carries the rule everything else depends on. A condition the pages neither show nor contradict is not said, and not said never fails a company. It comes from the written framework our own sourcing qualifies against, where only positive evidence may reject a company and every claim carries the page it came from.

Writing those lines is where our RevOps work with a partner starts, in the audit that comes before any agent is built. A criterion the sales team never agreed to is the first thing it disputes when the leads arrive.

Then save your list as companies.csv, one column headed domain, one company per row. Ours had 25, picked by hand for this run and mixed on purpose: software companies that sell to businesses, consumer brands, services companies, hardware, a nonprofit, and a few we expected to be hard to call. None is a partner of ours or on our own outreach list.

Step 2: Save qualify.py and give it your keys

Save the script below as qualify.py in the same folder. It is the exact file that produced every decision on this page.

# qualify.py: scrape each company's homepage with Firecrawl, have Claude quote the
# evidence for each condition in criteria.md, and let the code below make the call.
import csv, glob, json, os, re, sys, urllib.request

MODEL = "claude-sonnet-5-5"
PRICE_IN, PRICE_OUT = 2.00, 10.00     # dollars per million tokens for that model
MAX_PAGES, MAX_DOLLARS = 60, 2.00     # the run stops before it passes either
CRITERIA = open("criteria.md").read()
PROMPT = """Check one company against the fit criteria below, using only the pages given.
Reply with one JSON object and nothing else:
{"conditions": [{"status": "shown" or "contradicted" or "not_said",
                 "quote": "the sentence that shows it, copied exactly from the page, or empty",
                 "source": "the URL of that page"}, one object per condition, in order],
 "disqualifier": {"status": "shown" or "not_said", "quote": "...", "source": "..."},
 "next_page": "if any condition is not_said, the one link from the list most likely to settle it, else empty"}

""" + CRITERIA

def post(url, headers, body):
    req = urllib.request.Request(url, json.dumps(body).encode(), {"content-type": "application/json", **headers})
    with urllib.request.urlopen(req, timeout=180) as r:
        return json.load(r)

def scrape(domain, url, n):
    path = f"pages/{domain}-{n}.json"
    if os.path.exists(path):                      # saved pages are never paid for twice
        return json.load(open(path)), 0
    d = post("https://api.firecrawl.dev/v2/scrape", {"authorization": "Bearer " + os.environ["FIRECRAWL_API_KEY"]},
             {"url": url, "formats": ["markdown", "links"], "onlyMainContent": False, "maxAge": 0})["data"]
    page = {"url": url, "text": d.get("markdown", "")[:15000], "links": sorted(set(d.get("links", [])))[:80]}
    os.makedirs("pages", exist_ok=True)
    json.dump(page, open(path, "w"))
    return page, 1

def flat(s):
    return " ".join(re.sub(r"[*_#`>\[\]]", " ", s).lower().split())

def judge(pages):
    shown = "\n\n".join(f"PAGE {p['url']}\n{p['text']}\n\nLINKS\n" + "\n".join(p["links"]) for p in pages)
    try:
        r = post("https://api.anthropic.com/v1/messages",
                 {"x-api-key": os.environ["ANTHROPIC_API_KEY"], "anthropic-version": "2023-06-01"},
                 {"model": MODEL, "max_tokens": 1500, "system": PROMPT, "messages": [{"role": "user", "content": shown}]})
    except urllib.error.HTTPError as e:                  # say why, then treat it as no answer
        print("Claude refused the call:", e.read()[:160].decode(errors="replace"))
        return None, 0.0
    except Exception:
        return None, 0.0
    cost = (r["usage"]["input_tokens"] * PRICE_IN + r["usage"]["output_tokens"] * PRICE_OUT) / 1e6
    try:
        reply = "".join(b.get("text", "") for b in r["content"] if b["type"] == "text")   # skip the thinking block
        a = json.loads(re.search(r"\{.*\}", reply, re.S).group(0))
        text = flat(" ".join(p["text"] for p in pages))
        for c in a["conditions"] + [a["disqualifier"]]:   # a quote that is not on the page is no evidence
            if c["status"] != "not_said" and flat(c["quote"]) not in text:
                c["status"] = "not_said"
    except Exception:
        a = None                                         # an unreadable answer is not a verdict
    return a, cost

def decide(a):
    for i, c in enumerate(a["conditions"], 1):
        if c["status"] == "contradicted":
            return "fail", f"condition {i}", c
    if a["disqualifier"]["status"] == "shown":
        return "fail", "disqualifier", a["disqualifier"]
    missing = [str(i) for i, c in enumerate(a["conditions"], 1) if c["status"] != "shown"]
    if missing:
        return "unknown", "not said: " + ", ".join(missing), {"quote": "", "source": ""}
    return "pass", "all three shown", a["conditions"][1]

args = sys.argv[1:]
done = {r["domain"]: r for r in csv.DictReader(open("decisions.csv"))} if args == ["unknown"] else {}
if done:                                         # re-check only the unknowns, on fresh pages
    domains = [d for d, r in done.items() if r["decision"] == "unknown"]
    for d in domains:
        for f in glob.glob(f"pages/{d}-*.json"):
            os.remove(f)
else:
    domains = [r["domain"] for r in csv.DictReader(open("companies.csv"))][: int(args[0]) if args else None]
pages_paid, dollars, rows = 0, 0.0, []
for domain in domains:
    if pages_paid + 2 > MAX_PAGES or dollars + 0.05 > MAX_DOLLARS:
        print("budget reached, stopping before", domain); break
    try:
        home, paid = scrape(domain, "https://" + domain, 1)
    except Exception:
        rows.append([domain, "unknown", "page did not load", "", "", 0]); print(f"{domain:<26} unknown  page did not load"); continue
    pages_paid += paid
    pages = [home]
    for attempt in (1, 2):
        a, cost = judge(pages); dollars += cost
        decision, why, ev = decide(a) if a else ("unknown", "no answer", {"quote": "", "source": ""})
        if attempt == 2 or decision != "unknown" or (a or {}).get("next_page", "") not in home["links"]:
            break
        try:
            more, paid = scrape(domain, a["next_page"], 2)   # one more page, picked from the homepage's links
        except Exception:
            break
        pages_paid += paid; pages.append(more)
    rows.append([domain, decision, why, ev["quote"], ev["source"], len(pages)])
    print(f"{domain:<26} {decision:<8} {why:<17} {ev['quote'][:40]}")

head = ["domain", "decision", "why", "quote", "source", "pages"]
merged = {d: [r[k] for k in head] for d, r in done.items()}
merged.update({row[0]: row for row in rows})
rows = list(merged.values())
with open("decisions.csv", "w", newline="") as f:
    csv.writer(f).writerows([head] + rows)
counts = {k: sum(r[1] == k for r in rows) for k in ("pass", "fail", "unknown")}
print(f"\n{len(rows)} companies: {counts['pass']} pass, {counts['fail']} fail, {counts['unknown']} unknown")
print(f"pages scraped: {pages_paid} | Claude: ${dollars:.2f}")

For each company it reads the homepage through Firecrawl, the whole page with its navigation, and sends the text to Claude with your criteria. Claude never answers pass or fail. For each condition it answers shown, contradicted or not said, and it copies the sentence from the page that shows it.

The script then does the three things no model is trusted with here:

No invented quotes. A quote the script cannot find on the saved page counts as not said.
No fail on silence. A company fails on a contradicted condition or the disqualifier, never because a page said nothing.
All three, or no pass. Every condition shown, each with its sentence.

When a condition is not said, the script reads one more page, picked from links on the homepage itself, usually pricing. Whatever is still not settled after that is unknown, and unknown goes to a person.

What Reddit says

“anything that has to be exactly right gets code, anything that can be fuzzy gets the model.”
r/AI_Agents, Sep 2026

In the terminal, in that folder, paste these three lines with your own keys between the quotes. Each tool shows its key on the API keys page of its dashboard.

export FIRECRAWL_API_KEY="your Firecrawl key"
export ANTHROPIC_API_KEY="your Anthropic key"
export ANYMAILFINDER_API_KEY="your Anymailfinder key"

The keys last until you close that terminal, so run every step in the same window.

Step 3: Run it on the first two companies

python3 qualify.py 2

Two companies cost a few cents and tell you whether your keys, the model and the script agree before the whole list runs. Ours took three attempts.

Attempt 1. Both companies said no answer at $0.00, so every call to Claude was refused. Our first version sent temperature, a setting Sonnet 5.5 no longer accepts, and a refused call costs nothing. The script above prints the refusal’s own reason after Claude refused the call.
Attempt 2. Allbirds failed and Baremetrics still said no answer, with no refusal printed, so the reply had arrived and could not be read. Sonnet 5.5 thinks before it answers, the first block of its reply is that thinking, and our script was reading only the first block. The version above reads the text block wherever it sits.
Attempt 3. A pass and a fail, each with its sentence. The Baremetrics homepage did not show its pricing, so the script read the pricing page too and passed it on “Baremetrics is free while we’re tracking up to $180K ARR for you.” Allbirds failed on its own homepage, where the product is shoes.

Your pilot should end like our third attempt. If it prints no answer with no refusal, do not run the list: every company would come back the same way, and the likeliest cause is a change in the shape of Claude’s reply.

Step 4: Run it on the whole list

python3 qualify.py

Ours took four minutes for 25 companies: 7 pass, 10 fail and 8 unknown, from 34 pages and $0.72 of Claude.

Here is every company on that first pass, with the line that decided it:

CompanyFirst passWhat decided it
baremetrics.comPass“Your price is set by the annual recurring revenue we track inside Baremetrics, calculated from your live subscription data…”
allbirds.comFail, condition 2“Lightweight, bouncy, and wildly comfortable, Allbirds shoes…”
paddle.comPass“per Checkout transaction”
headspace.comFail, condition 3“Mental health app with expert-led meditations and tools”
ogilvy.comFail, disqualifier“David Ogilvy founded our company to be the teaching hospital of advertising.”
usersnap.comPass“love it, subscribe.”
frame.workFail, condition 2“The Consumer Electronics industry is broken. We’re here to fix it.”
cronitor.ioUnknownNo readable answer
constellationsoftware.comUnknownConditions 1, 2 and 3 not said
featurebase.appPass“The complete support suite for your team and AI, all in one subscription.”
etsy.comFail, condition 2“Etsy is a global online marketplace, where people come together to make, sell, buy, and collect unique items.”
getrewardful.comPass“Rewardful charges a subscription fee and takes 0% of affiliate-attributed revenue.”
tome.appUnknownConditions 1, 2 and 3 not said
nifty.pmUnknownConditions 2 and 3 not said
wikimediafoundation.orgUnknownCondition 2 not said
harvestapp.comUnknownCondition 2 not said
duolingo.comFail, condition 3“We make it easy to form a habit of language learning with game-like features…”
tidio.comPass“Paid plans from $29/mo, no surprise upgrades”
thoughtbot.comFail, condition 2“We specialize in building and modernizing software where reliability, compliance, and long-term maintainability matter as much as speed.”
woodpecker.coUnknownCondition 2 not said
sweetgreen.comFail, condition 1“Kale, romaine, roasted chicken, bread crumbs, parm crisps, lemon squeeze, Caesar dressing”
folk.appUnknownCondition 2 not said
calm.comFail, condition 3“Calm is the #1 mental health app designed to help you manage stress, sleep better, and live a happier, healthier life.”
hund.ioPass“Starting at $29/month”
oura.comFail, condition 2“Acheter un titre sur carte Oùra”

Read two columns before you trust the third.

Start with the reason column. Sweetgreen failed on condition 1, quoted from a salad recipe. The verdict is right and the reason is wrong, since Sweetgreen is a company with a product and fails because the product is lunch. The script fails a company on the first condition the model calls contradicted, so the reason is the model’s, and it is worth a glance.

Then the domain. oura.com is not Oura, the ring company, which lives at ouraring.com. It is a French regional transit card, and it failed for a correct reason about the wrong company. The model knows only the page it was given, so a wrong domain on your list becomes a confident decision about somebody else.

Step 5: Re-check the unknowns on fresh pages

python3 qualify.py unknown

Eight companies came back unknown on our first pass, and four were software companies we expected to pass. Their saved pages showed why. Our first version asked Firecrawl for each homepage’s main content, which is its default, and the main content leaves out the navigation, where all four kept their pricing link.

The Nifty homepage gave the script 3 links, Woodpecker’s 6 and Cronitor’s 10, none of them to pricing, so the second page went to a signup, a contact-sales page or a help article. The fix was one field, "onlyMainContent": False, which the script above already carries, and a mode that re-checks only the companies still unknown, on pages fetched again.

Cronitor, Harvest, Woodpecker and Folk passed on their pricing pages, for 15 more pages and $0.36. Four stayed unknown. The re-check never re-judges a company already decided, so a fail does not get a second chance to pass because the model read it differently on the day. When the criteria are what is wrong, a person edits criteria.md and runs the whole list again.

What Reddit says

“I’d rerun the analysis on the same saved website content, especially for companies near a qualification boundary.”
r/AI_Agents, Sep 2026

Step 6: Decide the rest by hand

Open decisions.csv in a spreadsheet. Each row carries the decision, the reason, the quote and the page it came from. For every unknown, read its pages, then type pass or fail in the decision column and your reason in why. Here are our four:

CompanyDecisionWhat its pages said
tome.appFailA 404 page, “DEPLOYMENT_NOT_FOUND.” The product is gone.
constellationsoftware.comFailNot the company’s own site but an investment research page about it. Constellation Software buys software businesses rather than selling one product.
wikimediafoundation.orgFail“We are the non-profit organization behind Wikipedia.” Our criteria need a line for nonprofits.
nifty.pmPass, as niftypm.comA thin product page. The company’s site, niftypm.com, prices its Pro plan at $79 a month for ten members.
A homepage that never mentions pricing is not a company that failed. It is a question, and a person answers it.

Read every pass’s quote while the file is open. The code checks that a quote is on the page and cannot check that it proves the condition. Woodpecker passed on “Email outreach campaigns,” which proves nothing about pricing. Its pricing page carries “$29/mo,” so the pass stood and we replaced the quote.

Last, correct each domain to where the company’s pages live now, because the next step searches for people by domain. harvestapp.com became getharvest.com and getrewardful.com became rewardful.com. With oura.com and the Constellation domain, five of our 25 domains were not where the company lives today, on a list picked by hand for this run.

On a partnership this queue belongs to the consultant who owns the sourcing, who reads the rows the pages could not settle and knows which of those companies sales has already spoken to. Eight of our 25 rows needed a person, which is the number to watch on your own first run.

Vlad Shvets
Founder @ Empact Partners
A model is cheap enough to read every company on a list, which is exactly why it should never decide on its own. Let it quote the page and let code make the call: no quote, no fail. Give every row the pages could not settle to a person, because a missing pricing page is how a good company drops off a list without anyone noticing.

Buy Contacts Only For The Companies That Passed

Step 7: Find the person at each company that passed

Save the script below as enrich.py.

# enrich.py: find the head of marketing, or the CEO where there is none, at each company that
# passed, with Anymailfinder. Addresses go to contacts.csv; the screen shows status and credits only.
import csv, json, os, sys, urllib.request

MAX_CREDITS = 60
WHO = ["marketing", "ceo"]                       # who buys go-to-market work at a company this size
ALL = sys.argv[1:] == ["all"]                    # "all" = every company, to price the other order
NAME = "enrich-first.csv" if ALL else "contacts.csv"

def find(domain):
    req = urllib.request.Request("https://api.anymailfinder.com/v5.1/find-email/decision-maker",
        json.dumps({"domain": domain, "decision_maker_category": WHO}).encode(),
        {"authorization": "Bearer " + os.environ["ANYMAILFINDER_API_KEY"], "content-type": "application/json"})
    with urllib.request.urlopen(req, timeout=190) as r:
        return json.load(r)

kept = [r for r in csv.DictReader(open(NAME)) if r["status"] == "valid"] if os.path.exists(NAME) else []
have = {r["domain"] for r in kept}               # an address already found is never paid for twice
rows = [r for r in csv.DictReader(open("decisions.csv"))
        if (ALL or r["decision"] == "pass") and r["domain"] not in have]
credits, found, out = 0, 0, []
for r in rows:
    if credits + 2 > MAX_CREDITS:
        print("credit limit reached, stopping before", r["domain"]); break
    try:
        a = find(r["domain"])
    except Exception as e:                       # a refused call is recorded, never guessed
        a = {"email_status": f"error {getattr(e, 'code', '')}".strip()}
    credits += a.get("credits_charged") or 0
    found += a.get("email_status") == "valid"
    out.append({"domain": r["domain"], "decision": r["decision"], "status": a.get("email_status"),
                "email": "" if ALL else a.get("valid_email") or "", "name": "" if ALL else a.get("person_full_name") or "",
                "title": "" if ALL else a.get("person_job_title") or "", "credits": a.get("credits_charged") or 0})
    print(f"{r['domain']:<26} {r['decision']:<8} {str(a.get('email_status')):<10} credits {a.get('credits_charged') or 0}")

with open(NAME, "w", newline="") as f:
    w = csv.DictWriter(f, fieldnames=["domain", "decision", "status", "email", "name", "title", "credits"])
    w.writeheader(); w.writerows(kept + out)
print(f"\n{len(out)} searched, {found} verified addresses found, {credits} credits"
      + (f" ({len(kept)} found earlier, kept)" if kept else ""))

The script asks Anymailfinder for the head of marketing at each company marked pass, and for the CEO where it finds none. A miss costs nothing and a verified address costs two credits. Addresses and names go to contacts.csv, and the terminal shows only the domain, the status and the credits.

python3 enrich.py

Our first run asked for marketing only and found 2 people in 12. At companies this size the person who buys go-to-market work is often the founder, so the second run added the CEO and found seven more: 9 of 12, 16 credits. Run it again after any change, and it keeps the addresses it already found, so none is paid for twice.

The search by role is the weaker lookup. Across 14,181 named people in our own sourcing, Anymailfinder found a valid address for 75.6%. Asked for a role at a domain instead, it found one 203 times in 452. If your list carries names, search by name.

Step 8: Check every address again in the week you send

Save the script below as verify.py, and run it on the day you load the list into your sending tool, never on the day you found the addresses.

# verify.py: check every address in contacts.csv again on the day you send.
# valid goes in the send; risky and invalid are held back.
import csv, datetime, json, os, urllib.request

def verify(email):
    req = urllib.request.Request("https://api.anymailfinder.com/v5.1/verify-email",
        json.dumps({"email": email}).encode(),
        {"authorization": "Bearer " + os.environ["ANYMAILFINDER_API_KEY"], "content-type": "application/json"})
    with urllib.request.urlopen(req, timeout=190) as r:
        return json.load(r)

today, credits, out = datetime.date.today().isoformat(), 0, []
for r in csv.DictReader(open("contacts.csv")):
    if not r["email"]:
        continue
    try:
        a = verify(r["email"])
    except Exception as e:                       # no answer means no send
        a = {"email_status": f"error {getattr(e, 'code', '')}".strip()}
    credits += a.get("credits_charged") or 0
    action = "send" if a["email_status"] == "valid" else "hold"
    out.append({**r, "checked": today, "check": a["email_status"], "action": action})
    print(f"{r['domain']:<26} {a['email_status']:<8} {action}")

with open("send-list.csv", "w", newline="") as f:
    w = csv.DictWriter(f, fieldnames=list(out[0].keys()) if out else ["domain"])
    w.writeheader(); w.writerows(out)
print(f"\n{len(out)} checked on {today}: {sum(o['action'] == 'send' for o in out)} send, "
      f"{sum(o['action'] == 'hold' for o in out)} hold, {credits:g} credits")
python3 verify.py

Ours ran minutes after the search, so all nine came back valid, as they had to. The check is for the week of the send. Nothing of ours goes out unless its address verified in the seven days before the send, and three records sit behind that rule:

The September bounces. 25 of our sends bounced from addresses that had verified valid on 13 September, 16 to 18 days before they went out. A fresh Anymailfinder check flagged 14 of the 25, and wrongly flagged 3 of 275 addresses that had been delivered.
The 21 October launch. In the hour before it, the seven-day rule took 195 addresses off a list of 3,080, every one of them verified earlier in October.
One practitioner’s list. Cody Schneider reported on 21 October that an enrichment provider marked 33 of his addresses valid, and MillionVerifier passed 10.

We use Anymailfinder’s own check because it beat EmailGuard on our known bounces: EmailGuard wrongly failed 19 of the 275 delivered addresses. It is not an independent opinion, though, since the company that found the address also checks it.

ZeroBounce and MillionVerifier are the independent second check, and on our pilot about a third of the bounces got past both verifiers we tested. A second check narrows the problem and does not end it.

Reading First Cost What It Saved, And Bought A Cleaner List

After the review we ran the other order on the same list: the same search for every company, as if nothing had been read first.

python3 enrich.py all

Every company that passed came back at 0 credits, because Anymailfinder does not charge for a search repeated within 30 days. The 13 that failed cost 18 credits for 9 addresses. Side by side, with every search charged the way a fresh account would pay and the pilot left out:

Read firstSearch everyone
Pages read with Firecrawl52, inside the free planNone
Claude reading them$1.08None
Companies searched for a person1225
Addresses found918
Addresses at companies that fail09
Anymailfinder credits, search and check19.839.6
Those credits at $0.049 each$0.97$1.94
Total$2.05$1.94

The two orders cost about the same on our list. Reading cost about four cents a company, and the searches it saved cost about the same.

The money bought different things. Searching everyone bought addresses at Ogilvy, Frame.work, Etsy, the Wikimedia Foundation, Duolingo, thoughtbot, Sweetgreen, Calm and Tome, whose website no longer exists and whose address still verified. Each one is a send that can only cost the domain it goes out from.

Vlad Shvets
Founder @ Empact Partners
On twenty-five companies and one contact each, reading first cost what it saved. It earned its place in the sends that never happened: nine addresses at companies that fail, one of them at a company whose website is gone. Judge the order on what you would have sent before you judge it on credits.

The credits stop being even once you buy more than one person a company. The reading costs the same whether you then search for one person or four, and the search does not: at four people a company the failing companies cost four times the credits, while the reading stays at $1.08.

The other lever is the model. Sonnet 5.5 costs $2 per million input tokens and a smaller model a fraction of that, but we have not run one on this list, so we cannot tell you what it would get wrong.

What Our Run Cannot Tell You

The thirteen companies we rejected were rejected on their own words, and none was one we wanted, so nothing good was lost here. On your list you will not know that. A thin site is protected by the rule that not said never fails. A site that says the wrong thing, like an old pricing page, is not, and only a person reading the fails finds it.

Deciding who is worth a call before anyone pays for one is RevOps work. When Empact Partners builds it, we audit the sourcing a team already does, build one agent for one job, put a consultant in charge of what it decides, and agree the measure first, such as credits spent per company that fits. We call that marketing engineering.

If your team is paying for contact data at companies nobody has decided are worth a call, book a call with me. We will look at your list together and see whether it is work for us.

Ready
To Connect?

Let's Partner