Back to site

Tickory For Developers

Build agents and backend automations against the Tickory API with scoped API keys, deterministic event payloads, and explanation endpoints for both triggers and no-match runs.

Auth bootstrap

Long-running agents should use X-API-Key, not bearer JWTs. Preferred path: create a scoped key in API Access. If you need automated bootstrap, use a short-lived bearer token once to mint or rotate the key, then keep the agent on the API-key surface only.

Quickstart

Tickory does not translate natural-language trading intent for you today. Your agent converts an intent into a scan payload, validates the CEL expression, creates the scan, then reads and explains alert events.

Example intent: Find liquid 15m spot pairs with RSI-14 below 30 so my agent can review pullback candidates.

  1. Create a scoped API key with manage_scans and read_events in API Access.
  2. Validate the expression before creating or updating the scan.
  3. Create the scan from your translated payload.
  4. Execute it or wait for a scheduled run, then read alert events.
  5. Call the explain endpoint on any event your agent needs to reason about.

Environment default

The examples below default to https://api.tickory.app. Override API_BASE or TICKORY_API_BASE with your local API base, for example http://localhost:8080, during development.

MCP install

If your agent host speaks MCP, install the standalone tickory-mcp server via npx @tickory/mcp or go install github.com/tickory/tickory-mcp@latest. This monorepo no longer ships the MCP binary.
01-create-api-key.sh
# Preferred: create the key in https://app.tickory.app/account/api-access
# API bootstrap only: use a short-lived bearer token once, then discard it.
export API_BASE="https://api.tickory.app"
export BOOTSTRAP_BEARER_TOKEN="short-lived-bearer-token"

curl -sS -X POST "$API_BASE/api/auth/api-keys" \
  -H "Authorization: Bearer $BOOTSTRAP_BEARER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "agent-production",
  "scopes": [
    "manage_scans",
    "read_events"
  ]
}'
02-validate-scan.sh
export TICKORY_API_KEY="tk_xxxxxxxx_yyyyyyyyyyyyyyyyyyyyyyyy"

curl -sS -X POST "$API_BASE/api/crypto/scans/validate" \
  -H "X-API-Key: $TICKORY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "cel_expression": "has_rsi_14 && rsi_14 < 30 && close > ma_50",
  "contract_type": "spot"
}'
03-create-scan.sh
curl -sS -X POST "$API_BASE/api/crypto/scans" \
  -H "X-API-Key: $TICKORY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Agent Oversold Pullback",
  "description": "Agent-generated from intent: Find liquid 15m spot pairs with RSI-14 below 30 so my agent can review pullback candidates.",
  "cel_expression": "has_rsi_14 && rsi_14 < 30 && close > ma_50",
  "timeframe": "15m",
  "hard_gates": {
    "min_volume_quote": 1000000,
    "require_rsi_14": true,
    "require_ma_50": true,
    "allowed_exchanges": [
      "binance"
    ],
    "allowed_contracts": [
      "spot"
    ]
  },
  "builder_config": {
    "source": "agent_quickstart",
    "intent": "Find liquid 15m spot pairs with RSI-14 below 30 so my agent can review pullback candidates."
  }
}'
04-list-alert-events.sh
curl -sS "$API_BASE/api/crypto/alert-events?scan_id=scan-user-1&limit=5" \
  -H "X-API-Key: $TICKORY_API_KEY"
05-explain-alert-event.sh
curl -sS "$API_BASE/api/crypto/alert-events/11111111-1111-1111-1111-111111111111/explain" \
  -H "X-API-Key: $TICKORY_API_KEY"

Auth Model

API keys are user-scoped and least-privilege. Every agent should request only the scopes it needs and keep separate keys for separate environments.

ScopeUse It ForKey EndpointsMCP Tools
manage_scansValidate expressions, create scans, update scans, and execute scans on demand.GET/POST /api/crypto/scans, POST /api/crypto/scans/validate, POST /api/crypto/scans/executetickory_list_scans, tickory_create_scan, tickory_update_scan, tickory_run_scan
read_eventsRead alert events, fetch one event, and request deterministic explanations.GET /api/crypto/alert-events, GET /api/crypto/alert-events/{eventId}, GET /api/crypto/alert-events/{eventId}/explain, GET /api/crypto/scans/runs/{runId}/explain-no-matchtickory_list_alert_events, tickory_get_alert_event, tickory_explain_alert_event
manage_routingCreate inbound sources and outbound routes for agent-driven routing workflows.GET/POST /api/alert-sources, PATCH/DELETE /api/alert-sources/{sourceId}, POST /api/alert-routesNone

