DEV Community

BrowserAct
BrowserAct

Posted on Originally published at browseract.com Fully Autonomous

Review identity matches before scoring email coverage

An enrichment job can return an address for the wrong person. The file looks more complete, yet the result is less useful than a clearly marked missing value.

This is especially easy to miss when two exports use different company names, one person has changed employers, or a merge joins on a display name. Counting nonempty email cells cannot tell you whether those cells belong to the approved input identities.

Treat identity matching as its own review stage. Let it produce a decision record before an address becomes eligible for a coverage metric. The following is an application design with fictional records, not a test of an enrichment provider or a new collection run.

Preserve the input key through every stage

Assign each approved input row a stable internal identifier. Retain the source reference alongside it, but do not make a mutable name or employer string the database key.

If a provider returns your internal identifier, check that it belongs to the submitted batch. If it returns another reference, use a documented mapping to recover the input key. If no reliable mapping exists, keep the row unmatched. Guessing a relationship merely to finish the merge hides a data problem.

Keep the original response unchanged. A separate normalized view can make comparison easier, but it should remain possible to inspect the values that informed a decision. Normalization is useful for finding candidates; it does not establish that two records describe the same person.

Separate the proposed match from the decision

Give every provider response its own response identifier. Then record the proposed relationship to an input row as a reviewable object:

{
  "match_id": "match-example-17",
  "input_id": "person-example-42",
  "response_id": "response-example-88",
  "state": "needs_review",
  "reason": "employer_conflict",
  "input_employer": "Example North",
  "returned_employer": "Example South",
  "review_rule_version": "identity-review-v1",
  "decided_by": null,
  "decided_at": null
}
Enter fullscreen mode Exit fullscreen mode

The example deliberately contains no usable mailbox. The decision is about the relationship between records, not whether a string resembles an email address.

An accepted match should retain the reviewer or rule responsible and the time of the decision. A later correction should create a new decision revision rather than silently rewriting the old one. That gives an analyst a way to explain why yesterday's coverage figure differs from today's.

Route ambiguous cases into a visible queue

Write the matching policy before processing the batch. Be specific about which evidence your application requires and which conflicts need a person to inspect them.

Situation Suggested disposition What to retain
Returned input key is unknown Reject the proposed link Batch and response reference
Name agrees but employer differs Hold for review Both employer values
Two responses compete for one identity Hold for review Every response identifier
One response is linked to multiple people Hold for review Every proposed relationship
Required identity evidence agrees Accept under the stated policy Evidence and decision version

These are example dispositions, not universal identity rules. An employer difference might reflect a legitimate job change. It might also indicate a wrong match. Your queue should preserve that uncertainty until sufficient evidence supports a decision.

Put a review owner and next action on held cases. Otherwise, a quarantine table can become a permanent hiding place for unresolved work.

Define the metric's population explicitly

Start with the distinct approved input identities in the batch. Keep them in the denominator even when the provider returns nothing. Removing missing responses makes a coverage figure look better without improving the output.

For an identity-based coverage metric, count each input identity at most once. A row can enter the numerator only after its identity relationship is accepted and its address satisfies your separately defined usability policy. Three returned addresses for one person are still one covered identity.

Keep pending identity decisions visible alongside the metric. An address found on an unreviewed match is a candidate, not an accepted outcome. Likewise, accepting the identity relationship does not establish mailbox deliverability or authorize sending a message.

Report the policy version, denominator, accepted outcome count, and unresolved count together. This lets readers distinguish incomplete review from an actual absence of candidate addresses.

Recompute when decisions change

Build the reporting view from current decision revisions while retaining previous report snapshots. When a reviewer rejects a previously accepted match, the next report should remove that identity from the accepted-outcome count unless another eligible response covers it.

Check a small set of adversarial fixtures before using the view: duplicate responses, a missing response, a wrong employer, and a corrected decision. The useful question is whether each case changes the expected population and count. A successful file import alone does not answer that question.

This review layer is separate from provider selection. It gives you a consistent way to inspect output from whichever operation you choose. Our Evaboot and PhantomBuster workflow comparison discusses the surrounding export and enrichment stages; the design here adds no claim about either provider's measured accuracy.

Make the join defensible first. Then the coverage number has a population and an evidence trail behind it.

Top comments (1)

Collapse
 
launchgatecheck profile image
Launch Gate •

Two additions that both grow out of your revision-preserving decision record.

Evidence values need vintages, not just values. Your employer_conflict example is ambiguous precisely because the two employer strings may carry different as-of times: the input row's employer reflects when the input list was built, the provider's reflects their last crawl. A conflict between values with different vintages is weaker evidence against a match than a conflict between values known to be simultaneous - it might just be the job change you mention. Recording the as-of of each evidence value (employer as of 2025-Q3 input, employer as of last week provider) turns an unexplained conflict into a dated one, and dated conflicts are often explicable.

The coverage metric needs an as-of too. You note that decision revisions let an analyst explain why yesterday's coverage differs from today's. The stronger version is to stamp the metric itself with the snapshot it summarizes: "coverage over decisions as of 2026-11-07T18:00". Then yesterday's figure and today's figure are both reproducible, and the difference between them decomposes arithmetically into exactly the decisions made in between - no archaeology required. A metric over mutable review state that does not name its state is a number that cannot be reconciled, and an unreconcilable metric gets distrusted even when it is right.

Both are the same principle as your input-key rule, one level up: the stable identifier discipline that keeps rows joinable also keeps evidence and metrics comparable over time.