Skip to content
ALL POSTS
§ · PROTOCOLS

A price is not an answer

Hand a model a bare number and it will state it as fact. Whatever doubt the answer needs has to arrive with the price.

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:

GET /v1/prices/latest
ONE CALL · TWO BODIES
PICK A SITUATION
DEFAULT RESPONSE
{
  "asset": "TSLA",
  "quote": "USD",
  "price": "320.00",
  "updated_at": "2026-07-23T14:12:03Z"
}
→ ANSWERSTSLA is trading at 320.00.
?include=age,confidence,source
{
  "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 }
}
↯ HOLDSThe last TSLA print is 320.00, but it is eight minutes old and the three venues behind it disagree by 110 bps. I would not quote it — ask me again in a minute.
Same price in both bodies. Only one of them carries the reason not to use it.
Both bodies are the real /v1/prices/latest shape, and confidence comes from the shipped confidenceForStaleness(). The two sentences are the most an agent could truthfully say from each body — not a transcript.

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.
  • confidencehigh inside 30 seconds, medium inside two minutes, low beyond 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

price-tool.ts
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.

§ · KEEP READING

The ledger is always open.

See where every price we publish came from, or head back for the rest of the posts.