An AI Overview competitor monitor records which domains Google cites for a fixed query set, then compares those observations by market and date. It should report direct brand mentions and source citations as different signals. The result is a dated dataset, not a universal visibility score or a claim that one competitor "owns" a topic.
A competitor monitor can answer a narrow, useful question: which domains were cited in Google AI Overviews for the searches we checked? It cannot tell you why Google selected them, and it should not turn a small keyword sample into a market-share claim.
Build the monitor around repeatable observations. Fix the query set, country, language, and collection schedule. Store every returned source and the answer text, then calculate metrics only from successful, eligible checks.
What should an AI Overview competitor monitor measure?
Measure citation presence by query, cited source slots by domain, and changes between observation periods. Keep direct mentions in ai_overview in a separate column. A company name in the answer is not the same event as a link to that company's website.
The Google AI Overviews API response tested on August 5, 2026 contained an ordered sources array. Each source included a URL, title, site, and position. That gives the monitor a defensible unit: one returned source record in one dated observation.
Inspect the source records for one query
Use one observation model for every domain
Do not create one table per competitor. Store results without deciding in advance which domains matter. A new source may appear later, and the raw observation should still be usable.
CREATE TABLE overview_run (
id BIGINT PRIMARY KEY,
query_text TEXT NOT NULL,
country_code CHAR(2) NOT NULL,
language_code VARCHAR(10) NOT NULL,
observed_at TIMESTAMP NOT NULL,
request_state VARCHAR(30) NOT NULL,
answer_text TEXT NULL
);
CREATE TABLE overview_citation (
run_id BIGINT NOT NULL,
source_position INTEGER NULL,
source_domain TEXT NOT NULL,
source_url TEXT NOT NULL,
source_title TEXT NULL
);
Add a query topic or campaign ID in your application model. That lets an analyst compare product questions with informational questions without pretending that they carry the same commercial weight.
Collect and normalize the returned citations
function normalizeDomain(source) {
const value = source.site || new URL(source.url).hostname;
return value.toLowerCase().replace(/^www\./, "").replace(/\.$/, "");
}
function citationRows(runId, payload) {
const sources = Array.isArray(payload?.data?.sources)
? payload.data.sources
: [];
return sources
.filter((source) => source && typeof source.url === "string")
.map((source) => ({
runId,
position: Number.isInteger(source.position) ? source.position : null,
domain: normalizeDomain(source),
url: source.url,
title: source.title ?? null
}));
}
Keep the URL as returned. Domain normalization is for grouping, not for erasing evidence. If two URLs redirect to one canonical page, enrich that in a separate job.
Log HTTP errors as failed runs. We saw temporary HTTP 500 responses during the live test. Counting those as zero citations would make every domain look weaker when the collector was the part that failed.
Build the collector from a live response
Competitor metrics that stay interpretable
Three definitions are enough for a first report, but they should not be forced into one score.
| Metric | Formula | What it says |
|---|---|---|
| Query citation rate | Queries citing domain / eligible successful queries | How often the domain appeared at least once |
| Source-slot share | Domain source rows / all source rows | How much of the returned citation list it occupied |
| Median source position | Median returned position for domain | Where its citations appeared in the source array |
An eligible query is one whose request succeeded and returned the kind of source data required by the metric. Publish that denominator beside the result. A percentage without its query count is easy to overread.
SELECT
source_domain,
COUNT(DISTINCT run_id) AS cited_runs,
COUNT(*) AS source_slots,
MIN(source_position) AS best_observed_position
FROM overview_citation
WHERE run_id IN (
SELECT id FROM overview_run
WHERE request_state = 'success'
AND observed_at >= :period_start
AND observed_at < :period_end
)
GROUP BY source_domain
ORDER BY cited_runs DESC, source_slots DESC;
Compare matched periods, not screenshots
A screenshot can verify one result. It cannot show a trend. Compare two periods built from the same query IDs and market settings. If the query list changed, label the comparison as unmatched or calculate only on the intersection.
| Change | Evidence to keep |
|---|---|
| Newly cited domain | First matching source URL and observation |
| Lost citation | Previous match plus current successful non-match |
| Different cited page | Old and new URLs for the same domain |
| Answer mention changed | Matched answer snapshots, separate from citations |
Use the single-domain citation checker when the job is a quick audit. Read the ranking and citation comparison before mixing classic rank positions into this report.
What this monitor cannot prove
The data shows what the API observed for the supplied inputs. It does not prove why a page was cited, whether every user saw the same result, or whether a content change caused the next citation change. Google says AI features can use related searches across subtopics and that the responses and links can vary.
Keep editorial explanations in a notes field, separate from collected facts. If an analyst believes a competitor page answers a missing subtopic, label that as a hypothesis to review, not as an API finding.