---
name: moltsets-deanonymize-web-traffic
description: Use this skill when the user has a batch of IP addresses from web traffic and wants to identify the companies behind them and surface likely contacts at those companies.
---

# Deanonymize Web Traffic

Resolve a batch of IP addresses to the companies behind them, then surface likely contacts at each company.

## Triggers

"deanonymise my website visitors", "who are these IPs from my logs", "identify anonymous traffic", "what companies are visiting", "find contacts at companies hitting my site", "resolve my web traffic". Chains IP to company to contacts at that company. Resource-intensive - mandatory credit check before running.

This identifies the **company** behind an IP and **likely contacts** at that company. It does NOT identify the specific individual who visited - there is no IPtoperson resolution.

## Chain

```
IP addresses
  └── ip_to_company                  (per-IP single call to company name + firmographics)
        └── search_people (company domain to ranked contacts at that company)
              └── optional enrich    (per-URL linkedin_to_best_email / linkedin_to_mobile_phone array up to 100)
```

## Set Expectations

Before firing any calls, give the user a brief, dry heads-up. Deadpan over enthusiastic - no filler, no corporate speak. This is the most intensive skill - set realistic expectations without overdramatising.

Facts to work with:
- Sequential rounds: IP to company to contacts at that company to optional enrich
- Each round feeds the next
- Company match is the reliable number; contact volume depends on how many contacts each company returns
- This finds the company and likely contacts - NOT the confirmed visitor
- Time: 60–120+ seconds for any meaningful batch
- Not-found results at each step are free

## 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 1 - Credit check (required)

Call `MoltSets:get_billing` + `MoltSets:get_usage` in parallel (free).

Estimate:
```
N IPs × ip_to_company cost                       = company lookup (one call per IP)
~70% of N (matched companies) × search_people = contact discovery
optional: contacts to enrich × linkedin_to_best_email (one call per URL) / linkedin_to_mobile_phone (array up to 100)
```

Company match is strong; contact discovery and enrichment scale with how many companies match and how many contacts each returns. "Not found" is free.

**If estimated cost > balance:** stop. Report shortfall, how many IPs the balance covers, ask to proceed partially or top up.

## `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 2 - Parse and clean IPs

Accept: line-separated, CSV column, log format. Deduplicate.

**Skip private IPs** (10.x, 192.168.x, 172.16–31.x, 127.x) - tell user these are excluded.

Report: "Processing X unique public IPs."

## Step 3 - Fan out the calls

`ip_to_company` and the email enrich tools take a single input each - fire one call per item in parallel (chunk into manageable parallel waves for large lists). Only `linkedin_to_mobile_phone` accepts a `linkedin_urls` array (max 100).

## Step 4 - Run chain

**Round 1:**
- `MoltSets:ip_to_company` with `ip_address: "<IP>"`, one call per IP in parallel, to company name + firmographics (incl. domain)

**Round 2 - contacts at each company:**
- For each matched company, call `MoltSets:search_people` with `company_domain: "<company domain>"` (or `query: "<company name>"` if no domain). Use `limit` (max 25) and `offset` for pagination.
- `search_people` supports people-level filters: narrow by `seniority`, `department`/`functional_area`, `country`/`state`/`city`, or `industry`/`linkedin_industry`/`naics_code` (exact enum values), and use `query` for free-text across name/title/company. Filters cost nothing on top of the call, so add them freely; a bare `query` is the least precise shape available. `title` and `seniority` also come back as response fields on each record.

**Round 3 (optional enrichment):**
- Collect the `linkedin_url` of the returned contacts
- `MoltSets:linkedin_to_best_email` (or `linkedin_to_business_email`) - one call per `linkedin_url`, fired in parallel
- `MoltSets:linkedin_to_mobile_phone` with `linkedin_urls` array (up to 100) if mobile is needed

## Step 5 - Merge and output

Join contacts back to the company, and the company back to the original IP.

| IP | Company (IP lookup) | Contact Name | Title | Seniority | LinkedIn | Email | Risk |
|---|---|---|---|---|---|---|---|

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:**
```
IPs processed:         200
Company matches:       143  (72%)
Companies w/ contacts: 121  (85% of matched)
Contacts surfaced:     ~480
Credits used:          ~XXX

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

## Edge cases

- These are likely contacts at the company, NOT the confirmed visitor - always frame output this way
- ISP/residential IPs to show under company lookup but note as "Residential/ISP - not a business"; skip contact lookup for these
- No domain returned for a matched company to fall back to `query` (company name) on `search_people`
- Stop immediately if credits run out mid-batch: output completed rows, report exact resume point
- Do not fabricate or infer identities - surface only what the API returns