Endpoint And Tool Map

Use the REST API when you control the transport directly. Use the MCP tool names below when you are embedding Tickory into an MCP-capable agent host.

GoalHTTPScopeMCP Tool
Create a scan from agent intentPOST /api/crypto/scansmanage_scanstickory_create_scan
Read recent alert eventsGET /api/crypto/alert-eventsread_eventstickory_list_alert_events
Fetch one alert eventGET /api/crypto/alert-events/{eventId}read_eventstickory_get_alert_event
Explain why an event triggeredGET /api/crypto/alert-events/{eventId}/explainread_eventstickory_explain_alert_event
Explain a no-match scan runGET /api/crypto/scans/runs/{runId}/explain-no-matchread_eventsHTTP only

No-event runs

If a scan run produces no alert events, use the HTTP-only /api/crypto/scans/runs/{runId}/explain-no-match endpoint to understand whether the CEL expression, hard gates, or readiness checks rejected candidates.

TypeScript Example

This example mirrors the curl flow: validate, create, list alert events, then explain the first event returned for the scan.

tickory-agent.ts
const apiBase = process.env.TICKORY_API_BASE ?? "https://api.tickory.app";
const apiKey = process.env.TICKORY_API_KEY;

if (!apiKey) {
  throw new Error("Set TICKORY_API_KEY before running this script.");
}

const intent = "Find liquid 15m spot pairs with RSI-14 below 30 so my agent can review pullback candidates.";
const scanPayload = {
  "name": "Agent Oversold Pullback",
  "description": "Agent-generated from intent: Find liquid 15m spot pairs with RSI-14 below 30 so my agent can review pullback candidates.",
  "cel_expression": "has_rsi_14 && rsi_14 < 30 && close > ma_50",
  "timeframe": "15m",
  "hard_gates": {
    "min_volume_quote": 1000000,
    "require_rsi_14": true,
    "require_ma_50": true,
    "allowed_exchanges": [
      "binance"
    ],
    "allowed_contracts": [
      "spot"
    ]
  },
  "builder_config": {
    "source": "agent_quickstart",
    "intent": "Find liquid 15m spot pairs with RSI-14 below 30 so my agent can review pullback candidates."
  }
};

async function tickory(path, init = {}) {
  const response = await fetch(`${apiBase}${path}`, {
    ...init,
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": apiKey,
      ...(init.headers ?? {}),
    },
  });

  if (!response.ok) {
    const body = await response.text();
    throw new Error(`${response.status} ${response.statusText}: ${body}`);
  }

  return response.json();
}

async function main() {
  console.log("Intent:", intent);

  const validation = await tickory("/api/crypto/scans/validate", {
    method: "POST",
    body: JSON.stringify({
      cel_expression: scanPayload.cel_expression,
      contract_type: "spot",
    }),
  });

  if (!validation.valid) {
    throw new Error(`scan validation failed: ${JSON.stringify(validation)}`);
  }

  const created = await tickory("/api/crypto/scans", {
    method: "POST",
    body: JSON.stringify(scanPayload),
  });

  const events = await tickory(
    `/api/crypto/alert-events?scan_id=${encodeURIComponent(created.id)}&limit=5`
  );

  const firstEvent = events.events?.[0];
  if (!firstEvent) {
    console.log("No alert events yet. Execute the scan or wait for a scheduled run.");
    return;
  }

  const explanation = await tickory(
    `/api/crypto/alert-events/${firstEvent.alert_event_id}/explain`
  );

  console.log(JSON.stringify({ created, firstEvent, explanation }, null, 2));
}

void main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Python Example

This version uses Python standard-library HTTP utilities so it stays copy-paste runnable without a separate dependency install step.

tickory_agent.py
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request

API_BASE = os.getenv("TICKORY_API_BASE", "https://api.tickory.app")
API_KEY = os.getenv("TICKORY_API_KEY")

if not API_KEY:
    raise SystemExit("Set TICKORY_API_KEY before running this script.")

