When large language models first gained the ability to call functions, developers faced a new class of engineering problem: how do you give an AI the precise vocabulary it needs to interact with domain-specific systems? A model trained on internet text knows what a stock price is in general terms, but it does not inherently know which API endpoint returns it, what parameters that endpoint expects, or how to interpret rate-limit errors when they appear. This is the gap that structured agent protocols are designed to close.

The TickDB SKILL protocol is one such specification. At its core, it is a machine-readable contract between a market data platform and any AI agent that wishes to query it. This article dissects that contract from first principles — examining the structure of the http://skill.md/ specification file, explaining how AI parsers interpret it, and demonstrating how Function Calling schemas are constructed for multi-turn market data conversations.

The Core Problem: AI Assistants and Domain-Specific APIs

Modern AI assistants excel at reasoning across general knowledge, but market data APIs present unique challenges that generic instruction-following cannot resolve. Consider a simple request: "What is the current bid-ask spread on NVIDIA?"

A human quant developer understands immediately that this requires querying an order book endpoint for a specific symbol. An AI assistant, unassisted, faces several failure modes:

  • Endpoint ambiguity: The AI does not know whether to call /depth, /quote, /realtime, or /ticker. Market data platforms vary in their endpoint naming conventions.
  • Parameter format uncertainty: Should the symbol be NVDA, NVDA.US, NASDAQ:NVDA, or NVD? The AI cannot guess correctly without explicit guidance.
  • Response interpretation: A JSON response containing nested arrays of bid/ask levels with quantities and timestamps requires structured parsing instructions that the model must receive before the call.
  • Error recovery: When a 2002 error (symbol not found) arrives, the AI needs a policy for whether to try alternative symbol formats or report the failure.

The SKILL protocol addresses all four failure modes by providing a standardized, versioned specification file that an AI can read, interpret, and follow without human intervention during execution.

Anatomy of the http://skill.md/ Specification File

The http://skill.md/ file is the central artifact of the SKILL protocol. It is a Markdown-formatted document that serves dual purposes: it is human-readable for developers reviewing the specification, and it is machine-parseable for AI agents that extract structured schemas from it.

File Structure Overview

A complete skill.md file contains five primary sections:

# SKILL Name

Brief description of what this SKILL enables.

## API Endpoints

Detailed endpoint specifications...

## Function Calling Schema

JSON Schema definitions for all callable functions...

## Data Models

Structured definitions of request/response objects...

## Error Handling

Standardized error codes and recovery strategies...

## Version

SKILL version, compatibility matrix...

Section 1: API Endpoints

This section enumerates every available endpoint with complete parameter documentation. For each endpoint, the specification includes:

  • HTTP method and path: GET /v1/market/depth
  • Authentication requirements: Header-based (X-API-Key) or query-parameter-based
  • Required parameters: Name, type, allowed values, default values
  • Optional parameters: Same metadata as required, plus whether the parameter has side effects
  • Rate limits: Requests per minute, burst limits, and the error code returned when exceeded
  • Example request: A fully formed curl or Python example with placeholder values replaced by illustrative constants
  • Example response: A representative JSON payload showing the actual data structure returned

Here is a distilled example of how an endpoint specification appears in skill.md:

### GET /v1/market/depth

Returns the current order book snapshot for a given symbol.

**Authentication**: Header (`X-API-Key`)

**Parameters**:

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| symbol | string | Yes | Symbol identifier. Format: `TICKER.EXCHANGE` (e.g., `AAPL.US`, `BTC.Binance`) |
| exchange | string | No | Override exchange for symbols listed on multiple venues. Defaults to primary. |
| depth | integer | No | Number of price levels to return. Range: 1–50. Default: 10. |

**Rate Limit**: 60 requests/minute per API key.

