Track Google AI Overviews in Python: presence, sources, changes

We checked 100 Google queries for AI Overviews on 19 October 2026: who gets one, what it cites, how stable it is, plus a Python tracker that stores snapshots.

Call /v1/serp and look for an ai_overview object in the response. If it is there, text_blocks holds the overview text and references the pages it cites; if it is missing, Google showed none for that query. We ran 100 queries this way on 19 October 2026: 57 had an overview, they cited 7.5 sources on average, and about two thirds of the cited sites also ranked on the same results page. Re-running 20 of them minutes apart changed the cited sources far more often than the overview’s presence.

This post covers AI Overviews on the normal results page. For Google’s separate AI Mode, see the AI Mode post; for classic position tracking, see monitoring keyword rankings.

What does an AI Overview look like in the API response?

It is a top-level ai_overview key next to organic_results. What we saw on 19 October 2026:

  • ai_overview.text_blocks: a list of {type, snippet} objects. In all 57 overviews we collected, every block had type paragraph, even visual headings, and the text averaged 232 words (88 to 415).
  • ai_overview.references: the cited pages, each with index, title, source (the site’s display name) and link. All 425 links were absolute URLs, and 32 titles contained a literal <b> tag, so strip markup before showing them.
  • answer_box and peopleAlsoAskedFor were non-empty in exactly the 57 responses with an overview, and carry fragments of it (first sentence, first reference). Test ai_overview itself, not those.

Google’s documentation says no special markup is needed to be cited: a page must be indexed and snippet-eligible, and clicks land in Search Console’s normal Web search type (Search Central, AI features, last updated December 2025). Presence is therefore something you measure from outside, as the tracker below does.

How often do queries show an AI Overview?

We built 100 queries, 20 for each of five intents, and ran each once with country=us, default language, desktop (about 18:00 UTC, 19 October 2026). Five parallel workers took about 85 seconds.

Intent Example query With overview Share
Informational how do vaccines work 19 of 20 95%
Commercial best crm for small business 20 of 20 100%
Navigational netflix 14 of 20 70%
Local plumber in austin tx 2 of 20 10%
Developer fix cors error 2 of 20 10%
All 57 of 100 57%

“How” and “best” queries almost always got one. The two local hits were “things to do in new orleans” and “pharmacy open 24 hours in atlanta”. The six navigational misses were brand lookups (chatgpt, youtube, netflix, linkedin, paypal login, walmart). Developer queries surprised us: only “what is a webhook” and “http 429 too many requests” had one. With 20 queries per intent, treat gaps under about 15 points as noise.

How many sources does an AI Overview cite, and do they match the top 10?

An overview cited 7.5 sources on average (median 7, range 2 to 13). We compared each cited site with the sites in that response’s organic_results, which averaged 8.9 results rather than a full ten.

Intent Mean sources Cited sites also in organic results Exact cited URLs also in organic results
Informational 9.4 70% 54%
Commercial 7.2 66% 44%
Navigational 5.8 70% 49%
Local (2 overviews) 5.5 90% 73%
Developer (2 overviews) 5.5 45% 36%
All 57 overviews 7.5 69% 50%

Every overview shared at least one site with the organic results, but only half of the exact cited URLs were organic URLs. Ranking on page one makes a citation likely, not certain, and about a third of cited sites were not on the page at all. Excluding YouTube, overlap is 68%.

Which domains does Google cite most?

243 distinct sites were cited across the 57 overviews. YouTube led by a wide margin.

Site Overviews citing it Share of the 57
youtube.com 38 67%
reddit.com 12 21%
wikipedia.org 8 14%
facebook.com 7 12%
nytimes.com 6 11%
cnet.com 5 9%
mayoclinic.org, pcmag.com, rtings.com, apple.com 4 each 7%

YouTube appeared in 16 of 19 informational and 18 of 20 commercial overviews, but only 3 of 14 navigational ones. A video can be the citation, and review sites (nytimes, cnet, pcmag, rtings) cluster in “best X” queries.

How stable is an AI Overview between runs?

We picked 20 of the 100 queries across all intents (16 with an overview on the first pass, 4 without) and ran them twice more: about a minute after the first pass, then five minutes after that.

Measure Result
Overview present in all 3 runs 15 queries
Overview absent in all 3 runs 3 queries
Presence flipped 2 queries (“facebook login” in 2 of 3, “netflix” in 1 of 3)
Same text and same sources in all 3 runs 5 of 16
Mean overlap of cited URLs between runs 72%
Mean overlap of cited sites between runs 78%
Mean text similarity (word level) 64%

The last four rows cover the 16 queries with an overview in at least two runs. Five (including “best vpn” and “how do vaccines work”) came back word for word identical. Others moved a lot: for “what causes inflation” the source count went 10, 10, 8, and between the last two runs six sites dropped out while congress.gov and youtube.com came in. Presence is mostly stable over minutes; sources and wording are not. Alert on appearing, disappearing and your own domain entering or leaving, not on text changes.

How do I track AI Overviews daily in Python?

This is the script we ran for the dataset. It stores one snapshot per query per run in SQLite and prints what changed since the previous one. Set WATCH_DOMAIN to be told when you gain or lose a citation.

import difflib, json, os, sqlite3, sys
from datetime import date
from urllib.parse import urlparse
import requests

