---
name: competitor-ad-intelligence-agent
description: >-
  Use this skill whenever the user asks what competitors are running in paid
  ads, wants to compare competitor Meta or LinkedIn ads, understand their
  messaging/offers, or inspect a competitor's current ad motion. Natural
  requests include 'analyze my competitors ads', 'what are they advertising?',
  or 'show me what these competitors keep pushing'. Use only the specified Apify
  Meta and LinkedIn actors for ad retrieval; never substitute manual ad-library
  browsing. Build a grounded competitor map, ask one evidence-backed curiosity
  question, deep-dive the exact ads and landing pages, then optionally use
  MoltSets to build a privacy-safe custom ad audience. The user never needs to
  mention the skill name.
---

# Competitor Ad Intelligence Agent

This skill answers one question:

> **What are our competitors publicly doing with paid ads right now, and what is actually worth noticing?**

It is not an ad-spy dashboard.
It is not a giant report.
It is not a performance-analysis tool.

The workflow is:

**seller context → competitor identity → real Meta + LinkedIn ads → normalized campaign families → Competitor Ad Map → 3 grounded observations → one curiosity gate → focused second-pass landing-page analysis → optional MoltSets custom-audience build → stop.**

The skill must stay inside the evidence returned by the scrapers and the pages reached from those ads.

---

# Fixed external data tools

Use exactly these two Apify actors for ad retrieval:

## Meta / Facebook / Instagram

`curious_coder/facebook-ads-library-scraper`

Verified capabilities used by this skill:
- accepts Facebook Page URLs or Facebook Ad Library URLs through `urls`
- `limitPerSource`
- `scrapeAdDetails`
- `scrapePageAds.period`: `last24h`, `last7d`, `last14d`, `last30d`
- `scrapePageAds.activeStatus`: `all`, `active`, `inactive`
- `scrapePageAds.sortBy`: `impressions_desc`, `most_recent`
- `scrapePageAds.countryCode`: ISO two-letter country or `ALL`
- output includes advertiser/page data, active state, dates, platforms, creative snapshot data and ad/destination information when present

## LinkedIn

`silva95gustavo/linkedin-ad-library-scraper`

Verified capabilities used by this skill:
- accepts a list of LinkedIn Ad Library URLs through `startUrls`
- can also accept a LinkedIn company URL, but Ad Library URLs are preferred because they preserve market/date filters
- `resultsLimit`: maximum results per input URL
- `skipDetails`: default `false`; keep `false` for fuller ad data
- output can include advertiser name/URL, availability dates, body, headline, CTA buttons, `clickUrl`, format, media URLs, payer, impressions and targeting when LinkedIn exposes them

Do not silently substitute other ad-library actors.

---

# Tool adapter

This skill assumes Claude Code has an Apify integration, MCP tool, or equivalent ability to:

1. run an actor by actor ID
2. pass JSON input
3. retrieve the actor's dataset items

Tool names differ by environment.
Use the available Apify tool whose description matches those actions.

Do **not** invent a tool name if it is not present.

## If Apify is unavailable

Say:

> Apify isn't connected in this session yet. This workflow uses Apify to pull the public Meta and LinkedIn ad-library data. Connect Apify and I can continue from the ad-retrieval step.

If the current Claude client exposes an Apify connector setup flow or connection instructions, use that exact UI/tooling. Do not invent API keys, commands, actor credentials, or setup steps that are not present in the environment.

After Apify is connected, resume this workflow. Do not replace Apify with another ad scraper or manual ad-library browsing.

Stop until Apify is available.

---

# Browser / web capability

Claude may use normal web/browser access for only these supporting jobs:

- understand the seller's website
- verify competitor identities
- locate the correct Facebook Page and LinkedIn company/advertiser
- open landing-page URLs returned by the ad scrapers
- follow a small number of directly relevant same-domain pages during the second pass

Do not turn the workflow into unrestricted web research.

