# AdPulse — Google Ads Competitor Intelligence (`p90dro/adpulse`) Actor

Competitive intelligence for Google Ads. Monitor competitors over time, detect evidence-backed creative changes, ad activity, format shifts, and actionable signals through dashboards, alerts, and structured outputs.

- **URL**: https://apify.com/p90dro/adpulse.md
- **Developed by:** [Pedro Henrique](https://apify.com/p90dro) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$100.00 / 1,000 competitor snapshots

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

## AdPulse

**AdPulse monitors Google Ads Transparency competitors over time and turns raw ad observations into evidence-backed change intelligence.** Give it exact advertiser IDs (recommended) or competitor names, choose a market, and run it manually or on a schedule. AdPulse preserves monitoring history, separates real creative changes from source noise, and produces an interactive dashboard, competitive brief, alerts, and structured data for downstream workflows.

### What you get

- **Competitor change monitoring** — baseline, newly observed, changed, missing, confirmed missing, and reappeared creative states with explicit evidence.
- **Creative intelligence** — exact-content families, format mix, lifecycle evidence, lineage, strategy deltas, and evidence-weighted testing opportunities.
- **Human-readable outputs** — an interactive dashboard and competitive brief in addition to raw structured data.
- **Automation-ready outputs** — Dataset rows plus versioned summaries, alert records, and a read-model API in the default Key-Value Store.
- **Fail-closed reliability** — blocked, rate-limited, unavailable, ambiguous, or schema-drifted source responses are never silently converted into “no ads.”
- **Optional advanced analysis** — semantic enrichment, ad-to-landing intelligence, cross-platform Meta evidence, and first-party experiment validation are opt-in.

### Quick start

For recurring monitoring, use exact Google Ads Transparency advertiser IDs and keep the same `stateNamespace` across runs:

```json
{
  "advertiserIds": ["AR16735076323512287233"],
  "searchTerms": [],
  "region": "BR",
  "maxAds": 100,
  "mode": "pulse",
  "stateNamespace": "my-monitor",
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "strictSchema": true
}
```

A first run establishes a baseline. Later runs compare against persisted history. Use `reconciliation` when you need stronger disappearance confirmation.

### Pricing model

The Store release is designed around one visible value unit: **one `competitor-snapshot` per resolved competitor in a completed run, priced at US$0.10 per snapshot**.

A run with 5 competitors therefore requests 5 snapshot units. AdPulse does not separately bill semantic provider calls, cache hits, retries, fingerprints, dashboard rendering, or internal deduplication. Partial or failed runs are not intentionally settled as successful snapshot units, and the PPE preflight refuses to start expensive work when the effective Apify event budget cannot cover the full admitted run.

AdPulse supports at most **50 competitor targets per run** and **100 creatives per competitor per run**. These ceilings keep the public value unit inside the operating envelope used for pricing acceptance. The public release gate remains fail-closed in code until the owner explicitly enables Store release after the Publishing checklist is complete. The intended public offer does **not** rely on a hidden second semantic charge.

### Main outputs

Start with **Dashboard** for the product view, **Competitive Brief** for a concise evidence-backed report, and **Dataset** for row-level creative observations. The output schema also links directly to run, alert, quality, intelligence, strategy, opportunity, lineage, landing-page, cross-platform, and first-party validation summaries. The default Key-Value Store is organized into named collections so these product outputs are easy to filter in Apify Console, while internal billing/economics records remain audit-only.

The Dataset has two views:

- **Competitor changes overview** — compact fields for normal review and integrations.
- **Full evidence detail** — the broader audit surface, including quality, fingerprint, semantic, lineage, and landing evidence.

### Optional features and secrets

Semantic enrichment is disabled by default. If you enable it, configure `OPENAI_API_KEY` in the Actor environment; AdPulse does not accept that secret in ordinary Actor input. Webhook delivery is also opt-in and reads the destination URL from an environment variable rather than persisting it in output records.

Landing-page intelligence only follows defensible public final URLs and never requests Google ad click trackers. Cross-platform Meta evidence requires an explicit Google advertiser ID plus a verified advertiser domain mapping so external evidence cannot silently rewrite Google monitoring identity.

### Evidence boundaries

AdPulse analyzes public advertising evidence. It does **not** infer spend, impressions, clicks, conversions, CTR, CPA, ROAS, profitability, or “winner/loser” status from ordinary Google Ads Transparency data. Semantic labels and opportunity hypotheses remain model- or rule-derived evidence with explicit provenance and coverage limits.

Google Ads Transparency is not a supported public API contract, so source behavior can change. AdPulse treats source blocks, ambiguity, incomplete pagination, and schema drift as explicit states instead of guessing.

### Store release safety

Public billing is disabled by default in the runtime. Private canary charging was accepted with a bounded two-snapshot / US$0.20 live test; production-scale Store billing requires a **third explicit owner release gate**, `ADPULSE_PPE_PUBLIC_STORE_RELEASE_ENABLED=true`, in addition to the billing and live-charge flags. The code also preserves a 50-snapshot public-run ceiling and full-run budget admission before expensive work.

See `docs/STORE_RELEASE.md` for the owner-only publication checklist and `docs/INNOVATION_ROADMAP.md` for engineering acceptance status.

### Engineering status and acceptance evidence

Implemented milestones:

1. Actor/GitHub foundation
2. Resilient HTTP collector
3. Nike + Brazil raw-collection regression
4. Stable creative normalizer
5. Durable State Store
6. Explainable multi-channel fingerprinting
7. Evidence-aware explainable Diff Engine
8. Dimensioned Evidence Quality Engine
9. Evidence-backed Intelligence Engine
10. Evidence-Bound Semantic Enrichment Engine (accepted on real Apify)
11. Evidence-Bound Opportunity Engine (accepted on real Apify)
12. Evidence-Aware Monitoring & Alert Engine (accepted on real Apify)
    12B. Monitoring Orchestration & Delivery (accepted on real Apify)
13. Evidence-Backed Competitive Brief / Reporting (accepted on real Apify)
14. Evidence Experience API & Dashboard (Stage 14.2 accepted on live Apify)
15. Private PPE Economics & Monetization Readiness (accepted; completed-run p95 gate closed)
16. Commercial Policy & PPE SDK Integration (bounded live-money canary accepted at 2 × US$0.10)

16.1. Post-Settlement Billing Consistency (accepted; commercial artifacts reconciled after settlement)
16.2. Store Readiness & Release Safety (code-ready; manual Apify Publishing gates remain pending)

Innovation Track (prioritized without replacing release gates):

- I1. Creative Lineage / Mutation Graph (accepted on real Apify)
- I2. Strategy Delta Engine (accepted on real Apify)
- I3. Ad ↔ Landing Page Intelligence (accepted on live Apify)
- I4. Cross-platform Content Graph (accepted live with Google + Meta)
- I5. Evidence-weighted Opportunity Engine (accepted on live Apify)
- I6. First-party Validation Loop (accepted on live Apify)

The backbone/release and innovation roadmaps are tracked together in
`docs/INNOVATION_ROADMAP.md`.

Stage 6 received real Apify acceptance on 2026-10-02 using two separate Nike/BR
runs against the same `stage6-nike-br-acceptance` namespace. The second run
advanced State Store v2 from revision 1 to 2 with all 14 creatives recognized
as existing, zero new creatives, and the same creative-set hash. All 14 content,
representation, and metadata fingerprints remained stable across runs even
though observation time changed and both JavaScript previews received different
runtime `htmlParentId` / `responseCallback` values. The compact acceptance
evidence is recorded in `benchmarks/stage6_apify_acceptance_2026-10-02.json`.

Stage 7 received real Apify acceptance on 2026-10-03 using four Nike/BR runs
against `stage7-nike-br-acceptance`. The sequence validated cold-start
`BASELINE` semantics, a stable complete rerun, controlled truncation with
missing inference suppressed, and complete recovery. The creative omitted by
the truncated run, `CR17955367121756094465`, returned as
`UNCHANGED/no_change` with `previousObservedRevision=2` and
`candidateRevision=4`, proving that incomplete revision 3 was not treated as
authoritative absence evidence. The compact acceptance evidence is recorded in
`benchmarks/stage7_apify_acceptance_2026-10-03.json`.

Stage 8 received real Apify acceptance on 2026-10-03 using four Nike/BR runs
against `stage8-nike-br-acceptance`. The accepted State sequence was
`0 -> 1 -> 2 -> 3 -> 4`: baseline kept trend/transition closed, the stable
second run opened them, controlled truncation preserved local creative
eligibility while closing every population-level Quality gate, and complete
recovery reopened those gates. The complete creative-set hash
`c5166b60557b976320c14029b5ad6ec3dcb9b4a3b78f72feff3a12eed724732c`
was replaced during truncation by
`f757b6e7b85f43e155ecd0de336d3e1bfd6ed3ea4ea9a84f08e5b639063e2a1b`
and restored on recovery. The omitted creative
`CR17955367121756094465` returned as `UNCHANGED/no_change` with
`previousObservedRevision=2` and `candidateRevision=4`, proving incomplete
revision 3 was not authoritative absence evidence. The compact acceptance
evidence is recorded in
`benchmarks/stage8_apify_acceptance_2026-10-03.json`.

Stage 9 received real Apify acceptance on 2026-10-03. Four Nike/BR runs against
`stage9-nike-br-acceptance` validated the sequence `0 -> 1 -> 2 -> 3 -> 4`:
baseline snapshot intelligence with trend closed, a stable complete rerun with
activity/persistence/trend enabled, controlled truncation that preserved local
family evidence while blocking all population intelligence, and complete recovery.
The recovery `trendDelta` correctly used revision 2 rather than truncated revision
3, and the omitted creative `CR17955367121756094465` returned
`UNCHANGED/no_change` with three actual monitoring observations while the other
13 creatives had four. A separate Nike + ADIDAS run against
`stage9-cross-advertiser-acceptance` made cross-advertiser reuse eligible across
two advertisers and returned zero exact shared families, with no fuzzy similarity
inference. The compact acceptance evidence is recorded in
`benchmarks/stage9_apify_acceptance_2026-10-03.json`.

Stage 10 received real Apify acceptance on 2026-10-04 using the corrected provider
boundary from commit `38a63f48662d49cfe9677f709a68def0828e880a`. A one-family
provider canary first proved successful structured vision enrichment. A clean Nike/BR
acceptance baseline against `stage10-nike-br-acceptance-v2` then produced semantic
analysis for all 12 eligible `image_snapshot` families: one cache hit from the canary
plus 11 new provider analyses, with zero provider or cache-persistence failures. The
two JavaScript previews remained explicitly unsupported, so population semantic mix
correctly stayed closed rather than publishing partial percentages. An identical
second run reused all 12 exact-family cache entries with **zero provider calls and zero
provider tokens**, while all 14 creatives remained `UNCHANGED`. Cached semantic
analyses were validated equal to the prior accepted results and no performance claim
was emitted. The compact acceptance evidence is recorded in
`benchmarks/stage10_apify_acceptance_2026-10-04.json`.

Stage 11 received real Apify acceptance on 2026-10-04 using the cache-only
Opportunity Engine from commit `38c2974e161a48c5ca741966fa54a7496e305a64`.
A Nike/BR run with Stage 10 disabled reused 12 accepted exact-family semantic cache
entries with zero provider calls and zero provider tokens while leaving two JavaScript
representations explicitly unsupported and population gap claims closed. A separate
Nike + ADIDAS baseline then proved cross-advertiser subset comparison with one explicit
ADIDAS semantic cache miss and no provider fallback. An identical rerun advanced both
targets from revision 1 to 2 with all 15 creatives `UNCHANGED`, opened transition/trend
Quality gates, and reproduced the Strategy output exactly apart from candidate revision,
including all five Nike hypothesis IDs. No absence or performance claim was emitted.
The compact acceptance evidence is recorded in
`benchmarks/stage11_apify_acceptance_2026-10-04.json`.

Innovation I1 Creative Lineage / Mutation Graph received real Apify acceptance on
2026-10-14 against the historical `stage7-nike-br-acceptance` namespace. Run A
advanced State 6 -> 7 and explicitly adopted Lineage with
`prior_snapshot_seed`, `historicalBackfillComplete=false`, and
`lineageStartedRevision=7`. Run B advanced 7 -> 8 with a synchronized sidecar while
preserving the original observation boundary. All 15 exact family -> lineage node IDs
remained stable, all creatives were `UNCHANGED`, three source-volatility-only
transitions created zero lineage mutations, and Lineage introduced zero provider calls
or performance/causal claims. Evidence is recorded in
`benchmarks/i1_creative_lineage_apify_acceptance_2026-10-14.json`.

Innovation I2 Strategy Delta Engine received real Apify acceptance on
2026-10-14 against the historical `stage7-nike-br-acceptance` namespace.
Run A advanced State 9 -> 10 and seeded the first synchronized Strategy Delta
baseline at revision 10 without inventing a retroactive 9 -> 10 comparison.
Run B advanced 10 -> 11 with the sidecar synchronized, preserved
`strategyDeltaStartedRevision=10`, compared format mix over 15/15 creatives and
all six semantic dimensions over the same 12 exact-family semantic records, and
reported zero movement. Semantic coverage remained 0.8, so subset comparisons
remained explicit and population semantic claims stayed blocked. Three
source-volatility-only Diff changes created zero Strategy Delta events. I2 added
zero provider calls and emitted no performance, spend, or causal-strategy claim.
Evidence is recorded in
`benchmarks/i2_strategy_delta_apify_acceptance_2026-10-14.json`.

Stage 12 received real Apify acceptance on 2026-10-04 using the Alert Engine from
commit `55ca256bdb675448b7af2f17ce10d6cb4c8af3ef`. A fresh Nike/BR monitor
baseline against `stage12-nike-br-alert-acceptance` observed 14 creatives and
seeded five existing Stage 11 hypothesis IDs while emitting zero alert events and
zero delivery recommendations. An identical second run advanced revision 1 to 2
with all 14 creatives `UNCHANGED/no_change`, opened transition/trend Quality
gates, and suppressed all five hypotheses as already seen. Before the third run,
only the Stage 12 alert sidecar key was deleted. The Actor advanced revision 2 to
3 with the same creative-set hash, reported `alertStateSynchronized=false`,
suppressed all five existing hypotheses under fail-closed deduplication, emitted
zero false alerts, and reseeded the current hypothesis set without a persistence
failure. No provider call, provider token, delivery recommendation, or performance
claim was emitted during acceptance. The compact evidence is recorded in
`benchmarks/stage12_apify_acceptance_2026-10-04.json`.

The Stage 3 historical creative benchmark was captured on 2026-10-01:

- advertiser: Nike, Inc.
- advertiser ID: `AR16735076323512287233`
- region: `BR`
- 15 unique creative IDs

The Stage 4 normalizer benchmark was captured on 2026-10-02. It records the
observed source-format distribution, preview-representation distribution, and
representative time-semantics assertions without treating them as a permanent
truth about Nike's inventory.

### Reliability rules

- A failed fetch is never represented as an empty result.
- CAPTCHA and Google `/sorry/` responses become `SOURCE_BLOCKED`.
- Exhausted HTTP 429 retries become `SOURCE_RATE_LIMITED`.
- Persistent transient failures become `SOURCE_UNAVAILABLE`.
- Malformed RPC exchanges become `SOURCE_PROTOCOL_ERROR`.
- Structural source drift becomes an explicit schema-change failure.
- Raw Google rows are preserved after normalization.
- Known source fields are type-checked before mapping.
- Unknown source fields are retained and reported instead of discarded.
- Creative format and preview representation are separate concepts.
- Source-reported days shown are never replaced by calendar-span math.
- Persistent state is versioned and never silently reset on schema mismatch.
- Truncated snapshots never replace the last untruncated snapshot.
- Known creatives are not deleted merely because a later scan does not return them.
- Overlapping state writers are detected before the metadata commit pointer moves.
- Older observations cannot rewind newer persisted state.
- Fingerprints never hash the entire normalized/raw JSON object.
- Content, source representation, advertiser metadata, lifecycle evidence, and source volatility are separate fingerprint dimensions.
- Signed preview plumbing such as `sig`, `htmlParentId`, and `responseCallback` is excluded from content identity.
- Missing content evidence produces an explicit insufficient-confidence result instead of a fabricated hash.
- Free-text advertiser resolution is conservative; ambiguous names require an
  explicit advertiser ID.
- Semantic labels are model inference with explicit provider/model/prompt provenance.
- Exact creative families are enriched once and cached; duplicate IDs do not multiply provider work.
- Population semantic mix requires complete inventory and complete semantic coverage.
- Semantic enrichment never turns longevity, recurrence, or visible copy into performance claims.
- Stage 11 reads semantic cache entries only; it never invokes a paid semantic provider or fallback.
- Strategy hypotheses are testing directions that require first-party validation, not performance predictions.
- Stage 12 never turns every observed difference into a notification; Diff and Quality gates remain authoritative.
- Cold-start baselines seed monitoring and strategic dedupe state without generating delivery recommendations.
- Representation-only, metadata-only, and indeterminate transitions do not become actionable alerts.
- Strategic hypotheses are deduplicated across synchronized monitor revisions.
- Stage 12B never changes Alert Engine eligibility; it consumes only accepted alert output.
- MISSING watch events can request reconciliation without becoming external notifications.
- Delivery outbox messages are deterministic and versioned; only `deliveryRecommended=true` alerts enter the outbox.
- Webhook delivery is optional, fail-soft, and uses deterministic idempotency keys.
- Stage 13 report claims resolve to exact creative/family/transition evidence IDs.
- 24h/7d reporting windows require persisted AdPulse observation history; source ad dates are never substituted.
- Volatile JavaScript preview plumbing is never promoted into a stable report evidence link.
- Competitive Brief generation performs zero provider calls and preserves all upstream performance/absence guardrails.
- Creative Lineage uses exact-content families as graph nodes before considering semantic similarity.
- Same-ID content mutation is an observed sequence; it is never rewritten as causal ancestry.
- Semantic variant relationships remain candidates with explicit confidence and ambiguity; tied top candidates select no parent.
- Creative Lineage performs zero paid provider calls and reuses current semantic results or validated exact-family cache only.
- Lineage continuity lives in a revision-bound sidecar and cannot rewrite State, Diff, Quality, or Alert truth.
- Public-ad persistence/reuse/lineage never becomes a winner, profitability, ROAS, CPA, or conversion claim.
- Strategy Delta compares exact revision-aligned evidence membership, not aggregate percentages alone.
- Hook/offer/CTA/message/visual/promise deltas always expose previous/current denominators and semantic coverage.
- Semantic profile changes block semantic delta comparison instead of masquerading as competitor movement.
- Partial semantic coverage remains subset evidence and cannot silently become a population-level strategy claim.
- Truncated inventory never replaces the last complete Strategy Delta baseline.
- Strategy Delta events are non-delivery evidence records and never imply spend, performance, or causal strategy intent.
- Display URLs are never silently promoted to exact landing URLs.
- I3 never requests Google ad click trackers; encoded destination evidence is decoded locally where defensible.
- Direct landing-page observation validates every URL/redirect against public DNS/IP safety rules.
- Raw landing-page HTML is never persisted; page message/action/offer/structure are separate fingerprint channels.
- Ad/page cochange is temporal evidence, never causal attribution or a performance claim.

See `docs/ARCHITECTURE.md`, `docs/NORMALIZER.md`, `docs/STATE_STORE.md`,
`docs/FINGERPRINTING.md`, `docs/DIFF_ENGINE.md`, `docs/QUALITY_ENGINE.md`,
`docs/INTELLIGENCE_ENGINE.md`, `docs/SEMANTIC_ENRICHMENT.md`,
`docs/OPPORTUNITY_ENGINE.md`, `docs/ALERT_ENGINE.md`, `docs/MONITORING_DELIVERY.md`,
`docs/COMPETITIVE_BRIEF.md`, `docs/CREATIVE_LINEAGE.md`,
`docs/STRATEGY_DELTA.md`, `docs/AD_LANDING_INTELLIGENCE.md`, and
`docs/INNOVATION_ROADMAP.md`.

### Actor input

For deterministic monitoring, prefer `advertiserIds`. `searchTerms` are
supported, but ambiguous advertiser names intentionally fail instead of silently
attaching monitoring state to the wrong entity.

Example:

```json
{
  "advertiserIds": ["AR16735076323512287233"],
  "searchTerms": [],
  "region": "BR",
  "maxAds": 100,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "mode": "pulse",
  "stateNamespace": "default",
  "strictSchema": true
}
```

`stateNamespace` is an advanced isolation key. Runs with the same namespace,
advertiser ID, and region share persistent history. Different namespaces keep
independent histories.

Each Dataset item contains the stable normalized observation plus the original
Google source row under `sourceRow`. `RUN_SUMMARY` contains collection,
normalization, and state-commit diagnostics. Expected AdPulse failures are saved
as `RUN_ERROR` before the run fails.

### Normalization semantics

The current inferred source enum is versioned:

```text
1 -> TEXT
2 -> IMAGE
3 -> VIDEO
other -> UNKNOWN
```

Known mappings are deliberately marked `formatConfidence = medium` because
Google does not publish a supported `SearchCreatives` schema.

Preview representation is independent:

```text
image_snapshot
javascript
mixed
unknown
```

For example, a TEXT creative may be archived as an image snapshot. AdPulse does
not relabel that creative as IMAGE merely because its preview is an `<img>`.

Time fields are also deliberately distinct:

```text
firstShownAt         source timestamp
lastShownAt          source timestamp
sourceDaysShown      source-reported count
sourceDateSpanDays   derived calendar-date span
observedAt           when AdPulse saw the row
```

These fields must not be substituted for one another.

### Durable state

The active fingerprint-aware State Store uses the named key-value store `adpulse-state-v2`. Each target
keeps a current snapshot, previous snapshot, latest untruncated snapshot, and
compact per-creative memory.

Creative state is distributed across 64 deterministic shards. Updated shards
are written under unique commit-specific keys and target metadata is moved last,
making the metadata record the commit pointer. Superseded shards are retired
after a successful commit.

State Store v2 still records observation memory only. Stage 7 reads that committed
history before the next State commit and performs change classification without
changing the Stage 6 state schema.

### Fingerprinting

Every normalized observation now receives explicit versioned fingerprint dimensions:

```text
contentFingerprint
representationFingerprint
metadataFingerprint
lifecycleFingerprint
sourceVolatilityFingerprint
```

The content channel uses only defensible content-bearing evidence. Image archive
locators are canonicalized without query/fragment noise. Google JavaScript
preview URLs retain only content-bearing parameters such as `creativeId`,
`itemIds`, and `assets`; signed/runtime parameters are excluded.

Fingerprinting is local and deterministic: no extra Google request, browser,
image download, LLM, or external database is required.

The active profile is versioned as
`adpulse-creative-fingerprint-v2`. Each dimension also exposes per-component
digests so the future Diff Engine can identify which semantic field changed
without storing signed preview URLs or re-hashing raw JSON. See
`docs/FINGERPRINTING.md`.

### Explainable Diff Engine

Stage 7 classifies creative transitions with policy
`adpulse-explainable-diff-v2`.

The first monitor run is `BASELINE`, never automatically `NEW`. A creative can
be called `NEW` only when a prior pagination-complete baseline proves it was
absent. Without that evidence the safer status is `NEWLY_OBSERVED`.

For existing creatives, Diff compares the fingerprint dimensions independently:

```text
content
representation
metadata
lifecycle
source_volatility
```

`CHANGED` is reserved for defensible content mutation. Representation, advertiser-metadata,
lifecycle, and source-volatility movement remain independently visible while non-content drift
keeps the creative status `UNCHANGED`. If content evidence cannot be compared safely, the result is
`INDETERMINATE` rather than a fabricated binary answer.

Absence follows an evidence ladder:

```text
truncated/incomplete -> no absence inference
one complete absence -> MISSING
prior complete absence + complete reconciliation -> CONFIRMED_MISSING
later return -> REAPPEARED
```

Every result includes deterministic reason codes, changed semantic component
names, confidence, attention level, alert eligibility, and a human-readable
explanation. Non-trivial compact transitions are also written to `DIFF_EVENTS`
in the default key-value store.

A small versioned Diff sidecar remembers already-confirmed missing creatives so
scheduled reconciliation does not repeatedly alert the same disappearance.
Stale sidecar revisions are ignored fail-safe rather than overriding State.

See `docs/DIFF_ENGINE.md`.

### Evidence Quality Engine

Stage 8 uses policy `adpulse-evidence-quality-v1` to decide which observations
and transitions are admissible to later competitive-intelligence layers.

It deliberately does not emit a weighted evidence score. Instead it reports
independent coverage dimensions for identity, content, format, lifecycle,
transition comparability, and creative-family readiness. A critical failure in one
dimension cannot be hidden by strengths in unrelated dimensions.

Per creative, the Dataset exposes gates for snapshot, transition, novelty,
creative-family, and lifecycle intelligence. Per advertiser target,
`QUALITY_SUMMARY` exposes separate gates for inventory analysis, format mix,
family concentration, lifecycle aggregation, trends, and absence reasoning.

A truncated run can therefore preserve valid local creative evidence while
blocking advertiser-wide population metrics. Likewise, a stale Diff sidecar blocks
absence intelligence without incorrectly invalidating content or format evidence.

Stage 8 does not infer spend, impressions, clicks, conversions, ROAS,
profitability, or "winner/loser" status from public Transparency data.

See `docs/QUALITY_ENGINE.md`. Stage 8 real Apify acceptance was completed on
2026-10-03 and is recorded under `benchmarks/stage8_apify_acceptance_2026-10-03.json`.

### Evidence-backed Intelligence Engine

Stage 9 uses policy `adpulse-evidence-backed-intelligence-v1` and consumes Stage 8
Quality gates without weakening them.

The v1 engine adds deterministic exact-content creative families, format mix,
family concentration, lifecycle distributions, observed activity, monitoring
persistence, complete-baseline trend deltas, and exact cross-advertiser family reuse.

A small Intelligence sidecar preserves the last complete aggregate baseline across
truncated runs, so recovery compares complete inventory to complete inventory rather
than treating a partial scan as the new reference.

Stage 9 intentionally does not call longevity, repeated observation, or family
concentration "performance". It makes no spend, impressions, clicks, conversions,
ROAS, profitability, or winner/loser inference from ordinary Google Ads
Transparency evidence.

See `docs/INTELLIGENCE_ENGINE.md`. Stage 9 real Apify acceptance was completed on
2026-10-03 and is recorded under `benchmarks/stage9_apify_acceptance_2026-10-03.json`.

### Evidence-Bound Semantic Enrichment Engine

Stage 10 introduces optional, cost-bounded semantic creative analysis under policy
`adpulse-evidence-bound-semantic-enrichment-v1`.

It is disabled by default. When enabled, Stage 10 currently analyzes only
Quality-admitted `image_snapshot` creatives with a vision-capable OpenAI Responses
API model. `OPENAI_API_KEY` is read only from the Actor environment; the secret is
never accepted in Actor input or persisted in Dataset/KVS/logs.

The semantic contract exposes visible-evidence-bound labels for hook, offer, CTA,
message angle, visual theme, and primary promise. Every signal includes confidence
and short evidence observations, and unsupported signals remain `unknown` / `none`
rather than being forced into a category.

Provider work is deduplicated by the Stage 9 exact `intelligenceFamilyId`. One exact
family receives at most one new provider analysis per run, and successful results are
cached by prompt version + provider + model + image detail + family ID. Cache hits do not consume
the run-level `maxFamiliesPerRun` budget.

Population semantic mix is stricter than local enrichment: it is emitted only when
Stage 8 inventory intelligence is eligible and every observed creative has semantic
coverage. Counts use exact content families so duplicate creative IDs cannot
artificially overweight a hook/CTA/offer pattern.

Stage 10 does not infer spend, impressions, clicks, conversions, CTR, ROAS,
profitability, audience targeting, or winner/loser status. Semantic labels are
model inference with explicit provenance, not source-authenticated facts.

See `docs/SEMANTIC_ENRICHMENT.md` and `docs/STAGE10_MARKET_RESEARCH.md`. Stage 10
real Apify acceptance was completed on 2026-10-04 and is recorded under
`benchmarks/stage10_apify_acceptance_2026-10-04.json`.

### Evidence-Bound Opportunity Engine

Stage 11 turns existing Quality, Intelligence, and cached Stage 10 semantic evidence
into deterministic creative testing hypotheses under policy
`adpulse-evidence-bound-opportunity-v1`.

Stage 11 is **cache-only**. It does not require `OPENAI_API_KEY`, does not invoke the
OpenAI API, and has no paid-provider fallback. Missing semantic cache entries remain
explicit cache misses. Every `STRATEGY_SUMMARY` hard-codes provider-call telemetry to
zero so a live acceptance run can prove the cost boundary operationally.

Strategy aggregation uses exact Stage 9 families. It reports observed semantic pattern
counts and dominant patterns over the available cached subset, then emits deterministic
contrast-test hypotheses that explicitly require validation with first-party campaign
metrics. It never labels a pattern as a winner or infers spend, CTR, ROAS, conversions,
or profitability.

Population-level gap claims are allowed only with complete inventory and complete
semantic coverage. With partial semantic coverage, cross-advertiser differences are
phrased only as categories observed in one available semantic subset and not the other.

See `docs/OPPORTUNITY_ENGINE.md` and `docs/STAGE11_MARKET_RESEARCH.md`. Stage 11
real Apify acceptance was completed on 2026-10-04 and is recorded under
`benchmarks/stage11_apify_acceptance_2026-10-04.json`.

### Evidence-Aware Monitoring & Alert Engine

Stage 12 turns the typed Diff, final Quality, and deterministic Strategy outputs into a
small notification stream under policy `adpulse-evidence-aware-alert-v1`.

It writes `ALERT_EVENTS`, `ALERT_SUMMARY`, and `RUN_SUMMARY.alerts`. Cold-start
baselines create no delivery recommendations. Proven NEW creatives, admitted content
changes, and confirmed disappearances can become actionable; one-observation missing
states and unproven novelty remain watch-level. REAPPEARED creatives are recovery
events.

Stage 12 also keeps a revision-aligned alert sidecar containing previously seen
strategic hypothesis IDs. Existing Stage 11 hypotheses therefore do not generate the
same strategic notification every run. Missing or stale alert state fails that
deduplication closed instead of calling every current hypothesis "new".

Stage 12 v1 deliberately stops at a `deliveryRecommended` boundary. It does not send
email, Slack, or arbitrary webhooks and does not add PPE billing calls. This keeps live
acceptance deterministic while providing a stable contract for the next product layer.

See `docs/ALERT_ENGINE.md` and `docs/STAGE12_MARKET_RESEARCH.md`. Stage 12 real Apify acceptance was completed on 2026-10-04 and is recorded under `benchmarks/stage12_apify_acceptance_2026-10-04.json`.

### Monitoring Orchestration & Delivery

Stage 12B operationalizes the accepted Stage 12 output under two new, separate
contracts: `adpulse-monitoring-orchestration-v1` and
`adpulse-alert-delivery-v1`.

The default cadence is configurable but starts at a 24-hour pulse, 7-day periodic
reconciliation, and a 6-hour reconciliation hint after a single complete MISSING
watch. That watch remains `deliveryRecommended=false`; orchestration may request a
stronger confirmation scan but cannot promote the alert itself.

Every run writes `ORCHESTRATION_SUMMARY`, `DELIVERY_OUTBOX`, and
`DELIVERY_SUMMARY`. The outbox contains only Stage 12 events already marked
`deliveryRecommended=true`, with deterministic message IDs and channel-ready
presentations for webhook/API/Slack/email.

Delivery defaults to `outbox`, which performs no network call. Optional `webhook`
mode reads the HTTPS destination from an Actor environment variable, adds
`Idempotency-Key`, retries only bounded transient failures, records successful
message IDs in a separate delivery sidecar, and leaves delivery failures fail-soft.

Apify schedules remain explicit platform resources rather than hidden Actor side
effects. Stage 12B publishes schedule intent; provisioning can use two exclusive
schedules with the same monitor namespace: routine `pulse` plus periodic
`reconciliation`.

See `docs/MONITORING_DELIVERY.md` and `docs/STAGE12B_APIFY_NOTES.md`.

Stage 12B received real Apify acceptance on 2026-10-04 using commit
`74ee33af933a95c866239bf9a4edbf3551e2ad7d`. A fresh Nike/BR baseline and
stable rerun proved the 24h pulse / 168h reconciliation / 6h MISSING-follow-up
cadence contract, zero alert noise for stable evidence, and zero delivery network
activity in default outbox mode. A separate clean semantic identity
(`imageDetail=low`) first proved 12 Stage 11 cache misses with no hypotheses.
The next run enabled Stage 10, analyzed all 12 eligible exact families with 12
provider calls and zero provider failures, after which Stage 11 read the newly
written cache without provider fallback and produced five evidence-bound hypotheses.
Stage 12 emitted five synchronized strategic alerts, all
`deliveryRecommended=true`, and Stage 12B produced exactly five versioned delivery
messages with webhook/API/Slack/email presentations while still performing zero
network calls in `outbox` mode. The Alert Engine policy remained
`adpulse-evidence-aware-alert-v1`, `alertContractReinterpreted=false`, and no
performance claim was emitted. Compact evidence is recorded in
`benchmarks/stage12b_apify_acceptance_2026-10-04.json`.

### Evidence-Backed Competitive Brief / Reporting

Stage 13 introduces policy `adpulse-evidence-backed-competitive-brief-v1`.

It writes `COMPETITIVE_BRIEF`, `REPORT_SUMMARY`, and
`RUN_SUMMARY.reporting`. The brief is deterministic and cache-only: the reporting
layer never invokes a paid semantic provider.

Unlike a generic summary layer, Stage 13 builds a claim-to-evidence graph. Pattern
claims and hypotheses reference exact Stage 9 family evidence; family records reference
their member creative IDs; creative evidence preserves content-fingerprint provenance
and source timestamps. JavaScript preview URLs with signed/runtime plumbing remain
visible in the Dataset but are not promoted to stable report links.

The report distinguishes:

- previous complete observation comparisons from unavailable transition evidence;
- 24-hour and 7-day monitoring windows from source first/last-shown dates;
- observed/derived claims from test hypotheses;
- available semantic-subset patterns from exhaustive population claims.

A bounded reporting-history sidecar stores compact observation summaries for future
24h/7d comparisons. Missing or malformed reporting history fails those time windows
closed without affecting State, Diff, Quality, Strategy, Alerts, or Delivery.

Every hypothesis still requires first-party validation, and every brief hard-codes
`performanceClaimMade=false`.

See `docs/COMPETITIVE_BRIEF.md`.

Stage 13 received real Apify acceptance on 2026-10-04 using source commit
`5203abf9a97af3ab107c11f90408318d9fabe3e7`. A fresh Nike/BR baseline
proved that the reporting layer creates a claim-to-evidence ledger without
fabricating prior-change or 24h/7d narratives, while keeping reporting provider
calls and paid-provider calls at zero. A stable rerun advanced revision 1 to 2
with all 14 creatives `UNCHANGED/no_change`, enabled the previous-observation
comparison, and produced the bounded conclusion that no evidence-backed creative
change was detected; 24h/7d windows correctly remained unavailable because the
persisted observation history was only minutes old. Creative/family evidence IDs
and hypothesis IDs remained stable while brief, claim, and transition identities
remained revision-scoped. A separate Nike + ADIDAS canary then proved subset-safe
cross-advertiser reporting: Nike had 12/14 semantic-family coverage while ADIDAS
had one observed family and zero cached semantic families. The comparison stayed
scoped to `available_cached_semantic_exact_families`, kept
`gapClaimEligible=false`, `absenceClaimMade=false`, and
`performanceClaimMade=false`, and did not convert ADIDAS's zero semantic
coverage into a competitor-absence claim. Compact evidence is recorded in
`benchmarks/stage13_apify_acceptance_2026-10-04.json`.

### Evidence Experience API & Dashboard

Stage 14 introduces two product-facing policies:

`adpulse-evidence-experience-v1` and `adpulse-read-model-api-v1`.

It writes `DASHBOARD.html`, `DASHBOARD_DATA`, `API_INDEX`,
`EXPERIENCE_SUMMARY`, and a non-billable `COMMERCIAL_METERING` preview.

The dashboard is a CQRS-style read model over the accepted engines. It never
recomputes creative identity, Diff status, Quality eligibility, Strategy hypotheses,
Alert severity, or absence claims. The user experience follows progressive disclosure:

```text
signal
  -> plain-language explanation
  -> exact evidence
  -> forensic fingerprint/source detail
```

The HTML dashboard is self-contained: no React, CDN, external JavaScript, external
CSS, browser-side AI, or browser-side evidence inference. Stable archived images can
render as evidence; volatile JavaScript preview URLs are never promoted to stable
dashboard links.

The read-model API exposes overview, competitors, timeline, creatives, exact families,
opportunities, signals, claims, evidence, comparison, filters, and guardrails.

Stage 14 also adds a separate bounded presentation timeline sidecar. It preserves
significant Diff/Alert events across runs without changing State, Diff, Quality,
Strategy, Alerts, or Delivery truth.

`COMMERCIAL_METERING` does not charge users. It measures candidate visible-value
units for the later PPE experiment. The candidate primary unit is one
`competitor-snapshot`; internal fingerprints, Diff comparisons, Quality gates,
cache hits, dashboard rendering, claim resolution, retries, and alert deduplication
remain explicitly non-billable.

See `docs/STAGE14_EXPERIENCE.md` and `docs/STAGE14_MARKET_PRICING.md`.

Stage 14 / 14.2 received live Apify acceptance on 2026-10-14 using final
renderer commit `b2c721bf578a6e23df2766bc882a724507301be6` and build
`0Kv6WY9nw5KWjzs4D`. A corrected Nike/BR stable rerun advanced revision
1 -> 2 with 15 unchanged creatives and two persisted monitoring snapshots.
A separate Nike + ADIDAS canary observed 16 creatives across two complete
inventories while preserving the semantic-subset boundary: Nike had 80%
semantic-family coverage and ADIDAS 0%, but Compare emitted no competitor-absence,
gap, or performance claim. The final UI passed the 12-locale/Arabic-RTL contract,
the corrected I5 hypothesis presentation, provider-free localization, and the
HTML/CSV/JSON/Print export contract. Commercial metering remained non-billable
and produced one candidate competitor-snapshot per visible target. Evidence is
recorded in
`benchmarks/stage14_experience_apify_acceptance_2026-10-14.json`.

Stage 14.1 adds provider-free dashboard localization for English, Brazilian Portuguese,
Spanish, French, German, Italian, Simplified Chinese, Japanese, Korean, Arabic, Hindi,
and Russian. Canonical evidence IDs/categories remain unchanged, Arabic uses RTL,
locale preference is saved in the browser, and `#lang=<locale>` can override the UI
without modifying an Apify signed request. Localization makes zero provider calls and
is explicitly non-billable internal work.

Stage 14.2 hardens that experience under
`adpulse-dashboard-localization-v2`: all 12 locales now carry the complete canonical
UI-string set; Arabic receives mixed-script bidi isolation; confidence, creative-format,
and event-kind labels are localized; primary Evidence cards hide raw internal scope
codes behind progressive technical disclosure; Evidence Health no longer publishes a
synthetic blended percentage; and Monitoring Pulse reconstructs the current aggregate
creative count from each competitor's latest persisted observation. These changes
remain browser-side, provider-free, non-billable, and do not change evidence identity.

Stage 14.2 also adds a low-cost `adpulse-dashboard-export-v1` layer. Users can export
the self-contained dashboard as HTML, export a canonical creative inventory as CSV,
download `DASHBOARD_DATA` as JSON, or use the browser print pipeline to print/save a
PDF. Export actions run entirely in the browser, make zero provider/backend calls,
remain non-billable, and never expose volatile JavaScript preview URLs in CSV.

### Private PPE Economics & Monetization Readiness

Stage 15 adds three private observability artifacts under
`adpulse-private-ppe-economics-v1`: `ECONOMICS_SUMMARY`, `COST_LEDGER`, and
`PPE_BENCHMARK_PLAN`.

The optional `economicsBenchmark` input reads a provisional safe subset of current
Apify Run usage. It does not enable billing and cannot alter evidence truth. Raw
platform usage is repriced across paid Apify cost tiers, semantic-provider token cost
stays separate, cache hits and retries remain internal work, and unknown non-zero usage
dimensions block a complete cost conclusion instead of being silently ignored.

Running-run cost telemetry is not treated as final. Private acceptance compares it
with finalized completed-run usage after Apify's eventual-consistency delay. The
pricing gate also refuses to calculate p95 from a single run and requires a
representative multi-run sample set.

See `docs/STAGE15_ECONOMICS.md` and `docs/STAGE15_MARKET_RESEARCH.md`.

### Commercial Policy & Billing Dry-Run

Stage 16 converts the accepted Stage 15 p95 evidence into a versioned beta pricing
candidate while keeping billing disabled. The candidate primary event is one visible
`competitor-snapshot` at USD 0.10. Semantic provider work remains included in the
snapshot candidate during beta rather than becoming a second charge. Each run emits
`COMMERCIAL_POLICY` with simulated customer charge, developer net, p95 cost allowance,
profit/margin, cadence forecast, and `actualChargeCalls=0`.

Stage 16 live dry-run acceptance is complete for visible snapshot metering and real
semantic provider work. Activation preflight is accepted, and the private Apify Console
PPE configuration now contains only `competitor-snapshot` at USD 0.10 with platform
usage included. A guarded SDK integration reads effective pricing/remaining budget
before expensive work and settles only after a completed run.

Runtime billing is disabled by default. `ADPULSE_PPE_BILLING_ENABLED=true` is required
for any charge path, and live platform charging additionally requires
`ADPULSE_PPE_ALLOW_LIVE_CHARGES=true`. The runtime is now locked to Apify SDK 4.1.0
and every settlement uses an idempotency key. CI/local tests prove idempotent replay
and fail-closed max-charge admission. The post-upgrade Apify platform canary is accepted: SDK 4.1.0 observed the configured
USD 0.10 event, admitted the full two-snapshot workload, and finished with zero charged
events. All non-billing technical gates are now closed. The next gate is explicit human
approval for a private, bounded two-snapshot live-money canary; public Store activation
remains blocked until that canary is accepted.

Accepted live evidence is recorded in
`benchmarks/stage16_live_dry_run_acceptance_2026-10-16.json`.

See `docs/STAGE16_COMMERCIAL_POLICY.md`.

### Innovation I3 live acceptance

Ad ↔ Landing Page Intelligence received real Apify acceptance on 2026-10-14.
A live Google renderer delimiter artifact first produced a false duplicate landing
identity; destination profile v2 corrected the parser boundary and failed the old
sidecar closed. The corrected baseline advanced State 12 -> 13 with one exact landing
URL, five display-only destinations, nine unavailable destinations, zero provider calls,
zero ad-click requests, and zero false events. The synchronized warm run advanced
13 -> 14, reused all 15 stable creative-detail records with zero detail lookups,
reobserved the single exact Nike landing page successfully at HTTP 200, retained
`rawHtmlPersisted=false`, and emitted zero page/cochange events. The user-observed
warm runtime was 17 seconds. Evidence is recorded in
`benchmarks/i3_ad_landing_apify_acceptance_2026-10-14.json`.

### Productization roadmap

With Stage 12B live acceptance complete, the Store/product track continues with:

1. HTML dashboard / human-readable monitoring view;
2. finish the friendly Store input/output presentation around the now-stable contracts;
3. complete the Stage 15 private Pay-per-event cost matrix before enabling billing;
4. private end-to-end tests with schedules, alert consumers, quotas, and failure cases;
5. Store beta publication.

The Stage 12 branch already starts item 2 by reorganizing the existing Apify input UI
into Competitors, Monitoring, Reliability, and Optional AI sections without breaking
the underlying input keys.

### Development

The project uses Python 3.14 and uv.

```bash
uv sync
uv run ruff check .
uv run ruff format --check .
uv run ty check adpulse tests
uv run pytest -q
```

Source/state canaries are manual regression tools:

```bash
uv run python -m adpulse.canary
uv run python -m adpulse.normalizer.canary
uv run python -m adpulse.state.canary
uv run python -m adpulse.fingerprint.canary
```

They distinguish legitimate source-data changes from transport, normalization,
or state regressions.

### Pipeline

```text
Collector
  -> Normalizer
  -> Fingerprinting
  -> Diff Engine (reads previous committed state)
  -> Quality creative gates
  -> Intelligence evaluation
  -> Semantic enrichment (optional provider/cache)
  -> Dataset
  -> Semantic cache + State / Diff / Intelligence sidecars
  -> Quality + Intelligence + Semantic summaries
  -> Opportunity Engine (read-only semantic cache, zero provider calls)
  -> STRATEGY_SUMMARY
  -> Alert Engine (Diff + final Quality + Strategy policy)
  -> ALERT_EVENTS + ALERT_SUMMARY
  -> Monitoring Orchestration
  -> ORCHESTRATION_SUMMARY
  -> Delivery Outbox / optional webhook
  -> DELIVERY_OUTBOX + DELIVERY_SUMMARY
  -> Competitive Brief / Reporting
  -> COMPETITIVE_BRIEF + REPORT_SUMMARY
  -> Experience Read Model / Dashboard
  -> DASHBOARD_DATA + API_INDEX + DASHBOARD.html
  -> Private Economics Observer
  -> COST_LEDGER + ECONOMICS_SUMMARY + PPE_BENCHMARK_PLAN
  -> Guarded PPE admission (before expensive work)
  -> Product/state/output completion
  -> Guarded PPE settlement (disabled by default; visible competitor snapshots only)
  -> PPE_BILLING + COMMERCIAL_POLICY
```

Each layer must have explicit acceptance criteria and regression coverage before
the next layer is treated as complete.

### Cross-platform Content Graph

Innovation I4 adds a provenance-preserving graph above Creative Lineage and Ad↔Landing evidence. Platform-specific families remain separate nodes; exact asset reuse, normalized copy reuse, shared exact landing destinations, and semantic concept similarity are represented as typed edges rather than silent identity merges.

The accepted graph substrate is Google-backed. A public Meta Ad Library second-source adapter is now implemented behind explicit advertiser/domain mapping and is in live validation. Graph comparison itself remains provider-free; when the optional Meta adapter is enabled, its bounded public-source requests are metered separately and remain non-billable internal work.

See docs/CROSS_PLATFORM_CONTENT_GRAPH.md.

### Innovation I4 Google substrate live acceptance

The Cross-platform Content Graph substrate plus Google Ads Transparency adapter was
accepted on live Apify on 2026-10-14. Two same-namespace runs advanced State 14 -> 15 -> 16,
kept all 15 graph node IDs stable, synchronized the revision-bound sidecar, created zero
same-platform cross-platform edges, added zero provider/source calls, and preserved all
identity/performance/causality guardrails. Warm runtimes observed by the user were 18s and
22s.

This is intentionally not full I4 acceptance. Cross-platform comparison remains
ineligible until a second real platform source is integrated.

Evidence: `benchmarks/i4_google_substrate_apify_acceptance_2026-10-14.json`.

#### Meta second-source adapter

I4 now includes an optional public Meta Ad Library SSR adapter. It requires an
explicit mapping from an existing Google advertiser ID to a Meta search query and
verified advertiser domain. Only public Meta ads whose landing/display evidence
matches that domain are admitted to the same advertiser entity.

The adapter uses anonymous public-library evidence, no login, no private account
data, no provider calls, and no GraphQL pagination in v1. Meta failure is fail-soft:
Google monitoring still completes, and the graph records the external-source status
instead of interpreting failure as absence.

### Innovation I4 full live acceptance

Cross-platform Content Graph received full live acceptance on 2026-10-14 using two
real public sources: Google Ads Transparency and Meta Ad Library.

Two same-namespace runs advanced State 16 -> 17 -> 18, kept the 15 accepted Google
nodes stable, retained one verified Meta Nike node with stable source identity/copy/
landing evidence, and preserved cross-platform eligibility. The Meta adapter examined
30 public SSR rows per run and admitted exactly one after nike.com ownership verification;
29 non-matching rows were rejected rather than merged.

No cross-platform edge was emitted because the current Google and Meta evidence did not
share exact copy or exact landing identity. This zero-edge result is accepted evidence
of noise suppression, not missing functionality.

All identity/performance/causality guardrails remained false, provider calls remained
zero, and observed runtimes were 20 seconds on both runs.

Evidence:
`benchmarks/i4_cross_platform_apify_acceptance_2026-10-14.json`.

### Evidence-weighted Opportunity Engine

Innovation I5 upgrades Stage 11 strategy hypotheses with deterministic evidence weighting.
Each test candidate exposes observed-family coverage, semantic coverage, per-dimension usable
coverage, source freshness, Strategy Delta comparability, ambiguity, limitations, and a stable
opportunity ID.

The 0–100 evidenceStrengthScore measures evidence sufficiency only. It is not a winner,
conversion, ROAS, spend, or business-value score. Coverage/freshness gates can cap confidence,
and strong observed-population gap claims are refused unless inventory, family, semantic,
dimension, and freshness gates are complete.

I5 makes zero provider calls and zero additional ad-source requests. All action-oriented
hypotheses require first-party validation.

See docs/EVIDENCE_WEIGHTED_OPPORTUNITY.md.

Innovation I5 received live Apify acceptance on 2026-10-14. Two consecutive same-namespace Nike/BR runs advanced State 18 -> 19 -> 20, kept all 15 creatives unchanged, reproduced the same five durable opportunity IDs with identical source-hypothesis bindings, scores and confidence classes, refused all six strong-gap dimensions under 80% semantic-family coverage, and introduced zero provider/performance/causal/market-absence claims. Evidence is recorded in `benchmarks/i5_opportunity_apify_acceptance_2026-10-14.json`.

### First-party Validation Loop

Innovation I6 links customer-owned experiment results to the durable `opp_*`
identities accepted in I5. The input is optional and disabled by default.

Each result is an immutable aggregate record containing a stable `validationId`,
advertiser/opportunity identity, a customer/system-declared outcome
(`validated`, `rejected`, or `inconclusive`), one primary metric, control/variant
values, optional aggregate sample sizes, timestamp, and source-system provenance.

AdPulse calculates descriptive absolute/relative deltas only. It does not infer
statistical significance, causal effect, or winner status from aggregate values.
A first-party result also never changes the public-competitor
`evidenceStrengthScore`, confidence caps, gap eligibility, or performance truth.

Replaying an identical validation record is idempotent. Reusing the same
`validationId` with different payload content fails closed and preserves the
original record. The persistent ledger is exposed through
`FIRST_PARTY_VALIDATION_SUMMARY`, `FIRST_PARTY_VALIDATION_LEDGER`, the Stage 14
read model, and API index.

See `docs/FIRST_PARTY_VALIDATION.md`.

Innovation I6 received live Apify acceptance on 2026-10-14 using a dedicated
`i6-nike-br-acceptance` namespace. Four successful runs advanced State
`0 -> 1 -> 2 -> 3 -> 4` and proved new-record ingestion, exact-payload
idempotency, fail-closed immutable-ID conflict handling, and ledger continuity while
ingestion was disabled. The accepted synthetic fixture remained customer-declared
`inconclusive`; AdPulse recorded descriptive deltas only and never inferred
significance, causality, winner status, or competitor performance. A modified
`validationId` payload was rejected while the original record, payload hash,
ingestion revision, and `firstPartyEvidenceId` remained intact. A separate Google
source timeout between successful runs did not advance State or mutate the I6 ledger.
Evidence is recorded in
`benchmarks/i6_first_party_validation_apify_acceptance_2026-10-14.json`.

# Actor input Schema

## `advertiserIds` (type: `array`):

Exact Google Ads Transparency advertiser IDs. Recommended for recurring monitoring because they keep history attached to the same advertiser. advertiserIds + competitor names may total at most 50 targets per run.

## `searchTerms` (type: `array`):

Competitor names to resolve when you do not know the advertiser ID. Exact IDs are safer for recurring monitoring; ambiguous matches fail instead of attaching history to the wrong advertiser. Combined with advertiser IDs, at most 50 targets are allowed.

## `region` (type: `string`):

Two-letter Google Ads Transparency country code, such as BR or US. Use 'anywhere' for an unfiltered market.

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

Maximum unique creatives collected per competitor in this run. Store release is intentionally capped at 100 creatives per competitor to keep the US$0.10 snapshot economics and source workload inside the accepted operating envelope. This is a source-work limit, not the number of billable competitor snapshots.

## `mode` (type: `string`):

Pulse is the normal low-cost recurring scan. Reconciliation is the stronger confirmation pass used when disappearance evidence needs a complete follow-up.

## `stateNamespace` (type: `string`):

Stable lowercase workspace key for monitoring history. Reuse the same value for runs that belong to the same monitor; use a different value to isolate histories.

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

Use Apify Proxy for more reliable source access. Recommended for Store runs; AdPulse rotates unhealthy source sessions without treating a failed fetch as an empty result.

## `strictSchema` (type: `boolean`):

Recommended. Stops the run if Google's undocumented response shape changes in a way that could make the data unsafe to interpret.

## `semanticEnrichment` (type: `object`):

Optional vision-based semantic analysis of eligible creative families. Disabled by default. If enabled, OPENAI_API_KEY must be configured as an Actor environment variable; secrets are never accepted in input or persisted to outputs.

## `landingIntelligence` (type: `object`):

Optionally resolve source-backed ad destinations and observe exact public landing pages when Google exposes a defensible final URL. Google ad click trackers are never requested.

## `monitoringPolicy` (type: `object`):

Intended monitoring cadence for pulse, periodic reconciliation, and follow-up after a one-observation MISSING watch. Apify schedules themselves remain platform-level.

## `delivery` (type: `object`):

Outbox mode writes deterministic delivery payloads only. Webhook mode additionally sends deliveryRecommended=true messages to the HTTPS URL stored in the selected environment variable.

## `economicsBenchmark` (type: `object`):

Internal cost-observability configuration retained for controlled owner benchmarks. Hidden from the Store form and disabled by default.

## `crossPlatformSources` (type: `object`):

Optional Meta Ad Library source mappings. Meta evidence is joined to a Google advertiser only through an explicit Google advertiser ID plus verified domain mapping.

## `firstPartyValidation` (type: `object`):

Optionally attach immutable customer-declared experiment results to stable opportunity IDs. AdPulse stores descriptive deltas but does not infer statistical significance, causal effect, winner status, or competitor performance.

## Actor input object example

```json
{
  "advertiserIds": [
    "AR16735076323512287233"
  ],
  "searchTerms": [
    "Nike"
  ],
  "region": "BR",
  "maxAds": 100,
  "mode": "pulse",
  "stateNamespace": "default",
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "strictSchema": true,
  "semanticEnrichment": {
    "enabled": false,
    "provider": "openai",
    "model": "gpt-5-mini",
    "maxFamiliesPerRun": 20,
    "imageDetail": "auto"
  },
  "landingIntelligence": {
    "enabled": false,
    "maxCreativeDetailsPerRun": 20,
    "maxPagesPerRun": 10,
    "pageTimeoutSeconds": 15,
    "maxHtmlChars": 1000000,
    "unavailableRetryRevisions": 7
  },
  "monitoringPolicy": {
    "pulseIntervalHours": 24,
    "periodicReconciliationIntervalHours": 168,
    "missingWatchReconciliationDelayHours": 6
  },
  "delivery": {
    "mode": "outbox",
    "webhookEnvVar": "ADPULSE_DELIVERY_WEBHOOK_URL",
    "maxAttempts": 3,
    "timeoutSeconds": 10
  },
  "economicsBenchmark": {
    "enabled": false,
    "scenario": "auto",
    "sampleGroup": "stage15-private-ppe"
  },
  "crossPlatformSources": {
    "metaAdsLibrary": []
  },
  "firstPartyValidation": {
    "enabled": false,
    "records": []
  }
}
```

# Actor output Schema

## `dashboard` (type: `string`):

No description

## `brief` (type: `string`):

No description

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

No description

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

No description

## `alertSummary` (type: `string`):

No description

## `qualitySummary` (type: `string`):

No description

## `intelligenceSummary` (type: `string`):

No description

## `strategySummary` (type: `string`):

No description

## `strategyDeltaSummary` (type: `string`):

No description

## `opportunitySummary` (type: `string`):

No description

## `opportunityEvidence` (type: `string`):

No description

## `lineageSummary` (type: `string`):

No description

## `lineageGraph` (type: `string`):

No description

## `adLandingSummary` (type: `string`):

No description

## `landingPageSnapshots` (type: `string`):

No description

## `adLandingEvents` (type: `string`):

No description

## `contentGraphSummary` (type: `string`):

No description

## `contentGraph` (type: `string`):

No description

## `firstPartyValidationSummary` (type: `string`):

No description

## `firstPartyValidationLedger` (type: `string`):

No description

## `api` (type: `string`):

No description

## `apiIndex` (type: `string`):

No description

## `diagnostics` (type: `string`):

No description

# API

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

## JavaScript example

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

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

// Prepare Actor input
const input = {
    "advertiserIds": [
        "AR16735076323512287233"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("p90dro/adpulse").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 = {
    "advertiserIds": ["AR16735076323512287233"],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("p90dro/adpulse").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 '{
  "advertiserIds": [
    "AR16735076323512287233"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call p90dro/adpulse --silent --output-dataset

```

## MCP server setup

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

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/PjVp4GqN86wzBWWBn/builds/zQdLDYUTNYMbdQxZP/openapi.json