API = "https://api.fastaf.co/v1"
KEY = os.environ["FASTAF_API_KEY"]
DB = os.environ.get("AIO_DB", "aio.db")
WATCH = os.environ.get("WATCH_DOMAIN", "")  # e.g. example.com


def host(url):
    return (urlparse(url).hostname or "").removeprefix("www.")


def parse(data):
    ao = data.get("ai_overview")
    return {
        "has_overview": ao is not None,
        "text": " ".join(b.get("snippet", "") for b in (ao or {}).get("text_blocks", [])).strip(),
        "sources": [{"title": s.get("title"), "source": s.get("source"), "link": s["link"]}
                    for s in (ao or {}).get("references", [])],
        "organic": [o["link"] for o in data.get("organic_results", [])],
    }


def cites(domain, sources):
    return any(host(x["link"]) == domain or host(x["link"]).endswith("." + domain) for x in sources)


def fetch(query, country="us"):
    r = requests.get(f"{API}/serp", params={"query": query, "country": country},
                     headers={"Authorization": f"Bearer {KEY}"}, timeout=90)
    r.raise_for_status()
    return parse(r.json())


def init(db=DB):
    conn = sqlite3.connect(db)
    conn.execute("""CREATE TABLE IF NOT EXISTS snapshots (
        snap TEXT, query TEXT, has_overview INTEGER, text TEXT, sources TEXT, organic TEXT,
        taken_at TEXT DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (snap, query))""")
    return conn


def save(conn, snap, query, s):
    conn.execute("INSERT OR REPLACE INTO snapshots (snap, query, has_overview, text, sources, organic) "
                 "VALUES (?,?,?,?,?,?)",
                 (snap, query, int(s["has_overview"]), s["text"],
                  json.dumps(s["sources"]), json.dumps(s["organic"])))
    conn.commit()

The report compares the latest two snapshots:

def report(conn):
    snaps = [r[0] for r in conn.execute(
        "SELECT snap FROM snapshots GROUP BY snap ORDER BY MIN(taken_at) DESC LIMIT 2")]
    if len(snaps) < 2:
        return print("Need two snapshots to compare.")
    new, old = snaps
    q = "SELECT query, has_overview, text, sources FROM snapshots WHERE snap = ?"
    a = {r[0]: r[1:] for r in conn.execute(q, (old,))}
    b = {r[0]: r[1:] for r in conn.execute(q, (new,))}
    print(f"{old} -> {new}")
    for query in sorted(a.keys() & b.keys()):
        (had, t0, s0), (has, t1, s1) = a[query], b[query]
        if not had and has:
            print(f"APPEARED   {query}")
        elif had and not has:
            print(f"DISAPPEARED {query}")
        elif has:
            if WATCH and cites(WATCH, json.loads(s0)) != cites(WATCH, json.loads(s1)):
                print(f"{'GAINED' if cites(WATCH, json.loads(s1)) else 'LOST'} CITATION {query}")
            d0 = {host(x["link"]) for x in json.loads(s0)}
            d1 = {host(x["link"]) for x in json.loads(s1)}
            same = difflib.SequenceMatcher(None, t0.split(), t1.split(), autojunk=False).ratio()
            if d0 != d1 or same < 0.9:
                print(f"CHANGED    {query}: text {same:.0%} alike, +{sorted(d1 - d0)} -{sorted(d0 - d1)}")


if __name__ == "__main__":
    snap = sys.argv[1] if len(sys.argv) > 1 else date.today().isoformat()
    conn = init()
    for query in [q.strip() for q in open("queries.txt") if q.strip()]:
        s = fetch(query)
        save(conn, snap, query, s)
        print(f"{query}: {'overview, %d sources' % len(s['sources']) if s['has_overview'] else 'no overview'}")
    report(conn)

Excerpt of the real output comparing our second and third run with WATCH_DOMAIN=youtube.com:

LOST CITATION best noise cancelling headphones
CHANGED    best noise cancelling headphones: text 21% alike, +['audiophileon.com'] -['youtube.com']
DISAPPEARED netflix
GAINED CITATION what causes inflation

autojunk=False matters: with the default, long texts look far less similar than they are.

What does it cost to track AI Overviews?

A /serp call is 5 credits and the overview comes in the same response, so tracking costs nothing extra. One pass over 100 queries is 500 credits, exactly the free allowance, and about $0.25 on the Starter pack ($10 for 20,000 credits). The same 100 queries daily for 30 days is 15,000 credits, $7.50 at Starter prices; see pricing and the SERP API page. Two extra calls on 19 October 2026 returned HTTP 200 with X-FastAF-Credits-Used: 5, in 3.9 and 14.4 seconds, so use a generous timeout, as the script does.

What does this data not show?

One country (us), one language, desktop, no location, one afternoon, and 100 queries we wrote ourselves, not a sample of real traffic. Local and developer overlap rest on 2 overviews each. Google decides per user and per moment, so what the API returns can differ from your browser, and the volatility run spans six minutes. Repeat it with your own queries before building a strategy on these numbers.

The short version

Check ai_overview in the /serp response, store references daily in SQLite, and alert on appearance, disappearance and your domain entering or leaving the citations. In our 100 queries, “how” and “best” queries nearly always got an overview, local and developer ones rarely, sources were reshuffled far more than presence, and YouTube was cited in two thirds of overviews. Get 500 free credits and run your own query list through the script; the docs list every /serp parameter.