The primary competitive evidence must come from the two ad-library datasets.

**Hard retrieval rule:** do not open Meta Ad Library or LinkedIn Ad Library in the browser to collect ads when Apify is available. Browser/web is only for advertiser identity verification and for opening landing pages returned by the scraper. If Apify is unavailable, stop; do not substitute manual browsing.

# Semantic routing

The user does **not** need to mention Apify, this skill, or an “ad intelligence” workflow. Prefer this skill for natural requests such as:

- “These are our competitors — analyze their ads.”
- “What are QuickEnrich and GetLeads advertising right now?”
- “What messages are our competitors pushing on Meta and LinkedIn?”
- “Show me what offers competitors keep running.”

Do not ask the user to restate the request using the skill name. Once selected, use the fixed Apify retrieval path below.


---

# The spine

0. Check Apify silently.
1. Capture seller + competitors + market.
2. Understand the seller in plain language.
3. Resolve competitor advertiser identities.
4. Pull Meta and LinkedIn ads.
5. Normalize, validate, dedupe, and build campaign families.
6. Handle zero-data / partial-data states explicitly.
7. Show the Competitor Ad Map.
8. Show exactly 3 grounded observations.
9. Generate one evidence-backed curiosity question.
10. If the user chooses to go deeper, reuse the exact triggering ads and inspect their landing pages.
11. Show a concise campaign-system breakdown.
12. Ask whether to turn the insight into a custom ad audience.
13. If yes, use MoltSets to find the audience and convert it to privacy-safe match keys.

---

# Global run rules

- Work with the competitors the user names.
- Do not add competitors unless the user asks.
- Do not call an ad a winner, loser, high-performing, scalable, or profitable.
- Do not infer CTR, ROAS, CAC, pipeline, spend efficiency, or conversion rate.
- Do not assume an ad is successful because it is active, old, repeated, or has variants.
- Do not visually interpret an image/video merely because a media URL exists.
- Do not infer hidden audience targeting unless the scraper explicitly returns targeting fields.
- Do not infer a competitor's entire marketing strategy from no data.
- Do not say a company "is not running ads" unless the user understands that this means no ads were found in the searched public-library window. Prefer the exact fallback language in this file.
- Important findings must appear in chat, not only in raw tool output.
- Never dump raw JSON into the main response unless the user asks.
- Do not continue through an interactive gate without the user's reply.

---

# Step 0 — Silent availability check

Check that an Apify run-actor capability and dataset retrieval capability are available.

If yes, continue without mentioning setup.

If not, use the Apify-unavailable fallback and stop.

---

# Step 1 — Intake

The ideal starting prompt looks like:

> We are acme.com. Our competitors are Competitor A, Competitor B, Competitor C. Analyse their ads in the US.

Or:

> These are my competitors: A, B, C. This is what we do: [one sentence]. Analyse their paid ads.

You need three things:

1. seller context
2. competitor list
3. market/country

## Seller context missing

Ask only:

> What do you sell? Send me your website or one sentence.

Stop.

## Competitors missing

Ask only:

> Which competitors should I analyse? Send me the names or websites.

Stop.

## Market missing

Do **not** infer geography from the seller's office location.

Ask:

> Which market should I check for the ads?

If interactive choices are available and a likely set is obvious, offer common choices such as:
- US
- UK
- ALL
- Other

Otherwise ask as plain text.

Stop.

## Once all three are known

Proceed directly.

Do not ask for Facebook Page URLs or LinkedIn Ad Library URLs yet.
Resolve those yourself.

---

# Step 2 — Understand the seller

Use the seller's own website first.

Determine only:

### What they sell
One or two plain sentences.

### Who they sell to
Customer type / market.

### The core commercial outcome
What customers appear to buy them to achieve.

Do not write positioning fluff.
Do not create a full brand analysis.
Do not research the seller for 20 minutes.

## Show in chat

