Market holidayJapan will be closed on 2026-11-03 (Culture Day), 5 days from now.All announcements
Article

Wiring Live Market Data into AI Agents: Tool Design and Context Trimming

Pushing tick streams into conversation context collapses on context budget, cost, and accuracy. We give the right split — streaming to your service, querying to the model — plus three levels of tool design, trimming rules, and a fix for freshness hallucination.

The most common mistake when wiring live market data into an AI agent is this: keep pushing WebSocket ticks into the conversation context so the model can "watch" prices move. That approach collapses in three places — context budget, cost, and accuracy.

The right approach is to expose market data as tools, not as context. Here is how.

Why you cannot put a tick stream into context

Start with the arithmetic. One symbol at 2 ticks per second, each tick roughly 180 bytes serialised — call it 45 tokens. That sounds modest until you scale it:

  • 10 symbols × 2 ticks/sec × 45 tokens = 900 tokens/second
  • One minute of conversation = 54,000 tokens
  • One hour of session = 3.24 million tokens

That is raw data alone, before model output and conversation history. The result: context overflows quickly, cost becomes untenable, and the model cannot find the signal in a wall of repeated ticks — it spends attention on meaningless price jitter rather than what the user actually asked.

The right split: streaming belongs to your system, querying belongs to the model

The key design decision is to separate real-time behaviour from intelligence:

ResponsibilityOwnerWhy
Continuously receive ticks, hold latest stateYour service (WebSocket)Deterministic work; no model needed
Decide whether a condition fired, whether to notifyYour service (state machine)Rules are explicit; models are unreliable here
Understand intent, choose what to query, explain resultsThe modelThis is what models are genuinely good at

In one sentence: the model never touches the stream, only query results. Your service subscribes to WebSocket continuously and holds the latest prices in memory or a cache; when the model needs something, it calls a tool to get a current snapshot or an aggregate.

There is a bonus: the model never needs to know about the WebSocket protocol, reconnect logic, or deduplication. Those are deterministic problems that belong to your service.

Tool design: three levels of abstraction

Tools exposed to the model should be designed by abstraction level, not by wrapping REST endpoints one-to-one:

  1. Snapshot query — get_quote(symbols) returns the latest price, change percentage, and freshness flags. The most frequently used tool.
  2. Range query — get_price_range(symbol, interval, period) returns candles or aggregate statistics, answering questions like "how has this moved over the past week".
  3. Conditional query — get_movers(market, direction, limit) returns a ranked list, answering "what moved most today".

Why not expose raw REST endpoints? Because models reason poorly about arbitrary parameter combinations and will construct invalid ones frequently. Collapsing parameters into a handful of tools with clear business semantics raises the success rate dramatically.

Trimming: what to give the model and what to withhold

Tool return values need deliberate trimming. The rule: return only the fields required to answer the current question.

  • Always include — symbol, last price, change percentage, data timestamp, and freshness flags (delayed / unavailable).
  • Optional — bid/ask and volume, only when the question involves liquidity.
  • Withhold — raw tick sequences, redundant fields, internal IDs, and full 50-row batch results unless the question genuinely needs them.

Among these, the data timestamp is the single most important field. The next section explains why.

Freshness: where hallucination thrives

Language models have no sense of time. If you tell one "BTC is currently 80,400", it treats that number as a present-tense fact — even if you retrieved it five minutes ago.

That produces three failure modes:

  1. Stale data presented as live. The model says "the current price is X" when X is out of date.
  2. Fabricated timestamps. Pressed on when that price applied, the model invents a plausible but fictional time.
  3. Delayed data presented as real-time. If the plan returns delayed data, the model will not volunteer that — it answers in real-time terms.

The fix is to make freshness a mandatory tool output rather than something the model infers:

  • Every tool return includes an as_of timestamp and a delayed flag.
  • The tool description instructs the model to cite the data timestamp in its answer.
  • Your service flags stale data proactively, e.g. "this reading is more than N seconds old; re-query recommended".

The model does not need to know the time. It only needs to quote the time your service gives it.

Python: tool definition and snapshot caching

Python
import json, time
import httpx

API_KEY = "your_api_key"
BASE = "https://api.tiqex.io/api/v1/market"
CACHE_TTL = 2.0                      # cache snapshots for 2s to avoid duplicate calls

