# GEO/AEO Brand Visibility Analyzer (`mehdi_badawi/geo-aeo-visibility`) Actor

Measure brand mentions, citations, explicit recommendation order, and run-to-run variance across AI-search observations you supply. Deterministic analysis without extra inference cost.

- **URL**: https://apify.com/mehdi\_badawi/geo-aeo-visibility.md
- **Developed by:** [Mehdi Badawi](https://apify.com/mehdi_badawi) (community)
- **Categories:** SEO tools, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 analyzed visibility observations

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

## GEO/AEO Brand Visibility Analyzer

Measure brand mentions, citations, explicit recommendation order, and
run-to-run variance across AI-search observations you supply. Results retain
the query, engine, timestamp, source, and every individual observation.

### Start in 30 seconds

1. Select **Try for free** and run with no input for a multi-engine demo.
2. Supply a target brand and normalized observations from your collection
   workflow.
3. Use the dataset for row-level evidence and `METRICS` for trend analysis.

**Price:** $0.005 per successfully analyzed observation, plus a $0.00005 start
event. Engine failures, failed runs, and demo observations are free.

This Actor does not query ChatGPT, Gemini, Claude, Perplexity, Copilot, or
Google AI Overviews. Upstream collection and inference remain separate.

### Why preserving variance is the product

In traditional SEO, search engine result pages (SERPs) are relatively stable. In AI-search engines (GEO/AEO), models exhibit high stochastic variance:

- An engine may cite your brand on run 1, omit it on run 2, mention it without a link on run 3, and time out on run 4.
- Averaging this into a single metric (e.g. "50% visibility") destroys critical operational intelligence: the volatility, position spread, citation domain distribution, and failure modes.

This Actor:

1. **Preserves every observation as an independent dataset row** with complete engine, query, timestamp, and source provenance.
2. **Emits deterministic variance metrics** (`outcomeDistribution`, `binaryCitationVariance`, `positionVariance`, `observedPositions`, `minPosition`, `maxPosition`, `medianPosition`, `citationUrlFrequencies`, `competitorShare`).
3. **Tracks longitudinal variance across scheduled runs** using key-value store persistence (`geo-aeo-visibility-state`).

### Visibility states

Every evaluated observation is classified into one of four deterministic states:

| State | Condition | Metrics |
|---|---|---|
| `cited` | Target brand domain appears in the engine's citation URLs | `isCited: true`, `isMentioned: true`, `brandRank: integer` |
| `uncited` | Target brand name/alias mentioned in answer text, but domain not cited | `isCited: false`, `isMentioned: true`, `brandRank: integer` |
| `missing_brand` | Neither brand name nor brand domain appears; competitor cited/mentioned | `isCited: false`, `isMentioned: false`, `brandRank: null` |
| `engine_failed` | Engine returned error, timeout, rate-limit, blocked, or empty response | `isCited: false`, `isMentioned: false`, `error: object` |

### Contract & Outputs

#### Dataset items (default dataset)

One record per observation preserving:

- `engine`: AI engine identifier (`perplexity`, `chatgpt`, `gemini`, `copilot`, etc.)
- `queryId` & `query`: stable query key and verbatim query text
- `capturedAt`: ISO-8601 instant when the observation was recorded
- `source`: provenance object (`collector`, `runId`, `model`, `sourceUrl`)
- `rawStatus`: upstream status (`success`, `error`, `timeout`, `blocked`, `rate_limited`)
- `visibilityState`: `cited` | `uncited` | `missing_brand` | `engine_failed`
- `isMentioned`: boolean
- `isCited`: boolean
- `brandRank`: explicit 1-based recommendation position when supported by the supplied answer structure; a mention alone does not create a rank
- `citedUrls`: list of URLs matching target brand domains
- `competitorsMentioned` & `competitorsCited`: list of competitor names present

#### Key-Value Store records

- `OUTPUT`: run envelope with aggregate counts, overall visibility rates, and summary.
- `METRICS`: granular per-query $\times$ per-engine metrics with variance calculations.
- `STATE`: durable snapshot of observation history for cross-run variance tracking.

### Local development & Testing

The Actor is 100% offline, deterministic, and requires no external API keys or paid services.

#### Run tests

```bash
npm test
```

Executes Node's built-in test runner (`node --test`), verifying normalization, all visibility states (`cited`, `uncited`, `missing_brand`, `engine_failed`), provenance preservation, multi-run variance calculations, and Actor adapter seams.

#### Run credential-free demo

```bash
npm start
```

When invoked without input, the Actor automatically loads built-in synthetic multi-engine observations covering all visibility and variance states, populating the default dataset and key-value store records without requiring credentials.

### Package layout

| Path | Purpose |
|---|---|
| `src/main.mjs` | Apify Actor adapter with storage and seam wiring |
| `src/demo-input.mjs` | Synthetic multi-engine demo dataset for credential-free runs |
| `src/core/contract.mjs` | Contract versions, constants, and state definitions |
| `src/core/canonical.mjs` | Deterministic domain matching and string utilities |
| `src/core/normalize.mjs` | Input schema validation, caps, and entity normalization |
| `src/core/evaluate.mjs` | Pure evaluation engine (no I/O, no wall-clock dependencies) |
| `src/core/variance.mjs` | Statistical variance, spread, and distribution calculations |
| `tests/fixtures/` | Synthetic query-result fixtures across multiple engines |
| `tests/core/` | Unit tests for evaluate and variance models |
| `tests/actor/` | Seam integration tests for Actor lifecycle and storage |
| `.actor/` | Actor specification and input/output/dataset/KVS schemas |
| `Dockerfile` | Apify Actor Node.js 22 runtime image |
| `package.json` | Pinned dependencies and scripts (`npm test`, `npm start`) |

### Non-goals

- No live search or LLM API calls (offline evaluation of supplied observations).
- No browser automation or scraping in this processing Actor.
- No paid engine credentials or external tokens required.

### Data, state, and support

Prompts and answers may be confidential. Minimize them, keep only the fields
needed for analysis, and do not use customer observations in public samples.
Use one `stateStoreName` per target brand; state from another brand is rejected
or isolated. Export `STATE` before recovery and delete its named store when the
retention period ends. Support owner: Mehdi Badawi through the Apify Store
support channel, with an initial-response target of two business days.

# Actor input Schema

## `contractVersion` (type: `string`):

Must equal 1.0.0 when provided. The Actor stamps the current contract version when omitted.

## `evaluationTime` (type: `string`):

ISO-8601 instant treated as evaluation timestamp (e.g. 2026-09-21T10:00:00Z).

## `targetBrand` (type: `object`):

Target brand name, recognized domains, and name aliases.

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

Optional list of competitor brands to track win-share when the target brand is absent or uncited.

## `queries` (type: `array`):

Declared set of target queries to track.

## `observations` (type: `array`):

Supplied query-result observations from AI search engines (ChatGPT, Perplexity, Gemini, Copilot, etc.).

## `stateStoreName` (type: `string`):

Key-value store name for persisting longitudinal variance history. Null disables cross-run persistence.

## `priorState` (type: `object`):

Optional prior run state for replaying or testing cross-run variance calculations without store access.

## Actor input object example

```json
{
  "contractVersion": "1.0.0",
  "stateStoreName": "geo-aeo-visibility-state"
}
```

# Actor output Schema

## `statusRows` (type: `string`):

One dataset item per observation: preserves engine, query, capture time, source provenance, visibility state, cited status, brand rank, citations, and competitor presence.

## `runOutput` (type: `string`):

Summary envelope containing run status, aggregate counts, overall visibility metrics, and nextState snapshot.

## `metrics` (type: `string`):

Deterministic query x engine visibility metrics preserving run-to-run variance, outcome distributions, position spread, and citation frequencies.

# API

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

## JavaScript example

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

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

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("mehdi_badawi/geo-aeo-visibility").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("mehdi_badawi/geo-aeo-visibility").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call mehdi_badawi/geo-aeo-visibility --silent --output-dataset

```

## MCP server setup

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

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/Q2ysNwvLQPWfEF8rF/builds/zqB7NRRiQ90JNAMjd/openapi.json
