---
name: moltsets-rb2b-companies-to-contacts
description: Use this skill when the user has an RB2B export and wants to find contacts at the companies that visited their website.
---

# RB2B Companies to Contacts

Extract visiting companies from an RB2B export, return ranked contacts at each (with title, seniority, and contact data included), and enrich.

## Triggers

"find contacts at companies who visited my site", "turn my RB2B company list into contacts", "who works at these visiting companies", "prospect into companies from RB2B", or when a user uploads an RB2B CSV and wants contacts at visiting companies.

## CSV parsing

Read the uploaded or pasted CSV. Extract companies from both row types:
- `ProfileType = Company` rows: use `CompanyName` + `Website`
- `ProfileType = Person` rows: use `CompanyName` + `Website` (ignore the individual - they may not be ICP)

Deduplicate by `Website` (domain). Sort companies descending by `AllTimePageViews` - highest intent first.

Report: "Found X unique companies. Top accounts by page views: [top 3 names + view count]."

Highlight any companies where `NewProfile = true` - first-time visitors worth flagging.

## Set Expectations

Before firing any calls, give the user a brief, dry heads-up. Deadpan over enthusiastic - no filler, no corporate speak.

Facts to work with:
- One search call per company, sequential - no batch search API
- N companies × ~3–5 seconds = search phase
- Enrichment adds one batch call after all searches complete
- For 10+ companies, flag it will take a while and report progress

## Risk scores - confirm before enriching

Every email MoltSets returns carries a deliverability risk score - a grade from A to F. Ask which grades to keep before running anything:

> "MoltSets grades email deliverability risk from A (best) to F (no data). Which grades should I keep? I'd recommend **A, B, C and F** - and dropping **D**, since D is a confirmed bounce, complaint or spam trap."

| Grade | Meaning | Best practice |
| --- | --- | --- |
| A | Strongest engagement signal available | Safe to send immediately |
| B | Solid | Fine in regular sends; on a new or warming domain, send a smaller batch before scaling |
| C | Deliverability can't be confirmed | Segment separately from A/B, warm at low volume, watch engagement, suppress non-responders after 1-2 attempts |
| D | Confirmed bounce, complaint or spam trap | Never send - fastest way to trip spam traps and damage sender reputation for the whole list |
| F | No data - unknown risk, not "safe" | Re-verify before sending, or treat like C |

Use whatever set the user confirms. Treat the grade as a filter, not a guarantee - it lowers risk, it doesn't remove it. Pair it with the user's own sending domain reputation and list hygiene.

## Step 0 - Note on search scope

`search_people` accepts both company and people filters: `company` (name), `company_domain` (exact), `country`, `state`, `city`, `seniority`, `department`/`functional_area`, `industry`, `linkedin_industry`, `naics_code`, `employee_range`, `revenue_range`, `query` (free-text name/role), plus `limit`/`offset`. You're scoping to specific companies here, so add `seniority`/`department`/`country` (enum values) only if the user wants a specific role slice - they cost nothing on top of the call. Title also comes back as a response field for ranking.

## `search_people` filter notes

