Natural Language Market Queries: AI Agent Integration with TickDB

A trader asks, “Show me the current order-book imbalance for Bitcoin on the five-minute chart,” and receives a precise answer within seconds. Behind that response is no magical market interpretation. The assistant must translate intent, select the correct data operation, supply the required symbol and parameters, validate the result, and present the data without inventing a price.

That translation layer is where most AI market-data integrations fail. A general-purpose model understands the language but does not automatically know whether a request needs a kline, a trade feed, or an order-book snapshot. It may also choose a historical endpoint for a live question, omit authentication, or return a confident answer based on stale context.

The solution is to expose market-data capabilities as explicit tools. The assistant uses function calling to select a tool, the tool adapter validates the request, and TickDB supplies the underlying market data. The AI remains responsible for intent interpretation and response composition; the deterministic layer remains responsible for data retrieval, timestamps, error handling, and authentication.

This article explains how to integrate a skill.md-compatible AI agent with TickDB through a production-oriented function-calling workflow.

1. Why Natural-Language Market Queries Need a Tool Layer

Language models are good at understanding approximate requests, but market data has strict operational boundaries. The distinction between a live quote and a historical candle is not a stylistic preference. It determines the endpoint, the timestamp semantics, and the meaning of the returned data.

The same natural-language phrase can also refer to several different datasets. “Show me Nvidia’s activity” might mean recent trades, an order-book snapshot, a one-minute candle, or a volatility calculation. The assistant needs enough metadata to disambiguate the request before it calls an API.

The following table illustrates the information that a market-data tool must preserve.

User intent Required tool metadata Data semantics
“What is AAPL trading at now?” Symbol, quote type, freshness Current real-time value
“Show AAPL’s last ten 1-hour candles” Symbol, interval, limit, time range Historical OHLCV
“Is Bitcoin’s order book bid-heavy?” Symbol, depth levels, calculation method Current order-book state
“What happened around the earnings release?” Symbol, event window, time range Historical event study
“Compare NVDA and AMD this month” Symbols, interval, comparison window Aligned historical series

A model that receives only a natural-language prompt and a raw API key has no reliable way to select the correct operation. The model needs a tool catalog that exposes semantic boundaries in a machine-readable format.

The critical design principle is separation of responsibility:

  • The model interprets intent and selects tools.
  • The tool schema constrains arguments.
  • The adapter validates and executes the request.
  • TickDB returns the data.
  • The model explains the result using the returned values.

The model should never construct a raw HTTP request by itself. It should not receive unrestricted access to an environment variable containing an API key. It should not decide how to handle an HTTP timeout or rate-limit response. Those concerns belong in a deterministic integration layer.

2. The skill.md Protocol and Agent Integration Model

The skill.md approach provides a portable way to describe a capability to an AI agent. The protocol is useful when the assistant must work across different model providers or coding environments, because the skill description, tool schema, and execution instructions can remain separate from the model-specific implementation.

A typical integration has three layers.

┌─────────────────────────────────────────────────────────────┐
│                    User / AI Assistant                      │
│  "What is the current bid-ask imbalance for BTC/USDT?"      │
└──────────────────────────────┬──────────────────────────────┘
                               │ natural-language intent
                               ▼
┌─────────────────────────────────────────────────────────────┐
│                   Agent Tool Registry                       │
│  - validate tool name                                        │
│  - validate arguments against JSON Schema                   │
│  - select allowed tools                                      │
└──────────────────────────────┬──────────────────────────────┘
                               │ function call
                               ▼
┌─────────────────────────────────────────────────────────────┐
│                 TickDB Skill / Tool Adapter                 │
│  - authentication                                           │
│  - endpoint selection                                        │
│  - timeouts                                                  │
│  - retries and rate-limit handling                           │
│  - response normalization                                    │
└──────────────────────────────┬──────────────────────────────┘
                               │ HTTPS / WebSocket
                               ▼
┌─────────────────────────────────────────────────────────────┐
│                           TickDB                            │
│  kline | trades | depth | symbols | market data operations   │
└─────────────────────────────────────────────────────────────┘

The agent tool registry is the security boundary. A model can propose a function call, but the registry must enforce which functions exist and what arguments each function accepts.

A skill.md-style skill description can therefore provide the following information to an AI runtime:

  1. The skill’s purpose.
  2. When the skill should be used.
  3. The available functions and their parameters.
  4. Authentication requirements.
  5. Execution and error-handling instructions.
  6. Any data limitations that the assistant should surface to the user.