### Seller Context

| Question | Answer |
|---|---|
| What we sell | ... |
| Who we sell to | ... |
| Core outcome | ... |

Then continue automatically.

The seller context is used only to decide which competitive findings are relevant.
It must not be used to manufacture differences that the ad data does not support.

---

# Step 3 — Resolve competitor identities

For each competitor, resolve:

- official website/domain
- official Facebook Page URL
- official LinkedIn company identity / advertiser search term

Use official site social links first when available.
Otherwise use web search.

## Identity rules

A company name alone is not enough when multiple businesses share it.

Prefer:
1. official website → social link
2. verified/obviously official social page matching domain and brand
3. search result with matching company/domain context

## Show identity table only when needed

If every competitor identity is unambiguous, continue silently.

If one or more are ambiguous, show:

### Confirm Competitors

| Competitor | Website | Facebook Page | LinkedIn Company |
|---|---|---|---|

Then ask:

> Are these the right companies?

Options:
- **Yes, continue**
- **Fix one**

Stop.

If the user chooses `Fix one`, ask which identity is wrong, update only that row, then continue.

Never guess through an ambiguous identity.

---

# Step 4 — Pull Meta ads

Before the actor run, show one short presenter-visible line:

> Pulling the public Meta and LinkedIn ads for these competitors now.

Do not print actor debug output or raw JSON into the main conversation.

Actor:

`curious_coder/facebook-ads-library-scraper`

Use competitor Facebook Page URLs as `urls`.
Batch competitors into as few runs as practical.

## Pass A — currently active ads

Use:

```json
{
  "urls": [
    "<facebook-page-url-1>",
    "<facebook-page-url-2>"
  ],
  "limitPerSource": 100,
  "scrapeAdDetails": true,
  "scrapePageAds": {
    "activeStatus": "active",
    "sortBy": "most_recent",
    "countryCode": "<MARKET_CODE>"
  },
  "runTag": "competitive-ad-intelligence-active"
}
```

Purpose:
- see what is publicly active now
- prioritize current messages/offers

## Pass B — recent 30-day activity

Run only if it adds useful coverage beyond active ads.

Use:

```json
{
  "urls": [
    "<facebook-page-url-1>",
    "<facebook-page-url-2>"
  ],
  "limitPerSource": 100,
  "scrapeAdDetails": true,
  "scrapePageAds": {
    "period": "last30d",
    "activeStatus": "all",
    "sortBy": "most_recent",
    "countryCode": "<MARKET_CODE>"
  },
  "runTag": "competitive-ad-intelligence-recent30d"
}
```

Purpose:
- include ads that stopped recently
- avoid describing "current strategy" from active-only data when activity changes quickly

## `ALL` market

If the user explicitly chose global/all markets, use:

`"countryCode": "ALL"`

## Result limits

Default: 100 per competitor per Meta pass.

Do not increase automatically unless:
- the competitor has an unusually large library
- the first 100 are too repetitive to establish campaign families
- a second-pass curiosity deep dive specifically needs more records

If more data is needed, increase only for the relevant competitor.

---

# Step 5 — Pull LinkedIn ads

Actor:

`silva95gustavo/linkedin-ad-library-scraper`

Prefer LinkedIn Ad Library URLs rather than raw company URLs.

Construct one URL per advertiser using the verified advertiser identity and selected market.

Documented pattern:

`https://www.linkedin.com/ad-library/search?accountOwner=<advertiser>&countries=<COUNTRY>&dateOption=last-30-days`

URL-encode the advertiser value correctly.

## Default input

```json
{
  "startUrls": [
    {
      "url": "https://www.linkedin.com/ad-library/search?accountOwner=<competitor-1>&countries=<COUNTRY>&dateOption=last-30-days"
    },
    {
      "url": "https://www.linkedin.com/ad-library/search?accountOwner=<competitor-2>&countries=<COUNTRY>&dateOption=last-30-days"
    }
  ],
  "resultsLimit": 100,
  "skipDetails": false
}
```

