Agents
This page is for developers writing their own agent loop with LangChain, the Vercel AI SDK, the OpenAI SDK, the Anthropic SDK, or raw HTTP. Drop the tool definitions below into your runtime’s tool-use schema.
Before you wire it in
Three things to know before pasting the JSON below.
The grammar. Flat WHERE over 421+ named columns — plus group_by/select/having as request parameters for rollups. No subqueries, no OVER; the only join is join=state on /v2/events (a parameter, not SQL you write). Schema fits in a system prompt; one grammar drives /scan and /scan?asof= (as q) and every subscribe endpoint (as q on scan subscribe, condition on the per-resource subscribes).
The loop. User asks in English → agent composes q → call /v2/scan live, asof for any past moment (date or ISO timestamp), /subscribe to register a webhook. State lives in the API; the agent stays stateless.
The system prompt. Paste in column + flag names from /docs/schema, the grammar bounds (flat WHERE, bare identifiers, fully-specified numeric literals), and the asset-symbol prefixes (X:BTCUSD for crypto — see Asset symbols).
Pick a runtime
Claude tool-use docs for the API surface. Pass the JSON below as the tools array on a Messages request.
All tools, one JSON blob
The whole set in one paste. Use this if your runtime accepts a tool array.
[
{
"name": "tickerbot_list_tickers",
"description": "List active tickers from the Tickerbot universe (~14,500 US equities, plus the US Treasury curve and Fed policy rates under `R:`, and major FX pairs, spot metals and crypto under `X:`). Use `tickers` for bulk lookup of named symbols (returns full rows); otherwise walks the universe alphabetically with `cursor` pagination. Supports filters: search, asset_type, exchange, sector, min_market_cap.",
"input_schema": {
"type": "object",
"properties": {
"tickers": {
"type": "string",
"description": "Comma-separated symbols (max 50). When set, returns full rows for these symbols and pagination params are ignored."
},
"limit": {
"type": "integer",
"description": "Page size. Max 1000. Default 50."
},
"cursor": {
"type": "string",
"description": "Opaque cursor from a prior response."
},
"search": {
"type": "string",
"description": "Case-insensitive substring filter on ticker/name. Orders results by market_cap desc."
},
"asset_type": {
"type": "string",
"description": "Filter by INSTRUMENT TYPE within equities: the stored value (CS, ETF, ADRC, PFD, FUND, UNIT, SP, ETS, WARRANT, RIGHT, ETN, ETV), or \"equity\" for the equity-like set. This is not an asset class — `asset_type=crypto` is rejected; crypto is in the main list under its X: symbols."
},
"exchange": {
"type": "string",
"description": "Filter by exchange (e.g. \"XNYS\", \"XNAS\", \"BATS\")."
},
"sector": {
"type": "string",
"description": "Exact-match sector filter (e.g. \"Technology\")."
},
"min_market_cap": {
"type": "number",
"description": "Minimum market cap in USD. Orders results by market_cap desc."
},
"universe": {
"type": "string",
"description": "Restrict to a universe slug (top_10, top_100, or a saved one)."
},
"asset_class": {
"type": "string",
"description": "Filter by asset class — `stocks`, `rates`, `crypto`, `fx`, or a comma-separated list. Omit for every class. Distinct from asset_type (the instrument type within equities)."
}
}
}
},
{
"name": "tickerbot_get_ticker",
"description": "Get the full current row for one ticker — every column on the schema (price, change, indicators like rsi_14, every boolean flag like above_sma_50, fundamentals like pe_ratio). Pass `asof` (YYYY-MM-DD or ISO timestamp like 2026-07-20T15:30:00Z) for the row as it stood at that past moment — date-only gives the close of that day.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Symbol. Case-insensitive. Equities: bare symbol (AAPL). Crypto: X-prefixed pair (X:BTCUSD) — bare BTC/ETH are US-listed ETFs, not spot crypto."
},
"asof": {
"type": "string",
"description": "Optional YYYY-MM-DD or ISO timestamp. Date-only returns the row at close of that day; a timestamp returns the row as of that moment (finest tier covering each column)."
}
},
"required": [
"ticker"
]
}
},
{
"name": "tickerbot_get_ticker_history",
"description": "Time-travel SNAPSHOT: the full wide row for one ticker as it stood at a past date (one row, not a series — for a multi-column time SERIES use tickerbot_get_series). Returns indicators, boolean flags, and the most-recent fundamentals known on that date. Unlimited depth on every plan.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Symbol."
},
"asof": {
"type": "string",
"description": "Target date as YYYY-MM-DD or full ISO timestamp."
}
},
"required": [
"ticker",
"asof"
]
}
},
{
"name": "tickerbot_get_ticker_coverage",
"description": "Data-coverage report for one ticker — which intervals and columns exist, from when, at what depth. The honesty endpoint: ask this before assuming a gap in bars/series is a data outage vs. genuinely-never-covered (e.g. sub-hour bars outside the active universe, fundamentals on non-equities).",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Symbol."
}
},
"required": [
"ticker"
]
}
},
{
"name": "tickerbot_get_ticker_bars",
"description": "Get OHLCV (open/high/low/close/volume) bars for one or more symbols at a given interval, oldest-first. Pass a comma-separated `ticker` list (up to 50) for a bulk response keyed by symbol. Use `asof` for a single point-in-time bar, or `before`+`limit` to back-page. `1d`/`1h` cover the full universe with full history; sub-hour intervals back-fill on demand.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Symbol, or comma-separated list (up to 50) for a bulk response keyed by symbol."
},
"interval": {
"type": "string",
"description": "Bar interval.",
"enum": [
"1s",
"1m",
"5m",
"15m",
"30m",
"1h",
"1d"
]
},
"limit": {
"type": "integer",
"description": "Most-recent N bars. Default 100."
},
"before": {
"type": "string",
"description": "Return the N bars ending strictly before this date/timestamp (YYYY-MM-DD or epoch-ms) — back-paging."
},
"from": {
"type": "string",
"description": "Window start (inclusive), YYYY-MM-DD / ISO / epoch-ms. Combines with `to`; page inside the window with `cursor`. Mutually exclusive with `asof` and `before` (400)."
},
"to": {
"type": "string",
"description": "Window end (inclusive; a bare YYYY-MM-DD means through the end of that day). Mutually exclusive with `asof` and `before` (400)."
},
"asof": {
"type": "string",
"description": "Return a single bar as of this date/timestamp (point-in-time)."
},
"cursor": {
"type": "string",
"description": "Continuation token from a prior response's `next_cursor` (sugar for `before`, and the way to page inside a from/to window)."
}
},
"required": [
"ticker",
"interval"
]
}
},
{
"name": "tickerbot_get_ticker_holdings",
"description": "Get an ETF's constituent holdings and their weights, heaviest first. When the ticker is not an ETF, `is_etf` is false and `holdings` is empty; `is_etf: true` with zero holdings means a real ETF whose holdings aren't ingested yet. (The reverse lookup \"which ETFs hold NVDA\" is a scan filter on the `etf_holders` column, not this tool.)",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "ETF symbol."
},
"limit": {
"type": "integer",
"description": "Max holdings returned. Max 5000. Default 500. When the cap cuts the list, the response sets `truncated: true` and `total` (the full holding count)."
}
},
"required": [
"ticker"
]
}
},
{
"name": "tickerbot_get_ticker_sectors",
"description": "Get an ETF's sector allocation (sector weights, heaviest first). When the ticker is not an ETF, `is_etf` is false and `sectors` is empty; `is_etf: true` with zero sectors means a real ETF whose sector data isn't ingested yet.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "ETF symbol."
}
},
"required": [
"ticker"
]
}
},
{
"name": "tickerbot_subscribe_ticker",
"description": "Register a webhook that fires when one ticker matches a condition. `condition` is a SQL WHERE-clause fragment scoped to that ticker (e.g. \"rsi_14 > 70 AND relative_volume > 2\"). Pass `target_url` for an https POST, or `channel:\"discord\"` + `discord_url` to post to Discord; omit for in-app.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Symbol."
},
"condition": {
"type": "string",
"description": "SQL WHERE fragment evaluated for this ticker."
},
"name": {
"type": "string",
"description": "Human-readable label."
},
"target_url": {
"type": "string",
"description": "Optional https URL for the `webhook` channel; omit for in-app."
},
"channel": {
"type": "string",
"description": "Delivery channel: `webhook` (POST to target_url), `discord` (embed to discord_url), `mobile_push` (to a registered device), or `in_app` (dashboard only). Inferred from the URL you pass if omitted.",
"enum": [
"webhook",
"discord",
"in_app",
"mobile_push"
]
},
"discord_url": {
"type": "string",
"description": "Discord incoming-webhook URL (https://discord.com/api/webhooks/…). Required when channel is \"discord\"."
},
"device_id": {
"type": "string",
"description": "Registered device id from the mobile app. Required when channel is \"mobile_push\"."
},
"cadence": {
"type": "string",
"description": "Evaluation cadence. Default realtime; hourly/nyse_open throttle. (`1m` accepted as a deprecated alias of realtime.)",
"enum": [
"realtime",
"hourly",
"nyse_open"
]
}
},
"required": [
"ticker",
"condition"
]
}
},
{
"name": "tickerbot_get_series",
"description": "THE series primitive: cross-ticker, multi-column time series on one aligned grid. Pick up to 25 columns (price, OHLCV, indicators like rsi_14, boolean flags, custom signals) and up to 50 tickers; get one flat row per ticker per interval step ({ticker, t, price, rsi_14, …}), cursor-paged backward. `transitions_only=true` with boolean columns returns only the rows where a flag CHANGED — \"every golden_cross flip this year\" in one call. All-time on every plan. Replaces looping asof snapshots per date, and replaces the sunset per-ticker history routes.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Single symbol (alias of `tickers`, wins when both are set). One of ticker/tickers is required."
},
"tickers": {
"type": "string",
"description": "Comma-separated symbols, max 50, all sharing one time grid. One of ticker/tickers is required."
},
"columns": {
"type": "string",
"description": "Comma list of columns (max 25). `fields` is a permanent alias. Defaults to a small set intersected with the interval's schema (intraday tiers carry fewer columns than daily — e.g. market_cap is daily-only)."
},
"interval": {
"type": "string",
"description": "Grid granularity. `1w` weekly, `1q` fiscal-quarterly (fundamentals).",
"enum": [
"1m",
"1h",
"1d",
"1w",
"1q"
]
},
"from": {
"type": "string",
"description": "Earliest timestamp (inclusive), YYYY-MM-DD or ISO."
},
"to": {
"type": "string",
"description": "Latest timestamp (inclusive; a bare date means through that day)."
},
"transitions_only": {
"type": "boolean",
"description": "Only rows where a boolean column changed value (requires at least one boolean column). Each row carries `transition_drivers` naming the flags that flipped."
},
"limit": {
"type": "integer",
"description": "Rows per page. Max 1000. Default 252."
},
"cursor": {
"type": "string",
"description": "Opaque cursor from a prior response — pages older."
}
},
"required": []
}
},
{
"name": "tickerbot_list_signals_catalog",
"description": "List the unified signal catalog: every built-in column on the schema (`kind: builtin`) plus the caller's custom signals (`kind: expression`). Built-in rows carry the audited spec metadata — description, category, update cadence, ticker coverage (`ticker_scope`), history depth (`history`/`history_since`), queryable resolutions, and asset classes. Use to discover what `q=` clauses and signal names are available before composing a scan.",
"input_schema": {
"type": "object",
"properties": {
"kind": {
"type": "string",
"description": "Filter by kind. Omit for both. (`custom` is accepted as a legacy alias of `expression`.)",
"enum": [
"builtin",
"expression"
]
},
"limit": {
"type": "integer",
"description": "Page size for custom slice. Max 200. Default 50."
},
"cursor": {
"type": "string",
"description": "Opaque cursor."
}
}
}
},
{
"name": "tickerbot_get_signals_match",
"description": "Find tickers that match a single signal right now (or at a past moment with `asof`). Booleans need no condition. Numerics need a `condition` like \">70\" or \"<=200\". Sorted by signal value desc for numerics.",
"input_schema": {
"type": "object",
"properties": {
"signal": {
"type": "string",
"description": "Column name on ticker (e.g. golden_cross_today, rsi_14, market_cap)."
},
"condition": {
"type": "string",
"description": "Required for numerics. Single bound: <op><value>, ops in (>, >=, =, !=, <, <=)."
},
"asof": {
"type": "string",
"description": "Optional YYYY-MM-DD or ISO timestamp. Date-only matches daily state; a timestamp matches the finest intraday state covering the query."
},
"universe": {
"type": "string",
"description": "Optional universe slug."
},
"sort_by": {
"type": "string",
"description": "Row order: `default` (alphabetic for booleans, highest-value-first for numerics) or `market_cap` (desc, adds market_cap to each row).",
"enum": [
"default",
"market_cap"
]
},
"include_active_since": {
"type": "boolean",
"description": "Built-in booleans only: adds `active_since` per row — when the flag last flipped true (from the spans archive)."
},
"limit": {
"type": "integer",
"description": "Page size. Max 200. Default 50."
},
"cursor": {
"type": "string",
"description": "Opaque cursor."
}
},
"required": [
"signal"
]
}
},
{
"name": "tickerbot_list_signal_events",
"description": "Occurrence SPANS of a boolean signal for one ticker, newest-first. For STATE flags (above_sma_50, in_uptrend) each row is a true-WINDOW: started_at when it flipped true, ended_at when it flipped back (null while still true), with prices at both ends. For EVENT flags (golden_cross, gap_up) each row is a point firing (started_at = ended_at). \"Golden crosses in June\" is from=2026-06-01&to=2026-06-30. Built-in booleans only — numerics and custom signals have no precomputed spans (use tickerbot_get_series).",
"input_schema": {
"type": "object",
"properties": {
"signal": {
"type": "string",
"description": "Built-in boolean flag name."
},
"ticker": {
"type": "string",
"description": "Symbol."
},
"from": {
"type": "string",
"description": "Window start (inclusive) on each span's started_at — YYYY-MM-DD, ISO, or epoch-ms."
},
"to": {
"type": "string",
"description": "Window end (inclusive) on started_at; a bare YYYY-MM-DD means through the end of that day."
},
"merge_gap_seconds": {
"type": "integer",
"description": "Interval-union: contiguous windows whose gap is ≤ N seconds collapse into one — de-fragments flags with thousands of per-tick rows (e.g. 3600 for daily flags). Default 0 = no merge. A cursor pins this; resend it unchanged when paging."
},
"limit": {
"type": "integer",
"description": "Page size. Max 1000, newest first."
},
"cursor": {
"type": "string",
"description": "Opaque cursor — pages older, inside the from/to window if one is set."
}
},
"required": [
"signal",
"ticker"
]
}
},
{
"name": "tickerbot_create_custom_signal",
"description": "Save a SQL WHERE expression as a named custom signal the caller can reference by name in future scans.",
"input_schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Snake_case identifier."
},
"expr": {
"type": "string",
"description": "SQL WHERE expression. Same grammar as scan `q`."
},
"description": {
"type": "string",
"description": "Optional human description."
}
},
"required": [
"name",
"expr"
]
}
},
{
"name": "tickerbot_update_custom_signal",
"description": "Edit one of the caller's custom signals — supply `expr`, `description`, `new_name`, or any combination. Providing `expr` recompiles it against the live column whitelist. Renaming is refused (409) while other custom signals reference the current name. Built-in signals are read-only; only custom signals the caller owns can be patched. Available on every plan.",
"input_schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "CURRENT slug — identifies which signal to edit."
},
"new_name": {
"type": "string",
"description": "Rename the signal to this slug (snake_case, must not collide with a built-in column or another of your signals)."
},
"expr": {
"type": "string",
"description": "New SQL WHERE expression. Re-validated on save."
},
"description": {
"type": "string",
"description": "New description."
}
},
"required": [
"name"
]
}
},
{
"name": "tickerbot_delete_custom_signal",
"description": "Delete one of the caller's custom signals. Cascade-safe by default: refused with 409 if another custom signal references it (the error lists the referencing signals). Pass `force: true` to delete anyway — existing references will break on next recompile. Available on every plan.",
"input_schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Custom signal slug."
},
"force": {
"type": "boolean",
"description": "When true, skip the reference check and delete anyway. Default false."
}
},
"required": [
"name"
]
}
},
{
"name": "tickerbot_subscribe_signal",
"description": "Register a webhook that fires when a signal turns true (booleans) or its value crosses a condition (numerics). Optional `ticker` restricts to one symbol; omit to watch the whole universe. Pass `target_url` for an https POST, or `channel:\"discord\"` + `discord_url` to post to Discord; omit for in-app.",
"input_schema": {
"type": "object",
"properties": {
"signal": {
"type": "string",
"description": "Column name (e.g. golden_cross_today, rsi_14)."
},
"ticker": {
"type": "string",
"description": "Optional ticker to restrict the watch to one symbol."
},
"universe": {
"type": "string",
"description": "Optional universe slug."
},
"condition": {
"type": "string",
"description": "Required for numerics: single bound like \">70\" or \"<=200\". Ignored for booleans."
},
"name": {
"type": "string",
"description": "Human-readable label."
},
"target_url": {
"type": "string",
"description": "Optional https URL for the `webhook` channel; omit for in-app."
},
"channel": {
"type": "string",
"description": "Delivery channel: `webhook` (POST to target_url), `discord` (embed to discord_url), `mobile_push` (to a registered device), or `in_app` (dashboard only). Inferred from the URL you pass if omitted.",
"enum": [
"webhook",
"discord",
"in_app",
"mobile_push"
]
},
"discord_url": {
"type": "string",
"description": "Discord incoming-webhook URL (https://discord.com/api/webhooks/…). Required when channel is \"discord\"."
},
"device_id": {
"type": "string",
"description": "Registered device id from the mobile app. Required when channel is \"mobile_push\"."
},
"cadence": {
"type": "string",
"description": "Evaluation cadence. Default realtime; hourly/nyse_open throttle. (`1m` accepted as a deprecated alias of realtime.)",
"enum": [
"realtime",
"hourly",
"nyse_open"
]
}
},
"required": [
"signal"
]
}
},
{
"name": "tickerbot_list_events",
"description": "THE events primitive — one timeline over every event log, cross-ticker: dividends, splits, insider transactions, and analyst actions (\"all splits this month\", \"every analyst action in my universe this week\", \"AAPL's full corporate history\" via `ticker`), plus two opt-in kinds that join only when named: `signal` (boolean-flag firings) and `news`. Rows are { ticker, ts, kind, payload }, newest first. Analyst payloads (history to 2012) carry firm/analyst/action/rating/price_target; filter them with `firm=`/`action=` (structured, case-insensitive — a `q` payload match is case-SENSITIVE) — actions: upgrades, downgrades, initiates_coverage_on, maintains, reiterates, assumes, reinstates, suspends, terminates_coverage_on. Use `q` for other payload conditions: it speaks the SQL grammar over exactly (ticker, ts, kind, payload jsonb); `group_by`/`select`/`having` roll the stream up (aggregates return `truncated: true` instead of paginating); `join=state` attaches the ticker's state as of each event. Requires at least one bound: a ticker scope (ticker/tickers/universe), a time window (from/to), or firm/action — q alone is not a bound.",
"input_schema": {
"type": "object",
"properties": {
"kind": {
"type": "string",
"description": "Comma list of kinds to include. Default is the four corporate kinds: dividend, split, insider, analyst. Two more are opt-in and join only when named: `signal` (boolean-flag firings) and `news`."
},
"signal": {
"type": "string",
"description": "kind=signal only. One built-in boolean flag (e.g. golden_cross). REQUIRED to use `q` or `join` on kind=signal — naming the signal is what keeps the query on an index; optional otherwise."
},
"transition": {
"type": "string",
"description": "kind=signal only. `enter` (false->true) or `exit` (true->false). Always optional — an ordinary filter.",
"enum": [
"enter",
"exit"
]
},
"firm": {
"type": "string",
"description": "kind=analyst only. Case-insensitive analyst-firm filter (e.g. \"Goldman Sachs\" matches \"goldman sachs\") — prefer this over a q payload match, which is case-sensitive."
},
"action": {
"type": "string",
"description": "kind=analyst only. Exact rating-action filter.",
"enum": [
"upgrades",
"downgrades",
"initiates_coverage_on",
"maintains",
"reiterates",
"assumes",
"reinstates",
"suspends",
"terminates_coverage_on"
]
},
"ticker": {
"type": "string",
"description": "Single-ticker filter, e.g. AAPL."
},
"tickers": {
"type": "string",
"description": "Comma-separated tickers, max 50. Mutually exclusive with `universe`."
},
"universe": {
"type": "string",
"description": "Universe slug (top_10, top_100, or a saved one) to scope the stream. Mutually exclusive with `tickers`."
},
"from": {
"type": "string",
"description": "Events at or after this ISO date/datetime (inclusive; a bare YYYY-MM-DD means from the start of that day). `since` accepted as alias."
},
"to": {
"type": "string",
"description": "Window end: a bare YYYY-MM-DD means through the END of that day (matching bars/series/spans); a timestamp is exclusive — events strictly before it. `until` accepted as alias."
},
"join": {
"type": "string",
"description": "join=state attaches each event's ticker STATE as of that event's moment (the replay join) under a `state` key — \"downgrades where rsi_14 was already under 40\" composes with q. Free on every plan.",
"enum": [
"state"
]
},
"interval": {
"type": "string",
"description": "Grain for join=state replay (finest covering tier by default).",
"enum": [
"1m",
"1h",
"1d"
]
},
"q": {
"type": "string",
"description": "SQL WHERE over (ticker, ts, kind, payload jsonb) — ONLY those four identifiers. Payload fields via jsonb operators: payload->>'firm' = 'Goldman Sachs', (payload->>'shares')::numeric > 1e6. On kind=signal this requires `signal` (the firing log is ~175M rows); kind=signal takes no group_by."
},
"select": {
"type": "string",
"description": "Aggregate-mode output columns (requires group_by). Default: group keys + COUNT(*) AS events."
},
"group_by": {
"type": "string",
"description": "Comma list of rollup keys — switches to aggregate rows, e.g. payload->>'firm' AS firm, or kind. `AS` names the JSON key; an un-named payload read is keyed by its payload key (payload->>'firm' -> firm)."
},
"having": {
"type": "string",
"description": "Post-aggregation filter (requires group_by), e.g. COUNT(*) > 5."
},
"order": {
"type": "string",
"description": "Aggregate-mode sort column/alias. Default: events."
},
"dir": {
"type": "string",
"description": "Aggregate-mode sort direction.",
"enum": [
"asc",
"desc"
]
},
"limit": {
"type": "integer",
"description": "Page size. Max 1000."
},
"cursor": {
"type": "string",
"description": "Opaque cursor from a prior response — carries the original filters, pass it alone (long q values must be resent alongside it)."
}
}
}
},
{
"name": "tickerbot_subscribe_events",
"description": "Create an event-trigger webhook: fires when NEW events land — dividends, splits, insider filings, analyst actions (\"every split in my universe\", \"Goldman downgrades on large caps\"). TWO composable filters: `q` filters the event's TICKER STATE (market_cap > 1e10); `event_q` filters the EVENT CONTENT in the /v2/events grammar (payload->>'firm' = 'Goldman Sachs'). Paid plans (webhook slots). Deliveries carry event: \"events.fired\" with an events array. Latency = ingest cadence: analyst ≤1h, corporate kinds daily — NOT sub-minute like state webhooks.",
"input_schema": {
"type": "object",
"properties": {
"kinds": {
"type": "string",
"description": "Comma list of kinds to fire on — any of: dividend, split, insider, analyst (e.g. \"split,analyst\"). NOTE: no enum here on purpose — a scalar enum would reject multi-kind values."
},
"tickers": {
"type": "string",
"description": "Scope to specific tickers (comma list, max 50). Mutually exclusive with universe; omit both for all tickers."
},
"universe": {
"type": "string",
"description": "Scope to a universe slug (top_10, top_100, or a saved one)."
},
"q": {
"type": "string",
"description": "Optional row-STATE filter evaluated against the event's ticker at fire time, e.g. market_cap > 1e10."
},
"event_q": {
"type": "string",
"description": "Optional event-CONTENT filter over (ticker, ts, kind, payload jsonb) — only those four identifiers, e.g. payload->>'firm' = 'Goldman Sachs' AND payload->>'action' = 'downgrades'."
},
"target_url": {
"type": "string",
"description": "HTTPS delivery URL. Omit for in-app delivery."
},
"channel": {
"type": "string",
"description": "Delivery channel.",
"enum": [
"webhook",
"discord",
"in_app",
"mobile_push"
]
},
"discord_url": {
"type": "string",
"description": "Discord incoming-webhook URL (channel discord)."
},
"device_id": {
"type": "string",
"description": "Registered device id from the mobile app (channel mobile_push)."
},
"name": {
"type": "string",
"description": "Display name."
}
},
"required": [
"kinds"
]
}
},
{
"name": "tickerbot_scan",
"description": "Run a SQL WHERE clause against the live ticker universe (or against a past moment with `asof` — unlimited depth on every plan). Returns matching tickers sorted by chosen column, OR — with `group_by` — aggregate rollups instead of rows (breadth stats: \"count of tickers above their 200dma by sector\", \"median RSI by sector on 2026-03-03\"). The `q` grammar is a flat WHERE: column names from the schema, AND/OR/NOT, comparison operators, numeric/string literals. No JOIN or subqueries. Example: `gap_up AND market_cap < 2000000000 AND NOT earnings_this_week`.",
"input_schema": {
"type": "object",
"properties": {
"q": {
"type": "string",
"description": "SQL WHERE expression. Max 4000 chars."
},
"universe": {
"type": "string",
"description": "Optional universe slug."
},
"asof": {
"type": "string",
"description": "Optional YYYY-MM-DD or ISO timestamp for a historical scan. Unlimited depth on every plan."
},
"order": {
"type": "string",
"description": "Sort column. Default day_change_pct (rows) / tickers (aggregate)."
},
"dir": {
"type": "string",
"description": "Sort direction.",
"enum": [
"asc",
"desc"
]
},
"fields": {
"type": "string",
"description": "Comma-separated extra columns to include (row mode only)."
},
"group_by": {
"type": "string",
"description": "AGGREGATE MODE: 1–6 comma-separated group keys (columns or expressions, e.g. `sector`). Results become rollup rows instead of tickers. Alias a key with `AS` to name its JSON key; un-named expressions are named for you."
},
"select": {
"type": "string",
"description": "Aggregate output items (requires group_by). Default: group keys + COUNT(*) AS tickers. Aggregates: count/avg/sum/min/max/stddev/string_agg + FILTER (WHERE …). Alias items with AS. Example: `sector, COUNT(*) AS n, AVG(rsi_14) AS avg_rsi`."
},
"having": {
"type": "string",
"description": "Aggregate filter (requires group_by). Example: `COUNT(*) >= 10`."
},
"limit": {
"type": "integer",
"description": "Page size. Max 100. Default 50. Aggregate mode does not paginate — response sets `truncated: true` when groups were cut."
},
"cursor": {
"type": "string",
"description": "Opaque cursor (row mode only)."
},
"full": {
"type": "boolean",
"description": "Row mode: return the FULL wide row for each match (every column) instead of the slim default projection."
}
},
"required": [
"q"
]
}
},
{
"name": "tickerbot_subscribe_scan",
"description": "Register a webhook that fires when matches for a scan query change. Pass `target_url` for an https POST, or `channel:\"discord\"` + `discord_url` to post an embed to Discord; omit for in-app delivery in the dashboard. `cadence` is real-time (1m) by default; throttle to hourly or nyse_open. Use to satisfy \"alert me when this happens\" prompts.",
"input_schema": {
"type": "object",
"properties": {
"q": {
"type": "string",
"description": "SQL WHERE expression — same grammar as scan."
},
"name": {
"type": "string",
"description": "Human-readable label. Defaults to a truncated version of the query."
},
"universe": {
"type": "string",
"description": "Optional universe slug to scope the watch."
},
"target_url": {
"type": "string",
"description": "Optional https URL to POST matches to (the `webhook` channel). Omit for in-app delivery."
},
"channel": {
"type": "string",
"description": "Delivery channel: `webhook` (POST to target_url), `discord` (embed to discord_url), `mobile_push` (to a registered device), or `in_app` (dashboard only). Inferred from the URL you pass if omitted.",
"enum": [
"webhook",
"discord",
"in_app",
"mobile_push"
]
},
"discord_url": {
"type": "string",
"description": "Discord incoming-webhook URL (https://discord.com/api/webhooks/…). Required when channel is \"discord\"."
},
"device_id": {
"type": "string",
"description": "Registered device id from the mobile app. Required when channel is \"mobile_push\"."
},
"cadence": {
"type": "string",
"description": "Evaluation cadence. Default realtime; hourly/nyse_open throttle. (`1m` accepted as a deprecated alias of realtime.)",
"enum": [
"realtime",
"hourly",
"nyse_open"
]
}
},
"required": [
"q"
]
}
},
{
"name": "tickerbot_search_news",
"description": "Search the news archive (back to 2015) with a SQL WHERE clause. Available on every plan. Columns on news_article include `time_published`, `title`, `summary`, `source`, `source_domain`, `category`, `authors`, `topics`, `tickers` (array), `overall_sentiment_score`, `overall_sentiment_label`, `url`. To filter to one ticker use `'NVDA' = ANY(tickers)` or the auto-unnest alias `tk = 'NVDA'`. Example: `q=tk='NVDA' AND time_published >= NOW() - INTERVAL '1 day'`. Supports group_by + having for aggregation (e.g. count of articles per day).",
"input_schema": {
"type": "object",
"properties": {
"q": {
"type": "string",
"description": "SQL WHERE on news_article. Optional when search or a scoping param is present."
},
"search": {
"type": "string",
"description": "Full-text search over title+summary (websearch grammar: \"apple earnings\", quoted phrases, OR, -negation). ANDs with q and the scoping params."
},
"select": {
"type": "string",
"description": "Comma-separated columns to include. Defaults to a slim set."
},
"group_by": {
"type": "string",
"description": "Comma-separated columns for aggregation. Alias a key with `AS` to name its JSON key; un-named expressions are named for you."
},
"having": {
"type": "string",
"description": "WHERE-style filter on aggregates. Requires group_by."
},
"order": {
"type": "string",
"description": "Sort column or SELECT alias. Default time_published (non-aggregate) or volume (aggregate)."
},
"dir": {
"type": "string",
"description": "Sort direction.",
"enum": [
"asc",
"desc"
]
},
"limit": {
"type": "integer",
"description": "Page size."
},
"cursor": {
"type": "string",
"description": "Opaque cursor."
}
},
"required": [
"q"
]
}
},
{
"name": "tickerbot_create_webhook",
"description": "The canonical webhook create — POST /v2/webhooks with an explicit `trigger` object: { type: \"scan\" | \"ticker\" | \"signal\" | \"event\", … } plus delivery fields. The subscribe tools above are flat sugar over exactly this; use this form when composing the trigger programmatically or when a sugar door doesn't fit. Trigger shapes: scan {type:\"scan\", q, universe?}; ticker {type:\"ticker\", ticker, condition}; signal {type:\"signal\", signal, ticker?, universe?, condition?}; event {type:\"event\", kinds, tickers?, universe?, event_q?}.",
"input_schema": {
"type": "object",
"properties": {
"trigger": {
"type": "object",
"description": "What fires the webhook: { type: \"scan\" | \"ticker\" | \"signal\" | \"event\", … } — see the tool description for each shape."
},
"name": {
"type": "string",
"description": "Display name. Defaults from the trigger."
},
"target_url": {
"type": "string",
"description": "HTTPS delivery URL (`webhook` channel). Omit for in-app."
},
"channel": {
"type": "string",
"description": "Delivery channel. Inferred from the URL you pass if omitted.",
"enum": [
"webhook",
"discord",
"in_app",
"mobile_push"
]
},
"discord_url": {
"type": "string",
"description": "Discord incoming-webhook URL (channel discord)."
},
"device_id": {
"type": "string",
"description": "Registered device id from the mobile app (channel mobile_push)."
},
"cadence": {
"type": "string",
"description": "Evaluation cadence. Default realtime. (`1m` accepted as a deprecated alias of realtime.)",
"enum": [
"realtime",
"hourly",
"nyse_open"
]
},
"columns": {
"type": "string",
"description": "Comma list of extra columns to include with each delivered match (`fields` accepted as alias)."
}
},
"required": [
"trigger"
]
}
},
{
"name": "tickerbot_patch_webhook",
"description": "Edit a webhook in place: `name`, `cadence`, `target_url`, `enabled` (pausing/resuming without losing match-state). The trigger (q) and channel are immutable by design — delete and re-create to change what fires or where it delivers. Unknown fields are a 400, never silently ignored.",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Webhook id (looks like `wh_…`)."
},
"name": {
"type": "string",
"description": "New display name."
},
"cadence": {
"type": "string",
"description": "New evaluation cadence.",
"enum": [
"realtime",
"hourly",
"nyse_open"
]
},
"target_url": {
"type": "string",
"description": "New HTTPS delivery URL."
},
"enabled": {
"type": "boolean",
"description": "false pauses deliveries; true resumes. (Re-enabling after auto-disable also works via tickerbot_enable_webhook, which additionally clears match-state.)"
}
},
"required": [
"id"
]
}
},
{
"name": "tickerbot_list_webhooks",
"description": "List the caller's webhook subscriptions (rules created via the subscribe tools), newest-first. Use `status` to filter to active or disabled rules.",
"input_schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Filter by status: `active` or `disabled`. Omit for all.",
"enum": [
"active",
"disabled"
]
},
"limit": {
"type": "integer",
"description": "Page size. Max 100. Default 50."
},
"cursor": {
"type": "string",
"description": "Opaque cursor."
}
}
}
},
{
"name": "tickerbot_get_webhook",
"description": "Fetch one webhook subscription by id (current state, match-set, schedule). Account-scoped: any key on the account can read any of the account's webhooks.",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Webhook id (looks like `wh_…`)."
}
},
"required": [
"id"
]
}
},
{
"name": "tickerbot_list_webhook_deliveries",
"description": "List recent deliveries (pings and fires) for a webhook, newest-first — for diagnosing failures. Returns metadata only (status, attempt, response code, error); the POST body is not stored.",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Webhook id."
},
"status": {
"type": "string",
"description": "Filter by delivery status.",
"enum": [
"pending",
"delivered",
"permanent_failure"
]
},
"from": {
"type": "string",
"description": "Only deliveries created at/after this moment — epoch seconds, epoch milliseconds (13+ digits), or ISO datetime. (90-day retention on every plan.)"
},
"to": {
"type": "string",
"description": "Only deliveries created at/before this moment — same value grammar as `from`; a date-only value means through the end of that UTC day."
},
"limit": {
"type": "integer",
"description": "Page size. Max 100. Default 50."
},
"cursor": {
"type": "string",
"description": "Opaque cursor."
}
},
"required": [
"id"
]
}
},
{
"name": "tickerbot_test_webhook",
"description": "Send a real-shape `webhook.fired` POST to the webhook's target_url synchronously, right now. The body is byte-identical to a real fire (same signing); the test marker rides in an `X-Tickerbot-Test: true` header. Returns the inline outcome (`delivered`, `http_status`, `elapsed_ms`, `error`). One-shot — a failed test never retries and never auto-disables the webhook. Fails with 400 if the webhook has no target_url (in-app deliveries have nothing to fire over the wire).",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Webhook id."
}
},
"required": [
"id"
]
}
},
{
"name": "tickerbot_enable_webhook",
"description": "Re-enable a disabled webhook — flips it back to `active` and clears its match-state so the next eval treats every currently-matching ticker as new. Use after fixing whatever caused auto-disable. No-op on an already-active webhook.",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Webhook id."
}
},
"required": [
"id"
]
}
},
{
"name": "tickerbot_delete_webhook",
"description": "Delete a webhook subscription by id. Use after listing webhooks when the user wants to remove an alert.",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Webhook id (looks like `wh_…`)."
}
},
"required": [
"id"
]
}
},
{
"name": "tickerbot_list_universes",
"description": "List universes — your saved ones and/or the built-in system universes (top_10, top_100). Filter with `owner` (like the signals catalog's `kind`): `me` (default, your own), `system` (built-ins), or `all` (both). Each row carries `system: true|false`.",
"input_schema": {
"type": "object",
"properties": {
"owner": {
"type": "string",
"description": "Which universes to list.",
"enum": [
"me",
"system",
"all"
]
},
"limit": {
"type": "integer",
"description": "Page size (applies to your own; system universes are a small fixed set returned in full on the first page)."
},
"cursor": {
"type": "string",
"description": "Opaque cursor."
}
}
}
},
{
"name": "tickerbot_list_system_universes",
"description": "List the built-in system universes (`top_10`, `top_100` — the most-actively-traded tickers by 30-day trailing dollar volume, rebalanced monthly). Available to every account regardless of plan. Use these slugs as `universe` in scans/signals or `universe_id` when subscribing.",
"input_schema": {
"type": "object",
"properties": {}
}
},
{
"name": "tickerbot_get_universe",
"description": "Get one universe by slug, including its ticker list.",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Universe slug."
}
},
"required": [
"id"
]
}
},
{
"name": "tickerbot_create_universe",
"description": "Create a new universe (named set of tickers) for scoping future scans.",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Optional slug (lowercase letters, digits, underscore). Auto-generated from name if omitted. Must be unique within the account."
},
"name": {
"type": "string",
"description": "Human-readable name."
},
"description": {
"type": "string",
"description": "Optional free-form notes."
},
"tickers": {
"type": "array",
"description": "List of ticker symbols.",
"items": {
"type": "string"
}
}
},
"required": [
"name",
"tickers"
]
}
},
{
"name": "tickerbot_update_universe",
"description": "Update one of the caller's universes. Pass `name`/`description` to relabel, `tickers` to replace the whole list, or `add`/`remove` to adjust subsets without replacing. System universes (`top_10`/`top_100`) cannot be edited.",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Universe slug."
},
"name": {
"type": "string",
"description": "New label."
},
"description": {
"type": "string",
"description": "New notes."
},
"tickers": {
"type": "array",
"description": "Replace the full ticker list.",
"items": {
"type": "string"
}
},
"add": {
"type": "array",
"description": "Add these tickers (deduplicated).",
"items": {
"type": "string"
}
},
"remove": {
"type": "array",
"description": "Remove these tickers.",
"items": {
"type": "string"
}
}
},
"required": [
"id"
]
}
},
{
"name": "tickerbot_delete_universe",
"description": "Delete one of the caller's universes. System universes (`top_10`/`top_100`) cannot be deleted. Webhooks that reference the deleted universe will fail on their next eval, so clean those up first.",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Universe slug."
}
},
"required": [
"id"
]
}
}
]Or: fetch the catalog at runtime
Don't embed the JSON above — pull the live catalog from the server, stays current as we add tools.
Tickerbot publishes the canonical tool catalog as a public, unauthenticated discovery endpoint. GET it once at agent startup and your runtime always reflects the latest surface — same source the @tickerbot/mcp-server npm package reads on subprocess spawn.
curl "https://api.tickerbot.io/mcp/tools"Response shape: { asOf, serverInfo, protocolVersion, tools }. The tools array is exactly what tools/list returns over MCP, including the dispatch metadata (endpoint.method, endpoint.path, endpoint.paramLocation) so your runtime can route tool calls to the right HTTP request without hard-coding the mapping.
When a new tool ships on Tickerbot, your agent picks it up on the next startup — no embedded JSON to update, no republish, no PR. Recommended over pasting the static blob above for anything beyond throwaway demos.
Per-tool reference
One block per tool, indexed by name.
tickerbot_list_tickers
GET /v2/tickers
{
"name": "tickerbot_list_tickers",
"description": "List active tickers from the Tickerbot universe (~14,500 US equities, plus the US Treasury curve and Fed policy rates under `R:`, and major FX pairs, spot metals and crypto under `X:`). Use `tickers` for bulk lookup of named symbols (returns full rows); otherwise walks the universe alphabetically with `cursor` pagination. Supports filters: search, asset_type, exchange, sector, min_market_cap.",
"input_schema": {
"type": "object",
"properties": {
"tickers": {
"type": "string",
"description": "Comma-separated symbols (max 50). When set, returns full rows for these symbols and pagination params are ignored."
},
"limit": {
"type": "integer",
"description": "Page size. Max 1000. Default 50."
},
"cursor": {
"type": "string",
"description": "Opaque cursor from a prior response."
},
"search": {
"type": "string",
"description": "Case-insensitive substring filter on ticker/name. Orders results by market_cap desc."
},
"asset_type": {
"type": "string",
"description": "Filter by INSTRUMENT TYPE within equities: the stored value (CS, ETF, ADRC, PFD, FUND, UNIT, SP, ETS, WARRANT, RIGHT, ETN, ETV), or \"equity\" for the equity-like set. This is not an asset class — `asset_type=crypto` is rejected; crypto is in the main list under its X: symbols."
},
"exchange": {
"type": "string",
"description": "Filter by exchange (e.g. \"XNYS\", \"XNAS\", \"BATS\")."
},
"sector": {
"type": "string",
"description": "Exact-match sector filter (e.g. \"Technology\")."
},
"min_market_cap": {
"type": "number",
"description": "Minimum market cap in USD. Orders results by market_cap desc."
},
"universe": {
"type": "string",
"description": "Restrict to a universe slug (top_10, top_100, or a saved one)."
},
"asset_class": {
"type": "string",
"description": "Filter by asset class — `stocks`, `rates`, `crypto`, `fx`, or a comma-separated list. Omit for every class. Distinct from asset_type (the instrument type within equities)."
}
}
}
}tickerbot_get_ticker
GET /v2/tickers/{ticker}
{
"name": "tickerbot_get_ticker",
"description": "Get the full current row for one ticker — every column on the schema (price, change, indicators like rsi_14, every boolean flag like above_sma_50, fundamentals like pe_ratio). Pass `asof` (YYYY-MM-DD or ISO timestamp like 2026-07-20T15:30:00Z) for the row as it stood at that past moment — date-only gives the close of that day.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Symbol. Case-insensitive. Equities: bare symbol (AAPL). Crypto: X-prefixed pair (X:BTCUSD) — bare BTC/ETH are US-listed ETFs, not spot crypto."
},
"asof": {
"type": "string",
"description": "Optional YYYY-MM-DD or ISO timestamp. Date-only returns the row at close of that day; a timestamp returns the row as of that moment (finest tier covering each column)."
}
},
"required": [
"ticker"
]
}
}tickerbot_get_ticker_history
GET /v2/tickers/{ticker}
{
"name": "tickerbot_get_ticker_history",
"description": "Time-travel SNAPSHOT: the full wide row for one ticker as it stood at a past date (one row, not a series — for a multi-column time SERIES use tickerbot_get_series). Returns indicators, boolean flags, and the most-recent fundamentals known on that date. Unlimited depth on every plan.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Symbol."
},
"asof": {
"type": "string",
"description": "Target date as YYYY-MM-DD or full ISO timestamp."
}
},
"required": [
"ticker",
"asof"
]
}
}tickerbot_get_ticker_coverage
GET /v2/tickers/{ticker}/coverage
{
"name": "tickerbot_get_ticker_coverage",
"description": "Data-coverage report for one ticker — which intervals and columns exist, from when, at what depth. The honesty endpoint: ask this before assuming a gap in bars/series is a data outage vs. genuinely-never-covered (e.g. sub-hour bars outside the active universe, fundamentals on non-equities).",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Symbol."
}
},
"required": [
"ticker"
]
}
}tickerbot_get_ticker_bars
GET /v2/tickers/{ticker}/bars/{interval}
{
"name": "tickerbot_get_ticker_bars",
"description": "Get OHLCV (open/high/low/close/volume) bars for one or more symbols at a given interval, oldest-first. Pass a comma-separated `ticker` list (up to 50) for a bulk response keyed by symbol. Use `asof` for a single point-in-time bar, or `before`+`limit` to back-page. `1d`/`1h` cover the full universe with full history; sub-hour intervals back-fill on demand.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Symbol, or comma-separated list (up to 50) for a bulk response keyed by symbol."
},
"interval": {
"type": "string",
"description": "Bar interval.",
"enum": [
"1s",
"1m",
"5m",
"15m",
"30m",
"1h",
"1d"
]
},
"limit": {
"type": "integer",
"description": "Most-recent N bars. Default 100."
},
"before": {
"type": "string",
"description": "Return the N bars ending strictly before this date/timestamp (YYYY-MM-DD or epoch-ms) — back-paging."
},
"from": {
"type": "string",
"description": "Window start (inclusive), YYYY-MM-DD / ISO / epoch-ms. Combines with `to`; page inside the window with `cursor`. Mutually exclusive with `asof` and `before` (400)."
},
"to": {
"type": "string",
"description": "Window end (inclusive; a bare YYYY-MM-DD means through the end of that day). Mutually exclusive with `asof` and `before` (400)."
},
"asof": {
"type": "string",
"description": "Return a single bar as of this date/timestamp (point-in-time)."
},
"cursor": {
"type": "string",
"description": "Continuation token from a prior response's `next_cursor` (sugar for `before`, and the way to page inside a from/to window)."
}
},
"required": [
"ticker",
"interval"
]
}
}tickerbot_get_ticker_holdings
GET /v2/tickers/{ticker}/holdings
{
"name": "tickerbot_get_ticker_holdings",
"description": "Get an ETF's constituent holdings and their weights, heaviest first. When the ticker is not an ETF, `is_etf` is false and `holdings` is empty; `is_etf: true` with zero holdings means a real ETF whose holdings aren't ingested yet. (The reverse lookup \"which ETFs hold NVDA\" is a scan filter on the `etf_holders` column, not this tool.)",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "ETF symbol."
},
"limit": {
"type": "integer",
"description": "Max holdings returned. Max 5000. Default 500. When the cap cuts the list, the response sets `truncated: true` and `total` (the full holding count)."
}
},
"required": [
"ticker"
]
}
}tickerbot_get_ticker_sectors
GET /v2/tickers/{ticker}/sectors
{
"name": "tickerbot_get_ticker_sectors",
"description": "Get an ETF's sector allocation (sector weights, heaviest first). When the ticker is not an ETF, `is_etf` is false and `sectors` is empty; `is_etf: true` with zero sectors means a real ETF whose sector data isn't ingested yet.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "ETF symbol."
}
},
"required": [
"ticker"
]
}
}tickerbot_subscribe_ticker
POST /v2/tickers/{ticker}/subscribe
{
"name": "tickerbot_subscribe_ticker",
"description": "Register a webhook that fires when one ticker matches a condition. `condition` is a SQL WHERE-clause fragment scoped to that ticker (e.g. \"rsi_14 > 70 AND relative_volume > 2\"). Pass `target_url` for an https POST, or `channel:\"discord\"` + `discord_url` to post to Discord; omit for in-app.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Symbol."
},
"condition": {
"type": "string",
"description": "SQL WHERE fragment evaluated for this ticker."
},
"name": {
"type": "string",
"description": "Human-readable label."
},
"target_url": {
"type": "string",
"description": "Optional https URL for the `webhook` channel; omit for in-app."
},
"channel": {
"type": "string",
"description": "Delivery channel: `webhook` (POST to target_url), `discord` (embed to discord_url), `mobile_push` (to a registered device), or `in_app` (dashboard only). Inferred from the URL you pass if omitted.",
"enum": [
"webhook",
"discord",
"in_app",
"mobile_push"
]
},
"discord_url": {
"type": "string",
"description": "Discord incoming-webhook URL (https://discord.com/api/webhooks/…). Required when channel is \"discord\"."
},
"device_id": {
"type": "string",
"description": "Registered device id from the mobile app. Required when channel is \"mobile_push\"."
},
"cadence": {
"type": "string",
"description": "Evaluation cadence. Default realtime; hourly/nyse_open throttle. (`1m` accepted as a deprecated alias of realtime.)",
"enum": [
"realtime",
"hourly",
"nyse_open"
]
}
},
"required": [
"ticker",
"condition"
]
}
}tickerbot_get_series
GET /v2/series
{
"name": "tickerbot_get_series",
"description": "THE series primitive: cross-ticker, multi-column time series on one aligned grid. Pick up to 25 columns (price, OHLCV, indicators like rsi_14, boolean flags, custom signals) and up to 50 tickers; get one flat row per ticker per interval step ({ticker, t, price, rsi_14, …}), cursor-paged backward. `transitions_only=true` with boolean columns returns only the rows where a flag CHANGED — \"every golden_cross flip this year\" in one call. All-time on every plan. Replaces looping asof snapshots per date, and replaces the sunset per-ticker history routes.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Single symbol (alias of `tickers`, wins when both are set). One of ticker/tickers is required."
},
"tickers": {
"type": "string",
"description": "Comma-separated symbols, max 50, all sharing one time grid. One of ticker/tickers is required."
},
"columns": {
"type": "string",
"description": "Comma list of columns (max 25). `fields` is a permanent alias. Defaults to a small set intersected with the interval's schema (intraday tiers carry fewer columns than daily — e.g. market_cap is daily-only)."
},
"interval": {
"type": "string",
"description": "Grid granularity. `1w` weekly, `1q` fiscal-quarterly (fundamentals).",
"enum": [
"1m",
"1h",
"1d",
"1w",
"1q"
]
},
"from": {
"type": "string",
"description": "Earliest timestamp (inclusive), YYYY-MM-DD or ISO."
},
"to": {
"type": "string",
"description": "Latest timestamp (inclusive; a bare date means through that day)."
},
"transitions_only": {
"type": "boolean",
"description": "Only rows where a boolean column changed value (requires at least one boolean column). Each row carries `transition_drivers` naming the flags that flipped."
},
"limit": {
"type": "integer",
"description": "Rows per page. Max 1000. Default 252."
},
"cursor": {
"type": "string",
"description": "Opaque cursor from a prior response — pages older."
}
},
"required": []
}
}tickerbot_list_signals_catalog
GET /v2/signals
{
"name": "tickerbot_list_signals_catalog",
"description": "List the unified signal catalog: every built-in column on the schema (`kind: builtin`) plus the caller's custom signals (`kind: expression`). Built-in rows carry the audited spec metadata — description, category, update cadence, ticker coverage (`ticker_scope`), history depth (`history`/`history_since`), queryable resolutions, and asset classes. Use to discover what `q=` clauses and signal names are available before composing a scan.",
"input_schema": {
"type": "object",
"properties": {
"kind": {
"type": "string",
"description": "Filter by kind. Omit for both. (`custom` is accepted as a legacy alias of `expression`.)",
"enum": [
"builtin",
"expression"
]
},
"limit": {
"type": "integer",
"description": "Page size for custom slice. Max 200. Default 50."
},
"cursor": {
"type": "string",
"description": "Opaque cursor."
}
}
}
}tickerbot_get_signals_match
GET /v2/signals/{signal}
{
"name": "tickerbot_get_signals_match",
"description": "Find tickers that match a single signal right now (or at a past moment with `asof`). Booleans need no condition. Numerics need a `condition` like \">70\" or \"<=200\". Sorted by signal value desc for numerics.",
"input_schema": {
"type": "object",
"properties": {
"signal": {
"type": "string",
"description": "Column name on ticker (e.g. golden_cross_today, rsi_14, market_cap)."
},
"condition": {
"type": "string",
"description": "Required for numerics. Single bound: <op><value>, ops in (>, >=, =, !=, <, <=)."
},
"asof": {
"type": "string",
"description": "Optional YYYY-MM-DD or ISO timestamp. Date-only matches daily state; a timestamp matches the finest intraday state covering the query."
},
"universe": {
"type": "string",
"description": "Optional universe slug."
},
"sort_by": {
"type": "string",
"description": "Row order: `default` (alphabetic for booleans, highest-value-first for numerics) or `market_cap` (desc, adds market_cap to each row).",
"enum": [
"default",
"market_cap"
]
},
"include_active_since": {
"type": "boolean",
"description": "Built-in booleans only: adds `active_since` per row — when the flag last flipped true (from the spans archive)."
},
"limit": {
"type": "integer",
"description": "Page size. Max 200. Default 50."
},
"cursor": {
"type": "string",
"description": "Opaque cursor."
}
},
"required": [
"signal"
]
}
}tickerbot_list_signal_events
GET /v2/signals/{signal}/{ticker}/events
{
"name": "tickerbot_list_signal_events",
"description": "Occurrence SPANS of a boolean signal for one ticker, newest-first. For STATE flags (above_sma_50, in_uptrend) each row is a true-WINDOW: started_at when it flipped true, ended_at when it flipped back (null while still true), with prices at both ends. For EVENT flags (golden_cross, gap_up) each row is a point firing (started_at = ended_at). \"Golden crosses in June\" is from=2026-06-01&to=2026-06-30. Built-in booleans only — numerics and custom signals have no precomputed spans (use tickerbot_get_series).",
"input_schema": {
"type": "object",
"properties": {
"signal": {
"type": "string",
"description": "Built-in boolean flag name."
},
"ticker": {
"type": "string",
"description": "Symbol."
},
"from": {
"type": "string",
"description": "Window start (inclusive) on each span's started_at — YYYY-MM-DD, ISO, or epoch-ms."
},
"to": {
"type": "string",
"description": "Window end (inclusive) on started_at; a bare YYYY-MM-DD means through the end of that day."
},
"merge_gap_seconds": {
"type": "integer",
"description": "Interval-union: contiguous windows whose gap is ≤ N seconds collapse into one — de-fragments flags with thousands of per-tick rows (e.g. 3600 for daily flags). Default 0 = no merge. A cursor pins this; resend it unchanged when paging."
},
"limit": {
"type": "integer",
"description": "Page size. Max 1000, newest first."
},
"cursor": {
"type": "string",
"description": "Opaque cursor — pages older, inside the from/to window if one is set."
}
},
"required": [
"signal",
"ticker"
]
}
}tickerbot_create_custom_signal
POST /v2/signals
{
"name": "tickerbot_create_custom_signal",
"description": "Save a SQL WHERE expression as a named custom signal the caller can reference by name in future scans.",
"input_schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Snake_case identifier."
},
"expr": {
"type": "string",
"description": "SQL WHERE expression. Same grammar as scan `q`."
},
"description": {
"type": "string",
"description": "Optional human description."
}
},
"required": [
"name",
"expr"
]
}
}tickerbot_update_custom_signal
PATCH /v2/signals/{name}
{
"name": "tickerbot_update_custom_signal",
"description": "Edit one of the caller's custom signals — supply `expr`, `description`, `new_name`, or any combination. Providing `expr` recompiles it against the live column whitelist. Renaming is refused (409) while other custom signals reference the current name. Built-in signals are read-only; only custom signals the caller owns can be patched. Available on every plan.",
"input_schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "CURRENT slug — identifies which signal to edit."
},
"new_name": {
"type": "string",
"description": "Rename the signal to this slug (snake_case, must not collide with a built-in column or another of your signals)."
},
"expr": {
"type": "string",
"description": "New SQL WHERE expression. Re-validated on save."
},
"description": {
"type": "string",
"description": "New description."
}
},
"required": [
"name"
]
}
}tickerbot_delete_custom_signal
DELETE /v2/signals/{name}
{
"name": "tickerbot_delete_custom_signal",
"description": "Delete one of the caller's custom signals. Cascade-safe by default: refused with 409 if another custom signal references it (the error lists the referencing signals). Pass `force: true` to delete anyway — existing references will break on next recompile. Available on every plan.",
"input_schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Custom signal slug."
},
"force": {
"type": "boolean",
"description": "When true, skip the reference check and delete anyway. Default false."
}
},
"required": [
"name"
]
}
}tickerbot_subscribe_signal
POST /v2/signals/{signal}/subscribe
{
"name": "tickerbot_subscribe_signal",
"description": "Register a webhook that fires when a signal turns true (booleans) or its value crosses a condition (numerics). Optional `ticker` restricts to one symbol; omit to watch the whole universe. Pass `target_url` for an https POST, or `channel:\"discord\"` + `discord_url` to post to Discord; omit for in-app.",
"input_schema": {
"type": "object",
"properties": {
"signal": {
"type": "string",
"description": "Column name (e.g. golden_cross_today, rsi_14)."
},
"ticker": {
"type": "string",
"description": "Optional ticker to restrict the watch to one symbol."
},
"universe": {
"type": "string",
"description": "Optional universe slug."
},
"condition": {
"type": "string",
"description": "Required for numerics: single bound like \">70\" or \"<=200\". Ignored for booleans."
},
"name": {
"type": "string",
"description": "Human-readable label."
},
"target_url": {
"type": "string",
"description": "Optional https URL for the `webhook` channel; omit for in-app."
},
"channel": {
"type": "string",
"description": "Delivery channel: `webhook` (POST to target_url), `discord` (embed to discord_url), `mobile_push` (to a registered device), or `in_app` (dashboard only). Inferred from the URL you pass if omitted.",
"enum": [
"webhook",
"discord",
"in_app",
"mobile_push"
]
},
"discord_url": {
"type": "string",
"description": "Discord incoming-webhook URL (https://discord.com/api/webhooks/…). Required when channel is \"discord\"."
},
"device_id": {
"type": "string",
"description": "Registered device id from the mobile app. Required when channel is \"mobile_push\"."
},
"cadence": {
"type": "string",
"description": "Evaluation cadence. Default realtime; hourly/nyse_open throttle. (`1m` accepted as a deprecated alias of realtime.)",
"enum": [
"realtime",
"hourly",
"nyse_open"
]
}
},
"required": [
"signal"
]
}
}tickerbot_list_events
GET /v2/events
{
"name": "tickerbot_list_events",
"description": "THE events primitive — one timeline over every event log, cross-ticker: dividends, splits, insider transactions, and analyst actions (\"all splits this month\", \"every analyst action in my universe this week\", \"AAPL's full corporate history\" via `ticker`), plus two opt-in kinds that join only when named: `signal` (boolean-flag firings) and `news`. Rows are { ticker, ts, kind, payload }, newest first. Analyst payloads (history to 2012) carry firm/analyst/action/rating/price_target; filter them with `firm=`/`action=` (structured, case-insensitive — a `q` payload match is case-SENSITIVE) — actions: upgrades, downgrades, initiates_coverage_on, maintains, reiterates, assumes, reinstates, suspends, terminates_coverage_on. Use `q` for other payload conditions: it speaks the SQL grammar over exactly (ticker, ts, kind, payload jsonb); `group_by`/`select`/`having` roll the stream up (aggregates return `truncated: true` instead of paginating); `join=state` attaches the ticker's state as of each event. Requires at least one bound: a ticker scope (ticker/tickers/universe), a time window (from/to), or firm/action — q alone is not a bound.",
"input_schema": {
"type": "object",
"properties": {
"kind": {
"type": "string",
"description": "Comma list of kinds to include. Default is the four corporate kinds: dividend, split, insider, analyst. Two more are opt-in and join only when named: `signal` (boolean-flag firings) and `news`."
},
"signal": {
"type": "string",
"description": "kind=signal only. One built-in boolean flag (e.g. golden_cross). REQUIRED to use `q` or `join` on kind=signal — naming the signal is what keeps the query on an index; optional otherwise."
},
"transition": {
"type": "string",
"description": "kind=signal only. `enter` (false->true) or `exit` (true->false). Always optional — an ordinary filter.",
"enum": [
"enter",
"exit"
]
},
"firm": {
"type": "string",
"description": "kind=analyst only. Case-insensitive analyst-firm filter (e.g. \"Goldman Sachs\" matches \"goldman sachs\") — prefer this over a q payload match, which is case-sensitive."
},
"action": {
"type": "string",
"description": "kind=analyst only. Exact rating-action filter.",
"enum": [
"upgrades",
"downgrades",
"initiates_coverage_on",
"maintains",
"reiterates",
"assumes",
"reinstates",
"suspends",
"terminates_coverage_on"
]
},
"ticker": {
"type": "string",
"description": "Single-ticker filter, e.g. AAPL."
},
"tickers": {
"type": "string",
"description": "Comma-separated tickers, max 50. Mutually exclusive with `universe`."
},
"universe": {
"type": "string",
"description": "Universe slug (top_10, top_100, or a saved one) to scope the stream. Mutually exclusive with `tickers`."
},
"from": {
"type": "string",
"description": "Events at or after this ISO date/datetime (inclusive; a bare YYYY-MM-DD means from the start of that day). `since` accepted as alias."
},
"to": {
"type": "string",
"description": "Window end: a bare YYYY-MM-DD means through the END of that day (matching bars/series/spans); a timestamp is exclusive — events strictly before it. `until` accepted as alias."
},
"join": {
"type": "string",
"description": "join=state attaches each event's ticker STATE as of that event's moment (the replay join) under a `state` key — \"downgrades where rsi_14 was already under 40\" composes with q. Free on every plan.",
"enum": [
"state"
]
},
"interval": {
"type": "string",
"description": "Grain for join=state replay (finest covering tier by default).",
"enum": [
"1m",
"1h",
"1d"
]
},
"q": {
"type": "string",
"description": "SQL WHERE over (ticker, ts, kind, payload jsonb) — ONLY those four identifiers. Payload fields via jsonb operators: payload->>'firm' = 'Goldman Sachs', (payload->>'shares')::numeric > 1e6. On kind=signal this requires `signal` (the firing log is ~175M rows); kind=signal takes no group_by."
},
"select": {
"type": "string",
"description": "Aggregate-mode output columns (requires group_by). Default: group keys + COUNT(*) AS events."
},
"group_by": {
"type": "string",
"description": "Comma list of rollup keys — switches to aggregate rows, e.g. payload->>'firm' AS firm, or kind. `AS` names the JSON key; an un-named payload read is keyed by its payload key (payload->>'firm' -> firm)."
},
"having": {
"type": "string",
"description": "Post-aggregation filter (requires group_by), e.g. COUNT(*) > 5."
},
"order": {
"type": "string",
"description": "Aggregate-mode sort column/alias. Default: events."
},
"dir": {
"type": "string",
"description": "Aggregate-mode sort direction.",
"enum": [
"asc",
"desc"
]
},
"limit": {
"type": "integer",
"description": "Page size. Max 1000."
},
"cursor": {
"type": "string",
"description": "Opaque cursor from a prior response — carries the original filters, pass it alone (long q values must be resent alongside it)."
}
}
}
}tickerbot_subscribe_events
POST /v2/events/subscribe
{
"name": "tickerbot_subscribe_events",
"description": "Create an event-trigger webhook: fires when NEW events land — dividends, splits, insider filings, analyst actions (\"every split in my universe\", \"Goldman downgrades on large caps\"). TWO composable filters: `q` filters the event's TICKER STATE (market_cap > 1e10); `event_q` filters the EVENT CONTENT in the /v2/events grammar (payload->>'firm' = 'Goldman Sachs'). Paid plans (webhook slots). Deliveries carry event: \"events.fired\" with an events array. Latency = ingest cadence: analyst ≤1h, corporate kinds daily — NOT sub-minute like state webhooks.",
"input_schema": {
"type": "object",
"properties": {
"kinds": {
"type": "string",
"description": "Comma list of kinds to fire on — any of: dividend, split, insider, analyst (e.g. \"split,analyst\"). NOTE: no enum here on purpose — a scalar enum would reject multi-kind values."
},
"tickers": {
"type": "string",
"description": "Scope to specific tickers (comma list, max 50). Mutually exclusive with universe; omit both for all tickers."
},
"universe": {
"type": "string",
"description": "Scope to a universe slug (top_10, top_100, or a saved one)."
},
"q": {
"type": "string",
"description": "Optional row-STATE filter evaluated against the event's ticker at fire time, e.g. market_cap > 1e10."
},
"event_q": {
"type": "string",
"description": "Optional event-CONTENT filter over (ticker, ts, kind, payload jsonb) — only those four identifiers, e.g. payload->>'firm' = 'Goldman Sachs' AND payload->>'action' = 'downgrades'."
},
"target_url": {
"type": "string",
"description": "HTTPS delivery URL. Omit for in-app delivery."
},
"channel": {
"type": "string",
"description": "Delivery channel.",
"enum": [
"webhook",
"discord",
"in_app",
"mobile_push"
]
},
"discord_url": {
"type": "string",
"description": "Discord incoming-webhook URL (channel discord)."
},
"device_id": {
"type": "string",
"description": "Registered device id from the mobile app (channel mobile_push)."
},
"name": {
"type": "string",
"description": "Display name."
}
},
"required": [
"kinds"
]
}
}tickerbot_scan
GET /v2/scan
{
"name": "tickerbot_scan",
"description": "Run a SQL WHERE clause against the live ticker universe (or against a past moment with `asof` — unlimited depth on every plan). Returns matching tickers sorted by chosen column, OR — with `group_by` — aggregate rollups instead of rows (breadth stats: \"count of tickers above their 200dma by sector\", \"median RSI by sector on 2026-03-03\"). The `q` grammar is a flat WHERE: column names from the schema, AND/OR/NOT, comparison operators, numeric/string literals. No JOIN or subqueries. Example: `gap_up AND market_cap < 2000000000 AND NOT earnings_this_week`.",
"input_schema": {
"type": "object",
"properties": {
"q": {
"type": "string",
"description": "SQL WHERE expression. Max 4000 chars."
},
"universe": {
"type": "string",
"description": "Optional universe slug."
},
"asof": {
"type": "string",
"description": "Optional YYYY-MM-DD or ISO timestamp for a historical scan. Unlimited depth on every plan."
},
"order": {
"type": "string",
"description": "Sort column. Default day_change_pct (rows) / tickers (aggregate)."
},
"dir": {
"type": "string",
"description": "Sort direction.",
"enum": [
"asc",
"desc"
]
},
"fields": {
"type": "string",
"description": "Comma-separated extra columns to include (row mode only)."
},
"group_by": {
"type": "string",
"description": "AGGREGATE MODE: 1–6 comma-separated group keys (columns or expressions, e.g. `sector`). Results become rollup rows instead of tickers. Alias a key with `AS` to name its JSON key; un-named expressions are named for you."
},
"select": {
"type": "string",
"description": "Aggregate output items (requires group_by). Default: group keys + COUNT(*) AS tickers. Aggregates: count/avg/sum/min/max/stddev/string_agg + FILTER (WHERE …). Alias items with AS. Example: `sector, COUNT(*) AS n, AVG(rsi_14) AS avg_rsi`."
},
"having": {
"type": "string",
"description": "Aggregate filter (requires group_by). Example: `COUNT(*) >= 10`."
},
"limit": {
"type": "integer",
"description": "Page size. Max 100. Default 50. Aggregate mode does not paginate — response sets `truncated: true` when groups were cut."
},
"cursor": {
"type": "string",
"description": "Opaque cursor (row mode only)."
},
"full": {
"type": "boolean",
"description": "Row mode: return the FULL wide row for each match (every column) instead of the slim default projection."
}
},
"required": [
"q"
]
}
}tickerbot_subscribe_scan
POST /v2/scan/subscribe
{
"name": "tickerbot_subscribe_scan",
"description": "Register a webhook that fires when matches for a scan query change. Pass `target_url` for an https POST, or `channel:\"discord\"` + `discord_url` to post an embed to Discord; omit for in-app delivery in the dashboard. `cadence` is real-time (1m) by default; throttle to hourly or nyse_open. Use to satisfy \"alert me when this happens\" prompts.",
"input_schema": {
"type": "object",
"properties": {
"q": {
"type": "string",
"description": "SQL WHERE expression — same grammar as scan."
},
"name": {
"type": "string",
"description": "Human-readable label. Defaults to a truncated version of the query."
},
"universe": {
"type": "string",
"description": "Optional universe slug to scope the watch."
},
"target_url": {
"type": "string",
"description": "Optional https URL to POST matches to (the `webhook` channel). Omit for in-app delivery."
},
"channel": {
"type": "string",
"description": "Delivery channel: `webhook` (POST to target_url), `discord` (embed to discord_url), `mobile_push` (to a registered device), or `in_app` (dashboard only). Inferred from the URL you pass if omitted.",
"enum": [
"webhook",
"discord",
"in_app",
"mobile_push"
]
},
"discord_url": {
"type": "string",
"description": "Discord incoming-webhook URL (https://discord.com/api/webhooks/…). Required when channel is \"discord\"."
},
"device_id": {
"type": "string",
"description": "Registered device id from the mobile app. Required when channel is \"mobile_push\"."
},
"cadence": {
"type": "string",
"description": "Evaluation cadence. Default realtime; hourly/nyse_open throttle. (`1m` accepted as a deprecated alias of realtime.)",
"enum": [
"realtime",
"hourly",
"nyse_open"
]
}
},
"required": [
"q"
]
}
}tickerbot_search_news
GET /v2/news/scan
{
"name": "tickerbot_search_news",
"description": "Search the news archive (back to 2015) with a SQL WHERE clause. Available on every plan. Columns on news_article include `time_published`, `title`, `summary`, `source`, `source_domain`, `category`, `authors`, `topics`, `tickers` (array), `overall_sentiment_score`, `overall_sentiment_label`, `url`. To filter to one ticker use `'NVDA' = ANY(tickers)` or the auto-unnest alias `tk = 'NVDA'`. Example: `q=tk='NVDA' AND time_published >= NOW() - INTERVAL '1 day'`. Supports group_by + having for aggregation (e.g. count of articles per day).",
"input_schema": {
"type": "object",
"properties": {
"q": {
"type": "string",
"description": "SQL WHERE on news_article. Optional when search or a scoping param is present."
},
"search": {
"type": "string",
"description": "Full-text search over title+summary (websearch grammar: \"apple earnings\", quoted phrases, OR, -negation). ANDs with q and the scoping params."
},
"select": {
"type": "string",
"description": "Comma-separated columns to include. Defaults to a slim set."
},
"group_by": {
"type": "string",
"description": "Comma-separated columns for aggregation. Alias a key with `AS` to name its JSON key; un-named expressions are named for you."
},
"having": {
"type": "string",
"description": "WHERE-style filter on aggregates. Requires group_by."
},
"order": {
"type": "string",
"description": "Sort column or SELECT alias. Default time_published (non-aggregate) or volume (aggregate)."
},
"dir": {
"type": "string",
"description": "Sort direction.",
"enum": [
"asc",
"desc"
]
},
"limit": {
"type": "integer",
"description": "Page size."
},
"cursor": {
"type": "string",
"description": "Opaque cursor."
}
},
"required": [
"q"
]
}
}tickerbot_create_webhook
POST /v2/webhooks
{
"name": "tickerbot_create_webhook",
"description": "The canonical webhook create — POST /v2/webhooks with an explicit `trigger` object: { type: \"scan\" | \"ticker\" | \"signal\" | \"event\", … } plus delivery fields. The subscribe tools above are flat sugar over exactly this; use this form when composing the trigger programmatically or when a sugar door doesn't fit. Trigger shapes: scan {type:\"scan\", q, universe?}; ticker {type:\"ticker\", ticker, condition}; signal {type:\"signal\", signal, ticker?, universe?, condition?}; event {type:\"event\", kinds, tickers?, universe?, event_q?}.",
"input_schema": {
"type": "object",
"properties": {
"trigger": {
"type": "object",
"description": "What fires the webhook: { type: \"scan\" | \"ticker\" | \"signal\" | \"event\", … } — see the tool description for each shape."
},
"name": {
"type": "string",
"description": "Display name. Defaults from the trigger."
},
"target_url": {
"type": "string",
"description": "HTTPS delivery URL (`webhook` channel). Omit for in-app."
},
"channel": {
"type": "string",
"description": "Delivery channel. Inferred from the URL you pass if omitted.",
"enum": [
"webhook",
"discord",
"in_app",
"mobile_push"
]
},
"discord_url": {
"type": "string",
"description": "Discord incoming-webhook URL (channel discord)."
},
"device_id": {
"type": "string",
"description": "Registered device id from the mobile app (channel mobile_push)."
},
"cadence": {
"type": "string",
"description": "Evaluation cadence. Default realtime. (`1m` accepted as a deprecated alias of realtime.)",
"enum": [
"realtime",
"hourly",
"nyse_open"
]
},
"columns": {
"type": "string",
"description": "Comma list of extra columns to include with each delivered match (`fields` accepted as alias)."
}
},
"required": [
"trigger"
]
}
}tickerbot_patch_webhook
PATCH /v2/webhooks/{id}
{
"name": "tickerbot_patch_webhook",
"description": "Edit a webhook in place: `name`, `cadence`, `target_url`, `enabled` (pausing/resuming without losing match-state). The trigger (q) and channel are immutable by design — delete and re-create to change what fires or where it delivers. Unknown fields are a 400, never silently ignored.",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Webhook id (looks like `wh_…`)."
},
"name": {
"type": "string",
"description": "New display name."
},
"cadence": {
"type": "string",
"description": "New evaluation cadence.",
"enum": [
"realtime",
"hourly",
"nyse_open"
]
},
"target_url": {
"type": "string",
"description": "New HTTPS delivery URL."
},
"enabled": {
"type": "boolean",
"description": "false pauses deliveries; true resumes. (Re-enabling after auto-disable also works via tickerbot_enable_webhook, which additionally clears match-state.)"
}
},
"required": [
"id"
]
}
}tickerbot_list_webhooks
GET /v2/webhooks
{
"name": "tickerbot_list_webhooks",
"description": "List the caller's webhook subscriptions (rules created via the subscribe tools), newest-first. Use `status` to filter to active or disabled rules.",
"input_schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Filter by status: `active` or `disabled`. Omit for all.",
"enum": [
"active",
"disabled"
]
},
"limit": {
"type": "integer",
"description": "Page size. Max 100. Default 50."
},
"cursor": {
"type": "string",
"description": "Opaque cursor."
}
}
}
}tickerbot_get_webhook
GET /v2/webhooks/{id}
{
"name": "tickerbot_get_webhook",
"description": "Fetch one webhook subscription by id (current state, match-set, schedule). Account-scoped: any key on the account can read any of the account's webhooks.",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Webhook id (looks like `wh_…`)."
}
},
"required": [
"id"
]
}
}tickerbot_list_webhook_deliveries
GET /v2/webhooks/{id}/deliveries
{
"name": "tickerbot_list_webhook_deliveries",
"description": "List recent deliveries (pings and fires) for a webhook, newest-first — for diagnosing failures. Returns metadata only (status, attempt, response code, error); the POST body is not stored.",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Webhook id."
},
"status": {
"type": "string",
"description": "Filter by delivery status.",
"enum": [
"pending",
"delivered",
"permanent_failure"
]
},
"from": {
"type": "string",
"description": "Only deliveries created at/after this moment — epoch seconds, epoch milliseconds (13+ digits), or ISO datetime. (90-day retention on every plan.)"
},
"to": {
"type": "string",
"description": "Only deliveries created at/before this moment — same value grammar as `from`; a date-only value means through the end of that UTC day."
},
"limit": {
"type": "integer",
"description": "Page size. Max 100. Default 50."
},
"cursor": {
"type": "string",
"description": "Opaque cursor."
}
},
"required": [
"id"
]
}
}tickerbot_test_webhook
POST /v2/webhooks/{id}/test
{
"name": "tickerbot_test_webhook",
"description": "Send a real-shape `webhook.fired` POST to the webhook's target_url synchronously, right now. The body is byte-identical to a real fire (same signing); the test marker rides in an `X-Tickerbot-Test: true` header. Returns the inline outcome (`delivered`, `http_status`, `elapsed_ms`, `error`). One-shot — a failed test never retries and never auto-disables the webhook. Fails with 400 if the webhook has no target_url (in-app deliveries have nothing to fire over the wire).",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Webhook id."
}
},
"required": [
"id"
]
}
}tickerbot_enable_webhook
POST /v2/webhooks/{id}/enable
{
"name": "tickerbot_enable_webhook",
"description": "Re-enable a disabled webhook — flips it back to `active` and clears its match-state so the next eval treats every currently-matching ticker as new. Use after fixing whatever caused auto-disable. No-op on an already-active webhook.",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Webhook id."
}
},
"required": [
"id"
]
}
}tickerbot_delete_webhook
DELETE /v2/webhooks/{id}
{
"name": "tickerbot_delete_webhook",
"description": "Delete a webhook subscription by id. Use after listing webhooks when the user wants to remove an alert.",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Webhook id (looks like `wh_…`)."
}
},
"required": [
"id"
]
}
}tickerbot_list_universes
GET /v2/universes
{
"name": "tickerbot_list_universes",
"description": "List universes — your saved ones and/or the built-in system universes (top_10, top_100). Filter with `owner` (like the signals catalog's `kind`): `me` (default, your own), `system` (built-ins), or `all` (both). Each row carries `system: true|false`.",
"input_schema": {
"type": "object",
"properties": {
"owner": {
"type": "string",
"description": "Which universes to list.",
"enum": [
"me",
"system",
"all"
]
},
"limit": {
"type": "integer",
"description": "Page size (applies to your own; system universes are a small fixed set returned in full on the first page)."
},
"cursor": {
"type": "string",
"description": "Opaque cursor."
}
}
}
}tickerbot_list_system_universes
GET /v2/universes/system
{
"name": "tickerbot_list_system_universes",
"description": "List the built-in system universes (`top_10`, `top_100` — the most-actively-traded tickers by 30-day trailing dollar volume, rebalanced monthly). Available to every account regardless of plan. Use these slugs as `universe` in scans/signals or `universe_id` when subscribing.",
"input_schema": {
"type": "object",
"properties": {}
}
}tickerbot_get_universe
GET /v2/universes/{id}
{
"name": "tickerbot_get_universe",
"description": "Get one universe by slug, including its ticker list.",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Universe slug."
}
},
"required": [
"id"
]
}
}tickerbot_create_universe
POST /v2/universes
{
"name": "tickerbot_create_universe",
"description": "Create a new universe (named set of tickers) for scoping future scans.",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Optional slug (lowercase letters, digits, underscore). Auto-generated from name if omitted. Must be unique within the account."
},
"name": {
"type": "string",
"description": "Human-readable name."
},
"description": {
"type": "string",
"description": "Optional free-form notes."
},
"tickers": {
"type": "array",
"description": "List of ticker symbols.",
"items": {
"type": "string"
}
}
},
"required": [
"name",
"tickers"
]
}
}tickerbot_update_universe
PATCH /v2/universes/{id}
{
"name": "tickerbot_update_universe",
"description": "Update one of the caller's universes. Pass `name`/`description` to relabel, `tickers` to replace the whole list, or `add`/`remove` to adjust subsets without replacing. System universes (`top_10`/`top_100`) cannot be edited.",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Universe slug."
},
"name": {
"type": "string",
"description": "New label."
},
"description": {
"type": "string",
"description": "New notes."
},
"tickers": {
"type": "array",
"description": "Replace the full ticker list.",
"items": {
"type": "string"
}
},
"add": {
"type": "array",
"description": "Add these tickers (deduplicated).",
"items": {
"type": "string"
}
},
"remove": {
"type": "array",
"description": "Remove these tickers.",
"items": {
"type": "string"
}
}
},
"required": [
"id"
]
}
}tickerbot_delete_universe
DELETE /v2/universes/{id}
{
"name": "tickerbot_delete_universe",
"description": "Delete one of the caller's universes. System universes (`top_10`/`top_100`) cannot be deleted. Webhooks that reference the deleted universe will fail on their next eval, so clean those up first.",
"input_schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Universe slug."
}
},
"required": [
"id"
]
}
}How to wire it up
The runtime calls the tool. Your code forwards to api.tickerbot.io.
When the LLM decides to call a tool, your code receives the tool name and arguments. Route the call to the matching Tickerbot endpoint (the endpoint field on each tool body above, or hard-coded in your handler), forward the bearer token, and return the JSON response to the LLM.
The OpenAPI spec covers anything generator-driven if your runtime isn't one of the three above.