# Google Ads Brand Bidding Monitor (`herazur/google-ads-brand-bidding-monitor`) Actor

Monitor branded Google searches to detect competitor ads, affiliate brand bidding, unauthorized resellers, new paid-search bidders, and ad-copy changes across countries and devices.

- **URL**: https://apify.com/herazur/google-ads-brand-bidding-monitor.md
- **Developed by:** [Furkan Toluç](https://apify.com/herazur) (community)
- **Categories:** Marketing, SEO tools, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.50 / 1,000 branded search checkeds

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

## Google Ads Brand Bidding Monitor

Monitor branded Google searches across countries and devices. Detect known competitors, unrecognized advertisers, authorized partners, repeated bidders, new landing domains, and ad-copy changes over time.

This Actor is a branded paid-search monitoring and compliance-signal product. It uses Apify's maintained `apify/google-search-scraper` as its collection primitive and adds deterministic normalization, evidence, classification, compact history, and conservative change detection. It is not a general Google scraper, an ad-spend estimator, or a legal trademark detector.

### What it does

For one brand per run, the Actor:

1. Checks up to ten branded terms in as many as five countries, on desktop and/or mobile.
2. Performs one to three independent observations per country-device market.
3. Extracts paid Google results and normalizes their registrable landing domains.
4. Classifies advertisers as `OFFICIAL`, `AUTHORIZED`, `KNOWN_COMPETITOR`, or `UNRECOGNIZED` from your lists.
5. Measures check coverage, landing-domain coverage, observation depth, and evidence strength.
6. Compares every successful market with its previous successful baseline under the same `monitorId`.
7. Writes one Dataset item per market, a run-level `REPORT.md`, an `OUTPUT` record, measured `COSTS.json`, and compact named-store state.

It is designed for paid search teams, affiliate managers, performance marketers, brand protection teams, agencies, and SaaS or ecommerce brands.

### Why repeated observations matter

Google Ads are probabilistic: auctions, targeting, budgets, and personalization cause results to vary. A duplicate ad row within one SERP is not repeated evidence. `REPEATED_BIDDER` requires the same landing domain in at least two independent observations of the current country-device market.

Most monitors should use the default of two observations. A single observation remains useful, but cannot produce strong repeated evidence.

Google paid-search observations depend on an external upstream Actor and can occasionally be slow or unavailable. Each observation is protected by a bounded runtime and charge ceiling. The parent makes one child call per logical observation and does not add a retry loop on top of the upstream Actor's own behavior. Failed observations are treated as missing evidence rather than as evidence that no ads exist.

### Input

```json
{
  "monitorId": "acme-brand-monitor",
  "brandName": "Acme",
  "brandTerms": ["acme", "acme pricing", "acme coupon", "acme login"],
  "officialDomains": ["acme.com"],
  "authorizedDomains": ["partner-a.com"],
  "competitorDomains": ["competitor-one.com", "competitor-two.com"],
  "countries": ["US", "GB", "DE"],
  "devices": ["desktop", "mobile"],
  "observationsPerMarket": 2,
  "languageCode": "en",
  "debug": false,
  "resetState": false
}
```

`monitorId` is the stable identity for a monitoring configuration. If you leave it empty, it is derived from the brand name. It is normalized and hashed before being used in a named Key-Value Store; arbitrary input is never used directly as a storage name. `resetState` removes only this monitor's `STATE` record.

The field was previously called `monitorKey`. Existing tasks, schedules, and API calls that still send `monitorKey` keep working and keep the same monitoring history; output records keep the `monitorKey` field name for compatibility.

The Store's example input (Squarespace, two branded terms, US, desktop, one observation) is a deliberately small demo that usually finishes in under a minute. Ads are probabilistic, so a single demo run may or may not observe sponsored results.

Domains may be entered as domains, subdomains, or URLs. They are reduced with Public Suffix List-aware logic, so `store.partner.co.uk` becomes `partner.co.uk` rather than `co.uk`.

### Output

The default Dataset contains one item per brand + country + device report—not one item per ad. A report includes a flat Store-friendly summary plus complete nested evidence:

```json
{
  "brandName": "Acme",
  "country": "US",
  "device": "mobile",
  "reportStatus": "SUCCESS",
  "baselineStatus": "EXISTING_BASELINE",
  "evidenceStrength": 100,
  "coverage": {
    "expectedChecks": 8,
    "successfulChecks": 8,
    "successfulCheckRate": 1,
    "landingDomainCoverage": 1
  },
  "summary": {
    "sponsoredAdsObserved": 6,
    "uniqueAdvertiserDomains": 3,
    "knownCompetitorsObserved": 1,
    "unrecognizedDomainsObserved": 1,
    "repeatedBidders": 2,
    "newLandingDomains": 1,
    "newAdCopies": 1
  },
  "advertisers": [],
  "findings": [],
  "changes": {
    "newDomains": ["competitor-two.com"],
    "newAdFingerprints": ["..."],
    "previouslyObservedDomainsNotSeenThisRun": ["coupon-example.com"]
  }
}
```

Evidence strength measures data quality, not advertiser severity. Its documented formula is:

```text
round(60 × successful-check coverage
    + 20 × effective landing-domain coverage
    + 20 × observation depth)
```

When successfully collected SERPs contain no ads, paid-ad parsing and landing-domain coverage are reported as `null` (not applicable) and do not penalize evidence strength. When no checks succeed, evidence strength is zero. Observation depth is 0.5 for one usable observation and 1 for two or more.

Report status thresholds are `SUCCESS` at 90%+ successful checks, `PARTIAL` from 50% through below 90%, and `INSUFFICIENT_EVIDENCE` below 50%. Absence-based signals require at least 80% coverage.

### Signals

- `COMPETITOR_ON_BRAND_TERM`: a landing domain matches your known competitor list.
- `AUTHORIZED_PARTNER_OBSERVED`: a landing domain matches your authorized list.
- `NEW_UNRECOGNIZED_ADVERTISER`: an unrecognized domain is new relative to a prior successful baseline.
- `REPEATED_BIDDER`: a domain appeared in at least two independent current-run observations.
- `TRADEMARK_IN_AD_COPY_SIGNAL`: monitored brand wording appeared in title or description; this is not a legal determination.
- `POTENTIAL_AFFILIATE_POLICY_RISK`: an unrecognized advertiser appeared on a coupon, promo, discount, or deal-oriented branded query; no affiliate relationship is asserted.
- `NEW_LANDING_DOMAIN` and `NEW_AD_COPY`: changes from the previous successful baseline.
- `OWN_AD_NOT_OBSERVED`, `PREVIOUS_BIDDER_NOT_OBSERVED`, and `NO_SPONSORED_ADS_OBSERVED`: conservative absence observations, emitted only with adequate check coverage.
- `INSUFFICIENT_EVIDENCE`: missing SERPs make absence-based conclusions unsafe.

### Monitoring over time

The first successful run establishes a baseline and never calls all current advertisers “new.” Later successful runs compare domains and normalized ad-copy fingerprints with the preceding successful market state. Incomplete markets do not overwrite a healthy baseline.

Save the Actor input as an Apify Task and schedule it daily or weekly to turn one-time branded SERP observations into a monitoring history. Reuse exactly the same `monitorId` for that monitor. This repeated use is the product's recurring-value mechanism.

### Pricing

The Actor uses Apify Pay Per Event pricing with one `search-checked` event: **$0.005 per branded Google search results page checked** on the Apify Free plan, with lower prices on paid Apify plans (down to $0.0035). One page is one brand term in one country on one device in one observation, so a run costs:

```text
terms × countries × devices × observations per market × $0.005
```

For example, the Store's demo input (2 terms × 1 country × 1 device × 1 observation) is two search checks ($0.01). Three terms in two countries on desktop and mobile with two observations each is 24 search checks ($0.12). Market reports, advertisers, ads, and findings are not charged separately, and pages that the upstream Actor fails to return are not charged.

Google results are collected by Apify's `apify/google-search-scraper`, whose own usage (about $0.0045 per results page at the time of writing) is billed separately to your account by that Actor. A small Actor start fee is also charged per run.

Before each observation starts, the Actor checks that your run's spending limit can cover every results page of that observation. When the limit is reached, it stops starting new observations, writes the reports collected so far, and finishes with the status `SUCCEEDED_BUDGET_LIMIT_REACHED`. `COSTS.json` records child run IDs, charged event counts, and actual upstream `usageTotalUsd` when Apify exposes it; otherwise it explicitly reports cost as unavailable. The `OUTPUT` record includes `searchPagesChecked` and `searchChecksCharged`.

### Integrations

Consume Dataset and Key-Value Store output through Apify's normal API and integration ecosystem, including schedules, webhooks, Make, Zapier, Google Sheets, and other standard platform connections. This Actor intentionally does not build custom Slack, email, CRM, or dashboard integrations.

### Limitations and interpretation

- Google Ads are probabilistic. **Not observed does not mean stopped bidding.**
- Missing upstream results are missing evidence, never evidence that no ads existed.
- Advertiser classifications depend on user-supplied official, authorized, and competitor domain lists.
- `UNRECOGNIZED` does not mean fraudulent, unauthorized, illegal, or infringing.
- Brand wording in ad copy is an observation, not a legal trademark conclusion.
- The Actor does not crawl landing pages, follow redirects, estimate ad spend, or estimate lost revenue.
- Country targeting follows the upstream Actor's country-level search controls; it is not proof of every user's experience within that country.

### Development and deployment

The child-observation safety policy is centralized in `src/config/child-observation-policy.ts`. Its initial calibration ceilings are 240 seconds and $0.50 per child observation. Before starting a child run, the Actor reads the upstream Actor's current pricing metadata. If the configured ceiling is below the upstream `minimalMaxTotalChargeUsd`, preflight fails explicitly and does not start a run or silently raise the ceiling. These are maximum safety bounds, not expected costs or a per-keyword pricing formula; revisit them after additional controlled live probes.

```bash
npm ci
npm run validate
apify push
```

A paid live probe runs only when both `APIFY_TOKEN` is available and `LIVE_TEST=1` is explicitly set. Keep it to one country, one device, one or two terms, one observation, and a maximum total charge of $0.50 where supported. Zero observed ads is a valid probe outcome.

### Post-launch product metrics

Targets: at least 10 external testers, 100 successful market reports, 3 users creating schedules, 30% or higher seven-day repeat-run rate, at least 95% deterministic classification precision, at least 90% landing-domain coverage, and zero false stopped-bidding claims. The leading behavioral metric is reuse of the same `monitorId`.

Reconsider further investment if known-positive branded queries rarely expose ads, landing-domain coverage stays below 80%, current upstream economics prevent a healthy margin, repeat behavior remains below 15% after 100+ reports, classification errors exceed 5%, support becomes excessive, or users consistently require a standalone dashboard.

# Actor input Schema

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

Stable name for this monitoring configuration. Reuse the same value on scheduled runs to build monitoring history and detect changes. If left empty, it is derived from the brand name.

## `brandName` (type: `string`):

Brand wording used for copy-presence signals.

## `brandTerms` (type: `array`):

One to ten exact branded queries to observe. The Actor does not invent more terms.

## `officialDomains` (type: `array`):

Domains owned by the monitored brand. URLs and subdomains are normalized to registrable domains.

## `authorizedDomains` (type: `array`):

Known affiliates, resellers, or other authorized advertisers.

## `competitorDomains` (type: `array`):

User-supplied competitors. The Actor only labels a domain as a known competitor when it is listed here.

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

One to five two-letter country codes supported by the maintained Google Search Results Scraper.

## `devices` (type: `array`):

Generate an independent market report for each selected device.

## `observationsPerMarket` (type: `integer`):

Independent child runs per country-device market. Repeated evidence requires at least two.

## `languageCode` (type: `string`):

Language code passed to the upstream search-language and interface-language controls.

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

Store bounded diagnostics, normalized records, child run IDs, and cost metadata in DEBUG.json.

## `resetState` (type: `boolean`):

Delete only the state record derived from this monitor ID before collecting new observations.

## Actor input object example

```json
{
  "monitorId": "acme-brand-monitor",
  "brandName": "Acme",
  "brandTerms": [
    "squarespace",
    "squarespace website builder"
  ],
  "officialDomains": [
    "squarespace.com"
  ],
  "authorizedDomains": [],
  "competitorDomains": [
    "wix.com",
    "godaddy.com"
  ],
  "countries": [
    "US"
  ],
  "devices": [
    "desktop"
  ],
  "observationsPerMarket": 1,
  "languageCode": "en",
  "debug": false,
  "resetState": false
}
```

# Actor output Schema

## `marketReports` (type: `string`):

One dataset item per brand-country-device market.

## `report` (type: `string`):

Human-readable report covering every processed market.

## `runSummary` (type: `string`):

Machine-readable OUTPUT record.

## `costTelemetry` (type: `string`):

Actual child Actor usage metadata when available; never an invented estimate.

# 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 = {
    "monitorId": "squarespace-us-desktop",
    "brandName": "Squarespace",
    "brandTerms": [
        "squarespace",
        "squarespace website builder"
    ],
    "officialDomains": [
        "squarespace.com"
    ],
    "competitorDomains": [
        "wix.com",
        "godaddy.com"
    ],
    "countries": [
        "US"
    ],
    "devices": [
        "desktop"
    ],
    "observationsPerMarket": 1
};

// Run the Actor and wait for it to finish
const run = await client.actor("herazur/google-ads-brand-bidding-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 = {
    "monitorId": "squarespace-us-desktop",
    "brandName": "Squarespace",
    "brandTerms": [
        "squarespace",
        "squarespace website builder",
    ],
    "officialDomains": ["squarespace.com"],
    "competitorDomains": [
        "wix.com",
        "godaddy.com",
    ],
    "countries": ["US"],
    "devices": ["desktop"],
    "observationsPerMarket": 1,
}

# Run the Actor and wait for it to finish
run = client.actor("herazur/google-ads-brand-bidding-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 '{
  "monitorId": "squarespace-us-desktop",
  "brandName": "Squarespace",
  "brandTerms": [
    "squarespace",
    "squarespace website builder"
  ],
  "officialDomains": [
    "squarespace.com"
  ],
  "competitorDomains": [
    "wix.com",
    "godaddy.com"
  ],
  "countries": [
    "US"
  ],
  "devices": [
    "desktop"
  ],
  "observationsPerMarket": 1
}' |
apify call herazur/google-ads-brand-bidding-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,herazur/google-ads-brand-bidding-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/H9L8fuhvNttWpizA3/builds/cQwjCl9rv5asm9sDx/openapi.json
