Install
$ agentstack add skill-zevenue-headless-gtm-06-resolution-email-person ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Resolution - Email + Person
Turn {domain (required), optional person_name, optional job_title} into a verified, send-safe email.
The one principle that governs every decision here: bounce rate, not find rate, is the signal that matters. A found-but-undeliverable email is worse than no email - it burns sender reputation. So this skill optimizes for deliverable contacts at minimum credit cost, and it never spends enrichment credit without the user's explicit go-ahead.
domain is the one mandatory input across every tool. Everything else can be asked for, inherited from upstream, or derived.
Provider + auth setup
Each provider reads its API key from an environment variable - AIARK_API_KEY, PROSPEO_API_KEY, BLITZ_API_KEY, FINDYMAIL_API_KEY, ZEROBOUNCE_API_KEY - loaded however your host provides them (shell export, .env loader, secrets manager); never hardcode a key. The skill runs with as few as one key set: at startup, detect which keys are present, use only those rungs, and skip the rest (log which rungs were skipped and why). Read references/providers.md for endpoints, request shapes, billing rules, and rate limits before making any call.
- AI Ark - rung 0, conditional: runs only if
AIARK_API_KEYis set. Two modes. (a) Passthrough: a row that already carries an AI Ark-verified email (source flagai_ark/pre_verified, or the caller says the list is an AI Ark export) is accepted as-is - don't re-find what's already verified. (b) Finder:POST /people/export/singleturns a LinkedIn profile URL or AI Ark person id (not name+domain) into a BounceBan-verified email - 1 credit per valid email, 0 on a miss. Strong on SMB operators where Prospeo is weak. ThePOST /peoplesearch bills separately (~0.5 credit per returned profile), so keep searches tightly filtered and prefer/people/export/single. - Prospeo - title-first finder, strong on mid-market + enterprise. Charged only when a request returns results (misses are free).
- Blitz - domain-first finder, strong on SMB. Free on Blitz's Unlimited plan (verify your own plan before treating it as free). Two steps:
domain-to-linkedin→employee-finder(nativejob_level/job_functionfilters); returns the person but not the email. - Findymail - email finder by name+domain (
POST /api/search/name- NOT/api/find, which 405s; seereferences/providers.md). - ZeroBounce - independent email validation (
GET /v2/validate). If its key isn't set, fall back to the finding provider's bundled verdict. Explicitcatch-allstatus (no false "ok" on catch-all domains), and unknown results consume no credit.
Shape of a run
Classify the input, dedupe by domain, show the estimation gate, then waterfall, validate, and emit records. The two gates below are the only fixed stops - everything else is judgment.
Input. Three shapes arrive: a domain + job title (confirm the title and how many people they want - don't guess the count), a domain + person name (require the full name; several people sharing it → return them all, noted), or a bulk file. Bulk JSONL from an upstream skill (01–04) is the chain standard - inherit its fields and ask only for genuine gaps. CSV/Excel: read the headers, inspect the actual cell data, remap any mislabeled columns and report what you remapped - a mislabeled column wastes credit on garbage, so reason about messy data before spending anything. references/input-handling.md has the full normalization table.
A missing domain can be derived from an email (everything after @) or a URL - flag the record derived. A personal LinkedIn profile URL (linkedin.com/in/…) is itself a person identifier and bypasses the domain requirement (resolve via Blitz email-enrichment or Findymail); a LinkedIn company URL is not - extract the domain from it instead.
Dedupe. Before anything costly, normalize each domain (lowercase; strip scheme, www., paths, and sub-domains like careers.acme.com → acme.com), keep one record per domain, and report N duplicates → M unique. The waterfall runs once per unique company. Also check prior runs/ folders for an already-resolved domain/person - never pay twice for the same contact.
Gate 1 - Estimation (mandatory, before any spend)
Run cheap count/preview queries (Prospeo's search returns matches without charging on a miss) and show, before spending any enrichment credit:
- People found for the requested titles
- People not found
- Closest (similar-title) people available for the not-found
- How many have emails
- How many have LinkedIn
- Credit + $ cost to enrich all
- Companies with zero contacts
- Coverage % (found ÷ requested)
Then get an explicit yes before enriching. Never auto-proceed - this gate is what makes the skill safe to point at a 5,000-row list.
Gate 2 - Validation-retry (optional, opt-in)
After enrichment is approved, offer one more opt-in: if a found email validates as invalid, re-run resolution through a different provider to find a working one. Make clear it costs more than usual, but don't run a second estimate pass (that would itself burn time/credit). Declined → validation still runs, bad emails are just flagged. Accepted → the retry loop in the validation step below is live.
Waterfall enrichment
Order (cost-first): AI Ark → Prospeo → Blitz (local title filter) → Findymail. Stop the moment you land a valid email (found-but-invalid is not a stop condition). Skip any rung whose key is absent.
Rung 0 - AI Ark (only if AIARK_API_KEY is set): passthrough rows return valid with email_source: ai_ark and skip the paid rungs entirely; otherwise resolve via /people/export/single with a LinkedIn profile URL (often already on the row, or free from Blitz's employee-finder) or person id. If you must find the person with AI Ark's search, pin it tight (domain + full name) - search bills per returned profile - and respect the per-run credit cap in references/providers.md. Worth trying early for SMB-operator targets.
Exact first, then broaden - capped at 3 attempts per company
Try the exact title strictly first; broaden to similar titles only on a miss. Misses are free on Prospeo, so trying is cheap - the cap keeps big lists from crawling.
| Attempt | Prospeo match_mode | Titles to send | Label a hit as | |---------|----------------------|----------------|----------------| | 1 | EXACT (optionally smart_intensity: STRICT) | the exact title (bundle all spellings in the include array - one call) | exact | | 2 | SIMILAR | + same-seniority synonyms | closest | | 3 | CONTAINS (optionally smart_intensity: LOOSE) | a wider same-seniority set | closest |
Use EXACT only on attempt 1. person_job_title.include takes an array, so all format variants of one logical title ("VP Marketing", "VP of Marketing", "Vice President, Marketing") go in a single call - they don't cost separate attempts.
The same-seniority guard (this is the crux of "closest")
When you broaden, the search can hand back a different-seniority role. Pin the requested band at query time - both finders have a native lever: Prospeo's person_seniority (bands: Founder/Owner, C-Suite, Partner, Vice President, Head, Director, Manager, Senior, Entry, Intern) and Blitz's job_level (C-Team/VP/Director/…) - so out-of-band people never come back.
- ✅ same person: "Business Development Director" ≈ "Head of Business Development" (both
Director/Headband) - ❌ different person: "CTO" (
C-Suite) ≠ "Director of Technology" (Director) - different band, rejected at query time
Only for results that arrive without a seniority field (a local title refine, or Findymail) do you apply the band check manually, dropping any out-of-band person before accepting them as closest. This guard is why broadening stays safe: you never email a junior when they asked for a chief, even though a looser title match might offer one.
Per-tool shaping
- Prospeo is title-first - send
company.websites+person_job_title+person_senioritytogether (free on a miss). - Blitz is domain-first but keys on the company LinkedIn URL:
domain-to-linkedin→employee-finderwithjob_level+job_functionfilters. It returns the person + LinkedIn but not the email - resolve the email separately (Blitz email-enrichment by LinkedIn, or Prospeo/Findymail by name+domain). - Findymail resolves the email by name+domain.
- If they asked for 1 and a rung returns several: prefer exact title → most senior → highest confidence.
Validate, then be honest about the verdict
Validate every obtained email (ZeroBounce, or the provider's bundled verdict) - validation always runs, and no email is ever presented as confirmed without it.
- valid → done. Return it.
- catch_all / risky → accept but flag as
catch_all(catch-alls are often the real address; don't throw away a likely-good contact - and don't upgrade the flag to valid). - spamtrap / abuse / donotmail (ZeroBounce-specific) → never email; treat as hard invalid and record the
sub_statusinnote- hitting a spamtrap is worse than a bounce. - hard invalid → if Gate 2 was accepted, re-run resolution excluding the provider that produced the bad email (naturally capped at the number of providers). Otherwise return it flagged
invalid- never pass it off as usable. - all providers exhausted, still invalid → return
no_email. Do not invent a genericinfo@/owner@.
Accuracy boosters
- Email-pattern inference: one verified email at a domain teaches you the company's pattern (
first.last@,flast@). Generate candidates from it for the people the providers missed, and validate them - "not found" becomes "found + verified" cheaply. Label the sourcepattern_inferred. - LinkedIn freshness check: before trusting a
closestmatch, sanity-check the person's current LinkedIn title against the requested one (finder caches go stale). Role changed → flag it innoterather than emailing the wrong person.
Output contract
Return one record per contact:
| Field | Values | |-------|--------| | company, input_domain | - | | person_name, linkedin_url | - | | requested_title, matched_title | - | | match_type | exact \| closest | | email | resolved address (empty if no_email) | | email_source | ai_ark \| prospeo \| blitz \| findymail \| pattern_inferred | | verification_status | valid \| catch_all \| invalid \| no_email | | confidence | 0–100 (combine verdict + match_type + freshness) | | note | optional flags - stale title, derived domain, etc. |
When this skill runs in the chain, add verification_status - do not overwrite the upstream confidence field.
Write results as records.jsonl under runs// per _shared/CONVENTIONS.md (CSV is an optional, derived human export). End with a run summary: companies in · unique after dedupe · processed · found / not found · coverage % · breakdown by match_type and email_source · any rows skipped, with the reason - a row you couldn't process is reported, never silently dropped.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Zevenue
- Source: Zevenue/headless-gtm
- License: MIT
- Homepage: https://zevenue.com
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.