How to Track Your Brand in ChatGPT, Perplexity & Gemini (With Code)
By IndexHot Team ·
You don’t need an enterprise contract to track your brand in AI search. With a fixed prompt list, a simple logger and a few API calls, you can build a repeatable mention, citation and rank tracker in an afternoon. Below are copy-ready Python and Node.js snippets.
What You’ll Build
The script reads a list of governed prompts, sends each to the platforms, codes every answer, and writes one CSV row per prompt × platform. From that log you can compute Mention Rate, Citation Rate and the IndexHot Score described in the methodology.
- Python 3.11+ with the
requestslibrary, or Node.js 18+ (built-infetch). - API keys for the platforms you want to test; store them in environment variables, never in source.
- A
prompts.csvfile with one prompt per row.
Step 1: Create the Prompt Library
Start with 50 prompts — 10 branded, 40 non-branded — and tag each row with its type so you can report the two scores separately.
type,prompt
branded,Is IndexHot accurate?
branded,What is IndexHot pricing?
non-branded,best AI search visibility tool
non-branded,how to check if ChatGPT mentions my brand
non-branded,GEO measurement software for B2B SaaS
Step 2 & 3: Run Prompts and Code Answers (Python)
OpenAI and Perplexity share the same chat-completions request shape (different base URL and key); Gemini uses its own endpoint, noted in the comments. Check each platform’s current docs for the exact model name.
# pip install requests
import csv, re, os, time, requests
BRAND = "IndexHot"
DOMAIN = "indexhot.com"
# OpenAI-compatible endpoints (set keys in your environment)
CLIENTS = {
"ChatGPT": ("https://api.openai.com/v1/chat/completions",
"gpt-4o", os.environ["OPENAI_API_KEY"]),
"Perplexity": ("https://api.perplexity.ai/chat/completions",
"sonar", os.environ["PPLX_API_KEY"]),
# Gemini: https://generativelanguage.googleapis.com/v1beta/
# models/gemini-2.0-flash:generateContent?key=...
}
def ask(base_url, model, key, prompt):
r = requests.post(
base_url,
headers={"Authorization": f"Bearer {key}"},
json={"model": model,
"messages": [{"role": "user", "content": prompt}]},
timeout=60)
r.raise_for_status()
return r.json()["choices"][0]["message"]["content"]
def code_answer(text):
hits = [m.start() for m in re.finditer(rf"\b{re.escape(BRAND)}\b", text, re.I)]
mentioned = bool(hits)
cited = DOMAIN.lower() in text.lower()
rank = text[:hits[0]].count("\n") + 1 if hits else "" # rough line rank
return mentioned, cited, rank
rows = []
with open("prompts.csv", newline="") as f:
for row in csv.DictReader(f):
for platform, (url, model, key) in CLIENTS.items():
answer = ask(url, model, key, row["prompt"])
mentioned, cited, rank = code_answer(answer)
rows.append({"platform": platform, "type": row["type"],
"prompt": row["prompt"], "mentioned": mentioned,
"cited": cited, "rank": rank})
time.sleep(1) # be polite to rate limits
with open("results.csv", "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=rows[0].keys())
w.writeheader(); w.writerows(rows)
Same Tracker in Node.js
// Node 18+, no dependencies: node track.mjs prompts.csv
import fs from "node:fs";
const BRAND = "IndexHot";
const DOMAIN = "indexhot.com";
async function ask(baseUrl, model, key, prompt) {
const res = await fetch(baseUrl, {
method: "POST",
headers: { "Authorization": `Bearer ${key}`, "Content-Type": "application/json" },
body: JSON.stringify({ model, messages: [{ role: "user", content: prompt }] }),
});
const data = await res.json();
return data.choices[0].message.content;
}
function codeAnswer(text) {
const re = new RegExp(`\\b${BRAND}\\b`, "ig");
const hits = [...text.matchAll(re)].map((m) => m.index);
return {
mentioned: hits.length > 0,
cited: text.toLowerCase().includes(DOMAIN),
rank: hits.length ? text.slice(0, hits[0]).split("\n").length : "",
};
}
const clients = {
ChatGPT: ["https://api.openai.com/v1/chat/completions", "gpt-4o", process.env.OPENAI_API_KEY],
Perplexity: ["https://api.perplexity.ai/chat/completions", "sonar", process.env.PPLX_API_KEY],
};
const lines = fs.readFileSync(process.argv[2], "utf8").trim().split("\n").slice(1);
const out = [];
for (const line of lines) {
const [type, ...rest] = line.split(",");
const prompt = rest.join(",");
for (const [platform, [url, model, key]] of Object.entries(clients)) {
const answer = await ask(url, model, key, prompt);
out.push({ platform, type, prompt, ...codeAnswer(answer) });
await new Promise((r) => setTimeout(r, 1000));
}
}
fs.writeFileSync("results.json", JSON.stringify(out, null, 2));
Step 4: Aggregate into Rates
With the log written, compute per-platform Mention Rate and Citation Rate. Add absorption scoring (see below) to complete the IndexHot formula.
import csv
with open("results.csv", newline="") as f:
rows = list(csv.DictReader(f))
def rate(items, key):
return sum(1 for i in items if i[key] in ("True", "true", True)) / len(items)
for platform in sorted({r["platform"] for r in rows}):
sub = [r for r in rows if r["platform"] == platform]
print(f"{platform}: MR={rate(sub, 'mentioned'):.0%} CR={rate(sub, 'cited'):.0%}")
Then apply S = 100 × (0.30·MR + 0.30·CR + 0.40·AR) per platform and combine ChatGPT, Perplexity and Gemini with the published 0.40 / 0.30 / 0.30 weights.
Scoring Absorption — the Honest Shortcut
Mention and citation are exact string checks; absorption is fuzzier. A practical first version is a claim-overlap heuristic: keep a list of distinctive phrases, numbers and product names from your own pages and count how many appear verbatim or near-verbatim in the answer.
CLAIMS = ["40% visibility lift", "governed question library", "absorption depth"]
def absorption(answer):
hits = sum(c.lower() in answer.lower() for c in CLAIMS)
return 2 if hits >= 2 else 1 if hits == 1 else 0
Treat this as depth-1 evidence and reserve depth 2 for a distinctive claim or figure you can trace to a specific page. For production work, an LLM judge with a fixed rubric is more robust — just keep the rubric and model version logged.
Frequently Asked Questions
Do I need official API access?
For automated tracking, yes. OpenAI and Perplexity share an OpenAI-compatible chat-completions API and Gemini has its own endpoint. If you can’t use APIs, drive the web apps with a browser-automation tool and feed the page text into the same coding functions.
Will API results match the consumer web apps?
Not exactly — model version, search settings and formatting can differ. Treat API runs as a consistent, reproducible signal and always log the surface and version so trends remain comparable.
How do you detect absorption in code?
Start with claim overlap: tokenize distinctive phrases, numbers and product names from your source pages and count matches in the answer. That is a solid depth-1 proxy; reserve depth 2 for a traceable distinctive claim or figure.
How often should the tracker run?
Monthly for the full library as a baseline, weekly during active GEO campaigns or around a model release. Run each prompt at least three times and keep the median.
Don’t want to maintain the tracker yourself?
Check your own brand’s AI visibility for free.
Check your brand on indexhot.com