AnswerLine Start free

Use cases

Competitor Analysis API

The calls that measure your brand in AI answers also measure every rival's: whether the assistants recommend them when buyers ask, and which of their pages get cited. The same keywords on Google Search show their organic listings and the ads they buy.

Who it's for

Competitive-intelligence tools, SEO teams running share-of-voice reports, and product marketers tracking how AI engines compare vendors.

What you get

Try it

curl -X POST "https://api.answerline.dev/v1/monitor/chatgpt" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"iPhone 17 or Galaxy S26: which should I buy?","country":"US"}'

Sync or queue it with POST /v1/async/task. Full field list on the ChatGPT page and in the API reference.

Engines for this workload

Competitive analysis as a measurement system

The result page that gives your position lists every rival’s position, the ads they paid for, and the features they hold. The AI answer that mentions you also shows who else the engine recommends and whose pages it cites. Competitor analysis collects that data across a fixed competitor set and a fixed keyword set, so every gain or loss can be attributed to someone.

Google Search supplies the organic, ad and feature data below. The AI side runs on ChatGPT, Gemini and AI Mode, in 213 countries, and Copilot, Perplexity and Grok, in any country.

Metrics

Metric Definition
Keyword overlap Keywords where both you and competitor C rank within the tracked depth ÷ keywords where either ranks.
Keyword gap Keywords where C ranks in the top 10 and you do not.
SERP share of voice For domain D: Σ over keywords of weight(rank of D) ÷ Σ over keywords of Σ over all results of weight(rank). Weights are a click curve you choose.
Ad presence rate Samples of a keyword’s result page where D has at least one ad ÷ samples taken.
Top-of-page ad share D’s ads with blockPosition top ÷ all top ads observed, across keywords and samples.
Ad copy churn Distinct (title, description) pairs seen for D in a period. High churn suggests active testing.
Feature ownership Keywords where D holds a given feature: an AI Overview source, a People Also Ask link, a shopping card store, a knowledge graph panel.
AI citation share D’s entries in result.sources ÷ all sources, over a prompt set on one engine.
AI mention share Answers whose text names brand B ÷ answers naming any tracked brand.
Recommendation order Rank of B among tracked brands by first appearance in the answer text, averaged over answers that mention B.

Share of voice needs the same denominator for every domain. Compute it over all results on the page, including domains you don’t track, so a new entrant shows up as your share dropping.

Architecture

keyword set ─┐                                ┌─> serp_results, ads
             ├─> scheduler ─> batches ─> webhooks ─┤
prompt set ──┘                                └─> ai_sources, ai_mentions
                                                        │
competitor registry (domains, brand names) ─────────> share-of-voice views, diffs

The submit, receive and reconcile loop is the one in the AI visibility tracking guide; delivery semantics are in async tasks and webhooks.

Competitor registry

Keep every domain and brand alias each competitor uses: main site, blog subdomain, docs, regional ccTLDs, product names. Label every other domain you see “untracked” but keep it; the top untracked domains by share of voice are the competitors you didn’t know you had.

Keyword and prompt design

  1. Category keywords (“project management software”), where share of voice is decided.
  2. Comparison keywords (“asana vs monday”, “alternatives to trello”), where competitors intercept each other’s brand demand and frequently buy ads.
  3. Competitor brand keywords (“monday pricing”), which show who bids on whose brand.
  4. AI prompts that mirror category and comparison intent in natural language: “Which project management tool is best for a 30-person agency?”, “Is Asana or Monday better for client work?”.

Tag each keyword with its group. A comparison keyword usually belongs to two competitors; store both.

Schedule

Keys:

serp:{keyword_id}:{country}:{run_date}:{sample}
ai:{engine}:{prompt_id}:{country}:{run_date}:{sample}

Storage schema

create table competitors (
  competitor  text not null,
  domain      text,
  alias       text,
  check (domain is not null or alias is not null)
);

create table observations (
  task_id     uuid primary key,
  kind        text not null,          -- 'serp' or 'ai'
  engine      text not null,
  subject_id  text not null,          -- keyword_id or prompt_id
  country     text not null,
  run_date    date not null,
  sample      int  not null,
  observed_at timestamptz,
  status      text not null default 'QUEUED'
);

create table serp_results (
  task_id  uuid references observations,
  rank     int  not null,
  domain   text not null,
  link     text not null,
  primary key (task_id, rank)
);

create table ads (
  task_id        uuid references observations,
  ordinal        int  not null,       -- index in result.ads
  block_position text,
  position       int,
  type           text,                -- RESULT or SHOPPING_CARD
  domain         text,
  title          text,
  description    text,
  store          text,
  copy_hash      text not null,
  primary key (task_id, ordinal)
);