**Example Request**:
```bash
curl -H "X-API-Key: $TICKDB_API_KEY" \
  "https://api.tickdb.ai/v1/market/depth?symbol=AAPL.US&depth=5"

Example Response:

{
  "symbol": "AAPL.US",
  "timestamp": 1718294400000,
  "bids": [
    {"price": 192.45, "size": 12500},
    {"price": 192.44, "size": 8300}
  ],
  "asks": [
    {"price": 192.46, "size": 10200},
    {"price": 192.47, "size": 6700}
  ]
}

This level of detail is intentional. A model parsing this specification must know not just the endpoint path, but the exact JSON key names (`bids`, `asks`, `price`, `size`) so it can construct valid queries and correctly interpret responses.

### Section 2: Function Calling Schema

The Function Calling schema is the most critical section for AI execution. It is a JSON Schema document that defines every callable function in a format compatible with OpenAI's Function Calling interface and equivalent implementations in other LLMs.

Each function definition includes:

- **Name**: A lowercase, underscore-separated identifier (`get_order_book_depth`)
- **Description**: A natural-language explanation of what the function does, written for an AI audience (not a human developer — the AI needs precise behavioral descriptions)
- **Parameters**: A JSON Schema describing all input parameters with types, constraints, and enumerated allowed values
- **Required fields**: Which parameters must be provided versus which have defaults

```json
{
  "name": "get_order_book_depth",
  "description": "Retrieves the current order book snapshot for a specified symbol, including bid and ask levels with quantities. Use this function when the user asks about order book depth, bid-ask spreads, liquidity at specific price levels, or buy/sell pressure. Returns up to 50 price levels per side.",
  "parameters": {
    "type": "object",
    "properties": {
      "symbol": {
        "type": "string",
        "description": "Market symbol in TICKER.EXCHANGE format. Examples: AAPL.US, NVDA.US, 700.HK, BTC.Binance"
      },
      "depth": {
        "type": "integer",
        "description": "Number of price levels to return per side. Range 1-50. Default: 10.",
        "default": 10,
        "minimum": 1,
        "maximum": 50
      }
    },
    "required": ["symbol"]
  }
}

The description field deserves particular attention. It is not a marketing blurb — it is an operational instruction that tells the model when to invoke this function. The description should enumerate:

  1. What the function does (concise, factual)
  2. When to call it (trigger scenarios: "when the user asks about spreads," "when comparing liquidity across symbols")
  3. What it returns (data shape and typical values)
  4. Constraints (limits on depth, frequency, or symbol coverage)

This level of specificity prevents two common AI failure modes: calling the wrong function for a given intent, and hallucinating parameter values that the API does not accept.

Section 3: Data Models

Market data responses contain nested, polymorphic structures. The depth endpoint returns bids and asks; the kline endpoint returns OHLCV arrays; the ticker endpoint returns 24-hour rolling statistics. Each of these has distinct field names, types, and semantic meanings.

The Data Models section provides canonical definitions for every response object:

{
  "OrderBookLevel": {
    "price": "number (decimal string for precision)",
    "size": "integer (number of contracts or shares)"
  },
  "OrderBookSnapshot": {
    "symbol": "string",
    "timestamp": "integer (Unix milliseconds)",
    "bids": "OrderBookLevel[] (sorted descending by price)",
    "asks": "OrderBookLevel[] (sorted ascending by price)"
  },
  "OHLCVCandle": {
    "timestamp": "integer (Unix milliseconds, interval start)",
    "open": "number",
    "high": "number",
    "low": "number",
    "close": "number",
    "volume": "integer"
  }
}

By defining these models explicitly, the specification enables an AI to construct valid output parsers, validate incoming data against expected schemas, and surface structural errors (such as missing fields) before they propagate into downstream calculations.

Section 4: Error Handling

Error codes are documented with their numeric values, semantic meanings, and prescribed recovery actions. This section is essential for multi-turn conversations where a failed API call must not terminate the interaction — the AI needs a policy for retry, alternative approaches, or graceful degradation.

