Designing an AI Agent That Answers "Find Me a Dentist Near Here"

An AI agent asked to find a dentist nearby has to do four things in order: work out where "here" is, call a tool that knows what is open today, fit the result into a context window that is already mostly full, and say where the facts came from. Each step has a wrong answer that looks right in testing. This walks through all four, with the code.

Why an AI agent cannot answer a near me question from its weights

Opening hours, phone numbers and closures change every week, and none of that is in a model's weights. Asked for a dentist in Lisbon with no tool, a model produces a name that sounds plausible, an address in roughly the right district, and a phone number with the right country code. All three can be wrong, and the failure is silent because the answer is well formed.

So the design question is not whether to give the AI agent a tool. It is which tool, when to call it, and what the wrapper does with a 402.

Choosing the tool: search or grounding

Two shapes of the same data are available, and picking the wrong one costs you context window rather than correctness.

GET /v1/places/searchGET /v1/ground/local
Built forDatabases and lead listsContext windows and tool calls
Per pageUp to 100 placesUp to 10, default 5
FieldsThe full summary recordOnly what an answer needs, two categories each
ExtrasPagingA text block and a citation per record
Cost1 credit per place1 per place, 2 with detail=true

For an agent, use the grounding endpoint. The difference that matters is the text field: the whole result as one pipe-separated line per business, which is what most people building agents end up assembling by hand from the JSON anyway. A JSON array repeats every field name once per record; the text block carries them zero times.

If your assistant supports the Model Context Protocol, skip the HTTP layer. The hosted MCP server publishes nine tools over the same key sent as a bearer token: local_search and count_local for finding businesses, place_details for one full record, read_page, company_by_domain, verify_emails, crawl_access, check_schema, and account_usage for the credits left. A refusal comes back to the model as a sentence rather than an HTTP status it has to interpret. Setting it up is covered in the MCP server article.

When to call count first

GET /v1/places/count is free and answers one question: how many businesses match, before you spend anything. For an agent, it is worth calling first in three situations.

  • The query might be empty. "Vegan sushi in Reykjavik" may have three matches or none. A free count that returns zero saves a paid search and lets the agent say so plainly instead of returning an empty list.
  • The user asked how many. "Are there many dentists around here?" is answered by the count alone. Fetching five records to answer a quantity question is waste.
  • The agent is about to widen the search. Count the narrow term and the broad term, then search only the one that has results.

The response carries count, exact and at_least. exact: true means the first result page was not full, so that number is the whole answer; at_least: true means fifty came back and a full search finds more. Have the agent say "at least 50" in that case rather than "50".

Two limits keep this honest: answers are cached for 24 hours per query, and each key may ask 50 fresh questions a day before count starts returning 429. Do not call it before every single search — call it when one of the three cases above applies.

Keeping results small for the context window

The instinct is to fetch ten results so the model can choose. Resist it. Ten records is ten credits instead of three, and the seven the model discards still occupied the prompt while it read them — address strings, category lists, review counts it never quotes.

Three to five results is the right size for a spoken or chat answer, which is why the endpoint defaults to five and caps at ten. Use detail=true only when the user has narrowed to a specific business or when the answer genuinely needs contact details or full-week hours; it doubles the cost per record and adds a website fetch per business.

Always send the country with the city. "Warsaw" resolves to Warsaw, Indiana under a US-defaulted geocoder. "Warsaw, Poland" cannot. If your agent gets a bare city name from the user, have it fill in the country before calling the tool — that single rule prevents more confidently wrong answers than any amount of prompt tuning.

Citing sources in the reply

Every grounded record carries a citation of { place_id, source, fetched_at }. That is three facts: which listing this came from, which data path answered, and the moment it was read.

Put the timestamp in the answer. "Open until 18:30 today, as listed 4 minutes ago" is a different claim from "open until 18:30", and it is the honest one — hours can change between the read and the reply. Keep the place_id in your own logs even if you do not show it, because it is what lets you re-fetch the same record later and prove what the data said on the day.

The same discipline applies in reverse if you run the business. Marked-up hours on your own site are what an assistant reads when it cannot reach a listing, which is the practical argument in the LocalBusiness schema guide.

Handling "the API says the quota is spent"

This is the failure that turns a careful agent into a confident liar. When the monthly credits run out, the API answers 402 with code quota_exceeded and charges nothing. If your tool wrapper swallows that and returns an empty list, the model sees "no results" and helpfully fills the gap from memory.

Return the error to the model as text it can act on. Three statuses need their own handling:

  • 402 quota_exceeded — the credits are spent. The agent should say live data is unavailable and stop. Nothing was charged, so retrying changes nothing until the month rolls over or the plan changes on the pricing page.
  • 429 rate_limited — too many requests this minute. Honour Retry-After and try once more. Free and Starter allow 60 requests a minute; the higher plans allow more.
  • 503 source_unavailable — the data path is degraded. Retry after the header says to, and check the status page, which is published from the daily canary run.

Write the rule into the system prompt as well: if the tool reports that data is unavailable, say so, and do not answer from prior knowledge. Models follow that instruction well when the tool result actually contains the words.

