An agent is asked what TSLA is trading at. The tool returns 320.00. The agent
answers "TSLA is trading at 320.00", and it has not lied — that is what came
back.
The number was eight minutes old, from the one venue of three that had stopped moving. Nothing in the payload said so, so nothing in the answer could.
This is not a prompt problem. You cannot instruct a model to be sceptical of a number that arrives with no grounds for scepticism. A hedge is a claim about evidence, and the evidence has to be in the tool result. Pick a case and compare the two bodies:
{ "asset": "TSLA", "quote": "USD", "price": "320.00", "updated_at": "2026-07-23T14:12:03Z" }
{ "asset": "TSLA", "quote": "USD", "price": "320.00", "updated_at": "2026-07-23T14:12:03Z", "age_ms": 483000, "confidence": "low", "source": { "count": 3, "spread_bps": 110 } }
The doubt is opt-in
GET /v1/prices/latest answers with four fields: the asset, the quote, the
price, and when it last moved. For a chart that is the right default — a mark
that is one second old and a mark that is two days old are drawn the same way,
and the reader can see the gap for themselves.
An agent has no chart. It gets the fields it asked for, and it says them in a
sentence. So it has to ask for the rest, which is one query parameter —
include=age,confidence,source:
age_ms— how long since the mark last moved.confidence—highinside 30 seconds,mediuminside two minutes,lowbeyond that.source— how many venues stand behind the price, and how far apart they are in basis points.
None of the three is a verdict. We publish what we know about the mark; what counts as good enough is the caller's to decide. That split matters more for an agent than for a dashboard, because an agent's judgement has to be written down somewhere a test can reach it.
Put the refusal in the tool
const LATEST = 'https://api.getliaw.com/v1/prices/latest' // The refusal lives in the tool, where a test can reach it. A model asked to // "be careful with stale prices" is a model asked to guess. export async function getPrice(asset: string) { const res = await fetch( `${LATEST}?assets=${asset}&include=age,confidence,source`, { headers: { Authorization: `Bearer ${process.env.LIAW_KEY}` } }, ) const [mark] = (await res.json()).data if (mark.confidence === 'low') { return { status: 'unusable', asset, last_price: mark.price, age_ms: mark.age_ms, venues: mark.source.count, } } return { status: 'ok', asset, price: mark.price, age_ms: mark.age_ms } }
Return the refusal as data, not an exception. status: 'unusable' with the
age still attached is something a model can say out loud — "the last TSLA print
was eight minutes ago, so I am not going to quote it" — while a thrown error is
something it apologises for, or retries four times, or hands to the user as a
stack trace.
Both branches carry age_ms, because a good mark deserves a timestamp too.
"118,450, printed a second ago" is a better answer than "118,450", and it costs
one field.
Quiet when nothing is wrong
A guard that fires on an ordinary Tuesday gets deleted by the third day. This one
mostly does not fire: a liquid market inside its own session comes back high,
takes the fast branch, and the agent answers exactly as it would have without any
of this — which is the third case in the panel above.
That is the whole trade. The fields are not there to make an agent cautious; they are there to make it specific. It can say how old the mark is, how many venues agreed on it, and — on the rare call where the honest answer is that it does not know — say that, instead of a number.
AI agents is the rest of the surface: six things a tool call can do with the feed, and the response body under every one of them.