# Facebook Ads Library Competitor Tracker (`herazur/meta-ads-competitor-change-radar`) Actor

Track competitor Facebook and Instagram ads using Meta Ad Library data. Detect new and stopped ads, copy, CTA, landing-page, offer, creative, and media-mix changes over time.

- **URL**: https://apify.com/herazur/meta-ads-competitor-change-radar.md
- **Developed by:** [Furkan Toluç](https://apify.com/herazur) (community)
- **Categories:** Social media, Automation, Lead generation
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $590.00 / 1,000 competitor reports

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Meta Ads Competitor Change Radar

Track new, stopped, changed, long-running, and variant-heavy Facebook and Instagram ads by competitor.

Meta Ads Competitor Change Radar is a stateful competitive-intelligence Apify Actor. It answers one narrow question: **What changed in my competitors' public Meta advertising since the last time I checked?**

It is designed for paid media agencies, DTC brands, performance marketers, competitive-intelligence teams, and growth marketers. It is not a generic scraper, performance detector, AI analyzer, or dashboard.

### What it does

For each competitor, the Actor:

1. validates one HTTPS Facebook Page or advertiser-scoped Meta Ad Library URL;
2. calls `curious_coder/facebook-ads-library-scraper` in an isolated child run;
3. normalizes the public evidence and validates that it represents one advertiser;
4. compares it with the last successful snapshot stored in a named key-value store;
5. detects supported changes using stable ad IDs;
6. groups conservative creative families and extracts duration, media, CTA, landing-domain, and English-oriented offer signals;
7. writes one concise Dataset item per competitor; and
8. advances state only after the report has been accepted.

The parent Actor does not scrape Facebook directly. It requires no Meta login or Meta API token. It uses the Apify run's authenticated execution context to call the upstream Actor, whose usage may have its own cost.

### Input

```json
{
  "monitorKey": "skincare-us",
  "competitors": [
    {
      "name": "Competitor A",
      "pageUrl": "https://www.facebook.com/examplebrand"
    },
    {
      "name": "Competitor B",
      "pageUrl": "https://www.facebook.com/examplebrand2"
    }
  ],
  "country": "US",
  "maxAdsPerCompetitor": 100,
  "durableAdDays": 30,
  "resetBaseline": false,
  "debug": false
}
```

Reuse the same `monitorKey`, country, and normalized competitor name on future runs. That tuple selects the previous snapshot. The first successful run creates a `BASELINE`; later runs compare against it. `resetBaseline: true` deliberately ignores the previous snapshot and replaces it only after successful report output.

The MVP accepts 1–5 competitors, requests 20–250 ads per competitor, and uses a 7–180 day long-running threshold. URLs must use HTTPS and a real `facebook.com` host or subdomain. Keyword inputs are not accepted. `ALL` is not accepted as a country because its reliability has not been established for the upstream source.

### Change language

- `LIKELY_NEW`: a previously unseen, high-confidence ad ID has a start time at or after the previous snapshot minus a six-hour clock-skew window.
- `NEWLY_OBSERVED`: a previously unseen stable ID clearly predates the prior snapshot, or its start date is unknown. It is not presented as a newly launched ad.
- `STOPPED_CONFIRMED`: the same high-confidence ID was previously active and is now explicitly inactive or has acquired a credible end date.
- `NOT_OBSERVED_THIS_RUN`: a previously observed high-confidence ID is absent from the current sample.
- `REACTIVATED`: the same high-confidence ID changed from explicitly inactive to active.
- `COPY_CHANGED`, `CTA_CHANGED`, and `LANDING_CHANGED`: both snapshots contain evidence for the relevant field and its deterministic fingerprint changed on the same stable ID.

**Absence never proves an ad stopped.** Meta Ad Library results can be sampled or limited, so absence produces `NOT_OBSERVED_THIS_RUN`, never `STOPPED_CONFIRMED`. Disappearing low-confidence derived IDs do not produce strong disappearance or stopped claims.

The status is `CHANGES_DETECTED` only when there is at least one likely-new, explicitly stopped, reactivated, or supported copy/CTA/landing change. `NEWLY_OBSERVED` or `NOT_OBSERVED_THIS_RUN` alone yields `NO_CONFIRMED_CHANGE`.

### Current snapshot intelligence

Every successful report includes observed/active/inactive counts, evidence coverage, media mix, top CTAs and landing domains, deterministic offer signals, up to ten oldest active ads, long-running counts, and conservative creative-family counts.

`LONG_RUNNING` means only that an ad is explicitly active and its public dates show at least `durableAdDays` of duration. It does not establish profitability.

`VARIANT_HEAVY` means an observed creative family contains at least three related variants. It does not establish spend, scaling, conversion quality, or profitability.

Offer extraction is deterministic and English-focused. It recognizes explicit signals such as percentage or fixed discounts, free shipping/trials/consultations/gifts, BOGO, promo codes, and limited-time phrases. It does not infer ambiguous offers.

### Creative-family grouping

Grouping is deterministic and false-merge averse:

1. equal Meta `collationId`: confidence `1.0`;
2. exact creative fingerprint: confidence `0.95`;
3. token Jaccard similarity ≥ `0.82`, only with the same known media type and exactly the same registrable landing domain: confidence `0.75`.

The implementation uses Union-Find. It does not use image similarity, embeddings, OCR, media downloads, or AI.

### Evidence strength and report threshold

Evidence strength is a transparent 0–100 score:

```text
round(
  stableIdCoverage × 45
  + startDateCoverage × 20
  + activeStatusCoverage × 20
  + contentCoverage × 15
)
```

A successful report requires a succeeded child run, at least one valid normalized ad, unambiguous advertiser identity, stable-ID coverage of at least 70%, and evidence strength of at least 60. Failures produce an uncharged `analysis_error` and never overwrite prior state.

### Output

The default Dataset contains exactly one item per requested competitor: either a `competitor_report` or an `analysis_error`. Its overview surfaces competitor, status, evidence, observed and active ads, likely new, confirmed stopped, long-running, variant-heavy families, primary change, and snapshot time.

The default run key-value store contains:

- `OUTPUT`: run-level counts;
- `REPORT.md`: a human-readable report; and
- `EVIDENCE.json`: normalized evidence from the current run only.

With `debug: true`, the default store also receives at most five raw and five normalized diagnostic records per competitor. Debug output never includes credentials.

Persistent comparison state lives separately in the named store `meta-ads-change-radar-state-v1` (configurable through `STATE_STORE_NAME`). Keys are `state-` plus 24 hexadecimal SHA-256 characters derived from monitor key, country, and normalized competitor name. State stores only compact diff evidence, uses `schemaVersion: 1`, and increments a revision. A compare-before-write check preserves existing history if concurrent runs race.

If the confidently resolved page ID changes for the same state key, the Actor reports `COMPETITOR_IDENTITY_CHANGED` and creates a fresh baseline instead of comparing different advertisers.

### Upstream call

One child Actor run is made per competitor, with controlled concurrency. The generated input is exactly:

```json
{
  "urls": [
    {
      "url": "<validated and normalized competitor Facebook URL>"
    }
  ],
  "scrapeAdDetails": false,
  "limitPerSource": 100,
  "scrapePageAds.activeStatus": "all",
  "scrapePageAds.sortBy": "most_recent",
  "scrapePageAds.countryCode": "US",
  "runTag": "<monitorKey>-<timestamp>-<competitorIndex>"
}
```

`limitPerSource` and `countryCode` reflect public input. No `period` is set, because that would damage long-running-ad evidence. No `count`, proxy override, or Meta credential is passed. Dataset retrieval is paginated and stops at `maxAdsPerCompetitor + 50`. Only `SUCCEEDED` child runs are analyzed.

Environment variables:

```text
META_ADS_ACTOR_ID=curious_coder/facebook-ads-library-scraper
META_ADS_CHILD_CONCURRENCY=2
META_ADS_CHILD_TIMEOUT_SECS=300
STATE_STORE_NAME=meta-ads-change-radar-state-v1
ENABLE_REPORT_CHARGING=false
```

### Monetization scaffold

Charging is disabled by default and must remain disabled for initial private validation. When explicitly enabled later, successful `BASELINE`, `CHANGES_DETECTED`, and `NO_CONFIRMED_CHANGE` reports are pushed with the single custom event `competitor_report`. State advances only if that event is accepted. Analysis errors are always pushed without a paid event. No dollar price is hard-coded.

When Pay-Per-Event is later configured in Apify Console, remove or avoid the automatic `apify-default-dataset-item` paid event. The intended model is Actor Start plus `competitor_report` only; otherwise one result could be charged twice.

### Local development

Node 24 is the production target.

```bash
npm ci
npm run typecheck
npm test
npm run build
npm run check
npx --yes apify-cli@latest validate-schema
npm audit
```

No live upstream run is part of automated validation. See [LIVE\_VALIDATION.md](LIVE_VALIDATION.md) for the manual sequence after private deployment.

### Private deployment readiness

1. Create a private GitHub repository named `meta-ads-competitor-change-radar`.
2. Push this project without local `storage`, `.env`, or credentials.
3. Create a private Apify Actor connected to that repository.
4. Build with the included Node 24 Dockerfile.
5. Keep `ENABLE_REPORT_CHARGING=false`.
6. Follow `LIVE_VALIDATION.md`, beginning with a one-competitor, 20-ad schema probe.
7. Do not publish or enable pricing until the validation targets are met.

### Limitations

- Results are sampled public Ad Library evidence and may be truncated.
- The upstream Actor is an external dependency and its schema can change.
- Only publicly available Ad Library fields are used.
- Offer extraction is English-oriented.
- No performance metrics, spend, profitability, conversion, or ROAS inference is made.
- Meta can change data availability or visibility.
- The parent does not crawl landing pages, fetch media, or enrich advertisers.

Long-running, variant-heavy, and newly observed signals are public-ad-library evidence. They do not prove profitability, spend, ROAS, or conversion performance.

# Actor input Schema

## `monitorKey` (type: `string`):

Reuse the same monitor key on future runs to compare against the previous snapshot.

## `competitors` (type: `array`):

One to five Facebook Page URLs or advertiser-scoped Meta Ad Library URLs. Each URL must identify one advertiser.

## `country` (type: `string`):

Uppercase ISO 3166-1 alpha-2 country code.

## `maxAdsPerCompetitor` (type: `integer`):

Requested upstream sample size. The upstream Actor can return a modest overage.

## `durableAdDays` (type: `integer`):

An explicitly active ad is LONG\_RUNNING when its observed duration reaches this threshold.

## `resetBaseline` (type: `boolean`):

Ignore the prior snapshot for supplied competitors and replace it only after successful report output.

## `debug` (type: `boolean`):

Save at most five raw and normalized records per competitor in the run's default key-value store.

## Actor input object example

```json
{
  "monitorKey": "skincare-us",
  "competitors": [
    {
      "name": "Competitor A",
      "pageUrl": "https://www.facebook.com/examplebrand"
    },
    {
      "name": "Competitor B",
      "pageUrl": "https://www.facebook.com/examplebrand2"
    }
  ],
  "country": "US",
  "maxAdsPerCompetitor": 100,
  "durableAdDays": 30,
  "resetBaseline": false,
  "debug": false
}
```

# Actor output Schema

## `reports` (type: `string`):

One concise report or analysis error per requested competitor.

## `summary` (type: `string`):

No description

## `markdownReport` (type: `string`):

No description

## `evidence` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("herazur/meta-ads-competitor-change-radar").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("herazur/meta-ads-competitor-change-radar").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call herazur/meta-ads-competitor-change-radar --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,herazur/meta-ads-competitor-change-radar"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/tt5N2xYwjh194IheZ/builds/6JWjKblWMCzeGXITh/openapi.json