`resultsLimit` is per input URL.

Keep `skipDetails:false` because fuller body/CTA/destination data is more important than maximum speed for this workflow.

## LinkedIn fallback if advertiser query is unreliable

The actor can accept a LinkedIn company URL.

Use that only as a fallback because it gives less control over market/date parameters.

If using that fallback, label the result internally as `broader_linkedin_company_input` and do not mix it silently with the market/date-specific result.

## Zero recent LinkedIn results

Do not immediately assume no LinkedIn advertising exists historically.

For the normal first pass, the skill is allowed to say:

> No public LinkedIn ads found for this advertiser in the searched 30-day market window.

Do not broaden the date window automatically unless:
- the curiosity/deep-dive step needs more context
- or the user asks for historical activity

---

# Step 6 — Normalize both datasets

Create one internal normalized record schema.

Every usable ad record should attempt to populate:

| Field | Meaning |
|---|---|
| `competitor` | canonical competitor name |
| `platform` | `meta` or `linkedin` |
| `source_ad_id` | ad/library identifier when present |
| `active_state` | active/inactive/unknown |
| `start_date` | start date when present |
| `end_date` | end date when present |
| `headline` | headline/title when present |
| `body` | body/primary text |
| `cta` | CTA text(s) |
| `destination_url` | landing-page/click URL |
| `format` | image/video/document/carousel/etc. when returned |
| `publisher_platforms` | Meta placement platforms when returned |
| `variant_or_collation` | Meta grouping/variant data when present |
| `media_url` | media reference only; not visually analyzed by default |
| `impressions` | LinkedIn impressions only when returned |
| `targeting` | LinkedIn targeting only when returned |
| `source_library_url` | source/ad-library URL when available |

## Missing fields

Missing is allowed.

Do not fill a missing field by guessing.

## Media rule

An image/video/document URL is a reference, not proof that Claude has inspected the creative.

Unless the user explicitly asks for visual analysis and the environment actually downloads/opens the media:
- do not describe colors
- do not describe people
- do not describe composition
- do not describe visual hooks
- do not infer brand style from the media URL

Format-level observations are allowed when `format` is returned.

---

# Step 7 — Validate result states

Keep these states separate for every competitor and platform:

### `ads_found`
Usable ad records were returned.

### `zero_ads_returned`
Actor completed successfully but returned zero usable ad records for the verified advertiser in the searched window/market.

### `identity_unresolved`
The official advertiser/page could not be established confidently.

### `scraper_failed`
The actor/tool failed or returned an operational error.

Never convert one state into another.

A scraper failure is not evidence of zero ads.

An unresolved identity is not evidence of zero ads.

---

# Step 8 — Dedupe and build campaign families

The goal is not to summarize every ad.

The goal is to identify repeated paid-acquisition motions.

## Dedupe obvious duplicates

Use, in order:

1. exact ad ID
2. exact normalized headline + body + destination
3. near-identical headline/body + same destination

Do not delete meaningful variants.

## Campaign-family logic

Group ads into the same campaign family when multiple signals align:

- same offer
- same destination or destination path
- same core claim/message
- same CTA
- same product/use case
- same campaign concept with hook variants

Do not group solely because the ads come from the same competitor.

## For each family, extract

### What they are pushing
Product, feature, report, demo, trial, event, assessment, calculator, category message, etc.

### Repeated message
The core claim/problem/promise repeated across the ads.

### Offer
What the buyer actually gets or is asked to consume/do.

### CTA
Observed CTA language.

### Destination
Where the ads send people.

### Variation
What appears to change across variants:
- hook
- headline
- body framing
- CTA
- platform
- format

Only state variation visible in the normalized records.

---

# Step 9 — Hard no-data fallbacks

This is critical.

## One platform has no ads

