Four levers do most of the work.
Batch by company, not by person. Fields like industry and trigger event depend on the company, not the individual. Enriching one company once and fanning the result across five contacts cuts your model calls by roughly 80% on multi-contact accounts, and it prevents two contacts at the same firm getting contradictory industry labels.
Cache on a stable key. Hash the company domain plus the field name plus a date bucket. Within the bucket, serve the cached answer. Trigger events are the field most sensitive to staleness, so let them expire faster than role and industry. If the cache reuses a value, keep the original source URL and the date it was first verified, so the row still carries provenance.
Cap tokens and stop early. Set a maximum output length in the request and give the model a fixed list of allowed source domains, such as the company's own site, the relevant registry, and a news index. A short allowed list reduces both wandering and output length.
Spend the expensive checks on a sample. Run the full page-fetch verification on 100% of values, because fetching is cheap and writing a wrong value is not. Spend larger model calls only on rows that pass the first filter and sit near the threshold.
Hunter's bulk workflow makes the batching point concrete: when revealing contacts for a set of companies, you can limit the number of email addresses revealed per company, which is cost control applied at the data layer rather than the prompt (Hunter's decision maker guide).
A small pipeline in Python makes the shape concrete:
import hashlib, json, time
def cache_key(domain, field, bucket):
return hashlib.sha256(f"{domain}|{field}|{bucket}".encode()).hexdigest()
def enrich_row(row, llm, cache, fetcher, threshold=0.85):
domain = row["domain"]
out = {}
for field in ("role", "industry", "trigger"):
key = cache_key(domain, field, time.strftime("%Y-%W"))
result = cache.get(key) or llm.extract(row, field)
cache.set(key, result)
if not result["value"] or not result["source_url"]:
out[field] = {"value": None, "reason": "not_found"}
continue
page = fetcher.text(result["source_url"])
quote_seen = result["evidence_quote"].lower() in page.lower()
if not quote_seen:
out[field] = {"value": None, "reason": "quote_not_found"}
elif result["confidence"] >= threshold:
out[field] = {"value": result["value"], "source_url": result["source_url"]}
else:
out[field] = {"value": None, "reason": "below_threshold"}
return out
Notice what the function refuses to do. It does not write a value on model say-so, it does not treat an empty page as a negative, and it does not hide the reason a field was dropped. Those three habits make the output auditable months later.