The protocol describes the capability; the adapter enforces the runtime behavior. Those concerns should not be collapsed into a prompt-only workflow.

3. Designing the Function-Calling Contract

A function schema should be narrow enough to prevent ambiguity and broad enough to support useful market requests. For example, one tool can cover current kline data and another can cover order-book depth. Separating them makes the model’s choice easier to evaluate and lets the adapter apply different safety rules.

3.1 Current kline tool

{
  "name": "get_current_kline",
  "description": "Get the most recent real-time OHLCV candle for one supported symbol.",
  "parameters": {
    "type": "object",
    "properties": {
      "symbol": {
        "type": "string",
        "description": "Normalized symbol, such as AAPL.US, BTC/USDT, or 0700.HK."
      }
    },
    "required": ["symbol"],
    "additionalProperties": false
  }
}

3.2 Historical kline tool

{
  "name": "get_historical_klines",
  "description": "Get historical OHLCV candles for a symbol, interval, and time range.",
  "parameters": {
    "type": "object",
    "properties": {
      "symbol": {
        "type": "string"
      },
      "interval": {
        "type": "string",
        "enum": ["1m", "5m", "15m", "1h", "4h", "1d"]
      },
      "limit": {
        "type": "integer",
        "minimum": 1,
        "maximum": 1000
      }
    },
    "required": ["symbol", "interval"],
    "additionalProperties": false
  }
}

3.3 Depth and imbalance tool

{
  "name": "get_order_book_imbalance",
  "description": "Get current order-book depth and calculate a buy/sell pressure ratio from the requested number of levels.",
  "parameters": {
    "type": "object",
    "properties": {
      "symbol": {
        "type": "string"
      },
      "levels": {
        "type": "integer",
        "minimum": 1,
        "maximum": 10,
        "default": 5
      }
    },
    "required": ["symbol"],
    "additionalProperties": false
  }
}

The levels maximum follows the supported market boundaries. TickDB provides L1 depth for US equities, L1–L10 for Hong Kong equities, and L1–L10 for crypto. Order-book depth is not supported for forex, precious metals, or indices. The adapter should reject unsupported combinations before making a request.

The schema should use explicit enums where possible. A free-form interval field invites invalid values such as "five-minute" when the downstream API expects "5m". Enum validation moves that error from the API response into the tool boundary, where it is easier to explain and log.

4. A Production-Oriented Python Adapter

The following example uses the OpenAI Python client because it supports tool definitions and function calls. The model-specific parts are isolated so that the same skill can be adapted to another provider. The implementation includes:

  • Environment-variable authentication.
  • Explicit tool arguments.
  • Request timeouts.
  • Exponential backoff with jitter.
  • Rate-limit handling for error code 3001.
  • Response validation.
  • A bounded model loop.
  • A guard against repeated function calls.

Install the required packages:

pip install openai requests

Set the environment variables before running the agent:

export OPENAI_API_KEY="your-openai-key"
export TICKDB_API_KEY="your-tickdb-key"

The code below assumes that the skill exposes the same logical functions used in the schemas. If a skill.md implementation uses a different manifest format, the adapter is the correct place to translate that format into these stable function names.

from __future__ import annotations

import json
import os
import random
import time
from dataclasses import dataclass
from typing import Any, Callable

import requests
from openai import OpenAI


TICKDB_BASE_URL = "https://api.tickdb.ai/v1"
TICKDB_API_KEY = os.environ.get("TICKDB_API_KEY")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")

if not TICKDB_API_KEY:
    raise RuntimeError("TICKDB_API_KEY is not set")

if not OPENAI_API_KEY:
    raise RuntimeError("OPENAI_API_KEY is not set")


@dataclass(frozen=True)
class ToolResult:
    ok: bool
    data: Any
    error: str | None = None


def validate_symbol(symbol: str) -> str:
    """Validate and normalize a symbol before dispatch."""
    normalized = symbol.strip().upper()

    if not normalized:
        raise ValueError("symbol must not be empty")

    # Keep the adapter permissive for supported symbol formats while
    # rejecting obvious injection-like or malformed values early.
    if any(character in normalized for character in " \t\r\n;?#"):
        raise ValueError("symbol contains unsupported characters")

    return normalized


