AnswerLine Start free

Use cases

Local Rank Tracking API

A search for a plumber in Austin and one in Boston return different result pages, and AI answers vary by market too. Location is a request parameter: AI Mode takes a city today, and as city targeting reaches Google Search and US-state targeting the assistants, the pipeline that tracks nationally tracks per market with no new integration.

Who it's for

Local SEO platforms, multi-location and franchise brands, and agencies reporting per-market rankings for clients.

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":"Who is the best emergency plumber in Austin, Texas?","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

The local measurement problem

A national rank averages hundreds of local result pages that no user sees. For a business that serves customers in a place, the unit of measurement is a keyword searched from a specific location, and the question is whether the business appears in the local pack, in the organic results beneath it, and in the AI answers for that market.

Local tracking multiplies rows: 40 keywords across 25 cities is 1,000 result pages per run. Design the grid, the identifiers and the metrics before sending anything.

Metrics

Metric Definition
Local pack rank position of the business within localResults for one keyword × location. Null when the pack appeared without it; “no pack” when the field is absent.
Pack presence rate Keyword × location cells whose page showed a local pack ÷ cells collected.
Pack share Cells where the business is in the pack ÷ cells that showed a pack.
Organic local rank Rank of the business’s domain in organicResults for the same cell (ordering rule in rank tracking).
Market coverage Locations where the business is in the pack for at least one keyword ÷ locations tracked.
Share of local voice Pack appearances of the business ÷ pack slots across all cells. Compute it for competitors from the same data.
AI local mention rate AI answers for a location whose place entries or text name the business ÷ AI answers collected for that location.
Rating gap Business rating minus the mean rating of other pack members in the same cell.

Keep pack share and presence rate separate. A keyword that stops triggering a local pack is a different event from a competitor taking your slot.

Architecture

locations × keywords × engines ──> weekly scheduler ──> batches (≤500)
                                                          │
  grid reports <── pack_slots / ai_places <── webhook receiver

Submission, signature checks, deduplication and reconciliation follow the AI visibility tracking guide, async tasks and webhooks. A grid of 1,000 cells on two engines is four batch calls.

Location design

  1. Use Google’s canonical names. location on Google Search and AI Mode expects the “City,Region,Country” form from Google’s geotarget list, for example Austin,Texas,United States. Store the exact string you checked and give it an id.
  2. Send country with location. Country is required; the location adds city-level precision.
  3. Use uule only if you already generate it. It replaces location and cannot be combined with it.
  4. Record each city’s state for the other engines. ChatGPT and Gemini are targeted by country (213 countries), so their grid is country-level; Gemini also takes the US state.

Keyword design

Local keywords come in three shapes that behave differently:

Track implicit and near-me keywords across the grid, and explicit keywords only from their own city. For AI engines, phrase prompts as a user would, both with the city (“Who is a reliable emergency plumber? I’m in Austin.”) and without it, relying on the market targeting.

Schedule and identity

Start weekly, because cost scales with cells, and move to daily only for cells that change frequently. Run each location at the same hour. The idempotency key:

local:{engine}:{keyword_id}:{location_id}:{run_date}

Storage schema

create table locations (
  location_id  int primary key,
  google_name  text not null unique,   -- "Austin,Texas,United States"
  country      text not null,
  us_state     text                    -- "TX", null outside the US
);

create table cells (
  task_id      uuid primary key,
  engine       text not null,
  keyword_id   int  not null,
  location_id  int  not null references locations,
  run_date     date not null,
  status       text not null default 'QUEUED',
  has_pack     boolean,
  unique (engine, keyword_id, location_id, run_date)
);

create table pack_slots (
  task_id   uuid references cells,
  position  int  not null,
  place_id  text,
  title     text not null,
  rating    numeric,
  reviews   int,                      -- parsed from strings like "26K"
  category  text,
  primary key (task_id, position)
);

create table ai_places (
  task_id   uuid references cells,
  source    text not null,            -- 'aimode.places', 'chatgpt.map', 'gemini.places', 'text'
  place_key text not null,            -- placeId, or provider:id, or normalized name
  name      text not null,
  position  int,
  primary key (task_id, source, place_key)
);

create table tracked_businesses (
  place_key text primary key,         -- your locations and competitors
  brand     text not null,
  is_ours   boolean not null
);