_cache: dict[str, tuple[float, dict]] = {}


def get_quote(symbols: list[str]) -> dict:
    """Tool implementation: return a snapshot with freshness fields attached."""
    key = ",".join(sorted(symbols))
    now = time.monotonic()
    hit = _cache.get(key)
    if hit and now - hit[0] < CACHE_TTL:
        return hit[1]

    resp = httpx.get(
        f"{BASE}/quote",
        params={"codes": key},              # up to 50 codes per call
        headers={"X-API-Key": API_KEY},
        timeout=10,
    )
    rows = resp.json().get("data", [])

    # Keep only what the model needs to answer; trim aggressively
    payload = {
        "as_of": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "source": "tiqex",
        "quotes": [
            {
                "symbol": r.get("code"),
                "last": r.get("last"),
                "changePercent": r.get("changePercent"),
                # Freshness flags are mandatory, so delayed data is never read as live
                "delayed": bool(r.get("delayed")),
                "unavailable": bool(r.get("unavailable")),
            }
            for r in rows
        ],
    }
    _cache[key] = (now, payload)
    return payload


# Tool description exposed to the model (OpenAI function-calling style)
TOOLS = [{
    "type": "function",
    "function": {
        "name": "get_quote",
        "description": (
            "Fetch the latest quote snapshot for one or more symbols. "
            "The result includes an as_of timestamp and a delayed flag. "
            "You must state the as_of time the data corresponds to, "
            "and if delayed is true you must tell the user the data is delayed."
        ),
        "parameters": {
            "type": "object",
            "properties": {
                "symbols": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Symbol codes, e.g. ['BTCUSDT', 'AAPL'], up to 50",
                }
            },
            "required": ["symbols"],
        },
    },
}]

if __name__ == "__main__":
    print(json.dumps(get_quote(["BTCUSDT", "AAPL"]), ensure_ascii=False, indent=2))

Three design choices matter: snapshot caching (so follow-up questions do not re-request), field trimming (only what is needed), and mandatory freshness output (as_of and delayed in both the return value and the tool description).

The 2-second cache TTL is deliberate. It prevents duplicate requests within a conversation turn while keeping data fresh. Set it to 60 seconds and the model's "current price" will visibly diverge whenever the market moves.

Conditional push belongs on the server, not in the model

A common architectural mistake is letting the model decide whether a price crossed a threshold. That is the wrong place for it, for three reasons:

  • Uncontrollable latency. Every evaluation costs a model call, far slower than a local comparison.
  • Non-deterministic results. The same price can produce different verdicts.
  • Uncontrollable cost. High-frequency evaluation means high-frequency calls.

The correct division: the server evaluates conditions with a state machine (deterministic); only when a condition fires does it call the model to generate an explanation or recommendation (intelligent). The model runs only when genuinely needed, so both cost and latency stay bounded.

FAQ

Can I feed WebSocket data straight into the model?

Not advisable. Tick streams consume enormous tokens (about 900/second for 10 symbols), exhausting context and producing unacceptable cost, while the model struggles to find signal in the noise. Have your service hold the latest state and let the model query on demand.

How do I stop the model treating stale prices as current?

Make the timestamp a mandatory tool output field and instruct the model in the tool description to cite the data time. Do not rely on the model to assess freshness — language models have no concept of time.

Should I expose raw REST endpoints or wrapped business tools?

Wrapped business tools. Collapsing parameters into a few semantically clear tools — snapshot, range, conditional — yields a much higher call success rate than letting the model construct HTTP requests freely.

Can delayed data be used in an agent?

Yes, but it must be labelled. The delayed flag in the tool return exists for exactly this purpose, and the tool description should require the model to disclose data freshness.

Should condition evaluation live on the server or in the model?

On the server. Threshold comparison, debouncing, and state machines are deterministic logic — faster, more accurate, and cheaper in code. The model handles explanation and recommendation once a condition fires.

Next steps

The core principle in one line: let the model query, not watch. See the AI agent solution for the wider pattern; snapshot field definitions are in the multi-market quote docs. If your service needs continuous ingestion, the subscription protocol is in the WebSocket streaming docs, and integration code is in the Python market data guide. Continuous subscription cost is largely independent of symbol count (handshake billing, pushes unmetered), but query volume counts against plan quotas — estimate it up front.