---
title: "Track Brand Mentions in AI Answers with an API"
description: "A daily Python job that sends fixed prompts to an LLM answers endpoint and Google's AI overview, logs brand mentions and cited domains, at 2 credits per prompt."
url: https://perceptlayer.com/blog/track-brand-mentions-in-ai-answers
---

[Blog](https://perceptlayer.com/blog) / [AI agents](https://perceptlayer.com/blog/category/ai-agents)

# Track Brand Mentions in AI Answers with an API

9 Oct 2026 Perceptlayer 8 min read

To track brand mentions in AI answers, send the same set of prompts every day to an AI answer engine and to Google's AI overview, then record which brands each answer names and which domains it links. With Perceptlayer that's `GET /v1/llm-answers/answer` with a `prompt` and `GET /v1/google-search/search-ai-overview` with a `query`, 1 credit each. This tutorial builds the job in Python with SQLite: 15 prompts a day is 900 credits a month.

## Key takeaways

-   Two calls per prompt per day: `/v1/llm-answers/answer` (required `prompt`, optional `model`, `locale`, `timezone`) and `/v1/google-search/search-ai-overview` (required `query`, optional `hl`, `gl`).
-   `/v1/llm-answers/models` lists the models you can pass as `model`, filtered by `engine` set to `chatgpt` or `perplexity`.
-   The recorded LLM answer gives `answer.text`, a `sources` list where each source has `url`, `attribution` and a `cited` flag, and `model.resolved`: match brands in the text and count only sources with `cited: true`.
-   One answer is one sample. Report the share of days a brand appears per prompt, over weeks, not whether it appeared today.
-   15 prompts × 2 calls × 30 days = 900 credits a month, inside the 1,000 free monthly credits. Failed calls cost nothing.

## What are you building?

A script you run once a day. It reads a fixed list of prompts and the brands you care about, asks an AI answer engine each prompt, fetches Google's AI overview for the same question, and stores one row per prompt, engine and day. Each row says which of your brands were named and which domains the answer linked. A second query turns the rows into a weekly share-of-answers table.

Fixed prompts matter more than clever ones. Brand visibility in AI answers is only comparable over time if you ask the same questions in the same words, with the same model and locale, every day. Pick 10 to 20 prompts a buyer would actually type, such as "best project management tool for a 10-person agency", rather than prompts that contain your brand name.

You need Python 3.10 or newer, the `requests` package and an API key. [Get a free API key](https://perceptlayer.com/signup); verifying your email gives you 1,000 free credits a month. The [authentication docs](https://docs.perceptlayer.com/authentication) cover the `X-API-Key` header.

## Which endpoints does the job call?

Two endpoints do the work, and a third helps you choose a model once. Each costs 1 credit per successful call, and a failed call costs nothing and returns `X-Credits-Used: 0`. We have a recorded response for the LLM answer endpoint, shown in Step 3, but not for the AI overview, so the code handles that one without depending on its field names.

| Endpoint | Required | Optional | Credits |
| --- | --- | --- | --- |
| [`/v1/llm-answers/answer`](https://perceptlayer.com/apis/llm-answers/answer) | `prompt` (up to 4,000 characters) | `q`, `model` (up to 64), `timezone`, `locale` (up to 16), `history` | 1 |
| [`/v1/google-search/search-ai-overview`](https://perceptlayer.com/apis/google-search/search-ai-overview) | `query` (up to 2,048 characters) | `hl`, `gl`, `google_domain`, `uule` | 1 |
| [`/v1/llm-answers/models`](https://perceptlayer.com/apis/llm-answers/models) | none | `engine`: `chatgpt` or `perplexity` | 1 |

The two answer sources behave differently, and you should expect that in the data. Google says AI Overviews appear only when its systems judge them additive to classic search, so they often don't trigger ( [Google Search Central](https://developers.google.com/search/docs/appearance/ai-features), 2026). An AI overview row with no text is a result, not an error: for that prompt, that day, there was no overview to appear in.

## Step 1: How do you choose prompts, brands and a model?

Put the prompts and brands in a config file so a change shows up in version control, and pin the model and locale. Call the models endpoint once to see which models it lists for an engine, then copy one into the config. The call costs 1 credit, so there's no need to repeat it daily.

```bash
curl "https://api.perceptlayer.com/v1/llm-answers/models?engine=chatgpt" \
  -H "X-API-Key: $API_KEY"
```

Then write `config.json`:

```json
{
  "model": "PASTE_A_MODEL_FROM_THE_MODELS_CALL",
  "locale": "en-US",
  "hl": "en",
  "gl": "us",
  "brands": ["Asana", "Trello", "Monday.com", "ClickUp", "Notion"],
  "prompts": [
    "best project management tool for a 10-person agency",
    "what is the easiest kanban app for a small team",
    "project management software with time tracking"
  ]
}
```

List your own brand and the four or five rivals a buyer compares you with. Spell brands the way they're written in prose, because the match is on words. If you'd rather not pin one, leave `model` out, and keep it out on every run so the series stays comparable.

## Step 2: How do you call both answer sources?

One function per source, each returning the parsed JSON or `None` with the error code printed. Pass the prompt as `prompt` to the answer endpoint and as `query` to the AI overview endpoint. Use a long timeout, since generating an answer can take a while.

```python
import os
import requests

BASE = "https://api.perceptlayer.com/v1"
HEADERS = {"X-API-Key": os.environ["API_KEY"]}

def call(path, params):
    r = requests.get(f"{BASE}/{path}", params=params, headers=HEADERS, timeout=120)
    if r.status_code != 200:
        print(path, r.status_code, r.json()["error"]["code"])
        return None, 0
    return r.json(), int(r.headers.get("X-Credits-Used", "0"))

def llm_answer(cfg, prompt):
    params = {"prompt": prompt, "locale": cfg["locale"]}
    if cfg.get("model"):
        params["model"] = cfg["model"]
    return call("llm-answers/answer", params)

def ai_overview(cfg, prompt):
    return call("google-search/search-ai-overview",
                {"query": prompt, "hl": cfg["hl"], "gl": cfg["gl"]})
```

Each call returns the credits it used, read from the `X-Credits-Used` header, so the job can log its own spend. The calls run one after another, so a run stays within a free account's 1 request a second; if you parallelise it and get `rate_limited` (429), wait the seconds in `Retry-After`.

## Step 3: How do you extract brands and cited sources?

For the LLM answer, read the fields the recording shows. Our recorded call with `prompt=What is the tallest mountain in Europe? Give two sources.` returns the answer text, the sources the engine looked at, and which of them it cited. Trimmed with `…`:

```json
{
  "success": true,
  "data": {
    "answer": {
      "text": "The tallest mountain in Europe is **Mount Elbrus**, located in the Caucasus Mountains of southwestern Russia. Its highest summit reaches **5,642 metres (18,510 feet)** above sea level. …",
      "length": 607
    },
    "sources": [
      { "title": "8/28/2023\nEurope: Physical Geography\nD O N AT E\nEd", "url": "https://media.nationalgeographic.org/assets/reference/assets/europe-physical-geography-1.pdf", "attribution": "media.nationalgeographic.org", "pub_date": "2025-06-13", "type": "search_result", "cited": false, … },
      { "title": "El'brus", "url": "https://education.nationalgeographic.org/resource/elbrus/", "attribution": "education.nationalgeographic.org", "pub_date": "2023-10-19", "type": "search_result", "cited": true, … },
      { "title": "Explore_EN", "url": "https://europa.eu/europago/explore/pdf/flip-book/lets-explore-europe-en/NA0114598ENC_002.pdf", "attribution": "europa.eu", "pub_date": "2014-09-02", "type": "search_result", "cited": false, … }
    ],
    "citations": […],
    "model": { "resolved": "gpt-6", "requested": "auto" },
    …
  }
}
```

Two details matter for tracking. Of the three sources, only one has `cited: true`, so "sources the engine read" and "sources it cited" are different lists; record both. And `model` shows `requested: auto` resolved to a specific model, so store `model.resolved` with every row, or a silent model change will look like a change in visibility.

The AI overview response isn't recorded, so for it the code walks every string: a brand counts if its name appears as a whole word, and a domain counts if any URL points at it.

```python
import re
from urllib.parse import urlparse

def strings(obj):
    if isinstance(obj, str):
        yield obj
    elif isinstance(obj, dict):
        for v in obj.values():
            yield from strings(v)
    elif isinstance(obj, list):
        for v in obj:
            yield from strings(v)

def find_brands(text, brands):
    return [b for b in brands
            if re.search(rf"(?<!\w){re.escape(b)}(?!\w)", text, re.IGNORECASE)]

def extract(response, brands, source):
    if source == "llm":
        data = response["data"]
        text = data["answer"]["text"]
        cited = sorted({s["attribution"] for s in data.get("sources", []) if s.get("cited")})
        model = data.get("model", {}).get("resolved")
        return find_brands(text, brands), cited, len(text), model
    texts = list(strings(response))
    blob = "\n".join(t for t in texts if not t.startswith("http"))
    domains = sorted({urlparse(t).netloc.removeprefix("www.")
                      for t in texts if t.startswith(("http://", "https://"))})
    return find_brands(blob, brands), domains, len(blob), None
```

For the AI overview the domain list includes every URL in the response, so image or tracking URLs can appear next to real citations. Once you've looked at an overview response in the playground, narrow it to the field that holds its sources. Keep every full response on disk as well, so you can re-extract old runs after you change the rules.

## Step 4: How do you store one row per prompt per day?

Use SQLite: one table, one row per day, prompt and source, with the mentions and domains as JSON text. Keeping the raw response next to the extracted fields means a better extractor next month can reprocess every past day.

```python
import datetime as dt
import json
import sqlite3

def run(config_path="config.json", db_path="ai_mentions.db"):
    cfg = json.load(open(config_path))
    db = sqlite3.connect(db_path)
    db.execute("""CREATE TABLE IF NOT EXISTS answers (
        day TEXT, prompt TEXT, source TEXT, mentioned TEXT, domains TEXT,
        text_chars INTEGER, model TEXT, raw TEXT, PRIMARY KEY (day, prompt, source))""")
    today, spent = dt.date.today().isoformat(), 0
    for prompt in cfg["prompts"]:
        for source, fn in (("llm", llm_answer), ("ai_overview", ai_overview)):
            data, credits = fn(cfg, prompt)
            spent += credits
            if data is None:
                continue
            mentioned, domains, chars, model = extract(data, cfg["brands"], source)
            db.execute("INSERT OR REPLACE INTO answers VALUES (?,?,?,?,?,?,?,?)",
                       (today, prompt, source, json.dumps(mentioned),
                        json.dumps(domains), chars, model, json.dumps(data)))
    db.commit()
    print(f"{today}: {spent} credits")

if __name__ == "__main__":
    run()
```

We ran the job end to end with the API calls mocked, feeding it the recorded LLM answer; we haven't run it live for this article. `INSERT OR REPLACE` on the `(day, prompt, source)` key makes a rerun on the same day overwrite rather than duplicate. A failed call writes no row, so a missing row means the call failed and cost nothing; a row with `text_chars` near zero means the call succeeded and there was little or no answer.

## Step 5: How do you schedule it and read the results?

Run the script once a day from cron, then query the share of days each brand appears, per source. Add the job with `crontab -e`:

```bash
15 6 * * * cd /path/to/job && API_KEY=sk_live_... /usr/bin/python3 track.py >> track.log 2>&1
```

After a week or more, this query gives each brand's share of answers by source:

```python
import json
import sqlite3
from collections import Counter

db = sqlite3.connect("ai_mentions.db")
rows = db.execute("SELECT source, mentioned FROM answers WHERE day >= date('now', '-28 days')").fetchall()
totals, hits = Counter(), Counter()
for source, mentioned in rows:
    totals[source] += 1
    for brand in json.loads(mentioned):
        hits[(source, brand)] += 1
for (source, brand), n in sorted(hits.items()):
    print(f"{source:12} {brand:12} {n}/{totals[source]} answers ({n / totals[source]:.0%})")
```

The output is one line per source and brand: how many of the period's answers named the brand, out of how many, and the percentage. Do the same with `domains` to see which sites the engines lean on for your category; those are the pages worth earning a mention on. For keeping a scheduled job inside a fixed number of credits, the guide to [budgeting credits per scheduled run](https://perceptlayer.com/blog/scheduled-agent-credit-budget) adds a hard cap.

## How should you read the numbers?

As rates over time, not facts about one day. An AI answer is one sample from a system that can answer the same prompt differently tomorrow, and the sources it cites are its choice, not a ranking. OpenAI's own help page notes that search results and citations can be incomplete, outdated or incorrect ( [OpenAI Help Center](https://help.openai.com/en/articles/9237897-chatgpt-search), 2026). Treat a brand's share of answers as a trend to watch, and check the cited pages before acting on them.

Keep the prompt set, model, locale and country fixed for as long as you want comparable numbers. When you change any of them, start a new series rather than mixing it into the old one. And compare sources side by side: a brand can be common in the AI overview and rare in the LLM answer, or the reverse, which tells you where to focus.

## What does it cost per prompt per day?

Two credits per prompt per day: 1 for the LLM answer and 1 for the AI overview. Every call is 1 credit on success and 0 on failure, so the monthly cost is prompts × 2 × days.

| Prompts | Calls a day | Credits a month (30 days) |
| --- | --- | --- |
| 15 | 30 | 15 × 2 × 30 = 900 |
| 50 | 100 | 50 × 2 × 30 = 3,000 |
| 50, two models each | 150 | 50 × 3 × 30 = 4,500 |

Fifteen prompts fit inside the 1,000 free monthly credits. Fifty prompts across two models is 4,500 credits a month, within the Starter pack's 10,000 for two months; purchased credits last 12 months. The [credits docs](https://docs.perceptlayer.com/credits) explain the balance and the [pricing page](https://perceptlayer.com/pricing) has what packs cost. Start by running one of your prompts in the playground on the [LLM Answers API page](https://perceptlayer.com/apis/llm-answers).

## APIs in this post

-   [LLM Answers **1 credit per call** · 5 endpoints](https://perceptlayer.com/apis/llm-answers)
-   [Google Search **1 credit per call** · 7 endpoints](https://perceptlayer.com/apis/google-search)

## Frequently asked questions

### How much does tracking one prompt cost?

Two credits a day: 1 for `/v1/llm-answers/answer` and 1 for `/v1/google-search/search-ai-overview`, so 60 credits a month per prompt. Failed calls cost nothing. The 1,000 free monthly credits cover 15 prompts tracked daily on both sources, and the [pricing page](https://perceptlayer.com/pricing) has what credit packs cost beyond that.

### Which AI engines can I track?

The models endpoint takes an `engine` of `chatgpt` or `perplexity` and lists the models it offers for that engine; pass one as `model` to the answer endpoint. Google's AI overview comes from its own endpoint on the [Google Search API](https://perceptlayer.com/apis/google-search). Pin the model you choose so day-to-day changes come from the answers, not your settings.

### Why is the AI overview empty for some prompts?

Google shows AI Overviews only when its systems decide they add something to classic results, so many queries don't get one. Store the empty result as a row with little or no text: the share of days an overview appears at all is itself worth tracking, alongside which brands it names when it does.

### Should my prompts include my brand name?

No, not for visibility tracking. A prompt that names your brand tells you how the engine describes you, which is useful, but it can't tell you whether you'd be recommended unprompted. Keep a separate small set of branded prompts if you want to monitor how the answer describes your product.

### Can I track answers in other countries and languages?

Yes. Set `locale` (up to 16 characters, such as `de-DE`) and `timezone` on the answer endpoint, and `hl` and `gl` on the AI overview endpoint. Treat each market as its own series with its own rows, because answers and cited sources differ by language and country.

ai search brand monitoring llm answers ai overviews python

## More from the blog

1.  [AI agents](https://perceptlayer.com/blog/category/ai-agents) 23 Oct 2026 Perceptlayer 8 min read
    
    ## [Build a Market Research Agent with Search and Trends Data](https://perceptlayer.com/blog/market-research-agent-search-trends)
    
    Wire Google Search, Trends and News into one Python agent, cap it at 8 credits per question, and see a recorded Trends response.
    
2.  [AI agents](https://perceptlayer.com/blog/category/ai-agents) 17 Sept 2026 Perceptlayer 8 min read
    
    ## [Build a Price Tracking Agent in Python with Live Data](https://perceptlayer.com/blog/price-tracking-agent-python)
    
    A Python price tracker that checks Google Shopping and eBay once per product per run, stores history in SQLite, flags drops and costs 2 credits a product.
    
3.  [AI agents](https://perceptlayer.com/blog/category/ai-agents) 11 Sept 2026 Perceptlayer 8 min read
    
    ## [OpenAI Agents SDK with a Remote MCP Server for Web Data](https://perceptlayer.com/blog/openai-agents-sdk-mcp-server)
    
    Connect the OpenAI Agents SDK to a Streamable HTTP MCP server with a Bearer header, cap call\_endpoint at 3 calls a question, and count credits per run.