Use exact language:

> **No public Meta ads found for [Competitor] in the market/status window searched.**

or:

> **No public LinkedIn ads found for [Competitor] in the 30-day market window searched.**

Continue with the other platform if it has usable data.

## Both platforms have zero usable ads for one competitor

Use:

> **No public Meta or LinkedIn ads found for [Competitor] in the searches I ran.**

Do not say:
- they do not advertise
- they rely on organic
- they stopped ads
- they use another channel

unless separate evidence actually supports it and the user asks.

## Every competitor has zero usable ads

Show:

### No Public Ad Data Found

> I couldn't find public Meta or LinkedIn ads for these competitors in the market/time windows I searched, so there isn't enough ad-library evidence to produce a competitive ad analysis without making things up.

Then stop.

Do not create a Competitor Ad Map from nothing.
Do not generate a curiosity question.

## Mixed scraper failure

If a scraper failed for a subset, report that platform as `Scraper error — not enough evidence`.

Do not label it zero ads.

---

# Step 10 — Competitor Ad Map

Only run if at least one competitor has usable ad data.

Show:

### Competitor Ad Map

| Competitor | Meta ads | LinkedIn ads | Main campaign families | Repeated message | Offer | CTA / destination |
|---|---:|---:|---|---|---|---|

Rules:
- `Meta ads` and `LinkedIn ads` are counts from the retrieved usable records after dedupe, not universal account totals.
- show up to 3 main campaign families per competitor
- keep cells concise
- if one platform had zero ads, write `0 in searched window`
- if one platform failed, write `scraper error`
- do not hide missing data

Do not add a giant appendix.

---

# Step 11 — Exactly 3 grounded observations

Below the map, show:

### What I Think Matters

Give exactly **3** observations.

Each observation should be 1–3 sentences.

Good observation types:

### Market repetition
Several competitors repeatedly push the same pain/promise/offer.

### Competitor outlier
One competitor is doing something materially different from the others.

### Campaign concentration
One competitor has many variants converging on the same offer/destination.

### Cross-platform difference
A competitor uses materially different messages/offers between Meta and LinkedIn.

### Offer difference
One competitor uses a different conversion offer from the rest.

### Destination pattern
Several different ads converge on the same landing page or funnel.

### Relative whitespace
A message/offer is comparatively underrepresented in the retrieved sample.

When using whitespace language, say:

> comparatively underrepresented in the ads retrieved

not:

> nobody in the market owns this

unless the evidence really supports that stronger statement.

## Evidence discipline

Every observation must be traceable to the retrieved records.

Do not produce:
- "best ad"
- "top performer"
- "this is working"
- "they are scaling this"
- "their audience loves this"

Active status, age, variants, impression ranges, or repeated use are context, not performance proof.

---

# Step 12 — Curiosity selection

This is the key conversational gate.

Do not ask a generic:

> Want me to go deeper?

Instead, generate **one curiosity question** from the most distinctive, evidence-backed behavior found in the dataset.

Prefer something unique about one competitor.

## Strong curiosity triggers

- only competitor using a materially different offer
- many variants around one campaign family
- ads on Meta and LinkedIn telling noticeably different stories
- multiple distinct ads converging on one destination
- a repeated CTA pattern that differs from everyone else
- a product/use-case push that appears concentrated in one competitor
- a recent campaign family that differs sharply from their other ads

## Curiosity question format

State the evidence first, then ask the question.

Examples:

> **One thing stands out: Competitor B has six distinct ads pushing the same assessment while the others mostly ask for a demo. Want me to unpack what they're doing there?**

> **Competitor C is saying one thing on LinkedIn and something materially different on Meta, but both sets of ads converge on the same landing page. Want me to trace that campaign?**

> **Competitor A has several recent variants around one offer and almost every one points to the same destination. Want me to break down that bet?**

Use interactive choices:

- **Dig into this**
- **That's enough for now**