has_pack null means the task has not completed; false means it completed and the page had no local pack.

Change detection

Compare each cell with its previous run:

End-to-end Python

Build the grid

AI Mode and Google Search take the city; ChatGPT and Gemini run per country.

import datetime as dt

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

def cell_payload(engine, keyword, loc):
    if engine == "AIMODE":
        return {"prompt": keyword, "country": loc["country"], "location": loc["google_name"]}
    if engine == "GOOGLE":
        return {"query": keyword, "country": loc["country"], "location": loc["google_name"]}
    return {"prompt": keyword, "country": loc["country"]}   # CHATGPT, GEMINI

def grid_tasks(keywords, locations, engines, run_date=None):
    run_date = run_date or dt.date.today().isoformat()
    return [
        {"taskType": e, "payload": cell_payload(e, kw["text"], loc),
         "idempotencyKey": f"local:{e}:{kw['id']}:{loc['location_id']}:{run_date}", "webhook": HOOK}
        for e in engines for kw in keywords for loc in locations
    ]

Submit in chunks of 500 and insert a cells row per accepted task, splitting the idempotency key back into engine, keyword, location and date.

For ChatGPT and Gemini, which are targeted by country, cities in the same country produce identical requests. Build one task per keyword × country for those engines and join it to every city in that country when reporting.

Parse a delivery

apply runs after signature verification, for webhook deliveries and reconciler fetches alike.

import re

def parse_count(text):
    """Review counts are display strings: "1,204", "26K", "1.2M"."""
    m = re.fullmatch(r"\(?([\d.,]+)\s*([KkMm]?)\)?", (text or "").strip())
    if not m:
        return None
    n = float(m.group(1).replace(",", ""))
    return int(n * {"": 1, "k": 1_000, "m": 1_000_000}[m.group(2).lower()])

def apply(db, d):
    task = d["task"]
    with db.transaction():
        done = db.execute("update cells set status = %s where task_id = %s and status <> 'COMPLETED' returning engine",
                          (task["status"], task["id"])).fetchone()
        if not done or task["status"] != "COMPLETED":
            return
        r = d["response"]["result"]
        engine = done[0]
        if engine == "GOOGLE":
            pack = r.get("localResults")
            db.execute("update cells set has_pack = %s where task_id = %s", (pack is not None, task["id"]))
            for p in pack or []:
                db.execute("insert into pack_slots values (%s,%s,%s,%s,%s,%s,%s) on conflict do nothing",
                           (task["id"], p["position"], p.get("placeId"), p["title"], p.get("rating"),
                            parse_count(p.get("reviews")), p.get("type")))
        elif engine == "AIMODE":
            for p in r.get("places", []):
                key = p.get("placeId") or p.get("title", "").lower()
                db.execute("insert into ai_places values (%s,'aimode.places',%s,%s,%s) on conflict do nothing",
                           (task["id"], key, p.get("title", ""), p.get("index")))
        elif engine == "CHATGPT":
            for p in r.get("map", []):
                key = f"{p.get('provider')}:{p.get('id')}" if p.get("id") else p.get("name", "").lower()
                db.execute("insert into ai_places values (%s,'chatgpt.map',%s,%s,%s) on conflict do nothing",
                           (task["id"], key, p.get("name", ""), p.get("position")))
        elif engine == "GEMINI":
            for p in r.get("places", []):
                db.execute("insert into ai_places values (%s,'gemini.places',%s,%s,%s) on conflict do nothing",
                           (task["id"], p.get("title", "").lower(), p.get("title", ""), p.get("index")))
        text = r.get("text", "")
        for (place_key, brand) in db.execute("select place_key, brand from tracked_businesses").fetchall():
            if re.search(rf"\b{re.escape(brand)}\b", text, re.I):
                db.execute("insert into ai_places values (%s,'text',%s,%s,null) on conflict do nothing",
                           (task["id"], place_key, brand))

The update where status <> 'COMPLETED' returns no row for a repeated delivery, so nothing is parsed twice.

Compute the grid metrics