{
  "errors": [
    {
      "code": 1001,
      "message": "Invalid API key",
      "severity": "fatal",
      "recovery": "Prompt user to verify their API key in the TickDB dashboard. Do not retry."
    },
    {
      "code": 1002,
      "message": "Missing API key",
      "severity": "fatal",
      "recovery": "Request that the user set the TICKDB_API_KEY environment variable."
    },
    {
      "code": 2002,
      "message": "Symbol not found",
      "severity": "recoverable",
      "recovery": "Attempt alternative symbol formats (e.g., add .US suffix). If all formats fail, report the failure and suggest the user verify the symbol via /v1/symbols/available."
    },
    {
      "code": 3001,
      "message": "Rate limit exceeded",
      "severity": "recoverable",
      "recovery": "Read the Retry-After header. Wait for the specified duration before retrying. If no header is present, wait 5 seconds. Maximum 3 retries."
    }
  ]
}

Section 5: Versioning and Compatibility

The SKILL specification includes a version field and a compatibility matrix that maps the spec version to supported API versions. This enables both the platform and consuming AI agents to negotiate capabilities:

## Version

**SKILL Version**: 1.2.0
**API Compatibility**: v1.0, v1.1
**Last Updated**: 2026-04-15

| Feature | SKILL 1.0 | SKILL 1.1 | SKILL 1.2 |
|---------|-----------|-----------|-----------|
| Order book depth | ✓ | ✓ | ✓ |
| OHLCV klines | ✓ | ✓ | ✓ |
| Multi-exchange symbols | ✗ | ✓ | ✓ |
| Depth channel (WebSocket) | ✗ | ✗ | ✓ |

How AI Parsers Interpret the Specification

An AI agent does not read skill.md the way a human does — line by line, parsing prose. Instead, a pre-processing step extracts structured data from the Markdown document into an internal representation that the model can reason over during Function Calling.

Extraction Pipeline

The typical parsing pipeline operates as follows:

  1. Section identification: The parser identifies section headers (## API Endpoints, ## Function Calling Schema) and treats each as a distinct knowledge domain.
  2. JSON extraction: Code blocks containing JSON (delimited by triple backticks) are parsed as structured data. The parser validates that the JSON is well-formed and conforms to expected schemas.
  3. Table parsing: Parameter tables are parsed into structured objects, extracting column headers as field names and rows as values.
  4. Schema compilation: The extracted Function Calling definitions are compiled into a tool manifest — the payload that is sent to the LLM to announce available functions.
  5. Example indexing: Example requests and responses are indexed by endpoint, enabling the parser to provide few-shot examples when the model generates a function call.

Tool Manifest Generation

The final output of the parsing pipeline is a tool manifest that conforms to the OpenAI Function Calling format (or equivalent):

{
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_order_book_depth",
        "description": "Retrieves the current order book snapshot for a specified symbol...",
        "parameters": {
          "type": "object",
          "properties": {
            "symbol": {
              "type": "string",
              "description": "Market symbol in TICKER.EXCHANGE format..."
            },
            "depth": {
              "type": "integer",
              "description": "Number of price levels to return per side...",
              "default": 10
            }
          },
          "required": ["symbol"]
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "get_historical_klines",
        "description": "Retrieves historical OHLCV candlestick data for backtesting...",
        "parameters": {
          "type": "object",
          "properties": {
            "symbol": {
              "type": "string",
              "description": "Market symbol in TICKER.EXCHANGE format..."
            },
            "interval": {
              "type": "string",
              "enum": ["1m", "5m", "15m", "1h", "4h", "1d", "1w"],
              "description": "Candlestick interval..."
            },
            "start_time": {
              "type": "integer",
              "description": "Start time in Unix milliseconds..."
            },
            "end_time": {
              "type": "integer",
              "description": "End time in Unix milliseconds..."
            },
            "limit": {
              "type": "integer",
              "description": "Maximum number of candles to return. Range: 1-1000. Default: 300.",
              "default": 300
            }
          },
          "required": ["symbol", "interval"]
        }
      }
    }
  ]
}

