# Gtm Pipeline:people Search

> Find contacts at specific companies or by persona. Use when you have a company list and need contacts with specific roles (company mode), or when building a persona-based prospect list without a company list (persona mode). Also triggers on "find contacts", "search for people at [company]", "find [role] contacts", "people search".

- **Type:** Skill
- **Install:** `agentstack add skill-keinsaasforever-gtm-pipeline-skills-gtm-people-search`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [keinsaasforever](https://agentstack.voostack.com/s/keinsaasforever)
- **Installs:** 0
- **Category:** [Search](https://agentstack.voostack.com/c/search)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [keinsaasforever](https://github.com/keinsaasforever)
- **Source:** https://github.com/keinsaasforever/gtm-pipeline-skills/tree/main/skills/gtm-people-search
- **Website:** https://www.keinsaas.com/de/research-agent

## Install

```sh
agentstack add skill-keinsaasforever-gtm-pipeline-skills-gtm-people-search
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# People Search

Find contacts at specific companies or by persona. Returns a CSV with LinkedIn profile URLs, ready for enrichment.

**Read `~/.claude/skills/gtm-pipeline/_shared/conventions.md` before executing.**

---

## When to Use

- You have a company list and need contacts with specific roles
- You need to build a persona-based prospect list (no company list)
- A demo was triggered and you need ~10 contacts matching a prompt

## Inputs

| Input | Required | Source |
|-------|----------|--------|
| Company list (CSV with names + domains) | For company mode | Company Search / Company Enrichment output, or user-provided |
| Target roles / job titles | Yes | User prompt or ICP definition |
| Location filter | Recommended | User prompt or ICP definition |
| Search mode | Yes | `company` (have company list) or `persona` (no company list) |

If company domains are missing, use SerpAPI domain lookup first (see Step 0 below).

---

## Step 0: Domain Lookup (SerpAPI) — When Domains Are Missing

Most people-search APIs require domains, not LinkedIn URLs.

```
GET https://serpapi.com/search
  ?engine=google_light
  &q={company_name}
  &location={target_country}
  &google_domain={country_google_domain}
  &api_key=YOUR_KEY
```

Extract domain:
```python
import urllib.parse
organic = response.get("organic_results", [])
link = organic[0].get("link", "")
domain = urllib.parse.urlparse(link).netloc.lstrip("www.")
```

**Spot-check all domains** before using — SerpAPI returns wrong results for generic names and global brands.

Env var: `SERPAPI_API_KEY`

---

## Provider Selection

### Company mode (have company list)

**Default flow: BetterContact first → FullEnrich fallback if BC returns 0 contacts. For DACH SMEs, add a Pipe0 Amplemarket pass on whatever FE missed — different index, additive coverage.**

| Priority | Provider | Cost | LinkedIn URLs | Key Strength |
|----------|----------|------|---------------|-------------|
| 1st | **BetterContact Lead Finder** | 0.10 cr/request | Yes | Cheaper fixed cost; good for high-volume batches |
| 2nd | **FullEnrich Finder** | 0.25 cr/person | Yes | Richest filters; richer fields (seniority, headcount, industry) |
| 3rd | **Pipe0 Amplemarket** | 3.00 cr/page (≤100 results) | Yes | **Cheap waterfall for FE/BC misses** — separate index covers different shops (+36% shop coverage on DACH SMEs where FE had 0 hits). Supports company domain + name filters |
| 4th | **Pipe0 Crustdata** | 5.00 cr/page | Yes | Final fallback — low additive value when run after Amplemarket (+1 shop out of 81 in DACH test) |
| 5th | **PhantomBuster Company Employees Export** | Free (LinkedIn account) | Yes | Good when SN not available; scrapes directly from LinkedIn company page |
| 6th | **PhantomBuster SN Search Export** | Free (SN account) | Yes | Full SN search power; use when you have a saved SN search URL |

**Indexes barely overlap on small EU SMEs** — running FE → Amplemarket → Crustdata is additive, not redundant. In an April 2026 DACH e-commerce run (81 FE-missed shops): Amplemarket recovered 29 shops (74 contacts, 35%), Crustdata added 1 more shop as fallback. Always run cheapest provider first.

**Two-tier search pattern** (proven on EU e-commerce campaigns):
1. **Tier 1** — E-commerce + Marketing titles (always run)
2. **Tier 2** — Leadership titles (CEO/MD/Founder) — only if Tier 1 returns 0 contacts AND company traffic ≤ 200K visits/month (large shops likely have dedicated e-comm/marketing staff indexed by FE)

Run the same two-tier logic for both BC (primary) and FE (fallback). BC cost is per-request regardless of results; FE cost is per-person returned.

When PhantomBuster is selected: read `_shared/phantombuster.md` and use the `/phantombuster` skill to generate the script. Phantom scripts: "LinkedIn Company Employees Export" (config key `PB_AGENT_EMPLOYEES`) and "Sales Navigator Search Export" (config key `PB_AGENT_SN_SEARCH`).

### Persona mode (no company list)

| Priority | Provider | Cost | Key Strength |
|----------|----------|------|-------------|
| 1st | **Parallel FindAll** | varies by processor | Discover people matching criteria from web sources |
| 2nd | **BetterContact Search** | TBD | Search without company filter |
| 3rd | **Pipe0 Amplemarket** | 3.00 cr/page | Structured filters (location, industry, employer revenue, founded year, departments, seniority) |
| 4th | **Pipe0 Crustdata** | 5.00 cr/page | Richest persona filters (experience, seniority, skills, education, certifications, career movement) |

**Do NOT use Parallel Task enrichment for people** — model guesses titles, returns wrong roles. Use FindAll.

### Manual / one-time

Both BetterContact and FullEnrich dashboards offer **free search** without API credits.

---

## Execution Protocol

### 1. Sandbox / Docs Check
- Verify request/response structure with 1 record, zero cost
- For Pipe0: use `"environment": "sandbox"`
- For FE/BC: review docs or test via dashboard

### 2. Test Batch (15 companies or 15 records)
- Run chosen provider on 15 companies in production
- **Print and review every row**: company names, contact names, job titles, locations, LinkedIn URLs
- Check for global domain contamination (BC: add `lead_location` filter for `.com` domains)
- Assess hit rate, data completeness, relevance

### 3. Review with User
- Present test results with hit rate and sample rows
- Flag issues (wrong country, irrelevant titles, missing LinkedIn URLs)
- Suggest improvements or provider switch if results are poor
- Get approval before full run

### 4. Full Run
- Submit remaining companies (skip test batch)
- **Save request IDs to file immediately** (recovery if crash)
- Poll with sufficient timeout
- Save results incrementally to CSV after each batch

### 5. Consolidation
- Merge test + full run results
- Deduplicate by LinkedIn URL (fallback: name + company)
- Clean names/titles (`.title()`, strip whitespace)
- Add `source` column
- Write to `csv/intermediate/contacts_found.csv`

---

## Provider A: FullEnrich Finder

**Endpoint:** `POST https://app.fullenrich.com/api/v2/people/search`
**Auth:** `Authorization: Bearer $FULLENRICH_API_KEY`

### Request
```json
{
  "offset": 0,
  "limit": 100,
  "current_company_domains": [
    {"value": "example.com", "exact_match": true, "exclude": false}
  ],
  "current_position_titles": [
    {"value": "Marketing Manager", "exact_match": false, "exclude": false},
    {"value": "Head of Marketing", "exact_match": false, "exclude": false}
  ],
  "person_locations": [
    {"value": "South Africa", "exact_match": false, "exclude": false}
  ]
}
```

### Available Filters
| Filter | Type | Example |
|--------|------|---------|
| `current_company_names` | object[] | `{"value": "Anthropic", "exact_match": true}` |
| `current_company_domains` | object[] | `{"value": "google.com", "exact_match": true}` |
| `current_company_linkedin_urls` | object[] | LinkedIn company URL |
| `current_company_industries` | object[] | `"Software Development"` |
| `current_company_types` | object[] | `"Public Company"`, `"Privately Held"` |
| `current_company_headquarters` | object[] | `"San Francisco"` |
| `current_company_headcounts` | object[] | `{"min": 50, "max": 200}` |
| `current_company_founded_years` | object[] | `{"min": 2020, "max": 2024}` |
| `current_position_titles` | object[] | `"Chief Technology Officer"` |
| `current_position_seniority_level` | object[] | `"Director"`, `"VP"`, `"C-level"` |
| `past_position_titles` | object[] | Past job title |
| `past_company_names` / `domains` | object[] | Previous employer |
| `person_names` | object[] | `"John Smith"` |
| `person_linkedin_urls` | object[] | Direct LinkedIn URL lookup |
| `person_locations` | object[] | `"South Africa"`, `"California"` |
| `person_skills` | object[] | `"JavaScript"`, `"Project Management"` |
| `current_position_years_in` | object[] | `{"min": 0, "max": 1}` (new in role) |
| `current_company_years_at` | object[] | `{"min": 2, "max": 5}` (tenure) |
| `person_universities` | object[] | `"Stanford University"` |
| `current_company_days_since_last_job_change` | object[] | `{"min": 0, "max": 90}` (recent hires) |

All filters support `exclude: true` for negative matching. Multiple filters within same field = AND logic.

Pagination: `offset` + `limit` (max 100/page, max offset 10,000). Beyond 10k: use `search_after` cursor.

### Response
Response fields are **nested** — `current_position_title` and `linkedin_url` do NOT exist at top level.
```python
people = response.get("people", [])
for person in people:
    name    = person.get("full_name", "")
    title   = person.get("employment", {}).get("current", {}).get("title", "")
    li_url  = (person.get("social_profiles", {}) or {}).get("linkedin", {}).get("url", "")
    loc_obj = person.get("location", {})
    location = f"{loc_obj.get('city', '')}, {loc_obj.get('country', '')}".strip(", ")
    # company info also nested: person["employment"]["current"]["company"]["name"]
```

### Key Notes
- `current_company_linkedin_urls` filter accepts LinkedIn company URLs directly (e.g. `https://www.linkedin.com/company/dojo-tech/`) — **no domain lookup needed** when you have LinkedIn company URLs in your input CSV
- Hit rate for EU SME audience (via LinkedIn URL filter): ~48% (15/31 companies). Very small/niche companies with few employees often have low FE index coverage — expect 0 results.

### Cost
**0.25 credits per person** returned.

### Docs
https://docs.fullenrich.com/api/v2/people/search/post

---

## Provider B: BetterContact Lead Finder

**Endpoint:** `POST https://app.bettercontact.rocks/api/v2/lead_finder/async`
**Auth:** `X-API-Key: $BETTERCONTACT_API_KEY`

### Submit
```json
{
  "filters": {
    "company": {
      "include": ["virginactive.co.za"]
    },
    "lead_location": {
      "include": ["South Africa"]
    },
    "lead_job_title": {
      "include": [
        "marketing manager", "head of marketing", "brand manager",
        "marketing director", "CMO", "digital marketing"
      ],
      "exact_match": false
    }
  },
  "max_leads": 10
}
```

Returns: `{ "success": true, "request_id": "abc123" }`

### Poll
```
GET https://app.bettercontact.rocks/api/v2/lead_finder/async/{request_id}
```
Done when: `response["status"] == "terminated"` (not "completed")
Typical wait: 30–60 seconds per request.

### Parse
```python
leads = response.get("leads", [])
for lead in leads:
    name    = lead.get("contact_full_name", "")
    title   = lead.get("contact_job_title", "")
    li_url  = lead.get("contact_linkedin_profile_url", "")
    company = lead.get("company_name", "")
    domain  = lead.get("company_domain", "")
    country = lead.get("contact_location_country", "")
    co_li   = lead.get("company_linkedin_url", "")
```

### Global Domain Contamination
When using a global domain (`.com` for Samsung, H&M, Amazon, etc.), BetterContact returns employees from **all countries**. Always add `"lead_location": {"include": ["Target Country"]}` for global-domain companies. Local ccTLD domains (`.co.za`, `.de`) are safe without it.

### Cost
**0.10 credits per request** (fixed, regardless of number of leads returned or zero results).

### Fields Returned
BC returns fewer fields than FE — no `seniority`, `companyHeadcount`, `companyIndustry`, or `roleStartDate`. Derive `linkedinProfileSlug` from the LinkedIn URL (`/in/`). Split `contact_full_name` into first/last manually.

### Filter Compatibility
When filtering contacts by job title keyword, BC uses `contact_job_title` (not FE's nested `employment.current.title`). Ensure your filter function checks both:
```python
title = (p.get("current_position_title") or
         p.get("contact_job_title") or          # BC format
         p.get("employment", {}).get("current", {}).get("title") or "")
```

### Hit Rate
- SA marketing roles, local (.co.za): ~65–70%
- DACH SME (March 2026): 1/3 companies (33%) — BC missed 2/3; FE found contacts at 2/3 on same set
- EU furniture e-commerce SME (April 2026): ~15–20% overall; T2 leadership tier recovers ~10% more

### When BC Finds Nothing
Fall back to FullEnrich. BC has lower index coverage for small EU SMEs, especially IT/PL markets. FE's richer filters and larger index often surface contacts BC misses — and vice versa. Running both covers ~25–30% of shops vs ~20% with either alone.

### Docs
https://doc.bettercontact.rocks/api-reference/endpoint/lead_finder_post

---

## Provider C: Parallel FindAll (Persona Mode)

**Endpoint:** `POST https://api.parallel.ai/v1beta/findall/runs`
**Required header:** `parallel-beta: findall-2025-09-15`
**Auth:** `x-api-key: $PARALLEL_API_KEY`

**Always ask which processor to use:** `core`, `core2x`, `pro`, `ultra`

### Request
```json
{
  "objective": "Find heads of marketing at e-commerce companies in South Africa with 50-500 employees",
  "entity_type": "people",
  "match_conditions": [
    {"name": "role", "description": "Person must hold a marketing leadership role (Head of Marketing, CMO, Marketing Director)"},
    {"name": "location", "description": "Person must be based in South Africa"},
    {"name": "company_size", "description": "Company must have roughly 50-500 employees"}
  ],
  "generator": "core",
  "match_limit": 25
}
```

**Writing good objectives:**
- Write like a research brief — detailed, with source guidance
- Describe what signals/sources to start from, not just what to find
- Include geographic, industry, and size constraints in the objective text AND as match_conditions

### Poll
```
GET /v1beta/findall/runs/{findall_id}         # status
GET /v1beta/findall/runs/{findall_id}/result   # results when complete
```
Timeout: 15 min for `core`/`core2x`, 30 min for `pro`/`ultra`

### Enrich FindAll Results
After FindAll, add structured fields:
```
POST /v1beta/findall/runs/{findall_id}/enrich
```
Always include `company_website` and `linkedin_company_url` in output schema.

### Rules
- **Always ask which processor to use** — never decide without asking
- **Never re-run before reviewing results**
- **Present the full request payload for review** before executing
- Assess accuracy using confidence score and reasoning

### Docs
Always check latest docs via context7 (`libraryName: parallel-web`) before building — endpoints and parameters evolve.

---

## Provider E: Pipe0 Amplemarket (Filter-Based)

**Endpoint:** `POST https://api.pipe0.com/v1/search/run/sync` (singular `search`)
**Search ID:** `people:profiles:amplemarket@1`
**Auth:** `Authorization: Bearer $PIPE0_API_KEY`
**Use curl** — Python requests blocked by Cloudflare.
**Best for:** Company-mode fallback when FE/BC miss (cheap at 3 cr/page) AND persona-mode filter search.

### Request (Company Mode)
```json
{
  "config": {"environment": "production", "dedup": {"strategy": "default"}},
  "search": {
    "search_id": "people:profiles:amplemarket@1",
    "config": {
      "limit": 5,
      "filters": {
        "current_employer_website_urls": {"include": ["example.com"]},
        "current_employer_names":        {"include": ["Example GmbH"]},
        "current_job_titles": ["CEO", "Geschäftsführer", "Founder", "Head of Marketing"],
        "current_locations": {"include": ["Germany"]}
      }
    }
  }
}
```

### ⚠ Filter Format Gotcha (Amplemarket-specific)
- **`current_job_titles` must be a PLAIN ARRAY** (`["CEO", "Founder"]`), NOT `{"include": [...]}` like the other fields. Crustdata uses the object form. Getting it wrong → 422 validation error.
- Most other filters use the `{"include": [...], "exclude": [...]}` object form.

### Available Filters (discovered via sandbox probe)
```
person_names, school_names,
current_locations, current_job_titles, current_departments,
current_job_functions, current_seniority_levels,
current_employer_names, current_employer_website_urls,
current_employer_linkedin_industries, current_employer_locations,
current_employer_investors, current_employer_founded_year,
current_employer_estimated_revenue, current_employer_open_positions_titles
```

### Response
```pyt

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [keinsaasforever](https://github.com/keinsaasforever)
- **Source:** [keinsaasforever/gtm-pipeline-skills](https://github.com/keinsaasforever/gtm-pipeline-skills)
- **License:** MIT
- **Homepage:** https://www.keinsaas.com/de/research-agent

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** yes
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-keinsaasforever-gtm-pipeline-skills-gtm-people-search
- Seller: https://agentstack.voostack.com/s/keinsaasforever
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
