# Facebook Ads Performance Calculator & Benchmarker (`fanndev/facebook-ads-benchmarker`) Actor

Benchmark a whole niche from Meta's public Ad Library: which CTAs, formats, headlines, landing domains, words and phrases dominate, and how long each choice stays on air. Longevity is the honest performance proxy - advertisers keep paying for creatives that work. Compares niches side by side.

- **URL**: https://apify.com/fanndev/facebook-ads-benchmarker.md
- **Developed by:** [Faisal Ahdan naufal](https://apify.com/fanndev) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Facebook Ads Performance Calculator & Benchmarker

Give it a niche. It samples what that niche is actually advertising on Meta right
now, and tells you what the niche has converged on: which calls-to-action,
which creative formats, which headlines, which landing domains, which words and
phrases — and **how long each of those choices stays on air**.

Run two niches and it also compares them side by side.

### Read this before you buy

**The Ad Library does not publish spend, impressions, CTR, CPM or ROAS for
commercial ads.** Meta publishes those only for political and issue ads. Any
tool claiming to calculate a commercial advertiser's CPM from public data is
guessing, and this one will not.

What public data *does* support is **revealed preference**. An advertiser keeps
paying for a creative that works and stops paying for one that does not, so how
long an ad has been running is a real signal — and it is one you can compute for
an entire niche. That is what this actor is built on, and every row says so:
each benchmark carries the `sampleSize` it came from, and the longevity metrics
are labelled as days on air, not as performance.

### What you get

**One `OVERVIEW` row per niche**

| Field | Meaning |
| --- | --- |
| `sampleSize` | ads the benchmark was computed from |
| `activeShare` | % of the sample still running |
| `videoShare` | % carrying a video creative |
| `distinctAdvertisers`, `adsPerAdvertiser` | is the niche crowded or dominated? |
| `medianCopyWords`, `p90CopyWords` | typical ad-copy length |
| `medianRunningDays`, `p90RunningDays`, `maxRunningDays` | how long ads survive here |

**Ranked rows** (`rank`, `value`, `count`, `share`):

- `TOP_CTA` — the buttons the niche uses
- `TOP_FORMAT` — VIDEO / IMAGE / DCO / carousel mix
- `TOP_ADVERTISER` — who is spending the most attention here
- `TOP_PLATFORM` — Facebook / Instagram / Messenger / Audience Network split
- `TOP_HEADLINE` — headlines repeated across the niche
- `TOP_LANDING_DOMAIN` — where the traffic is being sent
- `TOP_WORD`, `TOP_PHRASE_2`, `TOP_PHRASE_3` — the vocabulary of the niche

**Longevity rows** — median and p90 days on air, grouped by:

- `LONGEVITY_BY_CTA` — which button survives longest
- `LONGEVITY_BY_FORMAT` — video vs image, in days
- `LONGEVITY_BY_COPY_LENGTH` — short copy vs long copy, in days

**`COMPARISON` rows** when you run more than one niche: the same CTA, format,
word or phrase with its share in each niche next to it.

### Input example

```json
{
  "niches": [
    { "name": "skincare", "keywords": ["vitamin c serum", "retinol cream"] },
    { "name": "supplements", "keywords": ["protein powder", "creatine"] }
  ],
  "countries": ["US"],
  "sampleSizePerNiche": 200,
  "topN": 15,
  "includeAdSample": true
}
```

The simple form works too — `"niches": ["skincare serum", "protein powder"]` —
where each keyword becomes its own niche.

### How the sample is built

Each niche's ad budget is split across the searches that feed it, so a
two-keyword niche does not quietly collect twice as many ads as a one-keyword
niche and then look twice as confident. Ads are de-duplicated by archive id
across searches.

The sample is **not** filtered before it is measured. Filtering first would
benchmark the filter, not the niche.

### Honest limits

- Words are counted **once per ad**, not once per occurrence, so an ad that
  repeats its slogan six times does not outvote six advertisers who each said it
  once.
- Meta's dynamic-creative tokens (`{{product.name}}`) are stripped before word
  counting. They are real published text, but they are placeholders, and left in
  they would put "product" at the top of every ecommerce niche.
- Longevity groups with fewer than `minGroupSample` ads behind them are dropped
  rather than reported.
- `endDate` exists for running ads too — it is the current end of the flight, not
  a stop date — so "days on air" means *so far* for an active ad.
- A niche where the Ad Library returns nothing produces no benchmark rows and a
  warning in the log, not a fabricated zero.

### Running it on Apify: use a residential proxy for pagination

Facebook serves the **rendered first page** to any IP, including Apify's. But the
**first pagination request from a datacenter IP** comes back with
`Rate limit exceeded`, so a platform run with no proxy stops at the first 30 ads of each search, which caps the sample.

The rate limit is on Facebook's GraphQL endpoint, which every actor in this
family uses for its second page onwards. It was measured on 2026-09-20 with the
Ad Library actor, three runs of the same search within a minute:

| Run | Result |
| --- | --- |
| Apify, no proxy | 30 results, 1 page — log: `Rate limit exceeded` |
| Apify, `RESIDENTIAL` proxy | 60 results, 4 pages |
| Local machine, no proxy | 70 results, 5 pages |

So: switch the Apify proxy on and pick the **RESIDENTIAL** group whenever you
want more than the first page. Running from your own machine needs no proxy at
all.

The actor logs a warning naming the rate limit when it hits one, so a short run
is never silently mistaken for a short result set.

# Actor input Schema

## `niches` (type: `array`):

What to benchmark. Simple form: one keyword per line, each treated as its own niche. Grouped form (paste as JSON): \[{"name": "skincare", "keywords": \["serum", "moisturiser"], "advertisers": \["123456789"]}] so several searches feed one benchmark.

## `countries` (type: `array`):

ISO country codes. The Ad Library is country-scoped, so benchmarking two countries shows you how the same niche is sold in each.

## `sampleSizePerNiche` (type: `integer`):

The budget is shared across the searches that make up a niche, so a two-keyword niche does not quietly collect twice the ads of a one-keyword niche. Every benchmark row reports the sampleSize it was computed from.

## `activeStatus` (type: `string`):

Benchmarking currently-running ads tells you what the niche is doing now. Include stopped ads to see what it has tried.

## `adType` (type: `string`):

Leave on 'All ads' for commercial benchmarking.

## `mediaType` (type: `string`):

Restrict the sample to one creative type. Leaving this on 'Any' is what produces the image-vs-video split in the output.

## `searchType` (type: `string`):

Unordered matches the words in any order; exact phrase matches the phrase as typed.

## `publisherPlatforms` (type: `array`):

Restrict the sample to ads delivered on these Meta surfaces.

## `contentLanguages` (type: `array`):

Two-letter language codes, e.g. en, id, es. Worth setting when benchmarking a non-English market.

## `topN` (type: `integer`):

How many entries to keep in each top-N ranking (CTAs, words, headlines, advertisers).

## `minGroupSample` (type: `integer`):

Groups thinner than this are dropped from the longevity rankings. A median computed from two ads is noise wearing a number's clothes.

## `includeAdSample` (type: `boolean`):

Append the sampled ads as AD\_SAMPLE rows so you can check any benchmark against the creatives behind it.

## `exportFormats` (type: `array`):

Also write the results to the key-value store in these formats. The dataset is always produced regardless.

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

Recommended on the Apify platform if you want more than the first 30 ads of each search, which caps the sample. Facebook serves the rendered first page fine from any IP, but on Apify's datacenter IPs it answers the very first pagination request with "Rate limit exceeded" - measured 2026-09-20. Switching the Apify proxy on with the RESIDENTIAL group restored full pagination in the same test (60 ads over 4 pages against 30). Running from your own machine, no proxy is needed at all. The actor logs a warning naming this when it happens, so a short run is never silently mistaken for a short result set.

## Actor input object example

```json
{
  "niches": [
    "skincare serum",
    "protein powder"
  ],
  "countries": [
    "US"
  ],
  "sampleSizePerNiche": 150,
  "activeStatus": "active",
  "adType": "all",
  "mediaType": "all",
  "searchType": "keyword_unordered",
  "publisherPlatforms": [],
  "contentLanguages": [],
  "topN": 10,
  "minGroupSample": 3,
  "includeAdSample": false,
  "exportFormats": [],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (type: `string`):

Benchmark rows per niche, the ad sample they were computed from, and any error rows.

# 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 = {
    "niches": [
        "skincare serum",
        "protein powder"
    ],
    "countries": [
        "US"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("fanndev/facebook-ads-benchmarker").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 = {
    "niches": [
        "skincare serum",
        "protein powder",
    ],
    "countries": ["US"],
}

# Run the Actor and wait for it to finish
run = client.actor("fanndev/facebook-ads-benchmarker").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 '{
  "niches": [
    "skincare serum",
    "protein powder"
  ],
  "countries": [
    "US"
  ]
}' |
apify call fanndev/facebook-ads-benchmarker --silent --output-dataset

```

## MCP server setup

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

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/LIbhaKY6QDPMkoXbY/builds/2O9Tbz94Z1b3VyQ0t/openapi.json