This manifest is passed to the LLM via the platform's tool-calling interface. The model does not "memorize" the specification — it receives the manifest at inference time, enabling it to select the correct function and generate valid parameters for each query.

Multi-Turn Conversation State Management

Market data queries rarely happen in isolation. A realistic quant research workflow involves follow-up questions, parameter refinement, and cross-endpoint comparisons:

  1. User: "Show me the order book for AAPL."
  2. AI: Calls get_order_book_depth(symbol="AAPL.US"), receives response.
  3. User: "Now add TSLA to the comparison."
  4. AI: Calls get_order_book_depth(symbol="TSLA.US"), merges the two responses.
  5. User: "What about NVDA? And can you calculate the combined bid-ask spread across all three?"
  6. AI: Calls get_order_book_depth(symbol="NVDA.US"); upon receiving all three responses, computes the cross-symbol spread comparison and presents a formatted table.

This multi-turn behavior requires the AI to maintain conversation state — specifically, the accumulated responses from prior function calls. The SKILL protocol addresses state management through two mechanisms:

Mechanism 1: Response Caching

Function call responses are cached in the conversation context with a defined TTL (time-to-live). Cached responses allow the AI to reference prior data without re-querying the API, which is critical both for rate-limit management and for response latency.

Cache key: get_order_book_depth:AAPL.US:10
Cache value: {bids: [...], asks: [...], timestamp: 1718294400000}
TTL: 30 seconds (order book data is transient; stale data produces incorrect analysis)

Mechanism 2: Cross-Function Data Aggregation

When a user's follow-up request requires combining data from multiple endpoints (for example, merging real-time depth data with historical klines to compute realized volatility), the SKILL protocol defines aggregation functions that the AI can call after the individual endpoint results are available.

# Post-retrieval aggregation — not an API call, but a defined operation
def compute_spread_metrics(depth_a: OrderBookSnapshot, 
                            depth_b: OrderBookSnapshot,
                            depth_c: OrderBookSnapshot) -> dict:
    """
    Computes cross-symbol spread metrics from three order book snapshots.
    Each snapshot should be fetched within the last 30 seconds.
    """
    spreads = {}
    for snapshot in (depth_a, depth_b, depth_c):
        best_bid = snapshot['bids'][0]['price']
        best_ask = snapshot['asks'][0]['price']
        spreads[snapshot['symbol']] = {
            'spread_bps': round((best_ask - best_bid) / best_bid * 10000, 2),
            'mid_price': round((best_bid + best_ask) / 2, 4),
            'total_bid_liquidity': sum(level['size'] for level in snapshot['bids'][:5]),
            'total_ask_liquidity': sum(level['size'] for level in snapshot['asks'][:5]),
            'pressure_ratio': round(
                sum(level['size'] for level in snapshot['bids'][:5]) /
                sum(level['size'] for level in snapshot['asks'][:5]), 3
            )
        }
    return spreads

Production-Grade Integration Code

Connecting an AI assistant to the SKILL protocol requires more than parsing the specification — it requires a robust execution layer that handles authentication, rate limiting, timeouts, and error recovery. The following Python implementation demonstrates the recommended integration pattern:

import os
import json
import time
import random
import logging
from typing import Any, Optional
from dataclasses import dataclass, field
from datetime import datetime

import requests

# Configure structured logging for production monitoring
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
)
logger = logging.getLogger("tickdb_integration")


@dataclass
class TickDBConfig:
    """Configuration for the TickDB API integration."""
    api_key: str = field(default_factory=lambda: os.environ.get("TICKDB_API_KEY", ""))
    base_url: str = "https://api.tickdb.ai"
    timeout: tuple = (3.05, 10)  # (connect_timeout, read_timeout)
    max_retries: int = 3
    base_delay: float = 1.0
    max_delay: float = 30.0

    def validate(self) -> None:
        """Validate configuration at startup — fail fast."""
        if not self.api_key:
            raise ValueError(
                "TICKDB_API_KEY environment variable is not set. "
                "Generate an API key at tickdb.ai/dashboard"
            )