A near me agent tool wrapper in Python

A complete script: a free count, a grounded search, an answer with citations, and a quota error that stops the run instead of degrading into a guess. It uses httpx and the X-API-Key header.

"""Answer a 'near me' question from live business data."""
import os
import sys

import httpx

API = "https://api.locomint.io/v1"
KEY = os.environ["LOCOMINT_KEY"]

client = httpx.Client(base_url=API, headers={"X-API-Key": KEY}, timeout=60)


class DataUnavailable(Exception):
    """The agent must say this out loud, not work around it."""


def call(path, params):
    resp = client.get(path, params=params)
    if resp.status_code == 402:
        raise DataUnavailable("monthly credits are spent; nothing was charged")
    if resp.status_code in (429, 503):
        wait = resp.headers.get("Retry-After", "60")
        raise DataUnavailable(f"upstream busy, retry in {wait}s")
    resp.raise_for_status()
    return resp.json()


def count_local(query, location):
    """Free. Returns how many match and whether that is the whole answer."""
    data = call("/places/count", {"q": query, "location": location})["data"]
    return data["count"], data["exact"]


def local_search(query, location, limit=3, detail=False):
    """One credit per business returned, two with detail."""
    body = call("/ground/local", {
        "q": query,
        "location": location,
        "limit": limit,
        "detail": str(detail).lower(),
    })
    return body["data"], body["text"]


def answer(query, location):
    found, exact = count_local(query, location)
    if found == 0:
        return f"Nothing listed for '{query}' in {location}."

    how_many = f"{found}" if exact else f"at least {found}"
    places, prompt_block = local_search(query, location, limit=3, detail=True)

    lines = [f"{how_many} match '{query}' in {location}. The closest three:"]
    for place in places:
        facts = [place["name"]]
        if place.get("rating"):
            facts.append(f"{place['rating']}/5 from "
                         f"{place.get('review_count') or 0} reviews")
        facts.append(place.get("hours_today") or "hours not listed")
        facts.append(place.get("phone") or "no phone listed")

        cite = place["citation"]
        lines.append(" | ".join(facts))
        lines.append(f"    source: {cite['source']} {cite['place_id']}, "
                     f"read {cite['fetched_at']}")

    lines.append("")
    lines.append("Compact block for the model prompt:")
    lines.append(prompt_block)
    return "\n".join(lines)


try:
    print(answer("dentist", "Lisbon, Portugal"))
except DataUnavailable as exc:
    sys.exit(f"Tell the user and stop: {exc}")
finally:
    client.close()

The same call as curl, if you want to see the shape before writing any code:

curl "https://api.locomint.io/v1/ground/local" -G \
  --data-urlencode "q=dentist" --data-urlencode "location=Lisbon, Portugal" \
  --data-urlencode "limit=3" -H "X-API-Key: $LOCOMINT_KEY"

Note what the script never does: it never falls back to answering without data. Every path out of DataUnavailable ends in the user being told. The temptation is a bare except that returns an empty list so the demo does not crash, and that one line is what turns a quota error into a fabricated phone number.

When the business has no website

A large share of small local businesses have no site at all, and plenty of the rest only have a social page. In the record, website comes back empty and there is nothing to enrich, so emails, whatsapp and contact_form_url stay empty even with detail=true.

That is not a gap in the answer. For a "find me a dentist" question the useful facts are the phone number, the address and today's hours, and all three are on the record regardless. Have the agent lead with the phone number when there is no website, rather than saying it could not find anything.

One case needs care: a service-area business — a plumber, a mobile locksmith — has no storefront and therefore no street address in the source at all. The record is flagged service_area_business and its address comes back empty. An agent that prints an empty address line looks broken; one that says "call-out service, no walk-in address" reads as correct, because it is.

Where to start this week

Wire up one tool, not six. Give your agent local_search alone, ask it three real questions in a city you know, and read the transcript for two things: did it invent anything, and did it say when the data was read. Fix those before adding place_details or the count call.

Then break it deliberately. Point it at a key with no credits left and watch what it says. If it produces an answer anyway, the tool wrapper is hiding the error and that is the bug to fix first. The Local Grounding API page has the full response shape, and the free plan's 200 credits a month with no card is enough for a few hundred grounded answers while you get the behaviour right.

Frequently asked questions

Why can't a language model answer a near me question on its own?

Because opening hours, phone numbers and closures change constantly and nothing in a model's training data knows what is true today. Without a live source the model either refuses or invents an answer, and inventing is worse: a plausible phone number for a business that closed last year is harder to catch than a refusal.

How many results should an agent fetch per question?

Three to five is usually right. The grounding endpoint defaults to five and caps at ten on purpose, because the constraint is the context window rather than the database. Each business also costs a credit, so fetching ten to show three spends twice what the answer needed.

What should an agent say when the API returns a quota error?

It should tell the user that live data is unavailable right now and stop, rather than answering from memory. A 402 with code quota_exceeded means the monthly credits are spent and nothing was charged for that call. Make the tool return that as plain text the model can read, so it reports the limit instead of hallucinating a result around it.