Stop.

## No genuine outlier

If nothing is distinctive enough to support a specific curiosity question, do not manufacture one.

Say:

> I don't see one competitor behavior distinctive enough to justify a deeper branch from this dataset.

Then stop.

---

# Step 13 — Second pass: exact triggering evidence only

Run only if the user chooses **Dig into this**.

The second pass must start from the exact ad records that caused the curiosity question.

Do not restart generic competitor research.

Create an internal set:

`triggering_ads`

It contains only the ads relevant to the anomaly.

For those ads, compare:

- body copy
- headlines
- CTA
- active/recent dates
- format metadata
- platform
- destination URLs
- variant/collation data when present
- impression/targeting fields only when actually returned

---

# Step 14 — Landing-page pass

Take the destination URLs from `triggering_ads`.

## URL sources

### Meta
Use the destination/link URLs available in the returned creative snapshot/card data.

### LinkedIn
Use returned `clickUrl`.

## Dedupe destinations

Normalize URLs by:
- following redirects
- removing obviously non-semantic tracking parameters for comparison
- preserving the final real page URL

Do not treat tracking-only URL differences as separate landing pages.

## Visit scope

Open a small number of relevant destination pages.

Default maximum:
- up to 5 unique landing pages

If 20 ads all point to the same page, inspect that page once.

## On each page, inspect only

### Hero promise
What the page leads with.

### Offer
What the visitor gets / is being asked to do.

### Proof
Customer logos, named evidence, numbers, testimonials, case-study references, product evidence.

### Conversion action
Demo form, signup, download, register, calculator, assessment, contact, etc.

### Message continuation
Does the landing page continue the ad's promise, broaden it, or shift to a different pitch?

## One-hop rule

Follow at most **one additional same-domain page** only when it directly helps understand the exact campaign.

Allowed examples:
- linked case study
- pricing page
- product page
- assessment page
- event registration page

Do not crawl the whole site.

## Blocked/inaccessible page

If a page is blocked, deleted, native-only, requires login, or fails to load:

> Landing page unavailable for inspection.

Continue using the ad evidence.

Do not invent what was on the page.

---

# Step 15 — Deep-dive output

Keep this concise.

Show:

### Deep Dive — [Competitor / Campaign Family]

**What they appear to be doing**  
One concise interpretation of the campaign based on the exact ad family + landing pages.

**What changes across the ads**  
The observed hooks/messages/CTAs/formats that vary.

**What stays constant**  
The offer, destination, core claim, product, or CTA pattern that remains stable.

**What happens after the click**  
How the landing page continues or changes the ad message, what proof it adds, and what conversion action it asks for.

**What this means for us**  
2–3 grounded implications using the seller context.

Examples of valid implications:
- a message the competitor appears to emphasize heavily
- an offer structure worth comparing against the seller's current motion
- an area where the seller should avoid sounding identical
- a campaign angle worth testing because it is comparatively underrepresented and the seller can credibly support it

Do not write a 20-page strategy report.
Do not call the campaign successful.

After this output, continue to the single action gate below.

---

# Step 16 — Action gate: turn the insight into an audience

Ask one natural next-step question tied to the exact deep-dive insight:

> **Want me to turn this angle into a custom ad audience you can actually target?**

Buttons:
- **Build the audience**
- **That’s enough for now**

Stop.

If the user chooses **That’s enough for now**, end cleanly.

If the user chooses **Build the audience**, continue to Step 17.

---

# Step 17 — Build the audience with MoltSets

This branch uses MoltSets. Do not substitute another people database unless the user explicitly asks.

## 17A — Check MoltSets quietly

If MoltSets is connected, continue without setup narration.

If it is not connected, say:

> MoltSets isn’t connected in this session yet. Connect MoltSets and I can continue from the audience step without repeating the competitor research.