@dataclass
class APIResponse:
    """Standardized API response wrapper."""
    success: bool
    data: Optional[Any] = None
    error_code: Optional[int] = None
    error_message: Optional[str] = None
    retry_after: Optional[int] = None

    def __post_init__(self):
        if self.success and (self.error_code or self.error_message):
            logger.warning(
                f"Conflicting response state: success=True but error_code={self.error_code}"
            )


class TickDBClient:
    """
    Production-grade client for TickDB API integration with AI assistants.
    
    Supports:
    - REST API calls with automatic retry and exponential backoff
    - Rate-limit handling with Retry-After header support
    - Structured error classification for AI error-recovery logic
    - Configurable timeouts to prevent hanging in production environments
    """

    # Error code constants — must match the SKILL specification
    ERROR_INVALID_API_KEY = 1001
    ERROR_MISSING_API_KEY = 1002
    ERROR_SYMBOL_NOT_FOUND = 2002
    ERROR_RATE_LIMITED = 3001

    def __init__(self, config: Optional[TickDBConfig] = None):
        self.config = config or TickDBConfig()
        self.config.validate()
        self.session = requests.Session()
        self.session.headers.update({
            "X-API-Key": self.config.api_key,
            "Content-Type": "application/json",
            "User-Agent": "TickDB-SKILL-Client/1.2.0"
        })

    def _build_url(self, endpoint: str) -> str:
        """Construct full URL from endpoint path."""
        return f"{self.config.base_url}/v1{endpoint}"

    def _handle_error(self, response: requests.Response, retry_count: int) -> APIResponse:
        """
        Classify API errors and determine recovery strategy.
        Returns an APIResponse with classified error information.
        """
        try:
            error_body = response.json()
        except (json.JSONDecodeError, ValueError):
            error_body = {}

        code = error_body.get("code", 0)
        message = error_body.get("message", "Unknown error")
        retry_after = int(response.headers.get("Retry-After", 0))

        if code == 0:
            return APIResponse(success=True, data=error_body.get("data", error_body))

        if code in (self.ERROR_INVALID_API_KEY, self.ERROR_MISSING_API_KEY):
            logger.error(f"Fatal authentication error: {code} — {message}")
            return APIResponse(
                success=False,
                error_code=code,
                error_message=message
            )

        if code == self.ERROR_SYMBOL_NOT_FOUND:
            logger.warning(f"Symbol not found: {error_body.get('symbol', 'unknown')}")
            return APIResponse(
                success=False,
                error_code=code,
                error_message=message
            )

        if code == self.ERROR_RATE_LIMITED:
            logger.warning(f"Rate limited — Retry-After: {retry_after}s (attempt {retry_count})")
            return APIResponse(
                success=False,
                error_code=code,
                error_message=message,
                retry_after=retry_after if retry_after > 0 else 5
            )

        # Unhandled error codes
        logger.error(f"Unhandled API error {code}: {message}")
        return APIResponse(
            success=False,
            error_code=code,
            error_message=message
        )

    def _exponential_backoff_with_jitter(self, retry_count: int) -> float:
        """
        Compute delay with exponential backoff and full jitter.
        
        Formula: random(0, min(base_delay * 2^retry_count, max_delay))
        This prevents thundering-herd behavior when many clients
        retry simultaneously after a shared outage.
        """
        base_delay = self.config.base_delay
        max_delay = self.config.max_delay
        delay = min(base_delay * (2 ** retry_count), max_delay)
        jitter = random.uniform(0, delay * 0.1)
        return delay + jitter

    def _make_request(
        self,
        method: str,
        endpoint: str,
        params: Optional[dict] = None,
        json_body: Optional[dict] = None,
        retry_count: int = 0
    ) -> APIResponse:
        """
        Execute an HTTP request with timeout, retry logic, and error classification.
        """
        url = self._build_url(endpoint)
        
        logger.info(f"Request: {method} {url} params={params}")
        
        try:
            response = self.session.request(
                method=method,
                url=url,
                params=params,
                json=json_body,
                timeout=self.config.timeout
            )
        except requests.exceptions.Timeout:
            logger.error(f"Request timed out after {self.config.timeout}")
            return APIResponse(
                success=False,
                error_code=-1,
                error_message=f"Request timed out after {self.config.timeout}"
            )
        except requests.exceptions.RequestException as e:
            logger.error(f"Request failed: {e}")
            return APIResponse(
                success=False,
                error_code=-2,
                error_message=str(e)
            )

        # Classify the response
        result = self._handle_error(response, retry_count)

        # Retry logic for recoverable errors
        if not result.success and result.retry_after is not None:
            if retry_count < self.config.max_retries:
                delay = result.retry_after or self._exponential_backoff_with_jitter(retry_count)
                logger.info(f"Retrying in {delay:.2f}s (attempt {retry_count + 1}/{self.config.max_retries})")
                time.sleep(delay)
                return self._make_request(
                    method, endpoint, params, json_body, retry_count=retry_count + 1
                )
            else:
                logger.warning(f"Max retries exceeded for {endpoint}")

        return result

    def get_order_book_depth(
        self,
        symbol: str,
        depth: int = 10
    ) -> APIResponse:
        """
        Fetch order book depth snapshot.
        
        Args:
            symbol: Market symbol in TICKER.EXCHANGE format (e.g., AAPL.US, BTC.Binance)
            depth: Number of price levels to return (1-50, default: 10)
        
        Returns:
            APIResponse containing the order book snapshot or error details
        """
        return self._make_request(
            method="GET",
            endpoint="/market/depth",
            params={"symbol": symbol, "depth": depth}
        )

    def get_historical_klines(
        self,
        symbol: str,
        interval: str,
        start_time: Optional[int] = None,
        end_time: Optional[int] = None,
        limit: int = 300
    ) -> APIResponse:
        """
        Fetch historical OHLCV candlestick data for backtesting.
        
        Args:
            symbol: Market symbol in TICKER.EXCHANGE format
            interval: Candlestick interval (1m, 5m, 15m, 1h, 4h, 1d, 1w)
            start_time: Start time in Unix milliseconds
            end_time: End time in Unix milliseconds
            limit: Maximum candles to return (1-1000, default: 300)
        
        Returns:
            APIResponse containing OHLCV candles or error details
        """
        params = {
            "symbol": symbol,
            "interval": interval,
            "limit": limit
        }
        if start_time:
            params["start_time"] = start_time
        if end_time:
            params["end_time"] = end_time

        return self._make_request(
            method="GET",
            endpoint="/market/kline",
            params=params
        )

    def get_available_symbols(
        self,
        exchange: Optional[str] = None,
        asset_type: Optional[str] = None
    ) -> APIResponse:
        """
        List all available symbols. Use this to verify symbol formats
        before making targeted queries.
        
        Args:
            exchange: Filter by exchange (e.g., US, HK, Binance)
            asset_type: Filter by asset type (equity, crypto, forex, commodity)
        
        Returns:
            APIResponse containing the symbol list
        """
        params = {}
        if exchange:
            params["exchange"] = exchange
        if asset_type:
            params["type"] = asset_type

        return self._make_request(
            method="GET",
            endpoint="/symbols/available",
            params=params
        )


