Designing APIs for AI agents
A human reading a dashboard supplies context the payload does not carry. A model supplies confidence instead. That single difference inverts several long-standing API design instincts.
The instinct that stops working
Good API design has spent twenty years converging on terse, predictable responses. Return the data, nothing else. The client knows what it asked for and the documentation explains the rest.
That last assumption is what fails. A model calling a tool has the tool description and the response — not the documentation, not the schema page, not the caveat in the "limitations" section. Anything not in the payload is context that does not exist at the moment the answer is generated.
GET /price?symbol=HYPER
{ "price": 0.0412 }
# Which HYPER? Observed when? In what currency?
# Aggregated how? The model will not ask. It will answer.Rule 1: ambiguity is a result, not an error
A conventional API resolves ?symbol=HYPER to one asset, because returning a list would be unhelpful to a client that wants a price.
For an agent that behaviour is actively dangerous, because three different assets publish under that ticker and the model has no way to know it picked one. Ambiguity has to be representable.
GET /assets/resolve?q=HYPER
{
"query": "HYPER",
"ambiguous": true,
"candidates": [
{ "assetId": 4821, "symbol": "HYPER", "name": "HyperChainX" },
{ "assetId": 5310, "symbol": "HYPER", "name": "Hyperlane" },
{ "assetId": 6902, "symbol": "HYPER", "name": "Hyperpigmentation" }
],
"guidance": "Three assets publish under this symbol. Ask the user which one is meant before answering."
} Note guidance. Putting an instruction in a data payload feels wrong — and it is, if a human is reading it. When the consumer is a model, a sentence telling it what to do with an ambiguous result is the difference between a clarifying question and a confident error.
Rule 2: every value carries its own context
GET /assets/5310/market/latest
{
"asset": { "assetId": 5310, "symbol": "HYPER", "name": "Hyperlane" },
"metric": "price",
"value": 0.0412,
"unit": "USD",
"observedAt": "2026-08-31T09: 00: 00Z",
"cadence": "hourly",
"ageSeconds": 2140,
"limits": [
"Aggregated across venues; not a settlement price.",
"Hourly snapshot; intra-hour movement is not retained."
]
}Five things travel with the number: which asset, what metric, what unit, when it was observed, and how old that makes it. Plus the limits that matter for this specific value.
ageSeconds earns its place. A model handed a timestamp has to compute age against a current time it may not have. Handed an age, it can say "as of about thirty-five minutes ago" without arithmetic it is bad at.
Rule 3: limits belong in the response
The limits array is the same text as the limitations section of the documentation, shipped where it will actually be read. It costs a hundred bytes and it is the only mechanism that reliably stops a model from asserting something the data does not support.
The same applies to tool descriptions: they should state what the tool cannot answer, not just what it can. "Hourly cadence; cannot answer intraday questions" in a description prevents a class of wrong answers before the call is made.
Rule 4: nulls must survive serialization
Omitting a null field to save bytes is common and, here, harmful: the model cannot distinguish "not measured" from "not returned by this endpoint". Emit explicit nulls, and where it matters say why — "sentiment": null, "sentimentReason": "insufficient activity to classify".
Rule 5: pagination must be stable
An agent paging through results will interleave other tool calls between pages. Offset pagination over a shifting result set silently duplicates and drops rows, and the model will not notice. Cursor pagination over a stable ordering is not a nicety for this consumer; it is correctness.
What does not change
- Stable contracts. A model cannot adapt to a renamed field; it will just be wrong.
- Machine-readable errors. A code plus a human sentence — the code for routing, the sentence for explaining.
- Predictable shapes. Polymorphic responses that vary by parameter are as hard for a model as for a typed client.
The cost, honestly
Every rule above makes responses larger. For high-frequency machine-to-machine traffic that is a real cost and the trade may not be worth it. For agent consumption — a handful of calls, each feeding a generated answer a person will act on — a few hundred extra bytes to prevent a confident falsehood is not close.
If both matter, a verbosity parameter is a reasonable answer. The default should be verbose, because the failure mode of the terse version is silent.
Where we are
These rules constrain the public Moonlytics Data API, which is in development. The responses above are illustrations of the intended design, not endpoints you can call today — the agent surface is documented as planned. What already exists is the part these rules depend on: resolved identity, explicit timestamps, and per-category limits written in prose that a tool description can carry verbatim.
System state as of 2026-08-31.