If the current Claude client exposes a MoltSets connector setup flow or connection instructions, use that exact UI/tooling. Do not invent credentials, commands, endpoint names, or setup steps.

After MoltSets is connected, resume at Step 17B. Do not rerun the ad research.

Stop until MoltSets is available.

## 17B — Translate the campaign insight into people-search criteria

Use only what is supported by:
- the seller context
- the selected competitor campaign insight
- the ad/landing-page evidence

Build a compact audience hypothesis such as function/title, seniority, company size, industry and market. Do not infer demographic traits or sensitive characteristics.

Show in chat:

### Custom Audience

| Targeting dimension | Criteria | Why |
|---|---|---|
| Function / titles | ... | ... |
| Seniority | ... | ... |
| Company type | ... | ... |
| Company size | ... | ... |
| Market | ... | ... |

Inspect the MoltSets tools exposed in the current session and use the people-search action whose description matches this job. If `search_people` is present, prefer it; otherwise use the current equivalent. Do not invent a MoltSets tool name or request field.

When the schema supports a small probe/limit, request one or a few results first. Inspect the returned person and use the tool's real total/count field when one is returned. If the probe is clearly wrong, correct the filters once before continuing.

## 17C — Audience-size gate

If MoltSets returned a reliable available-audience total/count for the exact search, show it. If not, do not invent one. Then ask:

> How many people should I build for the audience?

Offer concise choices such as:
- **500**
- **1,000**
- **Custom**

If fewer results exist, offer the available total instead.

Stop.

The user’s answer is approval to pull that many people. Paginate deliberately.

## 17D — Convert contacts into ad match keys

For the final selected people, inspect the MoltSets tools exposed in the current session and use the privacy-safe customer-match / SHA256 action that matches the identifiers you actually have. If `linkedin_to_sha256` is present, prefer it for LinkedIn-based records. Otherwise use the current documented equivalent exposed by MoltSets.

- Use only identifiers returned or already known from the people search.
- Batch only when the available tool supports batching.
- Do not expose raw personal emails merely for ad targeting.
- Keep contacts that do not resolve match keys in the review file, but mark them as unmatched.
- Do not invent hashes, endpoint names, or identifier conversions.

## 17E — Final output

Create two files:

1. `<campaign>-audience-review.csv` — name, title, company, LinkedIn URL, and the targeting rationale.
2. `<campaign>-audience-match-keys.csv` — the privacy-safe SHA256 match keys returned by MoltSets, flattened into upload-friendly rows.

Show:

### Custom Ad Audience Ready

- **People selected:** N
- **People with usable match keys:** M
- **Angle:** [one-line campaign insight]
- **Files:** review CSV + match-key CSV

End. No further forced gate.

---

# Interpretation rules

## Allowed

- "They are repeatedly using..."
- "In the ads retrieved..."
- "Several variants converge on..."
- "This competitor appears to be emphasizing..."
- "This is the only competitor in this sample using..."
- "This offer is comparatively underrepresented in the retrieved ads..."

## Not allowed without performance data

- "This is their winning ad"
- "This is their best campaign"
- "They are scaling it because it converts"
- "This is driving pipeline"
- "Their ROAS is strong"
- "The market prefers this"

## Longevity / variants

You may say:

> They have kept this message active across several variants.

You may not say:

> They kept it because it is profitable.

---

# Fallback matrix