# Example usage within an AI agent's function-calling loop
def ai_function_call_handler(tool_name: str, parameters: dict) -> dict:
    """
    AI agent integration layer: receives function name and parameters
    from the LLM's tool call, executes the corresponding API operation,
    and returns structured results for the model to synthesize.
    
    This function bridges the gap between the LLM's intent and the API's
    actual response format, providing consistent error handling and
    response normalization.
    """
    client = TickDBClient()

    handlers = {
        "get_order_book_depth": lambda p: client.get_order_book_depth(**p),
        "get_historical_klines": lambda p: client.get_historical_klines(**p),
        "get_available_symbols": lambda p: client.get_available_symbols(**p),
    }

    handler = handlers.get(tool_name)
    if not handler:
        return {
            "success": False,
            "error": f"Unknown tool: {tool_name}. Available tools: {list(handlers.keys())}"
        }

    result = handler(parameters)

    if result.success:
        return {
            "success": True,
            "data": result.data
        }
    else:
        return {
            "success": False,
            "error_code": result.error_code,
            "error": result.error_message,
            "retry_after": result.retry_after,
            "recovery_hint": _get_recovery_hint(result.error_code, parameters)
        }


def _get_recovery_hint(error_code: int, original_params: dict) -> str:
    """Generate AI-readable recovery hints based on error classification."""
    hints = {
        2002: (
            f"Symbol '{original_params.get('symbol')}' was not found. "
            "Try listing available symbols via get_available_symbols to find valid formats. "
            "For US equities, use TICKER.US format (e.g., AAPL.US, not AAPL)."
        ),
        3001: (
            "Rate limit exceeded. Wait for the Retry-After duration before retrying. "
            "Consider reducing query frequency or caching responses for repeated lookups."
        ),
        1001: (
            "Invalid API key. Verify that TICKDB_API_KEY is set correctly. "
            "Generate a new key at tickdb.ai/dashboard if the current key has been rotated."
        )
    }
    return hints.get(error_code, "Unknown error — manual investigation required.")