def tickdb_request(
    method: str,
    path: str,
    *,
    params: dict[str, Any] | None = None,
    body: dict[str, Any] | None = None,
    max_attempts: int = 4,
) -> ToolResult:
    """Execute a TickDB request with timeout, backoff, and rate-limit handling."""
    url = f"{TICKDB_BASE_URL}{path}"
    headers = {
        "X-API-Key": TICKDB_API_KEY,
        "Accept": "application/json",
    }

    for attempt in range(max_attempts):
        try:
            response = requests.request(
                method,
                url,
                headers=headers,
                params=params,
                json=body,
                timeout=(3.05, 15),
            )

            # TickDB reports rate limiting with code 3001.
            if response.status_code == 429:
                retry_after = int(response.headers.get("Retry-After", "5"))
                time.sleep(max(retry_after, 1))
                continue

            response.raise_for_status()
            payload = response.json()

            code = payload.get("code", 0)
            if code == 0:
                return ToolResult(ok=True, data=payload.get("data"))

            if code in (1001, 1002):
                return ToolResult(
                    ok=False,
                    data=None,
                    error="TickDB authentication failed. Check TICKDB_API_KEY.",
                )

            if code == 2002:
                return ToolResult(
                    ok=False,
                    data=None,
                    error=(
                        f"Symbol not found: {params.get('symbol', body.get('symbol', 'unknown'))}. "
                        "Verify the symbol through the available-symbols operation."
                    ),
                )

            if code == 3001:
                retry_after = int(response.headers.get("Retry-After", "5"))
                time.sleep(max(retry_after, 1))
                continue

            return ToolResult(
                ok=False,
                data=None,
                error=f"TickDB error {code}: {payload.get('message', 'unknown error')}",
            )

        except requests.Timeout:
            if attempt == max_attempts - 1:
                return ToolResult(
                    ok=False,
                    data=None,
                    error="TickDB request timed out.",
                )

        except requests.RequestException as exc:
            if attempt == max_attempts - 1:
                return ToolResult(
                    ok=False,
                    data=None,
                    error=f"TickDB network error: {exc}",
                )

        # Exponential backoff with jitter prevents synchronized retries.
        delay = min(1.0 * (2**attempt), 15.0)
        jitter = random.uniform(0, delay * 0.1)
        time.sleep(delay + jitter)

    return ToolResult(
        ok=False,
        data=None,
        error="TickDB request exhausted its retry budget.",
    )


def get_current_kline(symbol: str) -> ToolResult:
    """Return the current real-time candle."""
    symbol = validate_symbol(symbol)

    return tickdb_request(
        "GET",
        "/market/kline/latest",
        params={"symbol": symbol},
    )


def get_historical_klines(
    symbol: str,
    interval: str,
    limit: int = 100,
) -> ToolResult:
    """Return historical OHLCV candles."""
    symbol = validate_symbol(symbol)

    if interval not in {"1m", "5m", "15m", "1h", "4h", "1d"}:
        return ToolResult(
            ok=False,
            data=None,
            error="Unsupported interval. Use 1m, 5m, 15m, 1h, 4h, or 1d.",
        )

    if not 1 <= limit <= 1000:
        return ToolResult(
            ok=False,
            data=None,
            error="limit must be between 1 and 1000.",
        )

    return tickdb_request(
        "GET",
        "/market/kline",
        params={
            "symbol": symbol,
            "interval": interval,
            "limit": limit,
        },
    )


def get_order_book_imbalance(symbol: str, levels: int = 5) -> ToolResult:
    """Return depth and compute a top-N buy/sell pressure ratio."""
    symbol = validate_symbol(symbol)

    if not 1 <= levels <= 10:
        return ToolResult(
            ok=False,
            data=None,
            error="levels must be between 1 and 10.",
        )

    # Depth support varies by market. The adapter should apply the
    # market-specific limits after the market classifier is resolved.
    result = tickdb_request(
        "GET",
        "/market/depth",
        params={"symbol": symbol, "levels": levels},
    )

    if not result.ok:
        return result

    data = result.data
    bids = data.get("bids", [])
    asks = data.get("asks", [])

    bid_volume = sum(float(level.get("size", 0)) for level in bids[:levels])
    ask_volume = sum(float(level.get("size", 0)) for level in asks[:levels])

    if ask_volume == 0:
        pressure_ratio = None
    else:
        pressure_ratio = bid_volume / ask_volume

    return ToolResult(
        ok=True,
        data={
            "symbol": symbol,
            "levels": levels,
            "bid_volume": bid_volume,
            "ask_volume": ask_volume,
            "pressure_ratio": pressure_ratio,
            "bids": bids,
            "asks": asks,
        },
    )