create table ai_sources (
  task_id  uuid references observations,
  position int,
  domain   text not null,
  url      text not null,
  primary key (task_id, url)
);

create table ai_mentions (
  task_id      uuid references observations,
  competitor   text not null,
  first_offset int  not null,
  primary key (task_id, competitor)
);

Change detection

Diff each run against the previous run of the same subject and market:

End-to-end Python

Build the tasks

import datetime as dt

HOOK = {"url": "https://hooks.example.com/competitive"}

def serp_task(kw_id, query, country, day, sample):
    return {"taskType": "GOOGLE", "payload": {"query": query, "country": country},
            "idempotencyKey": f"serp:{kw_id}:{country}:{day}:{sample}", "webhook": HOOK}

def ai_task(engine, prompt_id, prompt, country, day, sample):
    return {"taskType": engine, "payload": {"prompt": prompt, "country": country},
            "idempotencyKey": f"ai:{engine}:{prompt_id}:{country}:{day}:{sample}", "webhook": HOOK}

today = dt.date.today().isoformat()
tasks = [serp_task(k["id"], k["text"], "US", today, s) for k in keywords for s in range(k["samples"])]
tasks += [ai_task(e, p["id"], p["text"], "US", today, 0) for p in prompts for e in ("CHATGPT", "GEMINI")]

Submit in chunks of 500 and insert an observations row per accepted task. A serp: key has no engine segment, so record GOOGLE for those.

Parse a delivery

import hashlib, re
from urllib.parse import urlsplit

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

def apply(db, d):
    task = d["task"]
    with db.transaction():
        row = db.execute(
            "update observations set status = %s, observed_at = now() "
            "where task_id = %s and status not in ('COMPLETED','FAILED') returning kind",
            (task["status"], task["id"])).fetchone()
        if not row or task["status"] != "COMPLETED":
            return
        r = d["response"]["result"]
        if row[0] == "serp":
            organic = sorted(r.get("organicResults", []), key=lambda o: (o.get("page", 1), o.get("position", 0)))
            for rank, o in enumerate(organic, 1):
                db.execute("insert into serp_results values (%s,%s,%s,%s)", (task["id"], rank, host(o["link"]), o["link"]))
            for n, a in enumerate(r.get("ads", [])):
                domain = host(a.get("url")) if a.get("type") == "RESULT" else None
                copy = hashlib.sha1(f"{a.get('title','')}\n{a.get('description','')}".encode()).hexdigest()
                db.execute("insert into ads values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) on conflict do nothing",
                           (task["id"], n, a.get("blockPosition"), a.get("position"), a.get("type"), domain,
                            a.get("title"), a.get("description"), a.get("store"), copy))
        else:
            for s in r.get("sources", []):
                db.execute("insert into ai_sources values (%s,%s,%s,%s) on conflict do nothing",
                           (task["id"], s.get("position"), host(s["url"]), s["url"]))
            text = r.get("text", "")
            for competitor, alias in db.execute("select competitor, alias from competitors where alias is not null"):
                if m := re.search(rf"\b{re.escape(alias)}\b", text, re.I):
                    db.execute("insert into ai_mentions values (%s,%s,%s) on conflict (task_id, competitor) "
                               "do update set first_offset = least(ai_mentions.first_offset, excluded.first_offset)",
                               (task["id"], competitor, m.start()))

The status guard is the deduplication: a repeated delivery finds the row already final and parses nothing.

Text ads (type RESULT) are grouped by the host of their destination url; domain and displayedUrl are also returned if you prefer the advertiser as displayed. For shopping-style cards (type SHOPPING_CARD) the url is a Google aclk? redirect, so group those by store.

Compute share of voice and AI citation share

-- Click-weighted SERP share of voice per competitor for one day (ctr table holds your weights)
with weighted as (
  select coalesce(c.competitor, 'untracked:' || r.domain) as owner, w.weight
  from serp_results r
  join observations o using (task_id)
  join ctr w on w.rank = r.rank
  left join competitors c on c.domain = r.domain
  where o.kind = 'serp' and o.sample = 0 and o.run_date = '2026-09-14'
)
select owner, sum(weight) / sum(sum(weight)) over () as share_of_voice
from weighted group by owner order by share_of_voice desc limit 25;

-- Ad presence rate per advertiser per keyword over a week of samples
select o.subject_id as keyword_id, coalesce(a.domain, 'store:' || a.store) as advertiser,
       count(distinct o.task_id)::numeric
       / (select count(*) from observations o2
          where o2.kind = 'serp' and o2.subject_id = o.subject_id and o2.status = 'COMPLETED'
            and o2.run_date between '2026-09-08' and '2026-09-14') as ad_presence_rate