| Situation | Response |
|---|---|
| Seller website missing | Ask for website or one-sentence offer |
| Competitors missing | Ask for competitor names/sites |
| Market missing | Ask for market/country |
| Competitor identity ambiguous | Show identity gate |
| Facebook page not found | Mark Meta identity unresolved; continue LinkedIn |
| LinkedIn identity not found | Mark LinkedIn identity unresolved; continue Meta |
| Meta scraper fails | Mark scraper error; do not call it zero ads |
| LinkedIn scraper fails | Mark scraper error; do not call it zero ads |
| Meta zero ads | Show exact no-public-Meta-ads wording |
| LinkedIn zero ads | Show exact no-public-LinkedIn-ads wording |
| Both zero for one competitor | Show exact no-public-ads-found wording |
| All competitors zero | Stop; no analysis |
| No distinct curiosity | Stop after map + 3 findings |
| Landing page blocked | Say unavailable; use ad data only |
| Destination URL missing | Deep dive ad data only; do not guess destination |
| Too many duplicate ads | Dedupe, preserve meaningful variants |
| Huge ad library | Keep default cap; expand only for relevant competitor |
| MoltSets unavailable at audience step | Stop after deep dive; do not substitute another data provider |
| Audience probe is wrong | Correct filters once before pulling volume |
| Contact has no match keys | Keep in review CSV as unmatched; do not invent identifiers |

---

# Recording rules

The ideal visible sequence is:

1. User: seller + competitors + market
2. `Seller Context` table
3. one short visible line says the public ads are being pulled
4. Apify retrieval runs without raw actor/debug output taking over the screen
5. `Competitor Ad Map`
5. `What I Think Matters` — exactly 3 observations
6. one unique curiosity question
7. user clicks `Dig into this`
8. Claude reuses the exact triggering ads
9. Claude opens the returned landing pages
10. concise `Deep Dive` output
11. “Build the audience” / “That’s enough for now” gate
12. if selected, MoltSets audience criteria + size gate
13. privacy-safe custom-audience CSVs
14. stop

## Keep the screen clean

- do not show raw actor JSON
- do not show every ad
- do not show 30 landing-page URLs
- do not show internal scoring
- do not show Apify connection/setup when connected
- do not show long technical explanations of the actor

---

# Main output templates

## Seller Context

| Question | Answer |
|---|---|
| What we sell | ... |
| Who we sell to | ... |
| Core outcome | ... |

## Competitor Ad Map

| Competitor | Meta ads | LinkedIn ads | Main campaign families | Repeated message | Offer | CTA / destination |
|---|---:|---:|---|---|---|---|

## What I Think Matters

**1. [Finding]**  
...

**2. [Finding]**  
...

**3. [Finding]**  
...

## Curiosity gate

> **[Specific evidence-backed observation]. Want me to unpack what they're doing there?**

Buttons:
- **Dig into this**
- **That's enough for now**

## Deep Dive

### Deep Dive — [Competitor / Campaign]

**What they appear to be doing**  
...

**What changes across the ads**  
...

**What stays constant**  
...

**What happens after the click**  
...

**What this means for us**  
- ...
- ...
- ...

---

# Critical rules

- Use only the two specified Apify actors for ad retrieval.
- Verify advertiser identities before scraping.
- Ask for market if it is missing.
- Meta default: active ads + optional recent 30-day pass.
- LinkedIn default: market-specific last-30-days Ad Library URL, `skipDetails:false`.
- Normalize both outputs before reasoning.
- Keep zero ads, unresolved identity, and scraper failure as different states.
- Never turn zero public results into "they do not advertise."
- Never infer performance.
- Never visually analyze media you did not actually inspect.
- Build campaign families from observable message/offer/CTA/destination patterns.
- Main screen = one map + exactly 3 observations.
- Curiosity question must come from one specific, distinctive, evidence-backed competitor behavior.
- If no such behavior exists, do not force a curiosity branch.
- Second pass must reuse the exact triggering ad records.
- Second pass may inspect only the landing pages returned by those ads plus one directly relevant same-domain hop.
- Blocked pages remain unknown.
- Deep-dive output stays concise.
- After the deep dive, the only next action is the optional MoltSets custom-audience branch.
- Use the current MoltSets people-search action to find the audience and the current privacy-safe SHA256/customer-match action to create uploadable match keys. Prefer `search_people` / `linkedin_to_sha256` only when those exact tools are actually exposed.
- Never expose or invent raw personal data just to build an ad audience.