Implementing Function Calling with the SKILL Schema

With the client implementation in place, the final piece is wiring it into an AI agent's Function Calling loop. The integration follows a standard pattern regardless of which LLM platform you use (OpenAI, Anthropic, local models via Ollama, or cloud-hosted alternatives).

Step 1: Load the Tool Manifest

At agent initialization, load the SKILL's Function Calling schema and register it with the model:

import json

def load_tool_manifest(skill_md_path: str) -> list:
    """
    Parse the skill.md file and extract the Function Calling schema.
    
    In production, this file would be fetched from a versioned CDN URL
    (e.g., https://cdn.tickdb.ai/skill/v1.2.0/manifest.json) to ensure
    the agent always uses the latest specification.
    """
    with open(skill_md_path, 'r') as f:
        content = f.read()
    
    # Extract JSON blocks from the Markdown
    # In production, use a proper Markdown+JSON parser
    json_blocks = []
    in_code_block = False
    current_block = []
    block_lang = None
    
    for line in content.split('\n'):
        if line.startswith('```'):
            if in_code_block:
                # End of block
                block_text = '\n'.join(current_block)
                if block_lang == 'json':
                    try:
                        json_blocks.append(json.loads(block_text))
                    except json.JSONDecodeError:
                        pass
                in_code_block = False
                current_block = []
                block_lang = None
            else:
                # Start of block
                in_code_block = True
                block_lang = line[3:].strip()
        elif in_code_block:
            current_block.append(line)
    
    # Find the Function Calling schema among extracted JSON blocks
    for block in json_blocks:
        if 'tools' in block or 'function' in block:
            return block.get('tools', [block])
    
    raise ValueError("No Function Calling schema found in skill.md")

Step 2: Execute the Agent Loop