-- Pack share and presence per location for our businesses, one run
select l.google_name,
       avg(c.has_pack::int)                                         as pack_presence_rate,
       avg((exists (select 1 from pack_slots s join tracked_businesses b on b.place_key = s.place_id
                    where s.task_id = c.task_id and b.is_ours))::int)
         filter (where c.has_pack)                                  as pack_share,
       min((select min(s.position) from pack_slots s join tracked_businesses b on b.place_key = s.place_id
            where s.task_id = c.task_id and b.is_ours))             as best_pack_position
from cells c join locations l using (location_id)
where c.engine = 'GOOGLE' and c.run_date = '2026-09-14' and c.status = 'COMPLETED'
group by l.google_name
order by pack_share nulls first;

As a keyword-by-city heatmap, this is the report most local teams want: the gaps show where to invest in reviews, pages or listings. The same view over ai_places gives the city breakdown from AI Mode.

Cost planning

Price per async task at the Starter tier: Google Search $0.00078 for one page; AI Mode $0.00156, plus $0.00156 per expanded product cluster (up to 6) only with expandProducts; Gemini $0.0005; ChatGPT $0.00062. Async tasks carry no synchronous surcharge. Higher tiers, reached by 30-day spend, cost less. See balance and pricing.

Worked example: a 12-location franchise tracks 40 implicit-local keywords across its 12 cities plus 13 competitor cities, weekly.

Component Tasks per run Cost per task Cost per run
AI Mode by city 40 × 25 = 1,000 $0.00156 $1.56
Google Search, 1 page, country level (US) 40 $0.00078 $0.0312
ChatGPT, country level (US) 40 $0.00062 $0.0248
Total 1,080 $1.62

Four runs a month is $6.46; a month with five weekly runs is $8.08. Running AI Mode only in the 12 own cities saves 40 × 13 × $0.00156 = $0.8112 per run. Running Google Search on the city grid too makes its row 40 × 25 × $0.00078 = $0.78 per run.

A new account’s 100 free requests work on every engine on this page. Tiers are on pricing.

Pitfalls

Unchecked location names. Several places share a city name, and a hand-typed string may not be the canonical name you meant. Copy names from Google’s geotarget list, run one task per location, and check that the local pack and organic results look local before scheduling the full grid.

Counting reviews as numbers. reviews in the local pack is a display string such as “26K”. Parse it and keep the raw string for audit.

Name matching instead of ids. Business names change and collide (“Joe’s Plumbing”). Key Google and AI Mode places by placeId; fall back to normalized names only for text mentions, and review those matches.

Country-level AI answers reported as city-level. country is the finest targeting on ChatGPT, and a US state on Gemini. Label those cells “United States” (or the state) in reports, not “Austin”.

Prompts that restate the city. Adding the city to the prompt and to the targeting is fine, but then you are measuring the prompt, not the location. Keep one convention per series.

Uneven runs. Failed cells shrink a location’s denominator. Report completed cells per location, and resubmit tasks that failed with QUEUE_WAIT_EXCEEDED.

Which engines fit

Engine Local targeting What it adds
Google Search location or uule, country Local pack with placeId, rating, reviews, category, address; organic results beneath
AI Mode location or uule, country Conversational answer with places and map entries carrying placeId
ChatGPT country; US state coming soon map entries with provider ids, ratings and addresses when the answer includes businesses
Gemini country; US state places with ratings and reviews on local-intent prompts

Run AI Mode and Google Search on the city grid, and ChatGPT and Gemini per country (Gemini also per US state). Local AI answers covers the AI side, and the quickstart gets the first call running.

Questions

How do I target a city on Google Search?

Pass location as a Google canonical location name in City,Region,Country form, for example Austin,Texas,United States, together with country, or a pre-encoded uule string; location and uule are mutually exclusive. AI Mode takes the same fields.

Can AI engines be targeted below country level?

AI Mode can, with location or uule, and Gemini takes a US state. ChatGPT is targeted by country, in 213 countries.

Why is localResults missing from some responses?

localResults is omitted when no local pack appeared on the result page. Treat the field as optional and record its absence as a result.

How should I identify a business across runs?

Use placeId from Google's local pack and AI Mode places, which is stable across runs, rather than the displayed business name. ChatGPT map entries carry an id and a provider, so key them by both.

How much does a location grid cost?

Keywords times locations times runs times the per-task price. An AI Mode task is $0.00156, so 40 keywords across 25 cities weekly on AI Mode costs $1.56 per run. A one-page Google Search task is $0.00078.

More use cases