Ask a language model what time it is in Tokyo and you will get an answer. It will be well formatted, it will name the right timezone, and it will be wrong, because the model has no clock. Everything it knows was fixed when its training data was collected, and nothing in the way it produces text distinguishes "I remember this" from "this sounds like the kind of thing that goes here". A wrong exchange rate reads exactly like a right one.
That is the whole case for tools. Not that models are bad at facts, but that a certain kind of fact cannot be in the model at all, and the model will not reliably tell you when you have asked for one. Your job as the person building the agent is to recognise those questions in advance and route them to something that actually knows.
01What a model actually knows
A model's knowledge has three properties that matter here.
It has a cutoff. Training data stops at some date, and the model is then used for months or years after it. Anything that changed in between is invisible: a new holiday, a currency revaluation, a domain that started or stopped accepting mail.
It has no "now". Even for things that were true at the cutoff, the model does not know how much time has passed. It cannot compute "today" or "in three business days" without being told what today is.
It is compressed. The model did not memorise tables; it learned patterns. It will reproduce common facts (the capital of Japan) reliably and long-tail facts (which day of November 2026 is a US federal holiday, the MX record of a particular domain) with a fluency that has nothing to do with accuracy.
None of these is a bug to be fixed with a bigger model. A model trained yesterday still does not know the exchange rate an hour from now.
Hallucinated facts versus tool results
The difference between a hallucinated answer and a grounded one is not tone or confidence. It is provenance. A tool result carries evidence of where it came from: a timestamp, a source date, a field name you can check. Compare what an agent can say in each case:
| Question | Model alone | With a tool result |
|---|---|---|
| Time in Tokyo right now | A plausible time, no way to verify | time24 and date from the call, stamped at call time |
| USD to EUR rate | A rate from somewhere before the cutoff | exchangeRate plus date and lastUpdated |
Can support@myspace.com receive mail? | "It looks valid" (syntax only) | isMxValid, canReceiveMail, isDisposable from live DNS |
| Working days in the US, November 2026 | A guess around 20 | workingDaysCount with each holiday named |
The right-hand column is something you can log, show to a user and audit later. The left-hand column is something you can only hope about.
A model answers "The current USD to EUR rate is 0.92" with no tool call. What is the most accurate description of that number?
02Four kinds of questions that need a tool
You do not need a tool for everything. "What is a timezone?" or "Explain what an MX record is" are answered well from training. The questions that need a tool fall into four groups, and once you can name them you can design for them.
1. Anything about "now"
Current time, today's date, whether a market is open, what day of the week a deadline falls on relative to today. The model has no clock, so every one of these needs a tool or an injected timestamp.
curl "https://api.apiverve.com/v1/worldtime?city=Tokyo" \
-H "x-api-key: YOUR_API_KEY"Run it twice a minute apart: time24 changes. Note that date can already be tomorrow compared with your own.
When we ran this from a US afternoon, Tokyo's date was already 2026-10-23 while New York was still on the 22nd. That date-line crossing is exactly the kind of thing a model gets wrong when it reasons about "today" from a fixed point. On the free plan the response gives you city, country, timezone, time24 and date; the day, unix and daylight-saving fields are premium, so a tool built on this API should not depend on them.
2. Values that change
Exchange rates, prices, weather, rates and indexes. The model may know a typical range. It cannot know the current value.
curl "https://api.apiverve.com/v1/exchangerate?currency1=USD¤cy2=EUR" \
-H "x-api-key: YOUR_API_KEY"exchangeRate is a string. date and lastUpdated tell you how fresh it is.
Two details in that response matter for agents. exchangeRate comes back as a string ("0.890932" on our call), so your code should parse it before doing arithmetic rather than letting the model do string-to-number reasoning. And the response tells you when the rate was last updated. Pass that through to the model and instruct it to state the date with the number. An agent that says "0.8909 as of 22 October" is checkable; one that says "about 0.89" is not.
3. Verification of a specific thing
Is this email deliverable? Is this phone number real? Is this IP on a blocklist? A model can check that a string looks like an email address. It cannot resolve DNS.
curl "https://api.apiverve.com/v1/emailvalidator?email=support%40myspace.com" \
-H "x-api-key: YOUR_API_KEY"isRegexValid is what a model could guess. isMxValid and canReceiveMail need the network.
The response separates the two kinds of answer for you: isRegexValid is the syntax check a model could approximate, while isMxValid, canReceiveMail, hasSpf and hasDmarc come from live DNS lookups. Our call also returned isRoleAccount: true, because support@ is a shared mailbox rather than a person, which is useful if the agent is qualifying leads. The riskScore, riskLevel and typo suggestion are premium fields, so do not write prompts that assume they are present.
4. Lookups in tables too large or too local to memorise
Holidays per country, tax rates per region, routing numbers, airport codes. These are stable enough that the model has seen some of them and big enough that it will confidently fill gaps with plausible inventions.
curl "https://api.apiverve.com/v1/workingdays?country=US&month=11&year=2026" \
-H "x-api-key: YOUR_API_KEY"nonWorkingDays names each holiday, so the agent can explain the count.
For November 2026 the API returned workingDaysCount: 19 and listed Veterans Day (the 11th) and Thanksgiving Day (the 26th) in nonWorkingDays with holiday_name. A model asked "how many business days in November" will usually say 20 or 21 and will not mention that Veterans Day falls on a Wednesday that year.
Which of these questions can a model answer well WITHOUT a tool?
03Wire up a grounded answer
Here is the smallest useful agent: one tool, backed by World Time, behind a model with tool calling. The pattern is the same across the major model APIs. You describe the tool with a name, a description and a JSON Schema for its input. The model replies either with text or with a request to call the tool. You run the call on your server, send the result back, and the model writes its answer from it.
The examples use the Anthropic Messages API. Install @anthropic-ai/sdk or anthropic, set ANTHROPIC_API_KEY and APIVERVE_API_KEY, and pick a current model in ANTHROPIC_MODEL.
// Node 18+, ESM. npm install @anthropic-ai/sdk
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
const MODEL = process.env.ANTHROPIC_MODEL ?? 'claude-opus-5-5';
async function apiverve(id, params) {
const url = `https://api.apiverve.com/v1/${id}?${new URLSearchParams(params)}`;
const res = await fetch(url, { headers: { 'x-api-key': process.env.APIVERVE_API_KEY } });
const body = await res.json();
if (body.status !== 'ok') throw new Error(`${id} ${res.status}: ${body.error}`);
return body.data;
}
const tools = [{
name: 'get_local_time',
description: 'Current local time and date in a city, from a live clock. ' +
'Use for any question about the time or date somewhere now. Returns time24 (HH:MM:SS), date (YYYY-MM-DD) and IANA timezone.',
input_schema: {
type: 'object',
properties: { city: { type: 'string', description: 'City name, e.g. "Tokyo"' } },
required: ['city'],
},
}];
async function runTool(name, input) {
if (name !== 'get_local_time') throw new Error(`unknown tool ${name}`);
const data = await apiverve('worldtime', { city: input.city });
const c = data.foundCities[0];
return { city: c.city, country: c.country, timezone: c.timezone, time24: c.time24, date: c.date };
}
const messages = [{ role: 'user', content: process.argv[2] ?? 'What time is it in Tokyo, and is it already tomorrow there?' }];
for (let turn = 0; turn < 4; turn++) {
const res = await client.messages.create({ model: MODEL, max_tokens: 4096, tools, messages });
messages.push({ role: 'assistant', content: res.content });
if (res.stop_reason !== 'tool_use') {
for (const b of res.content) if (b.type === 'text') console.log(b.text);
break;
}
const results = [];
for (const block of res.content) {
if (block.type !== 'tool_use') continue;
const out = await runTool(block.name, block.input);
results.push({ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(out) });
}
messages.push({ role: 'user', content: results });
}# Python 3.9+. pip install anthropic requests
import json
import os
import sys
import anthropic
import requests
client = anthropic.Anthropic()
MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-opus-5-5")
def apiverve(api_id, params):
r = requests.get(
f"https://api.apiverve.com/v1/{api_id}",
params=params,
headers={"x-api-key": os.environ["APIVERVE_API_KEY"]},
timeout=15,
)
body = r.json()
if body.get("status") != "ok":
raise RuntimeError(f"{api_id} {r.status_code}: {body.get('error')}")
return body["data"]
TOOLS = [{
"name": "get_local_time",
"description": (
"Current local time and date in a city, from a live clock. "
"Use for any question about the time or date somewhere now. "
"Returns time24 (HH:MM:SS), date (YYYY-MM-DD) and IANA timezone."
),
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string", "description": 'City name, e.g. "Tokyo"'}},
"required": ["city"],
},
}]
def run_tool(name, tool_input):
if name != "get_local_time":
raise ValueError(f"unknown tool {name}")
c = apiverve("worldtime", {"city": tool_input["city"]})["foundCities"][0]
return {k: c[k] for k in ("city", "country", "timezone", "time24", "date")}
question = sys.argv[1] if len(sys.argv) > 1 else "What time is it in Tokyo, and is it already tomorrow there?"
messages = [{"role": "user", "content": question}]
for _ in range(4):
res = client.messages.create(model=MODEL, max_tokens=4096, tools=TOOLS, messages=messages)
messages.append({"role": "assistant", "content": res.content})
if res.stop_reason != "tool_use":
print("".join(b.text for b in res.content if b.type == "text"))
break
results = []
for block in res.content:
if block.type == "tool_use":
out = run_tool(block.name, block.input)
results.append({"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(out)})
messages.append({"role": "user", "content": results})A few choices in that code are deliberate and will come back in later lessons.
- The loop has a hard limit. Four turns is plenty for one tool. An agent loop without a ceiling is a billing incident waiting for an ambiguous prompt.
- The tool returns five fields, not the whole response. The raw World Time response includes ASCII names, ISO3 codes and premium fields. Every byte you return is input tokens on the next request and one more thing the model might misread.
- The API call happens on your server. The model never sees your APIVerve key. It sees a tool name and a result.
- Every
tool_usegets atool_resultwith the same id. The model may ask for several tools in one turn, and the results must all come back together in the next user message.
Make the model use the result, not its prior
Getting the tool called is half the job. The other half is making sure the final answer comes from the result. Two habits help.
Put the provenance in the tool output and ask for it in the answer. If the result includes date and timezone, a system prompt line such as "When you state a time, rate or count from a tool, say where and when it applies" turns those fields into part of the user-visible answer.
Tell the model what to do when the tool and its memory disagree. Models sometimes "correct" a tool result that looks surprising to them, such as a holiday on an unexpected day. A line like "Tool results are authoritative. If one surprises you, report it as returned and say it is unexpected" stops that.
Both lines belong in the system prompt, which you pass on every request:
const system = [
'You answer questions about times, dates, rates and lookups using the tools provided.',
'Never state a current time, date, rate or count from memory; call a tool.',
'When you state a value from a tool, say where and when it applies (city, date, last updated).',
'Tool results are authoritative. If one surprises you, report it as returned and say it is unexpected.',
].join(' ');
const res = await client.messages.create({ model: MODEL, max_tokens: 4096, system, tools, messages });04When not to call a tool
Tools cost time, tokens and credits, so the opposite mistake is real too. Stable conceptual knowledge ("what is a working day"), formatting and rewriting text the user already gave you, and arithmetic over numbers already in the conversation do not need a call. If an agent re-fetches the same rate three times in one conversation, the fix is a cache in your tool layer, not a smarter prompt; lesson four covers that.
A useful test before adding a tool: would a careful human expert need to look this up, or check a clock, before answering? If yes, the agent needs a tool. If a human would answer from memory and be right, the model probably can too.
Next: Choose and scope tools, where you decide which tools an agent gets and how to describe them so the model picks the right one.