TOOL_FUNCTIONS: dict[str, Callable[..., ToolResult]] = {
    "get_current_kline": get_current_kline,
    "get_historical_klines": get_historical_klines,
    "get_order_book_imbalance": get_order_book_imbalance,
}

TOOL_SCHEMAS = [
    {
        "type": "function",
        "function": {
            "name": "get_current_kline",
            "description": (
                "Get the current real-time OHLCV candle for one supported symbol."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "symbol": {
                        "type": "string",
                        "description": "Normalized symbol, such as AAPL.US.",
                    }
                },
                "required": ["symbol"],
                "additionalProperties": False,
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "get_historical_klines",
            "description": (
                "Get historical OHLCV candles for a symbol and interval."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "symbol": {"type": "string"},
                    "interval": {
                        "type": "string",
                        "enum": ["1m", "5m", "15m", "1h", "4h", "1d"],
                    },
                    "limit": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 1000,
                    },
                },
                "required": ["symbol", "interval"],
                "additionalProperties": False,
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "get_order_book_imbalance",
            "description": (
                "Get current order-book depth and calculate a top-N buy/sell "
                "pressure ratio."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "symbol": {"type": "string"},
                    "levels": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 10,
                    },
                },
                "required": ["symbol"],
                "additionalProperties": False,
            },
        },
    },
]


def run_tool_call(name: str, arguments: dict[str, Any]) -> str:
    """Dispatch a model-proposed tool call through the allowlist."""
    function = TOOL_FUNCTIONS.get(name)

    if function is None:
        return json.dumps(
            {
                "ok": False,
                "error": f"Tool '{name}' is not permitted.",
            }
        )

    try:
        result = function(**arguments)
    except TypeError as exc:
        return json.dumps(
            {
                "ok": False,
                "error": f"Invalid arguments for {name}: {exc}",
            }
        )
    except Exception as exc:
        return json.dumps(
            {
                "ok": False,
                "error": f"Tool execution failed: {exc}",
            }
        )

    return json.dumps(
        {
            "ok": result.ok,
            "data": result.data,
            "error": result.error,
        },
        default=str,
    )


def run_agent(question: str) -> str:
    """Run a bounded assistant loop with deterministic tool execution."""
    client = OpenAI(
        api_key=OPENAI_API_KEY,
        timeout=30.0,
        max_retries=2,
    )

    messages: list[dict[str, Any]] = [
        {
            "role": "system",
            "content": (
                "You are a market-data assistant. Use the available tools for "
                "all current or historical market claims. Never invent prices, "
                "timestamps, order-book values, or indicators. If a tool fails, "
                "explain the failure and do not substitute guessed data. "
                "Distinguish clearly between real-time and historical data."
            ),
        },
        {"role": "user", "content": question},
    ]

    for _ in range(6):
        response = client.chat.completions.create(
            model="gpt-4.1-mini",
            messages=messages,
            tools=TOOL_SCHEMAS,
            tool_choice="auto",
            temperature=0,
        )

        message = response.choices[0].message
        messages.append(message.model_dump(exclude_none=True))

        if not message.tool_calls:
            return message.content or "The assistant could not produce an answer."

        for tool_call in message.tool_calls:
            try:
                arguments = json.loads(tool_call.function.arguments or "{}")
            except json.JSONDecodeError:
                output = json.dumps(
                    {
                        "ok": False,
                        "error": "The function arguments were not valid JSON.",
                    }
                )
            else:
                output = run_tool_call(tool_call.function.name, arguments)

            messages.append(
                {
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": output,
                }
            )

    return "The tool-call limit was reached before the request completed."


if __name__ == "__main__":
    import sys

    if len(sys.argv) != 2:
        raise SystemExit(
            "Usage: python agent.py 'Show the latest AAPL 1-minute candle'"
        )

    answer = run_agent(sys.argv[1])
    print(answer)

The retry loop uses a bounded number of attempts, so a broken network cannot cause an infinite agent loop. The model can request tools, but it cannot bypass the TOOL_FUNCTIONS allowlist. That distinction is important: a prompt saying “do not call unauthorized functions” is not a security control.