- **Filters are free precision.** Execution cost comes from the free-text `query`, not the filters attached to it. A filters-only call (no `query`) runs **~40x cheaper**; a bare `query` is the least precise shape available and no cheaper than a filtered one. Never send `query` alone - at minimum attach `country` (~99% filled). Billing is unchanged either way: tokens are charged per record returned.
- **Full filter set:** `query`, `company`, `company_domain`, `country`, `state`, `city`, `seniority`, `department` *or* `functional_area` (same underlying data - never both), `industry`, `linkedin_industry`, `naics_code`, `employee_range`, `revenue_range`, plus `limit`/`offset`.
- **`employee_range` and `revenue_range` apply to the person's current employer**, so you can target by company size or revenue without a `search_companies` pass first. Prefer the numeric bands over legacy `"Small"`/`"Mid-Market"`/`"Enterprise"`/`"Unknown"`.
- **Three industry vocabularies:** `industry` (21 broad buckets), `linkedin_industry` (LinkedIn's ~150 niche labels), `naics_code` (any hierarchy level - "54" broad, "541120" surgical). Pick the one matching how specific the targeting actually is.
- **Fill rates:** `country` ~99%, `title` ~85%, `headline` ~65%, `seniority`/`industry`/`department` ~60%, `naics_code`/`linkedin_industry` ~50%. Every filter is exact-match and silently drops records with an empty field, so stacking three sparse ones can zero out a viable audience. Widen by dropping the sparsest filter first, not `country`.
- **Exact strings:** `"C Suite"` (space, not hyphen), `"Marketing & Advertising"`, `"Medical & Health"`, `"Texas"` not `"TX"`, `"United Kingdom"` not `"UK"`.
- **Size before you walk.** `search_linkedin_profile` with `count_only: true` returns a match count free of tokens; every search response carries `results.total`. Results are `_score`-ranked - the first page is the best page, 25 max per call.
- **Emails are already in the results** - records carry `business_email` and `business_email_risk_score`, so those rows need no enrichment call.

## Step 1 - Confirm scope

Tell the user: "I have X companies from the RB2B export. How many contacts per company? (Default: 3, max 25 per call)"

## Step 2 - Credit check

Call `MoltSets:get_billing` and `MoltSets:get_usage` with `period: billing_cycle` in parallel (free).

Estimate:
```
X companies × ~3 results    = ~X prospects
X prospects × linkedin_to_business_email cost = email cost
X prospects × linkedin_to_mobile_phone cost          = phone cost (if requested)
Total:                                   = ~XXX credits
```

## Step 3 - Probe (first company)

Run `search_people` with `limit: 1` for the highest-intent company:
- `company_domain`: the company's `Website` domain if available (most precise), else `company`/`query`: the company name
- optionally `seniority`, `department`/`functional_area`, `country` for a role slice (use enum values)
- title also returns as a response field for ranking

Check result quality before looping all companies.

## Step 4 - Loop all companies

For each company (ordered by `AllTimePageViews` desc):
1. `search_people` scoped by `company_domain` (preferred) or `company`/`query` (company name), optionally narrowed with `seniority`/`department`/`functional_area`/`country`, `limit`: target per company
2. Tag each result with source company and RB2B page view count for traceability
3. Flag zero-result companies - do not stop loop

Report progress periodically for 10+ company lists.

## Step 5 - Deduplicate

Remove duplicate LinkedIn URLs across all companies.

Report: "Found X unique contacts across Y companies."

## Step 6 - Enrich

Business email is often already on the search records - check first and only enrich the gaps. Credit confirm: "Found X contacts. Enriching [email/phone/both] = ~XXX credits. Proceed?"

Run in parallel:
- `MoltSets:linkedin_to_business_email` - SINGLE `linkedin_url`; one call per URL in parallel (the email gaps)
- `MoltSets:linkedin_to_mobile_phone` with `linkedin_urls: [array]` (the only batch-array endpoint; if requested)

Fire the per-URL email calls in parallel; chunk the phone array at 100 if needed.

## Step 7 - Output

| Company | Name | Title | Seniority | LinkedIn | Business Email | Risk | Mobile | RB2B Visits |
|---|---|---|---|---|---|---|---|---|

Include `RB2B Visits` (AllTimePageViews) so the user can see which accounts had highest intent.

Show the risk score on every row that has an email, including grades outside the accepted set - flag those rather than dropping them silently, so the user can see what was filtered and why.

Summary:
```
Companies processed:  X
Contacts found:       X  (avg X per company)
Zero-result accounts: X
Business emails:      X/N (XX%)
Mobile numbers:       X/N (XX%)  - if enriched
Credits used:         ~XXX

Email risk scores:
  A: X   B: X   C: X   D: X   F: X
  kept (accepted grades):  X
  filtered (excluded):     X
```

List zero-result companies separately.

## Edge cases

- Company name ambiguous to note in output, results may include false matches
- Large list (20+ companies) to set expectation it will take time
- Credits run out mid-loop to stop, output completed rows, report exact resume point
- `Website` domain preferred over company name for search scoping - pass it as the `company_domain` param (more precise than `company`/`query`)
