# Competitor Ads Monitor — New Google & Meta Ads, No Login (`chimerical_quicklime/competitor-ads-monitor`) Actor

Watch competitor domains or advertisers on Google Ads Transparency (and Meta Ad Library where reachable) and get only NEW ad creatives since the last run, with format, first/last shown dates, days running and preview link. Daily schedule. No login. MCP-ready. $10 per 1,000 alerts.

- **URL**: https://apify.com/chimerical\_quicklime/competitor-ads-monitor.md
- **Developed by:** [Khrystyna Skotte](https://apify.com/chimerical_quicklime) (community)
- **Categories:** SEO tools, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 changed ads

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Competitor Ads Monitor (Google + Meta)

Tell me when my competitors launch new ads. This actor checks the **Google Ads Transparency Center** and the **Meta Ad Library** (Facebook, Instagram, Messenger, Audience Network, Threads) for a list of competitors and outputs **only the creatives that are new since the last run**. It keeps a per-monitor memory of every ad it has already reported, so a scheduled run gives you a clean diff instead of the same thousand ads every day.

No login, no API key, no browser. Both platforms are read from their public ad-library endpoints over plain HTTP.

> Meta Ad Library support is experimental. From Apify's cloud, Meta currently answers anonymous requests with a rate limit (datacenter IPs) or a 403 challenge (residential IPs), so a Meta watch logs a warning and the run continues with Google results. Add "meta" to `platforms` to try it; the recipe works from residential home connections and may start working from the cloud again.

### What you get per changed ad

| Field | Description |
|---|---|
| `platform` | `google` or `meta` |
| `competitor` | The competitor exactly as you entered it |
| `advertiserId`, `advertiserName` | Google advertiser ID (`AR…`) / Meta page ID and the resolved name |
| `creativeId` | Google creative ID (`CR…`) / Meta ad archive ID |
| `adFormat` | `Text`, `Image`, `Video` (Google) or `Image`, `Video`, `Carousel`, `Dpa`… (Meta) |
| `headline`, `body` | Ad copy when the platform exposes it |
| `imageUrl`, `previewUrl`, `videoUrl` | Creative media |
| `landingUrl`, `ctaText` | Click-through URL and call to action (Meta; Google text ads carry `landingUrl`) |
| `publisherPlatforms` | Meta placements (`FACEBOOK`, `INSTAGRAM`, …) |
| `firstShownDate`, `lastShownDate`, `daysRunning` | Run dates |
| `regions` | Regions in which the ad was found |
| `adUrl` | Permalink to the ad in the platform's library |
| `changeType` | `new` (never reported before) or `reactivated` (an ad that had been idle for 30+ days and is showing again) |
| `firstSeenAt`, `monitorId` | When this monitor first reported the ad |

### Input

| Field | Default | Notes |
|---|---|---|
| `competitors` | `["nike.com", "adidas.com"]` | Domains, brand names, Google advertiser IDs (`AR…`) or Meta page IDs (numeric). Domains work best on Google; brand names are resolved to the verified Meta page with the most likes. |
| `platforms` | `["google", "meta"]` | Either or both. |
| `regions` | `["US"]` | ISO country codes. `ALL` = worldwide. Each extra region costs one extra scan per competitor. |
| `maxNewAdsPerCompetitor` | `5` | Stop scanning a competitor/platform once this many changed ads are found. |
| `firstRunMode` | `emitAll` | See *Baseline* below. |
| `webhookUrl` | `""` | Optional POST target, see *Webhook*. |
| `monitorId` | `default` | Name of the watch list. State lives in the named key-value store `ads-monitor-<hash of monitorId>`. |
| `maxItems` | `10` | Hard cap on output per run. |
| `proxyConfiguration` | off | Not needed normally. Switch on Apify residential proxy only if a platform starts blocking. |

### Scheduling a monitor

1. Create a **Task** from this actor with your competitors, regions, a `monitorId` such as `running-shoes` and `firstRunMode: "baseline"`.
2. Run it once. It records every current ad as already-seen and outputs nothing.
3. Add a **Schedule** (daily is plenty; ad libraries update once a day). Every later run outputs only the ads whose first-shown date is on or after the previous run (with a 7-day safety lookback) and that the monitor has not reported before.
4. Read the dataset of each run, or point `webhookUrl` at Slack, Zapier, Make, n8n or your own endpoint.

Keep the same `monitorId` on every run. A new `monitorId` starts a fresh baseline. To reset a monitor, delete the `ads-monitor-<hash>` key-value store (or its `STATE` record) from Storage.

#### Baseline vs emitAll

- `emitAll` (default): the first time a competitor is seen, its current ads are output (up to the caps). Good for a one-off look and for testing.
- `baseline`: the first time a competitor is seen, its current ads are silently recorded. Nothing is output until something genuinely new appears. Use this for scheduled monitors.

After the first run both modes behave identically. Adding a competitor to an existing monitor treats only that competitor as a first run.

#### Change types

- `new`: the ad ID was never reported by this monitor and its first-shown date is at or after the last run (minus 7 days). Older ads that the monitor simply never scanned before are absorbed into the baseline silently.
- `reactivated`: an ad that was already known but whose last-shown date jumped by more than 30 days since the monitor last saw it, i.e. the competitor revived a paused creative.

### Webhook payload

If `webhookUrl` is set, the actor sends one `POST` per run (even when nothing changed) with `Content-Type: application/json`:

```json
{
  "monitorId": "running-shoes",
  "runId": "abc123",
  "newCount": 3,
  "ads": [ { "platform": "meta", "competitor": "nike.com", "changeType": "new", "headline": "…", "adUrl": "…", "...": "..." } ]
}
```

`ads` holds the first 50 changed ads; the full list is always in the run's dataset.

### Platform coverage and limits

**Google Ads Transparency Center**

- Resolves domains through Google's domain index and names through advertiser suggestions. Pass an `AR…` advertiser ID to skip resolution.
- Google has no first-shown-date filter, so each run scans the advertiser's creatives ordered by last-shown date, up to 160 per competitor per region, and filters by first-shown date client-side. Very large advertisers that launch more than ~160 creatives between runs will have the overflow picked up on later runs.
- Google exposes text, image and video creatives with first/last shown dates and days running. Headline/body text is only present for text ads; image ads return the rendered image.

**Meta Ad Library**

- Resolves brand names and domains to a Facebook page through the Ad Library typeahead (exact name match, then verified page with the most likes). Pass a numeric page ID for full control. The page ID is cached in the monitor state after the first run.
- Only **active** ads are scanned, newest collations first, up to 160 per competitor per region. The Ad Library's own start-date filter is not used because it silently drops ads it should return; recency is decided from each ad's start date instead.
- Meta hides spend and impression ranges for non-political ads; those are not included.
- The Ad Library front door serves a JavaScript verification challenge; the actor replays it automatically. If Meta starts rejecting Apify's datacenter IPs, enable the residential proxy option. When Meta cannot be reached in a run, the run continues with Google and logs an error rather than failing.

**General**

- Up to 160 creatives are scanned per competitor, platform and region per run.
- State (`seen` ad IDs, per-competitor watermarks, resolved advertiser IDs) lives in a named key-value store so it survives across runs and Tasks.
- Pricing: a small start fee plus a per-record fee for each changed ad reported. Runs that find nothing new cost only the start fee.

# Actor input Schema

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

Domains (nike.com), brand names (Nike), Google advertiser IDs (AR…) or Meta page IDs (numeric) to watch.

## `platforms` (type: `array`):

Ad libraries to check: Google Ads Transparency Center and/or Meta (Facebook/Instagram) Ad Library. Google is the verified default; Meta Ad Library is experimental and may return rate-limit errors from cloud IPs (handled as a warning).

## `regions` (type: `array`):

ISO country codes to check (US, GB, DE…). Use ALL for worldwide.

## `maxNewAdsPerCompetitor` (type: `integer`):

Stop scanning a competitor on a platform once this many changed ads have been found.

## `firstRunMode` (type: `string`):

What to do the first time a competitor is seen: emit its current ads (emitAll) or just record them as the baseline and emit nothing (baseline). Use baseline for scheduled monitors.

## `webhookUrl` (type: `string`):

Optional. POSTed once per run with {monitorId, runId, newCount, ads: \[first 50]}. Leave empty to disable.

## `monitorId` (type: `string`):

Name of this watch list. Each monitor keeps its own seen-ads state; reuse the same ID on every scheduled run.

## `maxItems` (type: `integer`):

Maximum number of changed ads to output per run across all competitors.

## `proxyConfiguration` (type: `object`):

Optional. Not needed normally; switch on Apify residential proxy only if a platform starts blocking.

## Actor input object example

```json
{
  "competitors": [
    "nike.com",
    "adidas.com"
  ],
  "platforms": [
    "google"
  ],
  "regions": [
    "US"
  ],
  "maxNewAdsPerCompetitor": 5,
  "firstRunMode": "emitAll",
  "webhookUrl": "",
  "monitorId": "default",
  "maxItems": 10,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `records` (type: `string`):

Dataset of new / reactivated ad creatives found this run (JSON).

# 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("chimerical_quicklime/competitor-ads-monitor").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("chimerical_quicklime/competitor-ads-monitor").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 chimerical_quicklime/competitor-ads-monitor --silent --output-dataset

```

## MCP server setup

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

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/hLq45vOXaOFmD9kei/builds/cvagP1li0qLtmL0vl/openapi.json
