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.
Current API-key auth surface served directly from docs/openapi_api_keys.yaml.
Install via npx @tickory/mcp or go install github.com/tickory/tickory-mcp@latest.
Compatibility rules for payload_version, schema_version, and additive fields.
Auth bootstrap
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.
- Create a scoped API key with
manage_scansandread_eventsin API Access. - Validate the expression before creating or updating the scan.
- Create the scan from your translated payload.
- Execute it or wait for a scheduled run, then read alert events.
- Call the explain endpoint on any event your agent needs to reason about.
Environment default
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
npx @tickory/mcp or go install github.com/tickory/tickory-mcp@latest. This monorepo no longer ships the MCP binary.# 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"
]
}'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"
}'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."
}
}'curl -sS "$API_BASE/api/crypto/alert-events?scan_id=scan-user-1&limit=5" \
-H "X-API-Key: $TICKORY_API_KEY"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.
| Scope | Use It For | Key Endpoints | MCP Tools |
|---|---|---|---|
| manage_scans | Validate expressions, create scans, update scans, and execute scans on demand. | GET/POST /api/crypto/scans, POST /api/crypto/scans/validate, POST /api/crypto/scans/execute | tickory_list_scans, tickory_create_scan, tickory_update_scan, tickory_run_scan |
| read_events | Read 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-match | tickory_list_alert_events, tickory_get_alert_event, tickory_explain_alert_event |
| manage_routing | Create inbound sources and outbound routes for agent-driven routing workflows. | GET/POST /api/alert-sources, PATCH/DELETE /api/alert-sources/{sourceId}, POST /api/alert-routes | None |
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.
| Goal | HTTP | Scope | MCP Tool |
|---|---|---|---|
| Create a scan from agent intent | POST /api/crypto/scans | manage_scans | tickory_create_scan |
| Read recent alert events | GET /api/crypto/alert-events | read_events | tickory_list_alert_events |
| Fetch one alert event | GET /api/crypto/alert-events/{eventId} | read_events | tickory_get_alert_event |
| Explain why an event triggered | GET /api/crypto/alert-events/{eventId}/explain | read_events | tickory_explain_alert_event |
| Explain a no-match scan run | GET /api/crypto/scans/runs/{runId}/explain-no-match | read_events | HTTP only |
No-event runs
/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.
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.
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.