# Competitor Ad Change Brief — BYOD Evidence Monitor (`zinin/competitor-ad-change-brief`) Actor

Compare buyer-authorized Meta, Google Ads, or LinkedIn evidence snapshots. Get one auditable competitor-ad change report with baseline truth, materiality, confidence, evidence gaps, and unsent human-review actions. No source fetching or campaign automation.

- **URL**: https://apify.com/zinin/competitor-ad-change-brief.md
- **Developed by:** [Tim Zinin](https://apify.com/zinin) (community)
- **Categories:** Marketing, Automation, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $42.50 / 1,000 delivered competitor-ad change reports

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/platform/actors/running/actors-in-store#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

## Competitor Ad Change Brief — BYOD Evidence Monitor

Compare buyer-owned Meta, Google Ads, or LinkedIn evidence snapshots and receive one auditable change report with materiality, confidence, explicit gaps, and unsent review actions.
**Built for:** Performance marketing teams, agencies, brand operators, competitive-intelligence analysts, and data teams that already have lawful ad-library exports or normalized evidence rows.
**Commercial unit:** one delivered competitor-ad change report. **Live pricing contract:** $0.05 per delivered `result-found` report plus the configured start event (starting at $0.005). Invalid input, source failure, non-comparable evidence, ambiguous delivery, and true replay are not newly delivered paid reports.

![Competitor Ad Change Brief — BYOD Evidence Monitor: buyer evidence to review-ready outcome](https://raw.githubusercontent.com/TimmyZinin/apify-actor-assets/835cb06dd010a2eb2708dcb632a5b61384e2b11c/commercial115/competitor-ad-change-brief/readme-hero.webp)

### What you get

Competitive monitoring is often sold as collection. The expensive failure happens after collection: two exports cover different advertisers, countries, adapters, or time boundaries and a spreadsheet calls the difference a market event. Competitor Ad Change Brief — BYOD Evidence Monitor keeps that missing context attached to the result.
Provide exactly one bounded source: inline normalized ad rows or an Apify Dataset ID. Also provide a stable watch ID, explicit coverage evidence, and a source contract naming the production source Actor/version, adapter, query scope, and advertiser identity.
The output is decision support, not campaign control. Route material changes to a human campaign review; preserve uncertain or non-comparable observations without sending, pausing, or editing an ad. The Actor never sends a message, pauses an ad, changes a bid, edits a creative, files a complaint, or presents an inference as a platform fact.

#### The useful result in plain language

- One stable watch and report identity instead of an undocumented spreadsheet diff.
- Explicit baseline/current boundaries and an auditable source contract.
- Deterministic changes separated from evidence confidence and materiality.
- Negative and unchanged evidence retained when it affects interpretation.
- Data gaps and source risks stored as fields instead of hidden in prose.
- A conservative human-review action with `safeToAutomate=false`.
- One Dataset report for analysis and KVS `OUTPUT` for terminal workflow truth.
- Replay-safe delivery and PPE evidence for one commercial report unit.

### Who uses it

- Comparing periodic ad-library exports with a reproducible per-ad identity model.
- Building an internal review queue for new creatives, changed offers, landing domains, CTAs, formats, or stopped ads.
- Separating source coverage, evidence confidence, and change materiality in a warehouse or BI workflow.
- Giving an analyst a concise report while preserving the normalized evidence needed to reproduce it.

### Not a fit

- Scraping Meta, Google, or LinkedIn directly; no platform collector is bundled.
- Automatic campaign changes, takedowns, outreach, bidding, or legal conclusions.
- Claiming spend, impressions, conversion, reach, ownership, intent, or performance when those facts are absent from the supplied evidence.
- Comparing snapshots whose adapter, query scope, entity identity, or coverage is not compatible.
  If you still need a collector, connect an approved upstream Actor or internal export first. Do not paste private account credentials into this Actor. Keeping collection separate makes the provenance and permission boundary reviewable.

### Evidence-to-action workflow

![Competitor Ad Change Brief — BYOD Evidence Monitor: evidence-to-action workflow](https://raw.githubusercontent.com/TimmyZinin/apify-actor-assets/835cb06dd010a2eb2708dcb632a5b61384e2b11c/commercial115/competitor-ad-change-brief/readme-workflow.webp)

1. Collect the narrow advertiser cohort through a lawful source workflow.
2. Normalize it into inline rows or an Apify Dataset and declare coverage evidence.
3. Bind the observation to a stable watch ID and exact source contract.
4. Validate identity, bounds, adapter compatibility, coverage, and source exclusivity.
5. Acquire the per-watch lease so concurrent observations cannot race the baseline.
6. Compare the compatible snapshot with the persisted baseline deterministically.
7. Build one report with changes, materiality, confidence, gaps, and unsent actions.
8. Write Dataset, settle the result event, and persist KVS `OUTPUT`.
9. Route the report to a human analyst and keep external action outside the Actor.

### How to run

The Actor validates the declared source contract, normalizes stable ad identity, obtains a per-watch lease, and compares the current compatible snapshot with the persisted baseline. A first compatible observation creates a baseline report. Later complete comparable observations can classify new, changed, stopped, or unchanged ads. Two complete absences are required before an ad becomes stopped; partial or unavailable observations never advance that missing streak. Optional BYOK explanation is commentary only and cannot change deterministic findings.
Three concepts must remain separate:
| Concept | Question answered | It does not prove |
|---|---|---|
| Change | What differs between compatible observations? | Why a competitor changed something. |
| Materiality | How large is the deterministic observed difference? | Revenue, spend, conversion, or strategy. |
| Confidence | How well contract, coverage, and evidence support the classification? | Commercial importance or future outcome. |
A report can show a material change with low confidence when coverage is weak. It can show high confidence and no important change. Store those axes in separate columns and keep every gap visible.

### Input contract

Use **Try for free** in Apify Console or submit the same JSON through API, Task, schedule, webhook, Make, n8n, or an MCP-enabled agent. The public Task is a bounded contract example:

```json
{
  "schemaVersion": "1.0",
  "requestId": "auto",
  "watchId": "nike-us-demo-bootstrap",
  "coverage": { "status": "complete", "comparable": true, "closed": true, "evidenceRef": "buyer://public-demo/coverage", "expectedRows": 1 },
  "sourceContract": {
    "productionActorId": "zinin/brand-evidence-source",
    "productionActorVersion": "0.1.0",
    "adapter": "google-ads",
    "adapterVersion": "1.0.0",
    "queryScope": { "advertiserId": "AR16735076323512287233", "country": "US" },
    "entityIdentity": { "advertiserId": "AR16735076323512287233" }
  },
  "rows": [
    {
      "platform": "google",
      "advertiserId": "AR16735076323512287233",
      "pageId": null,
      "adId": null,
      "creativeId": "CR16854540057166479361",
      "isActive": true,
      "format": "IMAGE",
      "copy": { "primaryText": "Unisex Nike Sportswear Tech Fleece", "headline": "Product Listing Ad Rendering Service", "description": "Nike Sportswear hoodie" },
      "cta": "Shop now",
      "landingDomains": ["nike.com"],
      "media": ["https://tpc.googlesyndication.com/archive/simgad/3463432638140085355?sig=demo"],
      "offer": { "kind": null, "value": null, "currency": null, "code": null },
      "evidence": { "sourceRef": "https://adstransparency.google.com/advertiser/AR16735076323512287233/creative/CR16854540057166479361", "sourceRowId": "CR16854540057166479361" },
      "scrapedAt": "2026-08-06T11:03:09Z"
    }
  ],
  "maxAds": 100,
  "maxTotalChargeUsd": 0.055,
  "analysisModel": "openai/gpt-4o-mini"
}
```

#### Input responsibilities

| Area | Required discipline |
|---|---|
| Request identity | Reuse an ID for replay suppression; use a new ID for a genuinely new paid observation. |
| Watch identity | Keep one watch tied to one intended entity and query scope. |
| Source exclusivity | Provide inline rows or a Dataset ID, never both. |
| Coverage | Declare completeness, comparability, closure, and an evidence reference. |
| Source contract | Name upstream Actor/version, adapter/version, query scope, and entity identity. |
| Bounds | Keep rows, strings, media arrays, and total input inside the published limits. |
| Secrets | Put tokens in platform secrets, never Dataset evidence. |
Strict validation prevents malformed identity, ambiguous source selection, invalid nested rows, missing coverage, and incomplete source contracts from advancing a baseline. A failed or partial observation must never become a clean zero-change result.

### Output stores

Default Dataset contains the delivered business report for tables, exports, BI, and review queues. Default Key-Value Store record `OUTPUT` contains authoritative terminal, source, baseline, delivery, pricing, replay, error, and bootstrap state.
Do not infer success from process exit or Dataset count alone. Read `OUTPUT`, verify terminal and delivery state, then read the Dataset report. This matters for bootstrap, unavailable source, non-comparable evidence, replay, and ambiguous delivery.

#### Core report fields

| Field | Meaning |
|---|---|
| `reportId / eventId` | Stable identity for the delivered monitor outcome. |
| `watchId / entityId` | Stable buyer-defined watch identity used for state and downstream joins. |
| `status` | Baseline, complete, explicit partial, unavailable, non-comparable, or terminal delivery state. |
| `changes` | Bounded deterministic change records with before/after evidence where available. |
| `materialityScore / materialityBand` | Magnitude of observed changes; separate from evidence confidence. |
| `confidenceScore / confidenceBand` | Support from compatible source contract, closed coverage, and usable evidence. |
| `sourceEvidence` | Sanitized source references and row identities retained for review. |
| `dataGaps / confidenceRisks` | Missing, partial, conflicting, or inferred facts that constrain interpretation. |
| `recommendedAction / actionPriority` | Bounded human-review routing label, not an external action. |
| `safeToAutomate` | False for campaign or business action in this product. |
| `unsentActions` | Review suggestions carrying explicit not-sent and review-required guardrails. |
| `billing` | Delivery and charge evidence for the single paid report unit. |

#### Shared decision semantics

| Field | Contract |
|---|---|
| `recordType` | Stable semantic family for routing and schema evolution. |
| `entityId` | Stable monitor identity, not automatically a legal or platform-owned identity. |
| `observedAt` | Actor observation/finalization time, distinct from upstream publication time. |
| `firstSeenAt` / `lastSeenAt` | Explicit evidence boundaries; never infer tenure from missing observations. |
| `before` / `after` | Bounded comparable context supporting a transition. |
| `confidenceReasons` / `confidenceRisks` | Facts that support or weaken the classification. |
| `failureType` / `retryable` | Machine-readable handling kept separate from a successful report. |
| `billing` | Delivery event and charge receipt attached to the result. |

### Evidence and boundaries

#### `reportId / eventId`

**Meaning:** Stable identity for the delivered monitor outcome.
**Review questions:** Is the value present and type-correct? Is it supported by source evidence or a documented deterministic transformation? Does its meaning remain the same after export? Would a missing value be incorrectly converted to zero, false, or unchanged?
**Downstream rule:** Store the raw value with the observation time, source contract, confidence, and gaps. Do not overwrite it with a CRM disposition or an analyst conclusion.

#### `watchId / entityId`

**Meaning:** Stable buyer-defined watch identity used for state and downstream joins.
**Review questions:** Is the value present and type-correct? Is it supported by source evidence or a documented deterministic transformation? Does its meaning remain the same after export? Would a missing value be incorrectly converted to zero, false, or unchanged?
**Downstream rule:** Store the raw value with the observation time, source contract, confidence, and gaps. Do not overwrite it with a CRM disposition or an analyst conclusion.

#### `status`

**Meaning:** Baseline, complete, explicit partial, unavailable, non-comparable, or terminal delivery state.
**Review questions:** Is the value present and type-correct? Is it supported by source evidence or a documented deterministic transformation? Does its meaning remain the same after export? Would a missing value be incorrectly converted to zero, false, or unchanged?
**Downstream rule:** Store the raw value with the observation time, source contract, confidence, and gaps. Do not overwrite it with a CRM disposition or an analyst conclusion.

#### `changes`

**Meaning:** Bounded deterministic change records with before/after evidence where available.
**Review questions:** Is the value present and type-correct? Is it supported by source evidence or a documented deterministic transformation? Does its meaning remain the same after export? Would a missing value be incorrectly converted to zero, false, or unchanged?
**Downstream rule:** Store the raw value with the observation time, source contract, confidence, and gaps. Do not overwrite it with a CRM disposition or an analyst conclusion.

#### `materialityScore / materialityBand`

**Meaning:** Magnitude of observed changes; separate from evidence confidence.
**Review questions:** Is the value present and type-correct? Is it supported by source evidence or a documented deterministic transformation? Does its meaning remain the same after export? Would a missing value be incorrectly converted to zero, false, or unchanged?
**Downstream rule:** Store the raw value with the observation time, source contract, confidence, and gaps. Do not overwrite it with a CRM disposition or an analyst conclusion.

#### `confidenceScore / confidenceBand`

**Meaning:** Support from compatible source contract, closed coverage, and usable evidence.
**Review questions:** Is the value present and type-correct? Is it supported by source evidence or a documented deterministic transformation? Does its meaning remain the same after export? Would a missing value be incorrectly converted to zero, false, or unchanged?
**Downstream rule:** Store the raw value with the observation time, source contract, confidence, and gaps. Do not overwrite it with a CRM disposition or an analyst conclusion.

#### `sourceEvidence`

**Meaning:** Sanitized source references and row identities retained for review.
**Review questions:** Is the value present and type-correct? Is it supported by source evidence or a documented deterministic transformation? Does its meaning remain the same after export? Would a missing value be incorrectly converted to zero, false, or unchanged?
**Downstream rule:** Store the raw value with the observation time, source contract, confidence, and gaps. Do not overwrite it with a CRM disposition or an analyst conclusion.

#### `dataGaps / confidenceRisks`

**Meaning:** Missing, partial, conflicting, or inferred facts that constrain interpretation.
**Review questions:** Is the value present and type-correct? Is it supported by source evidence or a documented deterministic transformation? Does its meaning remain the same after export? Would a missing value be incorrectly converted to zero, false, or unchanged?
**Downstream rule:** Store the raw value with the observation time, source contract, confidence, and gaps. Do not overwrite it with a CRM disposition or an analyst conclusion.

#### `recommendedAction / actionPriority`

**Meaning:** Bounded human-review routing label, not an external action.
**Review questions:** Is the value present and type-correct? Is it supported by source evidence or a documented deterministic transformation? Does its meaning remain the same after export? Would a missing value be incorrectly converted to zero, false, or unchanged?
**Downstream rule:** Store the raw value with the observation time, source contract, confidence, and gaps. Do not overwrite it with a CRM disposition or an analyst conclusion.

#### `safeToAutomate`

**Meaning:** False for campaign or business action in this product.
**Review questions:** Is the value present and type-correct? Is it supported by source evidence or a documented deterministic transformation? Does its meaning remain the same after export? Would a missing value be incorrectly converted to zero, false, or unchanged?
**Downstream rule:** Store the raw value with the observation time, source contract, confidence, and gaps. Do not overwrite it with a CRM disposition or an analyst conclusion.

#### `unsentActions`

**Meaning:** Review suggestions carrying explicit not-sent and review-required guardrails.
**Review questions:** Is the value present and type-correct? Is it supported by source evidence or a documented deterministic transformation? Does its meaning remain the same after export? Would a missing value be incorrectly converted to zero, false, or unchanged?
**Downstream rule:** Store the raw value with the observation time, source contract, confidence, and gaps. Do not overwrite it with a CRM disposition or an analyst conclusion.

#### `billing`

**Meaning:** Delivery and charge evidence for the single paid report unit.
**Review questions:** Is the value present and type-correct? Is it supported by source evidence or a documented deterministic transformation? Does its meaning remain the same after export? Would a missing value be incorrectly converted to zero, false, or unchanged?
**Downstream rule:** Store the raw value with the observation time, source contract, confidence, and gaps. Do not overwrite it with a CRM disposition or an analyst conclusion.

### Pricing

This Actor uses PPE: $0.05 per delivered `result-found` report plus the configured start event (starting at $0.005). The live Apify pricing panel is the source of truth for the current tier. Billing is part of the result contract:

1. Validate input and reserve idempotent state.
2. Build and validate a useful report without advancing baseline prematurely.
3. Push the Dataset row with the configured result event.
4. Verify the aggregate charge receipt for the current write.
5. Advance durable delivery and baseline state only through the safe path.
6. Persist final `OUTPUT`, including ambiguous delivery.
7. Prevent a compatible replay from creating a second paid report.
   The current-run receipt must prove an exact named `result-found` counter delta of `+1` for the delivered report plus a valid linked aggregate receipt. `eventChargeLimitReached: true` can accompany a successfully delivered final unit; it is not evidence that the unit failed. Never retry an ambiguous request blindly: reconcile the original run, Dataset, KVS, and event ledger first.

### Decision routing

Route a material, adequately supported change to a human campaign review. Route an incomplete or
non-comparable observation to evidence repair, and route any delivery ambiguity to reconciliation.
Never map a change code directly to pausing, bidding, messaging, publishing, or another platform
action: the report deliberately keeps `safeToAutomate:false` and records every proposed action as unsent.

### Integration recipes

Keep `APIFY_TOKEN` in an environment variable or secret store.

#### cURL

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/zinin~competitor-ad-change-brief/runs?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @public-task.json
```

#### JavaScript

```javascript
import { ApifyClient } from 'apify-client';
import input from './public-task.json' with { type: 'json' };

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('zinin/competitor-ad-change-brief').call(input);
const output = await client.keyValueStore(run.defaultKeyValueStoreId).getRecord('OUTPUT');
if (!output?.value) throw new Error('Missing OUTPUT record');
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log({ output: output.value, reports: items });
```

#### Python

```python
import json
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ['APIFY_TOKEN'])
with open('public-task.json', encoding='utf-8') as handle:
    actor_input = json.load(handle)
run = client.actor('zinin/competitor-ad-change-brief').call(run_input=actor_input)
output = client.key_value_store(run['defaultKeyValueStoreId']).get_record('OUTPUT')
if not output: raise RuntimeError('Missing OUTPUT record')
items = list(client.dataset(run['defaultDatasetId']).iterate_items())
print({'output': output['value'], 'reports': items})
```

### Commercial playbooks

#### Human review queue

Key the destination by `entityId` and `eventId`. Create a task only for a delivered material change or a source/comparability issue. Include evidence, gaps, confidence, source contract, run ID, and observation time. Keep unsent actions as suggestions, never proof that action occurred.

#### Warehouse and BI

Store the raw report before flattening. Use separate columns for materiality, confidence, coverage, status, and recommended action. Retain a source-contract digest so analysts can exclude non-comparable history after an adapter or scope change.

#### Schedules

Use a cadence appropriate to source freshness. Generate a new request ID per intended observation. If a run is partial or unavailable, keep it visible and do not manufacture unchanged output.

#### n8n, Make, Zapier, and webhooks

Wait for terminal completion, read KVS `OUTPUT`, branch on explicit status, and fetch Dataset only for delivered reports. Route validation to input repair, retryable source failure to bounded retry, and delivery unknown to reconciliation.

#### Agents and MCP

A standard Apify MCP integration can expose the same schema-bound Actor call as a tool; this Actor does not add a separate always-on tool endpoint. Require an agent to cite source evidence, coverage, confidence risks, data gaps, and observation time. Prohibit invented spend, performance, intent, identity, legality, or causality. Preserve `safeToAutomate=false`.

### Sources and rights

This Actor does not crawl advertising platforms. It analyzes buyer-supplied rows or a buyer-selected Dataset and records the declared upstream contract. The buyer remains responsible for lawful source access, export rights, retention, and the truth of the supplied coverage claim.
Use opaque business identifiers where possible. Do not submit tokens, cookies, private dashboards, personal profiles, confidential strategy, or unnecessary personal data. Configure retention and access controls for Dataset, KVS, baselines, and exports. Technical accessibility does not establish permission to collect, retain, or resell data.
Limited permissions, bounded input, deterministic normalization, and fixed source behavior reduce risk. Optional BYOK calls are fixed-endpoint, bounded, and explanation-only. Provider failure cannot erase deterministic findings or advance billing/baseline state by itself.

### Honest limitations

- A reported creative change does not prove campaign strategy, spend, performance, infringement, or buyer intent.
- Source completeness is supplied by the caller and is validated structurally, not independently audited against a private ad platform account.
- Platform exports and upstream Actors may rename fields, paginate incompletely, omit removed ads, or change identifiers.
- Stopped detection requires two compatible complete absences; partial or unavailable observations are intentionally conservative.
- Optional LLM explanation may fail or be unavailable and never substitutes for deterministic changes or source evidence.
- A successful process exit is not enough; automation must read KVS `OUTPUT` and reconcile Dataset and billing state.
  Additional boundaries:
- Evidence describes the submitted observation, not the entire advertising market.
- Missing evidence is unknown, not zero.
- A source reference is a review pointer, not independent verification.
- A high-confidence factual change can still be commercially irrelevant.
- A low-confidence material-looking change requires more evidence, not faster automation.
- The Actor provides no legal, financial, investment, privacy, trademark, or platform-policy conclusion.

### Operating guide

- \[ ] Watch ID still represents the same advertiser and query scope.
- \[ ] Source Actor, version, adapter, and adapter version are expected.
- \[ ] Coverage supports the claimed comparison.
- \[ ] Observation time and upstream evidence time remain distinct.
- \[ ] Materiality and confidence are stored separately.
- \[ ] Every material finding has evidence or an explicit gap.
- \[ ] Partial and unavailable runs remain visible.
- \[ ] `safeToAutomate=false` blocks external action.
- \[ ] Dataset delivery and PPE reconcile with KVS `OUTPUT`.
- \[ ] Replays and retries cannot duplicate a paid report.

### Troubleshooting

#### Dataset is empty

Read KVS `OUTPUT`. Input may be invalid, watch bootstrap may be pending, source may be unavailable/non-comparable, delivery may need reconciliation, or the request may be a replay. Empty Dataset is not automatically a successful no-change result.

#### First observation has no historical changes

That is expected when no compatible baseline exists. Preserve the explicit baseline result; never backfill invented prior state.

#### Too many ads look new or stopped

Verify source contract, entity identity, scope, adapter version, pagination, and coverage. Changed identifiers or incomplete exports can create false differences.

#### Partial run did not advance missing streaks

That is deliberate. Partial absence cannot prove an ad stopped. Use a later complete compatible observation.

#### Optional explanation is unavailable

Use deterministic changes and evidence. Never retry commercial delivery only to obtain prose.

#### Event limit was reached

Inspect the named `result-found` counter before and after the current report. Only an exact `+1` delta plus the valid linked receipt proves settlement. Reconcile before retrying.

### Happy, partial, and failure output

These examples are projections of actual Apify runs, not invented sample outcomes. Identifiers, counters, statuses, and decision values were read back from the run, Dataset, and KVS records. The first example is a healthy production comparison. The second is the exact historical canary that exposed the first-invocation bootstrap defect addressed by the current implementation. Neither example claims campaign performance or business impact.

#### Comparable observation delivered on production

Run `5NsVfYTVEbzAebYmf` used production build `wBfp3Q84KfYN9UAra`. It completed `SUCCEEDED`, produced one Dataset report in Dataset `Hlvut2KqecKUQ35n0`, and stored terminal `OUTPUT` in KVS `fHFT9knhtOYqbxrgp`. The platform event ledger recorded one Actor start and one `result-found`. The Dataset and KVS projections agreed that one comparable Google Ads observation was processed against an existing baseline, no supported change was detected, and the baseline advanced.

```json
{
  "evidenceAccepted": true,
  "runId": "5NsVfYTVEbzAebYmf",
  "buildId": "wBfp3Q84KfYN9UAra",
  "status": "SUCCEEDED",
  "datasetId": "Hlvut2KqecKUQ35n0",
  "keyValueStoreId": "fHFT9knhtOYqbxrgp",
  "chargedEventCounts": {
    "apify-actor-start": 1,
    "result-found": 1
  },
  "datasetReport": {
    "reportId": "sha256:d0b8835c5a3632933792f66e5d6ad6bfa312f5ea1eea188fddf8f23de4e8836d",
    "requestId": "5NsVfYTVEbzAebYmf",
    "watchId": "nike-us-demo-bootstrap",
    "status": "complete",
    "baselineCreated": false,
    "coverage": {
      "status": "complete",
      "comparable": true,
      "acceptedRows": 1
    },
    "changeCount": 0,
    "decision": {
      "materialityScore": 0,
      "confidenceScore": 90,
      "recommendedAction": "NO_MATERIAL_AD_CHANGE",
      "actionPriority": "low",
      "safeToAutomate": false
    }
  },
  "output": {
    "status": "complete",
    "source": {
      "kind": "inline",
      "coverageStatus": "complete",
      "comparable": true,
      "inputRows": 1,
      "acceptedRows": 1,
      "invalidRows": 0,
      "truncated": false
    },
    "baseline": {
      "created": false,
      "advanced": true,
      "missingStreaksAdvanced": false
    },
    "delivery": {
      "resultEvents": 1,
      "chargeConfirmed": true,
      "ambiguous": false
    },
    "pricing": {
      "tier": "BRONZE",
      "startPriceUsd": 0.00475,
      "resultPriceUsd": 0.0475
    },
    "errors": []
  }
}
```

What this proves: a complete accepted observation can produce exactly one report, exactly one named result event, an explicit no-change decision, and an advanced compatible baseline. It does not prove that the upstream export captured every ad outside the caller-declared coverage contract, and it does not prove the absence of real-world campaign changes not represented in the supplied row.

#### Second accepted production comparison

Run `PnwdbR8ejvmW1yQH0` used the same production build `wBfp3Q84KfYN9UAra`
on 12 August 2026. It completed `SUCCEEDED`, wrote one report to Dataset
`XQHNBWSPecVczwnPW`, and stored `OUTPUT` in KVS `cmFUBQ5fLrJ0JV4nP`.
The platform ledger recorded start1/result1. The accepted inline row was compared
against the compatible `nike-us-demo-bootstrap` baseline. The report found zero
supported changes, assigned materiality `0/none`, evidence confidence `90/high`,
recommended `NO_MATERIAL_AD_CHANGE`, retained two explicit data gaps, and kept
`safeToAutomate:false`. Baseline state advanced; no action was sent.

```json
{
  "evidenceAccepted": true,
  "runId": "PnwdbR8ejvmW1yQH0",
  "buildId": "wBfp3Q84KfYN9UAra",
  "status": "SUCCEEDED",
  "startedAt": "2026-08-12T05:07:46.384Z",
  "finishedAt": "2026-08-12T05:07:50.982Z",
  "datasetId": "XQHNBWSPecVczwnPW",
  "keyValueStoreId": "cmFUBQ5fLrJ0JV4nP",
  "chargedEventCounts": {
    "apify-actor-start": 1,
    "result-found": 1
  },
  "usageTotalUsd": 0.00040797649284203845,
  "dataset": {
    "rows": 1,
    "reportId": "sha256:5ac14b51ae70d8a206cce55dd7dee92a41642b8f0d750203feebf290138a42c5",
    "watchId": "nike-us-demo-bootstrap",
    "status": "complete",
    "observedAt": "2026-08-12T05:07:50.152Z",
    "changeCount": 0,
    "materialityScore": 0,
    "materialityBand": "none",
    "confidenceScore": 90,
    "confidenceBand": "high",
    "recommendedAction": "NO_MATERIAL_AD_CHANGE",
    "actionPriority": "low",
    "safeToAutomate": false,
    "dataGaps": ["AD_PERFORMANCE_NOT_OBSERVED", "MEDIA_CONTENT_NOT_FETCHED"]
  },
  "output": {
    "status": "complete",
    "sourceAcceptedRows": 1,
    "baselineCreated": false,
    "baselineAdvanced": true,
    "resultEvents": 1,
    "chargeConfirmed": true,
    "ambiguous": false,
    "errors": []
  }
}
```

This independently confirms a second accepted paid comparison on the production
contract. It remains buyer-supplied evidence: the Actor did not log into Google Ads,
fetch the retained source URL, measure campaign performance, or verify complete
platform coverage.

#### Historical first-watch bootstrap failure

Run `BoebFM6W1GxY7PTRe` used candidate build `6cfeCwnT5Pex9jG0q`. It completed `SUCCEEDED` but produced no Dataset row and no result event. KVS `x3kak8YebUXRfYs3H` reported `watch_bootstrap` with one verified seed and a 2,000 ms wait requirement. This was a valid lock-protocol observation but an invalid first-use product outcome: the buyer had submitted compatible evidence and the Store contract promised that the same invocation would create a baseline report.

```json
{
  "evidenceAccepted": false,
  "runId": "BoebFM6W1GxY7PTRe",
  "buildId": "6cfeCwnT5Pex9jG0q",
  "status": "SUCCEEDED",
  "datasetId": "IPmfArQ8hkKztAlsb",
  "keyValueStoreId": "x3kak8YebUXRfYs3H",
  "datasetRows": 0,
  "chargedEventCounts": {
    "apify-actor-start": 1,
    "result-found": 0
  },
  "output": {
    "requestId": "BoebFM6W1GxY7PTRe",
    "watchId": "commercial115-competitor-20260811-r1-4d7c2a",
    "status": "watch_bootstrap",
    "baseline": {
      "created": false,
      "advanced": false,
      "missingStreaksAdvanced": false
    },
    "delivery": {
      "resultEvents": 0,
      "chargeConfirmed": false,
      "ambiguous": false
    },
    "bootstrap": {
      "status": "required",
      "seedCount": 1,
      "retryAfterMs": 2000
    },
    "errors": [
      {
        "code": "bootstrap_required",
        "retryable": true
      }
    ]
  }
}
```

The current runtime changes the orchestration without weakening the lock. When the first acquisition creates a seed, the same invocation waits only for the bounded consistency interval and attempts one reacquisition. Concurrent first invocations follow the same wait but still contend through the RequestQueue atomic lock, so at most one owner reads evidence or prepares delivery. A cleanup uncertainty, malformed seed, unavailable lock capability, or lost lease still fails closed. This historical result remains useful evidence of the defect; it is not presented as proof that the new code has already passed a cloud canary. That proof requires the separately controlled single candidate build and no-retry canary.

### Field dictionary

Use this mapping when landing results in a warehouse or routing them into a review system. Preserve the full report as the audit record; flattened columns are convenience indexes, not replacements for evidence.

| Field | Operational use | Boundary |
|---|---|---|
| `reportId` | Stable report join and reconciliation key. | Deterministic digest, not a signature. |
| `requestId` | Current invocation and delivery-intent identity. | `auto` resolves from the trusted platform run ID. |
| `watchId` | Long-lived comparison stream selected by the buyer. | Reusing it across different entities or scopes is invalid. |
| `observedAt` | Actor report construction time. | It is not the source collection timestamp. |
| `comparability` | Exact upstream Actor, version, adapter, scope, and entity contract. | A changed contract starts a new baseline. |
| `coverage` | Accepted source status and row count. | Complete means caller-declared closed evidence matched; it is not independently crawled. |
| `changes[].changeId` | Stable identifier for one supported change finding. | It reflects normalized supplied facts only. |
| `changes[].changeTypes` | Closed list of creative, offer, CTA, format, landing, new, or stopped changes. | No intent or performance is inferred. |
| `decisionSummary.materialityScore` | Deterministic prioritization signal. | Materiality is separate from confidence and business value. |
| `decisionSummary.confidenceScore` | Evidence confidence under the declared coverage contract. | It is not model accuracy or source exhaustiveness. |
| `decisionSummary.sourceEvidence` | Source-provided references and observation identifiers. | References are retained but never fetched or independently authenticated. |
| `decisionSummary.dataGaps` | Machine-readable reasons not to over-interpret the result. | A non-empty list should remain visible downstream. |
| `decisionSummary.recommendedAction` | Review routing code. | It is a suggestion, never an executed ad-platform action. |
| `decisionSummary.safeToAutomate` | Automation safety switch. | It is always false for business action in this product. |
| `unsentActions` | Human-review checklist derived from supported facts. | Every entry remains review-required and not sent. |
| `delivery.resultEvents` | Named result units confirmed for this invocation. | Replays report zero new delivery. |
| `delivery.ambiguous` | Manual-reconciliation flag after an uncertain push or counter. | Never blind-retry when true. |
| `baseline.advanced` | Whether the canonical watch state moved to the delivered observation. | Partial, failed, or ambiguous observations do not advance it. |
| `bootstrap.status` | Seed protocol state in KVS `OUTPUT`. | Normal first use is now completed inside one invocation; uncertain reconciliation can still surface explicitly. |

### FAQ

#### Does this Actor log into ad platforms?

No. It is BYOD evidence analysis. Supply lawful normalized rows or a Dataset produced by your own approved collection workflow.

#### Why does the first run return a report instead of changes?

A stateful comparison needs a compatible baseline. The first useful observation records that baseline explicitly so later comparisons have a defensible origin.

#### Can an incomplete export mark an ad as stopped?

No. Missing streaks advance only on complete comparable observations, and stopped requires two such absences.

#### Does the AI explanation decide materiality?

No. Deterministic comparison builds the changes and materiality. BYOK explanation is optional commentary and cannot mutate the result.

#### Can I automatically pause a competitor campaign?

No. The Actor cannot control another advertiser and emits only unsent review actions with `safeToAutomate=false`.

#### What should I store downstream?

Keep the report ID, watch ID, observation times, source contract, evidence, confidence, gaps, changes, recommended action, run ID, and billing receipt together.

### Support

For a reproducible issue, provide run ID, Actor version, sanitized input, source-contract shape, `OUTPUT` status, Dataset count, and whether it happened during baseline, comparison, replay, or reconciliation. Never send a token, private export, or signed storage URL.
Competitor Ad Change Brief — BYOD Evidence Monitor is intentionally conservative: it makes a bounded comparison explainable, preserves uncertainty, and routes evidence to a human. That is more commercially useful than a confident alert whose source scope, baseline, delivery, or meaning cannot be defended.

# Actor input Schema

## `schemaVersion` (type: `string`):

The supported input contract version. Keep 1.0 unless a published migration says otherwise.

## `requestId` (type: `string`):

Use auto for a Task-safe trusted Actor run ID; explicit values remain idempotency keys for direct callers.

## `watchId` (type: `string`):

Stable watch identity for one advertiser, source contract, and query scope. Changed methodology requires a new compatible baseline.

## `rows` (type: `array`):

One to 100 canonical rows; use this or datasetId.

## `datasetId` (type: `string`):

One buyer Dataset selected for read-only access.

## `coverage` (type: `object`):

Runtime requires a closed comparable declaration before advancing the baseline.

## `sourceContract` (type: `object`):

Upstream Actor identity, adapter, scope, and entity contract.

## `maxAds` (type: `integer`):

Bound the accepted evidence rows processed in one report so cost and comparison scope remain inspectable.

## `maxTotalChargeUsd` (type: `number`):

Caller charge cap for the Actor start and one result event; the paid path fails closed when the cap is insufficient.

## `openrouterApiKey` (type: `string`):

Secret caller-owned key used only for one bounded explanation and never stored in a Dataset or report.

## `analysisModel` (type: `string`):

Fixed explanation model identifier used only when the optional caller-owned key is supplied.

## Actor input object example

```json
{
  "schemaVersion": "1.0",
  "requestId": "auto",
  "watchId": "nike-us-google-ads",
  "rows": [
    {
      "platform": "google",
      "advertiserId": "AR-demo",
      "pageId": null,
      "adId": null,
      "creativeId": "CR-demo",
      "isActive": true,
      "format": "IMAGE",
      "copy": {
        "primaryText": "Example supplied copy",
        "headline": "Example headline",
        "description": null
      },
      "cta": "Shop now",
      "landingDomains": [
        "example.com"
      ],
      "media": [
        "buyer://media/fingerprint-1"
      ],
      "offer": {
        "kind": null,
        "value": null,
        "currency": null,
        "code": null
      },
      "evidence": {
        "sourceRef": "buyer://evidence/ad-1",
        "sourceRowId": "ad-1"
      },
      "scrapedAt": null
    }
  ],
  "datasetId": "buyerAdEvidenceDatasetId",
  "coverage": {
    "status": "complete",
    "comparable": true,
    "closed": true,
    "evidenceRef": "buyer://coverage/run-001",
    "expectedRows": 1
  },
  "sourceContract": {
    "productionActorId": "buyer/ad-evidence-source",
    "productionActorVersion": "1.0.0",
    "adapter": "google-ads",
    "adapterVersion": "1.0.0",
    "queryScope": {
      "advertiserId": "AR-demo",
      "country": "US"
    },
    "entityIdentity": {
      "advertiserId": "AR-demo"
    }
  },
  "maxAds": 100,
  "maxTotalChargeUsd": 0.055,
  "openrouterApiKey": "set-in-the-secret-input-field",
  "analysisModel": "openai/gpt-4o-mini"
}
```

# Actor output Schema

## `OUTPUT` (type: `string`):

Closed business output envelope. A new watch may first return status watch\_bootstrap with no report or result charge while the persistent RequestQueue seed is verified.

# 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 = {
    "schemaVersion": "1.0",
    "requestId": "auto",
    "watchId": "nike-us-demo-bootstrap",
    "rows": [
        {
            "platform": "google",
            "advertiserId": "AR16735076323512287233",
            "pageId": null,
            "adId": null,
            "creativeId": "CR16854540057166479361",
            "isActive": true,
            "format": "IMAGE",
            "copy": {
                "primaryText": "Unisex Nike Sportswear Tech Fleece",
                "headline": "Product Listing Ad Rendering Service",
                "description": "Nike Sportswear hoodie"
            },
            "cta": "Shop now",
            "landingDomains": [
                "nike.com"
            ],
            "media": [
                "https://tpc.googlesyndication.com/archive/simgad/3463432638140085355?sig=demo"
            ],
            "offer": {
                "kind": null,
                "value": null,
                "currency": null,
                "code": null
            },
            "evidence": {
                "sourceRef": "https://adstransparency.google.com/advertiser/AR16735076323512287233/creative/CR16854540057166479361",
                "sourceRowId": "CR16854540057166479361"
            },
            "scrapedAt": "2026-08-06T11:03:09Z"
        }
    ],
    "coverage": {
        "status": "complete",
        "comparable": true,
        "closed": true,
        "evidenceRef": "buyer://public-demo/coverage",
        "expectedRows": 1
    },
    "sourceContract": {
        "productionActorId": "zinin/brand-evidence-source",
        "productionActorVersion": "0.1.0",
        "adapter": "google-ads",
        "adapterVersion": "1.0.0",
        "queryScope": {
            "advertiserId": "AR16735076323512287233",
            "country": "US"
        },
        "entityIdentity": {
            "advertiserId": "AR16735076323512287233"
        }
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("zinin/competitor-ad-change-brief").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 = {
    "schemaVersion": "1.0",
    "requestId": "auto",
    "watchId": "nike-us-demo-bootstrap",
    "rows": [{
            "platform": "google",
            "advertiserId": "AR16735076323512287233",
            "pageId": None,
            "adId": None,
            "creativeId": "CR16854540057166479361",
            "isActive": True,
            "format": "IMAGE",
            "copy": {
                "primaryText": "Unisex Nike Sportswear Tech Fleece",
                "headline": "Product Listing Ad Rendering Service",
                "description": "Nike Sportswear hoodie",
            },
            "cta": "Shop now",
            "landingDomains": ["nike.com"],
            "media": ["https://tpc.googlesyndication.com/archive/simgad/3463432638140085355?sig=demo"],
            "offer": {
                "kind": None,
                "value": None,
                "currency": None,
                "code": None,
            },
            "evidence": {
                "sourceRef": "https://adstransparency.google.com/advertiser/AR16735076323512287233/creative/CR16854540057166479361",
                "sourceRowId": "CR16854540057166479361",
            },
            "scrapedAt": "2026-08-06T11:03:09Z",
        }],
    "coverage": {
        "status": "complete",
        "comparable": True,
        "closed": True,
        "evidenceRef": "buyer://public-demo/coverage",
        "expectedRows": 1,
    },
    "sourceContract": {
        "productionActorId": "zinin/brand-evidence-source",
        "productionActorVersion": "0.1.0",
        "adapter": "google-ads",
        "adapterVersion": "1.0.0",
        "queryScope": {
            "advertiserId": "AR16735076323512287233",
            "country": "US",
        },
        "entityIdentity": { "advertiserId": "AR16735076323512287233" },
    },
}

# Run the Actor and wait for it to finish
run = client.actor("zinin/competitor-ad-change-brief").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 '{
  "schemaVersion": "1.0",
  "requestId": "auto",
  "watchId": "nike-us-demo-bootstrap",
  "rows": [
    {
      "platform": "google",
      "advertiserId": "AR16735076323512287233",
      "pageId": null,
      "adId": null,
      "creativeId": "CR16854540057166479361",
      "isActive": true,
      "format": "IMAGE",
      "copy": {
        "primaryText": "Unisex Nike Sportswear Tech Fleece",
        "headline": "Product Listing Ad Rendering Service",
        "description": "Nike Sportswear hoodie"
      },
      "cta": "Shop now",
      "landingDomains": [
        "nike.com"
      ],
      "media": [
        "https://tpc.googlesyndication.com/archive/simgad/3463432638140085355?sig=demo"
      ],
      "offer": {
        "kind": null,
        "value": null,
        "currency": null,
        "code": null
      },
      "evidence": {
        "sourceRef": "https://adstransparency.google.com/advertiser/AR16735076323512287233/creative/CR16854540057166479361",
        "sourceRowId": "CR16854540057166479361"
      },
      "scrapedAt": "2026-08-06T11:03:09Z"
    }
  ],
  "coverage": {
    "status": "complete",
    "comparable": true,
    "closed": true,
    "evidenceRef": "buyer://public-demo/coverage",
    "expectedRows": 1
  },
  "sourceContract": {
    "productionActorId": "zinin/brand-evidence-source",
    "productionActorVersion": "0.1.0",
    "adapter": "google-ads",
    "adapterVersion": "1.0.0",
    "queryScope": {
      "advertiserId": "AR16735076323512287233",
      "country": "US"
    },
    "entityIdentity": {
      "advertiserId": "AR16735076323512287233"
    }
  }
}' |
apify call zinin/competitor-ad-change-brief --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zinin/competitor-ad-change-brief"
        }
    }
}

```

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/wUsW85K42ATtd9K16/builds/ZzTk4gvihjAomsWZ2/openapi.json