def run_market_data_agent(user_query: str, tool_manifest: list) -> str:
    """
    Execute a multi-turn market data query through an AI agent with
    function-calling capabilities.
    
    The agent receives:
    1. The user's natural-language query
    2. The SKILL tool manifest (available functions + descriptions)
    
    The agent responds by:
    - Calling functions when needed (with validated parameters)
    - Synthesizing results into natural-language responses
    - Asking clarifying questions if the query is ambiguous
    """
    messages = [
        {
            "role": "system",
            "content": (
                "You are a quantitative research assistant with access to real-time "
                "and historical market data via TickDB. Use the provided tool manifest "
                "to call functions when you need live or historical data. "
                "Always verify symbol formats before querying. "
                "When you encounter errors, use the recovery_hint to determine the next step. "
                "Present quantitative results with appropriate precision (bps, percentages, etc.)."
            )
        },
        {"role": "user", "content": user_query}
    ]

    max_turns = 10
    turn = 0

    while turn < max_turns:
        turn += 1

        # Generate next response (model decides to call tools or respond)
        response = model.generate(messages, tools=tool_manifest)

        if not response.tool_calls:
            # Model has finished — return the final response
            return response.content

        # Execute each tool call and append results
        for tool_call in response.tool_calls:
            function_name = tool_call.function.name
            parameters = json.loads(tool_call.function.arguments)

            logger.info(f"Tool call [{turn}]: {function_name} params={parameters}")

            result = ai_function_call_handler(function_name, parameters)

            messages.append({
                "role": "assistant",
                "content": None,
                "tool_calls": [tool_call]
            })
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(result)
            })

    return "Agent reached maximum turn limit. Please refine your query."

Comparing SKILL Protocol to Alternative Approaches

For teams evaluating how to integrate LLMs with market data systems, three broad approaches exist. Each has distinct trade-offs.

Dimension Raw API Documentation OpenAPI + Custom Wrapper SKILL Protocol
AI parseability Low — requires manual instruction following Medium — structured schema but ambiguous domain semantics High — explicit intent mapping and recovery policies
Maintenance burden High — prompts must be manually updated when APIs change Medium — OpenAPI spec must be kept in sync Low — spec and manifest are co-versioned
Error recovery DIY — each integration implements its own retry logic Partial — OpenAPI describes errors but not recovery policies Full — errors include recovery_hint fields for AI agents
Multi-turn state Not addressed Not addressed Defined caching TTLs and aggregation functions
Supported LLM platforms Any (prompt-dependent) OpenAI-compatible Any Function Calling–compatible platform
Setup complexity Low Medium Medium (requires understanding the protocol spec)

The SKILL protocol's advantage is not raw capability — a skilled engineer could implement all of these features using OpenAPI and custom prompts. The protocol's value is standardization: it provides a shared vocabulary between the platform provider and the AI developer, reducing the iteration cycles required to get a new AI integration working correctly.

Practical Deployment Recommendations

Teams adopting the SKILL protocol should consider three deployment tiers:

Deployment tier Use case SKILL version Caching strategy
Individual researcher Ad-hoc queries, strategy ideation Latest stable No caching — fresh data for each query
Quant team Shared research notebooks, backtest validation Pinned to validated version 30-second TTL for real-time data; no TTL for historical
Production trading system Live signal generation, automated execution Pinned with hash verification 5-second TTL with staleness alerts; historical data cached indefinitely

For teams using AI coding assistants (Cursor, Copilot, Claude Desktop, etc.), the SKILL can be packaged as a downloadable tool manifest. Search for and install the tickdb-market-data SKILL in your AI tool's marketplace to enable instant function-calling access to real-time market data within your existing development workflow.

Closing

Structured agent protocols like the SKILL specification represent a practical answer to a real engineering problem: LLMs need domain-specific guidance to interact safely and correctly with specialized APIs. Without explicit endpoint documentation, parameter constraints, error recovery policies, and multi-turn state management, an AI assistant querying a market data API will produce unreliable results — hallucinated symbols, incorrect parameter formats, and unhandled rate-limit errors.

The SKILL protocol does not eliminate the need for careful engineering. The code you write to execute function calls must still handle timeouts, implement backoff and jitter, classify errors, and cache responses appropriately. What the protocol provides is a stable contract between the data platform and the AI layer — ensuring that both sides speak the same language when defining what data is available, how to request it, and what to do when something goes wrong.

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