INTENT = "Find liquid 15m spot pairs with RSI-14 below 30 so my agent can review pullback candidates."
SCAN_PAYLOAD = {
    "name": "Agent Oversold Pullback",
    "description": "Agent-generated from intent: Find liquid 15m spot pairs with RSI-14 below 30 so my agent can review pullback candidates.",
    "cel_expression": "has_rsi_14 && rsi_14 < 30 && close > ma_50",
    "timeframe": "15m",
    "hard_gates": {
        "min_volume_quote": 1000000,
        "require_rsi_14": True,
        "require_ma_50": True,
        "allowed_exchanges": [
            "binance"
        ],
        "allowed_contracts": [
            "spot"
        ]
    },
    "builder_config": {
        "source": "agent_quickstart",
        "intent": "Find liquid 15m spot pairs with RSI-14 below 30 so my agent can review pullback candidates."
    }
}


def tickory(path, method="GET", body=None):
    data = None if body is None else json.dumps(body).encode("utf-8")
    request = urllib.request.Request(
        f"{API_BASE}{path}",
        data=data,
        method=method,
        headers={
            "Content-Type": "application/json",
            "X-API-Key": API_KEY,
        },
    )
    try:
        with urllib.request.urlopen(request) as response:
            return json.load(response)
    except urllib.error.HTTPError as error:
        payload = error.read().decode("utf-8", errors="replace")
        raise RuntimeError(f"{error.code} {error.reason}: {payload}") from error


def main():
    print("Intent:", INTENT)

    validation = tickory(
        "/api/crypto/scans/validate",
        method="POST",
        body={
            "cel_expression": SCAN_PAYLOAD["cel_expression"],
            "contract_type": "spot",
        },
    )
    if not validation.get("valid"):
        raise RuntimeError(f"scan validation failed: {json.dumps(validation)}")

    created = tickory("/api/crypto/scans", method="POST", body=SCAN_PAYLOAD)
    query = urllib.parse.urlencode({"scan_id": created["id"], "limit": 5})
    events = tickory(f"/api/crypto/alert-events?{query}")

    first_event = next(iter(events.get("events", [])), None)
    if not first_event:
        print("No alert events yet. Execute the scan or wait for a scheduled run.")
        sys.exit(0)

    explanation = tickory(
        f"/api/crypto/alert-events/{first_event['alert_event_id']}/explain"
    )
    print(json.dumps({
        "created": created,
        "first_event": first_event,
        "explanation": explanation,
    }, indent=2))


if __name__ == "__main__":
    main()

Rate Limits And Retries

  • API keys are rate-limited with the same per-minute limiter as the main API. The default server setting is 100 requests per minute, and rate-limited responses include Retry-After: 60.
  • Retry transport failures and HTTP 5xx with exponential backoff plus jitter. Start at 1 second, cap at 30 seconds, and preserve idempotency on client retries.
  • Do not blindly retry 400, 401, or 403 responses. Fix the request body, API key, or scope mismatch first.
  • Treat 404 as terminal unless you just created the resource and are waiting on eventual scheduling side effects.
  • Use cursor pagination on /api/crypto/alert-events instead of polling large windows repeatedly.

Error Model

  • Most handler errors return JSON with error, message, and sometimes code or details.
  • API-key auth failures come from middleware and can return compact JSON such as {"error":"Invalid API key"} or {"error":"API key does not have the required scope"}.
  • Rate-limit failures can return HTTP 429 with a plain-text Too Many Requests body plus Retry-After: 60.
  • Every response includes X-Request-ID. Log it alongside the failing payload so support can trace the request quickly.

Troubleshooting

  • 403 with a valid key usually means the key is missing the required scope for that endpoint.
  • A create-scan call that succeeds but produces no events usually needs an explicit /api/crypto/scans/execute call or a schedule before events exist to read.
  • If /api/crypto/alert-events/{eventId}/explain returns 404, verify you are using an event ID from the same account that owns the API key.
  • If validation fails, call /api/crypto/scans/validate before create/update and surface the validator warnings back to the agent operator.

Versioning Policy

  • The REST paths in this guide are the stable agent surface for the current API-key contract.
  • Alert-event and explain responses include payload_version. MCP responses include schema_version: v1. Persist and log those markers in downstream agents.
  • Additive response fields can appear without a path change. Agents should ignore unknown fields and key off documented required fields.
  • Breaking changes should ship behind a new response version marker, a new MCP schema version, or a new endpoint contract instead of silently mutating existing fields.

Need the full contract surface? Start with the OpenAPI spec and keep the versioning policy in the same agent package so future changes are explicit.