The implementation also keeps authentication out of the model context. The model receives only the tool result. The TickDB API key remains in the process environment and is used by the adapter.

4.1 Handling Live WebSocket Data

REST is appropriate for a question-answering interaction. WebSocket streaming is more appropriate when the agent must monitor a changing market condition or maintain a continuously updated indicator.

For a WebSocket-enabled depth skill, the client should include a heartbeat and reconnect logic. The following pattern demonstrates the required resilience behavior independently of the model layer.

import json
import os
import random
import time

import websocket


API_KEY = os.environ.get("TICKDB_API_KEY")
if not API_KEY:
    raise RuntimeError("TICKDB_API_KEY is not set")

WS_URL = "wss://api.tickdb.ai/v1/stream/depth"

# The exact subscription message depends on the deployed skill version.
SUBSCRIPTION = {
    "cmd": "subscribe",
    "channel": "depth",
    "symbol": "BTC/USDT",
}


def calculate_pressure_ratio(bids: list[dict], asks: list[dict], levels: int = 5) -> float | None:
    """Calculate bid volume divided by ask volume for the top N levels."""
    bid_volume = sum(float(row.get("size", 0)) for row in bids[:levels])
    ask_volume = sum(float(row.get("size", 0)) for row in asks[:levels])

    if ask_volume == 0:
        return None
    return bid_volume / ask_volume


def connect_depth_stream() -> None:
    """Connect to depth data with heartbeat and jittered reconnection."""
    retry = 0

    while True:
        try:
            # ⚠️ Use a dedicated async client such as aiohttp or websockets
            # for high-frequency production workloads.
            ws = websocket.create_connection(
                f"{WS_URL}?api_key={API_KEY}",
                timeout=10,
            )
            ws.send(json.dumps(SUBSCRIPTION))
            retry = 0

            while True:
                # WebSocket heartbeat / keepalive.
                ws.send(json.dumps({"cmd": "ping"}))

                try:
                    raw_message = ws.recv()
                except websocket.WebSocketTimeoutException:
                    continue

                message = json.loads(raw_message)

                if message.get("cmd") == "pong":
                    continue

                bids = message.get("bids", [])
                asks = message.get("asks", [])
                ratio = calculate_pressure_ratio(bids, asks, levels=5)

                if ratio is not None:
                    print(
                        json.dumps(
                            {
                                "symbol": message.get("symbol"),
                                "timestamp": message.get("timestamp"),
                                "pressure_ratio": ratio,
                            }
                        )
                    )

        except (
            websocket.WebSocketException,
            TimeoutError,
            ConnectionError,
            json.JSONDecodeError,
        ) as exc:
            retry += 1
            delay = min(1.0 * (2 ** (retry - 1)), 30.0)
            jitter = random.uniform(0, delay * 0.1)

            print(f"Depth stream disconnected: {exc}; reconnecting in {delay + jitter:.2f}s")
            time.sleep(delay + jitter)

The WebSocket client demonstrates the operational behavior required for a resilient stream. A production implementation should additionally handle server-sent error codes, subscription confirmations, message-sequence gaps, and process-level shutdown signals.

5. Turning Tool Results into Reliable Answers

A successful API call does not automatically produce a trustworthy response. The assistant should preserve several properties when presenting the result:

  1. Freshness: State whether the data is real-time or historical.
  2. Time basis: Include the candle timestamp or the observation window.
  3. Units: Keep prices and volumes in the units returned by the data source.
  4. Scope: Identify the symbol and interval.
  5. Calculation boundary: Explain how a derived metric was calculated.

For example, a depth tool may return the following normalized result:

{
  "ok": true,
  "data": {
    "symbol": "BTC/USDT",
    "levels": 5,
    "bid_volume": 184.6,
    "ask_volume": 102.3,
    "pressure_ratio": 1.8045,
    "bids": [],
    "asks": []
  },
  "error": null
}

A good assistant response would say:

At the latest observation, BTC/USDT had a top-five-level buy/sell pressure ratio of 1.80. Total displayed bid volume was 184.6 units and displayed ask volume was 102.3 units. This snapshot indicates stronger displayed bid volume at that timestamp; it is not a forecast of future price movement.