from ads a join observations o using (task_id)
where o.run_date between '2026-09-08' and '2026-09-14'
group by o.subject_id, advertiser;

-- AI citation share per competitor per engine
select o.engine, coalesce(c.competitor, 'untracked') as owner,
       count(*)::numeric / sum(count(*)) over (partition by o.engine) as citation_share
from ai_sources s join observations o using (task_id)
left join competitors c on c.domain = s.domain
where o.run_date = '2026-09-14'
group by o.engine, owner;

Organic share of voice filters on sample = 0 so ad sampling does not multiply organic counts.

Cost planning

Price per request at the Starter tier: Google Search $0.00103 for one page, plus $0.00071 per extra page and $0.00071 for the AI Overview; ChatGPT $0.0006; Gemini $0.0006; Copilot $0.00274; AI Mode $0.00206. Higher tiers, reached by 30-day spend, cost less. See balance and pricing.

Worked example: a SaaS company tracks 4 competitors in one market.

Workload Volume Cost
400 category and comparison keywords, organic, daily 400 × $0.00103 × 30 $12.36
60 paid-competition keywords, 3 extra ad samples a day 60 × 3 × $0.00103 × 30 $5.56
80 AI prompts on ChatGPT and Gemini, weekly 80 × ($0.0006 + $0.0006) × 4 $0.384
20 key prompts, 2 extra samples each, both engines 20 × 2 × $0.0012 × 4 $0.192
Monthly total $18.50

Adding the AI Overview to the daily organic run adds 400 × $0.00071 × 30 = $8.52. Running organic weekly instead drops the first row to $1.65. Adding Copilot and AI Mode to the weekly prompts adds 80 × ($0.00274 + $0.00206) × 4 = $1.54. Tiers are on pricing.

Pitfalls

Ignoring untracked domains. Share of voice computed only across named competitors always sums to 100% of a shrinking pie. Include every domain.

One ad sample per day. A single sample can miss an advertiser that appears in most auctions, or catch one that rarely appears. Sample repeatedly and report presence rates, not yes/no.

Grouping shopping ads by URL. Sponsored shopping cards carry a Google redirect as url. Group by store.

Subdomains and ccTLDs. A competitor’s blog, docs and country sites belong to the competitor. Map them in the registry, or share of voice under-counts large competitors.

Brand names that are common words. Match aliases with word boundaries, prefer multi-word aliases, and review a sample of matches whenever you add an alias.

Comparing AI engines on raw counts. Engines return different numbers of sources per answer. Compare citation share within an engine, not source counts across engines.

Counting ChatGPT answers without search. ChatGPT decides on its own whether to search; disableWebSearch changes nothing today. An answer with no sources showed no search, so compute citation share over answers with sources and report the share without.

Which engines fit

Engine Competitive signal
Google Search Organic positions for every domain, text and shopping ads with blockPosition, shopping cards by store, AI Overview sources
ChatGPT Which brands the answer recommends, in what order, and which domains it cites; its ads list comes back empty on the answers served today
Gemini Source lists and inline citationPills per answer for citation share
Perplexity sources and inline citationPills per answer for citation share
Copilot Microsoft’s Bing-grounded answer with its sources and citationPills; shopping cards are coming
Grok The web and X searches behind each answer in searchQueries
AI Mode Google’s conversational answer with its own sources and shopping cards

Competitor AI answer monitoring and tracking ads in AI answers go deeper on the AI side. The quickstart sends a first request.

Questions

What is SERP share of voice?

The share of click-weighted visibility a domain holds across a keyword set: for each keyword, weight each organic result by a click-through value for its rank, sum the weights per domain, and divide by the total weight across all domains.

Can I see which competitors buy ads on my keywords?

Yes. Google Search responses include result.ads with the advertiser domain, title, description, displayedUrl and blockPosition (top, bottom, middle or rhs) for text ads, and store and price for shopping-style sponsored cards.

How do I measure which competitors AI engines cite?

Run category prompts on ChatGPT, Gemini, Perplexity, Copilot, Grok or AI Mode and group result.sources by domain. Citation share per domain is its source count divided by all sources across the answers; mention share uses brand names found in result.text.

How often should competitor data be refreshed?

Organic positions and AI answers are usually tracked daily or weekly. The advertisers shown can differ from one search to the next, so sample ad presence several times a day on the keywords where paid competition matters.

Does the API return search volume or traffic estimates?

No. It returns what the result page or AI answer contained at the time of the request. Combine it with your own volume or click data if you need traffic estimates.

More use cases