The final sentence is particularly important. A pressure ratio is a descriptive market-state metric, not a recommendation. The tool should not transform a snapshot into a trading instruction.

The same discipline applies to historical requests. If a user asks for the last ten hourly candles, the answer should not describe a live quote. If the request asks for a current value and the adapter can only obtain a historical candle, the assistant should disclose that limitation rather than silently substituting a stale value.

6. Symbol Resolution and Capability Boundaries

Symbol normalization is one of the most common sources of false assistant failures. The model may produce BTCUSDT, BTC-USDT, BTC/USDT, or a name such as “Bitcoin” when the downstream API expects a specific exchange symbol.

A robust skill should perform two operations:

  1. Resolve the user’s asset name to a supported symbol.
  2. Verify that the resolved symbol supports the requested operation.

The second step matters because market coverage is not uniform.

Data operation US equities Hong Kong equities Crypto Forex / precious metals / indices
Historical OHLCV Supported Supported Supported Asset-dependent
Current real-time candle Supported Supported Supported Asset-dependent
Trade-level data Not supported Supported Supported Asset-dependent
Order-book depth L1 L1–L10 L1–L10 Not supported

The table does not mean that every symbol within a supported market supports every endpoint. The adapter should treat symbol availability and operation compatibility as separate checks.

For example, “Show me the current trade tape for AAPL” should not be routed to a trades tool. US equity tick-level trades are outside TickDB’s supported data boundary. A useful response can instead offer an appropriate alternative, such as a current kline or available order-book depth at L1.

The agent should expose these boundaries to the user clearly:

TickDB does not provide US equity tick-level trades. I can retrieve the current AAPL kline or the supported L1 depth snapshot instead.

That response is more useful than a generic error and prevents the model from fabricating a trade print.

7. Reconnecting the AI Assistant to a Live Skill Manifest

The skill.md protocol is useful for distributing instructions, but production deployments also need a mechanism for loading and validating the skill. A simple manifest loader can retrieve a skill definition from an internal service or local file.

from pathlib import Path
import json
import re


SKILL_URL = "http://skill.md/"


def load_local_skill(path: str) -> dict:
    """Load and validate a skill manifest from a local file."""
    raw = Path(path).read_text(encoding="utf-8")

    # The manifest may contain YAML front matter followed by instructions.
    # Adapt this parser to the exact manifest format used by your runtime.
    if "---" in raw:
        front_matter, _ = raw.split("---", 1)
        metadata = front_matter.strip()
    else:
        metadata = raw.strip()

    if not metadata:
        raise ValueError("The skill manifest is empty")

    # This validation intentionally remains format-agnostic.
    # Parse the manifest with the schema required by your skill runtime.
    if "market" not in metadata.lower():
        raise ValueError("The manifest does not describe a market-data capability")

    return {
        "source": path,
        "instructions": metadata,
    }

The important point is not the parser. It is the separation between a distributable skill description and the executable adapter. The manifest tells the agent what the capability does; the adapter determines how the request is authenticated, validated, and executed.

In a real deployment, the manifest loader should also verify:

  • The manifest’s tool names against the runtime registry.
  • Tool parameters against a JSON Schema.
  • Required environment variables.
  • Allowed symbols and markets.
  • Whether the skill is read-only.
  • Maximum request limits and time budgets.
  • The version of the deployed implementation.

A skill should not be able to register an arbitrary executable function merely because its text contains a function name. The runtime registry must remain the source of truth.

8. Deployment Patterns by User Segment

Different users need different levels of latency, auditability, and model autonomy. A universal agent configuration is rarely optimal.

User segment Recommended pattern Model permissions Operational priority
Individual developer Synchronous function calls Read-only market tools Simple setup and clear errors
Active trader REST tool calls plus optional WebSocket alerts Read-only tools with bounded refresh Freshness and low latency
Quant team Shared skill gateway with structured logs Allowlisted tools and approved symbols Reproducibility and auditability
Institutional platform Isolated service boundary, SSO, policy engine Policy-controlled tools Availability, permissions, and monitoring
AI coding assistant Local skill adapter Read-only market tools Schema discovery and safe execution

For a team deployment, the AI assistant should not connect directly to a production account. A gateway can provide:

  • Server-side API-key storage.
  • Per-user symbol permissions.
  • Request quotas.
  • Structured request and response logs.
  • Tool-call tracing.
  • Prompt and schema version tracking.
  • Cached symbol metadata.
  • Circuit breaking for repeated upstream failures.

The gateway also provides a natural place to implement content-security and data-access policies. For example, a user may be allowed to query US equities but not crypto depth, or a research environment may be restricted to completed historical periods.

9. Testing the Integration

Testing an AI market-data integration requires two separate test layers: deterministic tests for the adapter and behavioral tests for the model.

9.1 Deterministic adapter tests

Test the adapter independently of the model:

  • A valid symbol reaches the correct endpoint.
  • An invalid interval is rejected before the request.
  • A missing API key produces a clear configuration error.
  • A 3001 response honors Retry-After.
  • A 2002 response identifies an invalid symbol.
  • A timeout returns a structured failure.
  • The adapter never returns partially parsed or fabricated data.

9.2 Function-calling tests

Test whether the model selects the correct tool for representative requests:

Request Expected tool
“What is the latest AAPL candle?” get_current_kline
“Show AAPL hourly candles from the last month” get_historical_klines
“Calculate BTC’s five-level order-book imbalance” get_order_book_imbalance
“Show me AAPL trades” No trades tool; explain boundary
“What is the price of a made-up ticker?” Validate symbol and report failure

A useful test harness records the proposed function name, arguments, tool result, and final answer. This makes it possible to distinguish a model-selection error from an API or adapter error.

The test suite should also include adversarial cases:

  • A user asks the agent to print the API key.
  • A user requests a symbol with path traversal characters.
  • The model attempts to call an unregistered function.
  • The upstream API returns malformed JSON.
  • The request exceeds the permitted time range.
  • The model receives an empty data set and tries to fill the gap from memory.

A reliable agent refuses unsupported operations and reports failures. It does not treat a tool error as permission to improvise.

10. Common Failure Modes

The model selects historical data for a live question

The tool descriptions should make the distinction explicit. Use separate function names such as get_current_kline and get_historical_klines, and instruct the assistant to state the data type in the answer.

The API key appears in model context

Keep credentials in the server-side adapter. The model should receive only structured results. Never include the key in a prompt, tool description, or model-generated trace.

The agent interprets a signal as advice

The system instruction should require descriptive language. Derived metrics such as pressure ratios should be presented as observations, not predictions or trading recommendations.

The skill claims unsupported market coverage

Capabilities should be encoded in the adapter and validated before execution. Product descriptions are not sufficient because model prompts are not a reliable policy engine.

The agent loops on the same tool call

Use a maximum tool-call count, detect repeated calls, and return an explicit failure. A bounded loop is more reliable than trusting the model to terminate voluntarily.

The response omits timestamp semantics

A price without a timestamp is an incomplete market observation. Include the observation time and distinguish the event time from the retrieval time.

11. Operational Checklist

Before deploying the skill, verify the following controls:

  • Tool names are registered in an explicit allowlist.
  • JSON Schema rejects unsupported intervals and invalid parameter types.
  • TickDB authentication uses X-API-Key loaded from TICKDB_API_KEY.
  • Every HTTP request has a connect and read timeout.
  • Retries use exponential backoff and jitter.
  • Error code 3001 honors Retry-After.
  • Error codes 1001, 1002, and 2002 receive specific messages.
  • Live tools distinguish real-time data from historical data.
  • Depth requests enforce market-specific level limits.
  • Unsupported US equity trade requests are rejected.
  • The agent cannot read the API key from tool arguments.
  • Tool calls have a maximum count and execution budget.
  • Logs exclude credentials and sensitive payloads.
  • Answers disclose timestamps, units, and calculation scope.
  • Derived signals are not presented as investment advice.

Next Steps

If you are an individual developer, start with one narrowly scoped skill, such as current kline queries. Add historical candles only after the function-call loop and error handling work reliably.

If you want to build a market-data assistant, define the tool schemas around user intent, implement a server-side adapter, and expose the agent through a skill manifest compatible with your AI runtime. The skill.md description at http://skill.md/ can provide the portable capability description, while your adapter supplies the execution guarantees.

If you need real-time monitoring, combine REST requests for user questions with a WebSocket depth stream for continuous indicators. The stream should include heartbeat handling, jittered reconnection, and bounded retries.

If you operate an institutional platform, place the agent behind a policy gateway that controls API credentials, symbols, request volume, and auditability. The model should select tools; the gateway should enforce permissions.

This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results.