View as markdown
For runtime authors

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 413+ named columns — no JOIN, no GROUP BY, no OVER. Schema fits in a system prompt; one q string drives /scan, /scan?asof=, and every subscribe endpoint.

The loop. User asks in English → agent composes q → call /v2/scan live, ?asof= to replay history, /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.

36 tools · Claude tool use
[
  {
    "name": "tickerbot_list_tickers",
    "description": "GET /v2/tickers. List active tickers, paginated. Returns the universe of tracked symbols (~12,947 US equities + top 100 crypto by market cap) in alphabetical order with a slim payload (`ticker`, `name`, `asset_type`, `exchange`, `market_cap`). Use cursor pagination to walk the full list, or pass `?tickers=...` for bulk lookup of named symbols with full rows.",
    "input_schema": {
      "type": "object",
      "properties": {
        "tickers": {
          "type": "string",
          "description": "Comma-separated list of symbols (up to 50). When set, the response returns the full row for each requested symbol; pagination params are ignored. Symbols missing from our universe come back with `_not_found: true`."
        },
        "limit": {
          "type": "integer",
          "description": "Page size for the alphabetical walk. Max 1000.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor from the previous response's `next_cursor` field. Continues the walk from after that page."
        },
        "search": {
          "type": "string",
          "description": "Substring filter over `ticker` (case-insensitive) and `name`. When set, results are ordered by `market_cap DESC NULLS LAST`."
        },
        "asset_type": {
          "type": "string",
          "description": "Filter by asset type — typically `equity` or `crypto`. Matched case-insensitively against the stored value."
        },
        "exchange": {
          "type": "string",
          "description": "Filter by exchange code (uppercase). Common values: `XNYS`, `XNAS`, `BATS`."
        },
        "sector": {
          "type": "string",
          "description": "Filter by sector name. Exact match against the stored `sector` field."
        },
        "min_market_cap": {
          "type": "number",
          "description": "Minimum market cap (USD). Results are ordered by `market_cap DESC NULLS LAST` when set."
        }
      }
    }
  },
  {
    "name": "tickerbot_get_ticker",
    "description": "GET /v2/tickers/{ticker}. Get the full current state of one ticker, or a snapshot at a past date. Returns the entire ticker row. Every field on the schema page, including current values for all numeric signals and the current value of every boolean flag. Pass `?asof=YYYY-MM-DD` to return the row exactly as it was at the close of that day — see the dedicated [as-of snapshot](/docs/endpoints/tickers/history) page for frozen-fields and coverage semantics.",
    "input_schema": {
      "type": "object",
      "properties": {
        "ticker": {
          "type": "string",
          "description": "Ticker symbol. Case-insensitive. Equities: exchange symbol (`AAPL`). Crypto: bare symbol (`BTC`)."
        },
        "asof": {
          "type": "string",
          "description": "Optional. Returns the row as it was at the close of the given `YYYY-MM-DD` (or full ISO). Sourced from `signal_daily_state`. Every plan gets the full history window, Free included. Coverage note: price, technical indicators, and financial-statement fields reconstruct to the exact date; a few vendor-sourced fields (`forward_pe`, `short_interest`, ownership %, analyst estimates) are populated forward-only and may be `null` in older snapshots while historical backfill is in progress — a `null` there means \"not yet backfilled,\" not \"no value existed.\""
        }
      },
      "required": [
        "ticker"
      ]
    }
  },
  {
    "name": "tickerbot_get_ticker_history",
    "description": "GET /v2/tickers/{ticker}/history. Get the full ticker row as of a past date. Time-travel to any historical date and pull the whole wide row as it stood then, including the boolean flag values, technical indicators, and the most-recent fundamentals known on that date. Useful for \"what did we know\" reconstructions and replaying screens across history.",
    "input_schema": {
      "type": "object",
      "properties": {
        "ticker": {
          "type": "string",
          "description": "Ticker symbol."
        },
        "asof": {
          "type": "string",
          "description": "Target date as `YYYY-MM-DD` or full ISO timestamp `YYYY-MM-DDTHH:MM:SSZ`. A time component is truncated to the calendar day (`_meta.resolution: \"daily\"`). The response returns the most-recent daily snapshot on or before this date, with **no lookback limit** — it walks back as far as needed and 404s only if the ticker has no state at all on/before the date. (This differs from `/v2/scan?asof=` and `/v2/signals/{signal}?asof=`, which cap carry-forward at 14 days and return empty rather than 404.)"
        }
      },
      "required": [
        "ticker",
        "asof"
      ]
    }
  },
  {
    "name": "tickerbot_get_ticker_bars",
    "description": "GET /v2/tickers/{ticker}/bars/{interval}. Open/high/low/close/volume bars for a symbol at a given interval. Simple OHLCV bars from 1-minute through daily, with point-in-time (`asof`) and back-paging (`before`/`limit`). `1d` and `1h` cover the full universe with full history; sub-hour intervals cover the active universe and back-fill on demand. Pass a comma-separated symbol list for a bulk response keyed by symbol.",
    "input_schema": {
      "type": "object",
      "properties": {
        "ticker": {
          "type": "string",
          "description": "Ticker symbol, or a comma-separated list (up to 50) for a bulk response keyed by symbol."
        },
        "interval": {
          "type": "string",
          "description": "Bar interval — one of `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 — back-paging."
        },
        "asof": {
          "type": "string",
          "description": "Return a single bar as of that date/timestamp (point-in-time)."
        },
        "cursor": {
          "type": "string",
          "description": "Continuation token from a prior response's `next_cursor`; sugar for `before`."
        }
      },
      "required": [
        "ticker",
        "interval"
      ]
    }
  },
  {
    "name": "tickerbot_get_ticker_events",
    "description": "GET /v2/tickers/{ticker}/events. Discrete event log for one ticker. Returns dividends, splits, and insider transactions for the ticker, newest-first, in a unified envelope. Each event has a `ts`, a `kind`, and a `payload` whose shape depends on the kind. (Analyst rating changes are a separate feed: `/v2/analyst/events`.)",
    "input_schema": {
      "type": "object",
      "properties": {
        "ticker": {
          "type": "string",
          "description": "Ticker symbol."
        },
        "kind": {
          "type": "string",
          "description": "Filter by event kind. Omit for all three merged into one timeline.",
          "enum": [
            "dividend",
            "split",
            "insider"
          ]
        },
        "before": {
          "type": "string",
          "description": "Return events strictly before this date (YYYY-MM-DD). Back-paging cursor; pass the previous response's `next_cursor`."
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 1000.",
          "default": 100
        }
      },
      "required": [
        "ticker"
      ]
    }
  },
  {
    "name": "tickerbot_get_ticker_holdings",
    "description": "GET /v2/tickers/{ticker}/holdings. Constituent holdings of an ETF, weight-ranked. Returns the ETF's constituents and their weights, heaviest first. When the ticker is not an ETF, `is_etf` is false and `holdings` is empty. The reverse lookup (\"which ETFs hold NVDA\") is a `/v2/scan` filter on the `etf_holders` column, not an endpoint.",
    "input_schema": {
      "type": "object",
      "properties": {
        "ticker": {
          "type": "string",
          "description": "ETF symbol."
        },
        "limit": {
          "type": "integer",
          "description": "Max holdings returned. Max 5000.",
          "default": 500
        }
      },
      "required": [
        "ticker"
      ]
    }
  },
  {
    "name": "tickerbot_get_ticker_sectors",
    "description": "GET /v2/tickers/{ticker}/sectors. Sector allocation of an ETF, weight-ranked. Returns the ETF's sector weights, heaviest first. When the ticker is not an ETF, `is_etf` is false and `sectors` is empty.",
    "input_schema": {
      "type": "object",
      "properties": {
        "ticker": {
          "type": "string",
          "description": "ETF symbol."
        }
      },
      "required": [
        "ticker"
      ]
    }
  },
  {
    "name": "tickerbot_post_ticker_subscribe",
    "description": "POST /v2/tickers/{ticker}/subscribe. Create a webhook that fires when this ticker matches a condition. Subscribes a single ticker. `condition` is a WHERE-clause fragment evaluated when this ticker is matched — you do not need to add `ticker = '…'` yourself; the endpoint scopes the predicate to the path ticker automatically. Plan-gated by webhook tier; counts against your account-wide webhook cap. Returns the same shape as `GET /v2/webhooks/{id}`, plus a `test_url` pointing at `POST /v2/webhooks/{id}/test` — subscribing does NOT auto-fire anything at your endpoint; hit the test URL to send a real-shape `webhook.fired` and validate your receiver.\n\nCustom signals referenced in `condition` are expanded and frozen into the webhook at creation — editing the signal later will not change this subscription (re-subscribe to apply). See Create a custom signal.",
    "input_schema": {
      "type": "object",
      "properties": {
        "ticker": {
          "type": "string",
          "description": "Ticker symbol (case-insensitive)."
        },
        "condition": {
          "type": "string",
          "description": "WHERE-clause fragment using signal/column names from the schema."
        },
        "target_url": {
          "type": "string",
          "description": "https:// URL to POST when the condition fires. Omit for in-app delivery (visible in the dashboard)."
        },
        "channel": {
          "type": "string",
          "description": "Delivery channel. `webhook` (POST to `target_url`), `discord` (post an embed to `discord_url`), `in_app` (dashboard only), or `mobile_push` (notify a phone signed in to the Tickerbot mobile app; requires a `device_id` from `POST /v2/devices/register`). Inferred when omitted: `webhook` if `target_url` is set, `discord` if `discord_url` is set, else `in_app`. `slack` is reserved and returns `501`. See the Delivery channels guide.",
          "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`. Stored as a posting credential and stripped from list/get responses, which set `channel_config_present: true` instead."
        },
        "cadence": {
          "type": "string",
          "description": "How often to evaluate. `realtime` (the default) is evaluated on every data refresh (~1×/min); `hourly` and `nyse_open` throttle to a batch schedule. `1m` is a deprecated alias for `realtime`.",
          "enum": [
            "realtime",
            "hourly",
            "nyse_open"
          ]
        },
        "name": {
          "type": "string",
          "description": "Human-readable label (up to 80 chars). Defaults to `<TICKER>: <condition>`."
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated extra columns to include in each fired payload match row, beyond the standard set (`ticker`, `name`, `asset_type`, `price`, `day_change_pct`, `market_cap`). Each must be a real column; an unknown column is rejected at creation."
        }
      },
      "required": [
        "ticker",
        "condition"
      ]
    }
  },
  {
    "name": "tickerbot_list_signals_catalog",
    "description": "GET /v2/signals. Built-in signals plus your custom signals, each tagged with `kind`. Returns the unified signal catalog: every built-in column on the live schema (tagged `kind: 'builtin'`, carrying `name`, `type`, `source`) plus the caller's own custom signals (tagged `kind: 'expression'`, carrying `name`, `description`, `expr`, `created_at`, `updated_at`).\n\nPagination applies only to the custom-signal slice — built-ins are returned in their entirety on every page since they're a small fixed set.",
    "input_schema": {
      "type": "object",
      "properties": {
        "kind": {
          "type": "string",
          "description": "Filter by kind. Omit to return both.",
          "enum": [
            "builtin",
            "custom"
          ]
        },
        "limit": {
          "type": "integer",
          "description": "Page size for the custom-signal slice. Max 200.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor from a prior response."
        }
      }
    }
  },
  {
    "name": "tickerbot_get_signals_match",
    "description": "GET /v2/signals/{signal}. Find tickers that match a signal right now, or at a past date with ?asof=. Return the set of tickers where the named signal is true (for boolean flags) or where the numeric value satisfies a single-bounded condition. Sorted by signal value descending for numerics, alphabetically by ticker for booleans. Pass `?asof=YYYY-MM-DD` to get the match set as it was at the close of that day — sourced from `signal_daily_state`.",
    "input_schema": {
      "type": "object",
      "properties": {
        "signal": {
          "type": "string",
          "description": "A column on `ticker`. Boolean flags (e.g. `golden_cross`, `above_sma_50`) are detected automatically; numerics (e.g. `rsi_14`, `market_cap`, `pe_ratio`) require a condition."
        },
        "condition": {
          "type": "string",
          "description": "Required for numeric signals. Single bound, format `<op><value>`. Operators: `>`, `>=`, `=`, `!=`, `<`, `<=`. Examples: `>70`, `<=200`, `!=0`. Ignored for boolean flags."
        },
        "asof": {
          "type": "string",
          "description": "Optional. Run the match against historical daily state at the close of the given `YYYY-MM-DD` (or full ISO). Every plan gets full history, Free included."
        },
        "universe": {
          "type": "string",
          "description": "Optional. Scope to a system or caller-owned universe slug."
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 200.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor from the previous response."
        }
      },
      "required": [
        "signal"
      ]
    }
  },
  {
    "name": "tickerbot_get_signal_asof",
    "description": "GET /v2/signals/{signal}. Run the match set at the close of a past date — same endpoint as live, with ?asof=. Time-travel form of `GET /v2/signals/{signal}`. Pass `?asof=YYYY-MM-DD` (or full ISO) and the server returns the set of tickers where the signal was true (booleans) or where the numeric value satisfied your `condition` at the close of that day. Sourced from `signal_daily_state`. Same response envelope as the live call, with `_meta.resolution: \"daily\"` and `_meta.frozen_fields` listing the columns whose values aren't historized.",
    "input_schema": {
      "type": "object",
      "properties": {
        "signal": {
          "type": "string",
          "description": "A column on `ticker`. Boolean flags are detected automatically; numerics require a `condition`."
        },
        "asof": {
          "type": "string",
          "description": "Target date as `YYYY-MM-DD` or full ISO timestamp. The server returns the match set as of the close of this day."
        },
        "condition": {
          "type": "string",
          "description": "Required for numeric signals. Single-bounded condition: `<op><value>`. Operators: `>`, `>=`, `=`, `!=`, `<`, `<=`. Ignored for boolean flags."
        },
        "universe": {
          "type": "string",
          "description": "Scope to a system or caller-owned universe slug."
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 200.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor from the previous response."
        }
      },
      "required": [
        "signal",
        "asof"
      ]
    }
  },
  {
    "name": "tickerbot_get_signal_history",
    "description": "GET /v2/signals/{signal}/{ticker}/history/{interval}. Time series of one signal for one ticker. Returns a sequence of `{t, v}` bars for the requested signal × ticker at the chosen resolution. `t` is an ISO timestamp (or `YYYY-MM-DD` for daily/weekly). `v` is the signal value at that bar (a number for numeric signals, `true`/`false` for boolean flags).",
    "input_schema": {
      "type": "object",
      "properties": {
        "signal": {
          "type": "string",
          "description": "A column on the wide-state tables (matches the schema page 1:1)."
        },
        "ticker": {
          "type": "string",
          "description": "Ticker symbol."
        },
        "interval": {
          "type": "string",
          "description": "Resolution. Daily-only signals (SMAs, RSI, MACD, fundamentals) are not stored at minute/hourly resolution; request `1d` for those. Minute (`1m`) history is computed for the ~1,000 most-liquid tickers only — a ticker outside that set returns sparse/empty; hourly (`1h`) and daily (`1d`) cover the full universe. `1q` (quarterly) is a distinct surface: it returns per-fiscal-quarter fundamentals for `eps`, `eps_estimate`, `eps_surprise`, `eps_surprise_pct`, `revenue`, `gross_profit`, and `free_cash_flow` (20+ quarters), with `fiscal_period` on each bar.",
          "enum": [
            "1m",
            "1h",
            "1d",
            "1w",
            "1q"
          ]
        },
        "from": {
          "type": "string",
          "description": "Earliest bar timestamp (inclusive). YYYY-MM-DD or ISO. Defaults to your plan's history window."
        },
        "to": {
          "type": "string",
          "description": "Latest bar timestamp (inclusive). YYYY-MM-DD or ISO. Defaults to \"now\"."
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 1000.",
          "default": 252
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor. Walks back in time page-by-page from the most-recent bar."
        }
      },
      "required": [
        "signal",
        "ticker",
        "interval"
      ]
    }
  },
  {
    "name": "tickerbot_list_signal_events",
    "description": "GET /v2/signals/{signal}/{ticker}/events. Run-length-encoded occurrences — windows for state flags, points for event flags. Per-bar booleans (`history`) are useful for historical analysis; per-occurrence records are what you want for chart overlays and dashboards. This endpoint returns each *firing* of a signal on a ticker as a single row.\n\nFor **state**-style signals (`above_sma_50`, `in_uptrend`, `at_52w_high`, …) each row is a true-window: `started_at` is when the condition flipped false→true, `ended_at` is when it flipped back (or null if it's still true). `start_price` and `end_price` carry the price at the open/close of the window.\n\nFor **event**-style signals (`golden_cross`, `gap_up`, `breakout`, …) each row is a point firing: `started_at` is when the event fired and `ended_at` is the same value. `start_price` is the price at that bar.\n\n**This endpoint serves boolean flags only** — occurrence spans come from the precomputed `flag_events` table, which exists only for built-in boolean flags. A numeric field (`rsi_14`, `adx_14`, …) has no on/off windows: to see it over time use `…/history/{interval}`; to find the days a numeric threshold held use `/v2/scan?asof=`. Custom (expression) signals have no precomputed spans and are rejected with 400.",
    "input_schema": {
      "type": "object",
      "properties": {
        "signal": {
          "type": "string",
          "description": "Built-in boolean flag name (e.g. `above_sma_50`, `golden_cross`). Numeric fields are not valid here; custom signals return 400."
        },
        "ticker": {
          "type": "string",
          "description": "Ticker symbol (case-insensitive)."
        },
        "limit": {
          "type": "integer",
          "description": "Max rows returned, newest first. Clamped to 5000.",
          "default": 2000
        },
        "merge_gap_seconds": {
          "type": "integer",
          "description": "Interval-union: contiguous windows whose gap is ≤ N seconds collapse into one (de-overlaps the writer's per-tick fragmentation). Default 0 = no merge.",
          "default": 0
        }
      },
      "required": [
        "signal",
        "ticker"
      ]
    }
  },
  {
    "name": "tickerbot_post_signal_subscribe",
    "description": "POST /v2/signals/{signal}/subscribe. Create a webhook that fires when any ticker matches this signal. For boolean signals (e.g. `at_52w_high`, `golden_cross`), the predicate is `signal = true` and `condition` is ignored. For numeric signals (e.g. `rsi_14`, `change_1m`), `condition` is required and takes the shape `\">70\"`, `\"<30\"`, `\">=100\"`, `\"=50\"`. Optionally restrict the scope to a single ticker or a universe. Response includes a `test_url` — subscribing does NOT auto-fire; hit it to send a real-shape `webhook.fired` and validate your receiver.\n\n`{signal}` may be one of your custom signals, resolved as a boolean signal. The subscription freezes the signal SQL at creation — editing the signal later will not change it (re-subscribe to apply). See Create a custom signal.",
    "input_schema": {
      "type": "object",
      "properties": {
        "signal": {
          "type": "string",
          "description": "Signal name from the schema (case-insensitive)."
        },
        "condition": {
          "type": "string",
          "description": "Required for numeric signals; ignored for boolean signals. Shape: `\">70\"`, `\"<30\"`, `\">=100\"`, `\"=50\"`."
        },
        "ticker": {
          "type": "string",
          "description": "Restrict to a single ticker. Default: any ticker."
        },
        "universe": {
          "type": "string",
          "description": "Restrict to a system or user-owned universe (e.g. `top_100`)."
        },
        "target_url": {
          "type": "string",
          "description": "https:// URL to POST when fired. Omit for in-app delivery."
        },
        "channel": {
          "type": "string",
          "description": "Delivery channel. `webhook` (POST to `target_url`), `discord` (post an embed to `discord_url`), `in_app` (dashboard only), or `mobile_push` (notify a phone signed in to the Tickerbot mobile app; requires a `device_id` from `POST /v2/devices/register`). Inferred when omitted: `webhook` if `target_url` is set, `discord` if `discord_url` is set, else `in_app`. `slack` is reserved and returns `501`. See the Delivery channels guide.",
          "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`. Stored as a posting credential and stripped from list/get responses, which set `channel_config_present: true` instead."
        },
        "cadence": {
          "type": "string",
          "description": "`realtime` (the default) is evaluated on every data refresh (~1×/min); `hourly` and `nyse_open` throttle to a batch schedule. `1m` is a deprecated alias for `realtime`.",
          "enum": [
            "realtime",
            "hourly",
            "nyse_open"
          ]
        },
        "name": {
          "type": "string",
          "description": "Human-readable label (up to 80 chars)."
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated extra columns to include in each fired payload match row, beyond the standard set (`ticker`, `name`, `asset_type`, `price`, `day_change_pct`, `market_cap`). Each must be a real column; an unknown column is rejected at creation."
        }
      },
      "required": [
        "signal"
      ]
    }
  },
  {
    "name": "tickerbot_create_custom_signal",
    "description": "POST /v2/signals. Save a named boolean SQL predicate you can reference like a built-in signal. Pro and above. Persists a named **boolean** predicate you can reference anywhere a built-in signal goes. A custom signal is always boolean — the expression must evaluate to true/false (a non-boolean root is rejected at compile).\n\nOnce saved, the name resolves in these contexts, expanded to its SQL before the query runs:\n\n- **Scan** — inside `/v2/scan` `q` (live and `asof`), mixed freely with columns: `q = my_squeeze AND market_cap > 1e9`.\n- **Signal** — as the signal itself on `GET /v2/signals/{name}` (live and `asof`) and `GET /v2/signals/{name}/{ticker}/history`; it behaves like a built-in boolean signal (matches where the predicate is true, `condition` is ignored).\n- **Ticker** — inside a `POST /v2/tickers/{ticker}/subscribe` `condition`.\n- **Subscribe** — as the signal on `POST /v2/signals/{name}/subscribe`, or referenced from a scan/ticker subscribe `q`/`condition`.\n- **Composition** — inside another custom signal's `expr`; nested references are inlined, and recursion is detected.\n\nIt does **not** resolve on `GET /v2/signals/{name}/{ticker}/events` — occurrence spans are precomputed for built-in flags only, so that endpoint returns `400 not_supported_for_custom_signal` and points you at `/history` or `/scan?asof=`.\n\nResolution is an exact match against your own custom signal names. A name can never collide with a built-in column (create returns 409 `name_collision`), so the two never ambiguate; any identifier that isn't one of your custom signals is left as raw SQL.\n\n**Freeze on subscribe:** the subscribe endpoints expand the custom signal into the stored webhook query at creation time, so a subscription is a snapshot. Editing or deleting the custom signal later does **not** change existing subscriptions — re-subscribe to apply a new definition. Ad-hoc reads (scan/signal GETs) always reflect the current definition.\n\nThe expression is compiled at create time against the live column whitelist; references to other custom signals you own are inlined first. Names are scoped per-user: `(user_id, name)` is the key.\n\n**Pro tier for both create AND consume.** Custom signals are a Pro-plan feature end to end: creating one requires Pro, and so does *referencing* one — in a scan `q`, a ticker `condition`, as a signal name, or via history/stats/subscribe. A request that references a custom signal on a lower tier returns `403 custom_signals_tier_required` — including a downgraded account whose custom signals still exist (they can no longer be consumed). Free/Hobby can still list the `/v2/signals` catalog (metadata only).",
    "input_schema": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Slug — `^[a-z][a-z0-9_]{0,63}$`. Must not collide with any built-in column name."
        },
        "expr": {
          "type": "string",
          "description": "Boolean SQL predicate. May reference built-in columns and other custom signals you own. Must evaluate to true/false. Max 4000 chars."
        },
        "description": {
          "type": "string",
          "description": "Free-form notes. Max 500 chars."
        }
      },
      "required": [
        "name",
        "expr"
      ]
    }
  },
  {
    "name": "tickerbot_patch_custom_signal",
    "description": "PATCH /v2/signals/{name}. Edit the expression or description of one of your custom signals. Pro and above. Partial update — supply `expr`, `description`, or both. Providing `expr` recompiles against the live column whitelist (same validator as create). Built-in signals are read-only — only custom signals you own can be patched. Write access requires Pro or above; Free/Hobby return `403 custom_signals_tier_required`.\n\n**Existing subscriptions do not change.** Scan/signal/ticker `subscribe` endpoints freeze a custom signal's SQL into the webhook at creation time, so editing the signal here updates ad-hoc reads and *new* subscriptions but leaves already-created webhooks firing on the old definition. Re-subscribe to apply the change.",
    "input_schema": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Custom signal slug."
        },
        "expr": {
          "type": "string",
          "description": "New SQL expression. Re-validated and re-inlined against your other custom signals."
        },
        "description": {
          "type": "string",
          "description": "New description."
        }
      },
      "required": [
        "name"
      ]
    }
  },
  {
    "name": "tickerbot_delete_custom_signal",
    "description": "DELETE /v2/signals/{name}. Remove a custom signal — refused by default if anything references it. Pro and above. By default, DELETE is cascade-safe: it refuses with 409 `signal_referenced` if any of your other custom signals reference this signal by name. The response carries a `referencing_signals` array so you can audit before deciding.\n\nPass `?force=true` to delete anyway. Existing references will compile to `unknown_column` errors next time the consumer recompiles, so use force when you've already fixed the references. Write access requires Pro or above; Free/Hobby return `403 custom_signals_tier_required`.",
    "input_schema": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Custom signal slug."
        },
        "force": {
          "type": "boolean",
          "description": "When `true`, skip the reference check and delete. References will break on next recompile.",
          "default": "false"
        }
      },
      "required": [
        "name"
      ]
    }
  },
  {
    "name": "tickerbot_get_scan",
    "description": "GET /v2/scan. GET form of POST /v2/scan — same parameters, query string instead of a JSON body. Convenience GET that takes the same parameters as [`POST /v2/scan`](/docs/endpoints/scan/live) in the query string. Useful for short queries and quick interactive testing. For production code, long WHERE clauses, or when `q` contains characters that are awkward to URL-encode, prefer the POST form.",
    "input_schema": {
      "type": "object",
      "properties": {
        "q": {
          "type": "string",
          "description": "SQL WHERE expression. Max 4000 chars. Semicolons, comments, and write-side keywords (INSERT/UPDATE/DELETE/DROP/etc.) are rejected."
        },
        "universe": {
          "type": "string",
          "description": "Slug of a system universe (`top_10`, `top_100`) or one of your own (`/v2/universes`). Scopes the scan to those tickers. When omitted, the scan runs across all ~12,947 tracked tickers."
        },
        "asof": {
          "type": "string",
          "description": "Optional. Run the WHERE against historical daily state for the given `YYYY-MM-DD` (or full ISO). Plan history depth applies."
        },
        "order": {
          "type": "string",
          "description": "Column to sort by. Must be a valid identifier (lowercase letters, digits, underscore).",
          "default": "day_change_pct"
        },
        "dir": {
          "type": "string",
          "description": "Sort direction.",
          "enum": [
            "asc",
            "desc"
          ],
          "default": "desc"
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated list of extra columns to include in each result row. Default columns are always present: ticker, name, asset_type, price, day_change_pct, gap_pct, relative_volume, market_cap."
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 100.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor from the previous response."
        }
      },
      "required": [
        "q"
      ]
    }
  },
  {
    "name": "tickerbot_post_scan_asof",
    "description": "POST /v2/scan. Run a SQL WHERE against the close of a past trading day — same endpoint as live, with `asof` set. Time-travel form of [`POST /v2/scan`](/docs/endpoints/scan/live). Pass `\"asof\": \"YYYY-MM-DD\"` (or full ISO) in the body and the server evaluates your `q` against the daily-state snapshot of every ticker on that day. Same SQL grammar, same response envelope as live; the `_meta.resolution: \"daily\"` marker and `_meta.frozen_fields` list signal that the universe was reconstructed from historical state. The same endpoint also accepts GET with parameters in the query string — handy for short queries and quick interactive testing.",
    "input_schema": {
      "type": "object",
      "properties": {
        "q": {
          "type": "string",
          "description": "SQL WHERE expression. Same grammar as live scan. Max 4000 chars."
        },
        "asof": {
          "type": "string",
          "description": "Target date as `YYYY-MM-DD` or full ISO timestamp. The server evaluates the WHERE against the close-of-day snapshot for this date."
        },
        "universe": {
          "type": "string",
          "description": "Slug of a system or caller-owned universe. Scopes the scan to those tickers as of the same date."
        },
        "order": {
          "type": "string",
          "description": "Column to sort by. Same column whitelist as live scan.",
          "default": "day_change_pct"
        },
        "dir": {
          "type": "string",
          "description": "Sort direction.",
          "enum": [
            "asc",
            "desc"
          ],
          "default": "desc"
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated list of extra columns to include in each result row. Default columns are always present."
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 100.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor from the previous response. Pass the prior `next_cursor` to get the next page."
        }
      },
      "required": [
        "q",
        "asof"
      ]
    }
  },
  {
    "name": "tickerbot_get_scan_asof",
    "description": "GET /v2/scan. GET form of POST /v2/scan with `asof` — same parameters, query string instead of a JSON body. Convenience GET that takes the same parameters as [`POST /v2/scan` with `asof`](/docs/endpoints/scan/asof) in the query string. Useful for short queries and interactive testing — for production code or long WHERE clauses, prefer the POST form.",
    "input_schema": {
      "type": "object",
      "properties": {
        "q": {
          "type": "string",
          "description": "SQL WHERE expression. Same grammar as live scan. Max 4000 chars."
        },
        "asof": {
          "type": "string",
          "description": "Target date as `YYYY-MM-DD` or full ISO timestamp."
        },
        "universe": {
          "type": "string",
          "description": "Slug of a system or caller-owned universe."
        },
        "order": {
          "type": "string",
          "description": "Column to sort by.",
          "default": "day_change_pct"
        },
        "dir": {
          "type": "string",
          "description": "Sort direction.",
          "enum": [
            "asc",
            "desc"
          ],
          "default": "desc"
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated extra columns."
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 100.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor from the previous response."
        }
      },
      "required": [
        "q",
        "asof"
      ]
    }
  },
  {
    "name": "tickerbot_post_scan",
    "description": "POST /v2/scan. Run a SQL WHERE against the current universe. Filter the live ticker universe with a SQL WHERE clause expressed in our SQL grammar. Send `q` and the other parameters in a JSON body — no URL-encoding, no length limits to fight, and the same shape on every paging request. Columns in `q`, `order`, and `fields` are the customer-facing names listed on /docs/schema and /docs/flags. No translation layer. The same endpoint also accepts GET with parameters in the query string — handy for short queries and quick interactive testing.",
    "input_schema": {
      "type": "object",
      "properties": {
        "q": {
          "type": "string",
          "description": "SQL WHERE expression. Max 4000 chars. Semicolons, comments, and write-side keywords (INSERT/UPDATE/DELETE/DROP/etc.) are rejected."
        },
        "universe": {
          "type": "string",
          "description": "Slug of a system universe (`top_10`, `top_100`) or one of your own (`/v2/universes`). Scopes the scan to those tickers. When omitted, the scan runs across all ~12,947 tracked tickers."
        },
        "asof": {
          "type": "string",
          "description": "Optional. Run the WHERE against historical daily state for the given `YYYY-MM-DD` (or full ISO). Plan history depth applies. See [As-of scan](/docs/endpoints/scan/asof) for the dedicated reference."
        },
        "order": {
          "type": "string",
          "description": "Column to sort by. Must be a valid identifier (lowercase letters, digits, underscore).",
          "default": "day_change_pct"
        },
        "dir": {
          "type": "string",
          "description": "Sort direction.",
          "enum": [
            "asc",
            "desc"
          ],
          "default": "desc"
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated list of extra columns to include in each result row. Default columns are always present: ticker, name, asset_type, price, day_change_pct, gap_pct, relative_volume, market_cap."
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 100.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor from the previous response. Pass the prior `next_cursor` to get the next page."
        }
      },
      "required": [
        "q"
      ]
    }
  },
  {
    "name": "tickerbot_post_scan_subscribe",
    "description": "POST /v2/scan/subscribe. Create a webhook that fires when any ticker matches an arbitrary WHERE clause. Subscribes to a free-form `q` (same grammar as `POST /v2/scan`). The webhook delivers the match set every time it changes (tickers entering or leaving). Optionally scope to a `universe`. Max `q` length is 1500 chars. Response includes a `test_url` — subscribing does NOT auto-fire anything at your endpoint; call it to send a real-shape `webhook.fired` POST instantly and validate your receiver.\n\nCustom signals referenced in `q` are expanded and frozen into the webhook at creation — editing the signal later will not change this subscription (re-subscribe to apply). See Create a custom signal.",
    "input_schema": {
      "type": "object",
      "properties": {
        "q": {
          "type": "string",
          "description": "WHERE-clause expression using signal/column names. Max 1500 chars."
        },
        "universe": {
          "type": "string",
          "description": "System or user-owned universe to scope the scan (e.g. `top_100`)."
        },
        "target_url": {
          "type": "string",
          "description": "https:// URL to POST when the match set changes. Omit for in-app delivery."
        },
        "channel": {
          "type": "string",
          "description": "Delivery channel. `webhook` (POST to `target_url`), `discord` (post an embed to `discord_url`), `in_app` (dashboard only), or `mobile_push` (notify a phone signed in to the Tickerbot mobile app; requires a `device_id` from `POST /v2/devices/register`). Inferred when omitted: `webhook` if `target_url` is set, `discord` if `discord_url` is set, else `in_app`. `slack` is reserved and returns `501`. See the Delivery channels guide.",
          "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`. Stored as a posting credential and stripped from list/get responses, which set `channel_config_present: true` instead."
        },
        "cadence": {
          "type": "string",
          "description": "`realtime` (the default) is evaluated on every data refresh (~1×/min); `hourly` and `nyse_open` throttle to a batch schedule. `1m` is a deprecated alias for `realtime`.",
          "enum": [
            "realtime",
            "hourly",
            "nyse_open"
          ]
        },
        "name": {
          "type": "string",
          "description": "Human-readable label (up to 80 chars). Defaults to `scan: <q>`."
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated extra columns to include in each fired payload match row, beyond the standard set (`ticker`, `name`, `asset_type`, `price`, `day_change_pct`, `market_cap`). Each must be a real column; an unknown column is rejected at creation."
        }
      },
      "required": [
        "q"
      ]
    }
  },
  {
    "name": "tickerbot_list_analyst_events",
    "description": "GET /v2/analyst/events. Per-event analyst rating history — upgrades, downgrades, initiations, and price-target changes. Filter-based reader over the analyst_events archive (per-event analyst actions back to 2012). The same feed powers the `recent_*` analyst flags on /v2/scan and the consensus snapshot columns on the ticker object — this endpoint gives you the underlying discrete events.\n\nAll filter params are AND-combined, and at least one of `ticker`, `tickers`, `since`, `until`, `firm`, or `action` is required (an unfiltered full-history scan is rejected with 400). Rows come back newest-first (timestamp DESC, then event_id ASC); page older with the `cursor` from a prior response. When a cursor is supplied, every filter except `limit` is ignored.",
    "input_schema": {
      "type": "object",
      "properties": {
        "ticker": {
          "type": "string",
          "description": "Single-ticker filter. Takes precedence over `tickers`."
        },
        "tickers": {
          "type": "string",
          "description": "Comma-separated ticker filter. Max 50 symbols."
        },
        "since": {
          "type": "string",
          "description": "Events at or after this instant. ISO date or datetime."
        },
        "until": {
          "type": "string",
          "description": "Events strictly before this instant. ISO date or datetime."
        },
        "firm": {
          "type": "string",
          "description": "Case-insensitive analyst-firm match."
        },
        "action": {
          "type": "string",
          "description": "Exact rating-action filter.",
          "enum": [
            "upgrades",
            "downgrades",
            "initiates_coverage_on",
            "maintains",
            "reiterates",
            "assumes",
            "reinstates",
            "suspends"
          ]
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 1000 on Scale/Enterprise.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque pagination cursor from a prior response's `next_cursor`. Ignores all filters except `limit`."
        }
      }
    }
  },
  {
    "name": "tickerbot_list_news_scan",
    "description": "GET /v2/news/scan. SQL-style scan over the news_article archive. Returns articles or aggregate rollups. One endpoint, two shapes. Without `group_by`, each result is one article (default columns: `id`, `time_published`, `title`, `source`, `tickers`, `overall_sentiment_score`, `overall_sentiment_label`). With `group_by` set, results are aggregate rollups — default SELECT becomes `<group_by cols>, COUNT(*) AS volume`. Use the `tk` alias (auto-UNNEST of `tickers`) to roll up per ticker without writing the join.\n\n\"As-of\" is just a WHERE filter on `time_published` — there is no dedicated asof parameter.\n\nBoth `GET` and `POST` are supported; `POST` body takes the same parameter names for queries too long to fit in a URL.",
    "input_schema": {
      "type": "object",
      "properties": {
        "q": {
          "type": "string",
          "description": "WHERE clause over the news_article table. Max 4000 chars."
        },
        "select": {
          "type": "string",
          "description": "Comma-separated columns/expressions to return. Defaults to article columns (no `group_by`) or `<group_by cols>, COUNT(*) AS volume` (with `group_by`)."
        },
        "group_by": {
          "type": "string",
          "description": "Comma-separated group keys. Switches the response to aggregate rows. Use `tk` to roll up per ticker."
        },
        "having": {
          "type": "string",
          "description": "HAVING clause on the aggregate. Requires `group_by`."
        },
        "order": {
          "type": "string",
          "description": "Sort column or alias. Defaults to `time_published` (article rows) or `volume` (aggregate rows)."
        },
        "dir": {
          "type": "string",
          "description": "Sort direction.",
          "enum": [
            "asc",
            "desc"
          ],
          "default": "desc"
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 1000 on Scale/Enterprise.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque pagination cursor from a prior response's `next_cursor`."
        }
      },
      "required": [
        "q"
      ]
    }
  },
  {
    "name": "tickerbot_list_webhooks",
    "description": "GET /v2/webhooks. List every webhook subscription on this account. Returns webhook subscriptions for the current account (user-scoped, not key-scoped — every key on the same account sees the same registry). Ordered newest-first by `created_at`. Each row carries a `subscription_origin` discriminator naming which subscribe endpoint created it (`ticker` | `signal` | `scan`). Use `?status=` to filter; pass `next_cursor` back via `cursor=` to paginate.",
    "input_schema": {
      "type": "object",
      "properties": {
        "status": {
          "type": "string",
          "description": "Filter by status.",
          "enum": [
            "active",
            "disabled"
          ]
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 100.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor from the previous response."
        }
      }
    }
  },
  {
    "name": "tickerbot_get_webhook",
    "description": "GET /v2/webhooks/{id}. Fetch one webhook subscription. Returns the current state of a webhook subscription. Visibility is account-scoped (not key-scoped) — every API key on this account can fetch every webhook on this account. Webhooks owned by a different account return 404.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Webhook id returned by a subscribe endpoint (`POST /v2/tickers/{T}/subscribe`, etc.)."
        }
      },
      "required": [
        "id"
      ]
    }
  },
  {
    "name": "tickerbot_delete_webhook",
    "description": "DELETE /v2/webhooks/{id}. Delete a webhook subscription. Hard delete. Any pending deliveries already enqueued may still fire (they capture the target URL + signing secret at queue time). To pause-rather-than-delete, just stop accepting deliveries at your target; after enough consecutive failures the subscription flips to `disabled` and you can re-enable later via `POST /v2/webhooks/{id}/enable`.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Webhook id."
        }
      },
      "required": [
        "id"
      ]
    }
  },
  {
    "name": "tickerbot_list_webhook_deliveries",
    "description": "GET /v2/webhooks/{id}/deliveries. Inspect recent deliveries for a webhook. Returns the most-recent deliveries (pings and fires) for a webhook, newest-first by `created_at`. Useful for diagnosing failures and verifying signatures. The request body sent to your target is not stored on the delivery row — only the metadata (status, attempt, response code, error). Account-scoped: any key on the same account can read any webhook's deliveries.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Webhook id."
        },
        "status": {
          "type": "string",
          "description": "Filter by delivery status.",
          "enum": [
            "pending",
            "delivered",
            "permanent_failure"
          ]
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 100.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor."
        }
      },
      "required": [
        "id"
      ]
    }
  },
  {
    "name": "tickerbot_enable_webhook",
    "description": "POST /v2/webhooks/{id}/enable. Re-enable a disabled webhook. Flips a webhook back to `active` and clears its match-state cache so the next eval treats every currently-matching ticker as new and fires a single `webhook.fired`. Use after fixing whatever caused auto-disable. To validate your receiver before letting real fires hit it, call `POST /v2/webhooks/{id}/test`.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Webhook id."
        }
      },
      "required": [
        "id"
      ]
    }
  },
  {
    "name": "tickerbot_test_fire_webhook",
    "description": "POST /v2/webhooks/{id}/test. Send a real-shape `webhook.fired` POST to your endpoint, instantly. Fires a real-shape `webhook.fired` payload to the webhook's `target_url` synchronously. The request body is **byte-identical** to a real fire — same field set, same signing, same `X-Tickerbot-Signature` HMAC — so tools that introspect payload shape (n8n, Zapier, custom field mappers) lock the right schema the first time. The test marker rides in an `X-Tickerbot-Test: true` HTTP header (not the body), so receivers that don't care get a payload indistinguishable from production, and those that do can filter on the header. Use this any time to validate endpoint reachability, payload parsing, and signature verification.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Webhook id."
        }
      },
      "required": [
        "id"
      ]
    }
  },
  {
    "name": "tickerbot_list_universes",
    "description": "GET /v2/universes. List your universes. A universe is a named list of tickers you can reference from `/v2/scan?universe=`, `/v2/signals/{signal}?universe=`, and `/v2/tickers?universe=`, or pass as `universe_id` when subscribing on a resource. System universes (`top_10` and `top_100`, the top-N most-actively-traded tickers, rebalanced monthly) are available to every account at `GET /v2/universes/system`.",
    "input_schema": {
      "type": "object",
      "properties": {
        "limit": {
          "type": "integer",
          "description": "Page size. Max 100.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor from the previous response."
        }
      }
    }
  },
  {
    "name": "tickerbot_create_universe",
    "description": "POST /v2/universes. Create a universe. Create a named ticker list owned by the calling account. Body: `{ id?, name, description?, tickers }`. If `id` is omitted, a slug is generated.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Optional slug. Lowercase letters, digits, underscore. Must be unique within your account."
        },
        "name": {
          "type": "string",
          "description": "Human-readable label."
        },
        "description": {
          "type": "string",
          "description": "Free-form notes."
        },
        "tickers": {
          "type": "array",
          "description": "Ticker symbols. Validated against your plan scope + the active universe.",
          "items": {
            "type": "string"
          }
        }
      },
      "required": [
        "name",
        "tickers"
      ]
    }
  },
  {
    "name": "tickerbot_list_system_universes",
    "description": "GET /v2/universes/system. List system universes (top_10, top_100). System universes are read-only ticker lists rebalanced monthly by 30-day trailing dollar volume. Available to every account regardless of plan.",
    "input_schema": {
      "type": "object",
      "properties": {}
    }
  },
  {
    "name": "tickerbot_get_universe",
    "description": "GET /v2/universes/{id}. Fetch a single universe. Returns the universe doc. Use `top_10`/`top_100` to fetch a system universe; any other slug must be one your account owns.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Universe slug."
        }
      },
      "required": [
        "id"
      ]
    }
  },
  {
    "name": "tickerbot_patch_universe",
    "description": "PATCH /v2/universes/{id}. Update a universe. Update `name`, `description`, or `tickers`. To add/remove ticker subsets without replacing the full list, pass `{ add: [...], remove: [...] }` instead of `tickers`.",
    "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 /v2/universes/{id}. Delete a universe. Permanently delete one of your universes. System universes (`top_10`, `top_100`) cannot be deleted — they return `403 system_universe_immutable`. Subscribed webhooks that reference the deleted universe will fail on their next eval with `unknown_universe`; clean them up first via `/v2/webhooks/{id}` and re-subscribe.",
    "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.

Discovery endpoint
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": "GET /v2/tickers. List active tickers, paginated. Returns the universe of tracked symbols (~12,947 US equities + top 100 crypto by market cap) in alphabetical order with a slim payload (`ticker`, `name`, `asset_type`, `exchange`, `market_cap`). Use cursor pagination to walk the full list, or pass `?tickers=...` for bulk lookup of named symbols with full rows.",
  "input_schema": {
    "type": "object",
    "properties": {
      "tickers": {
        "type": "string",
        "description": "Comma-separated list of symbols (up to 50). When set, the response returns the full row for each requested symbol; pagination params are ignored. Symbols missing from our universe come back with `_not_found: true`."
      },
      "limit": {
        "type": "integer",
        "description": "Page size for the alphabetical walk. Max 1000.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor from the previous response's `next_cursor` field. Continues the walk from after that page."
      },
      "search": {
        "type": "string",
        "description": "Substring filter over `ticker` (case-insensitive) and `name`. When set, results are ordered by `market_cap DESC NULLS LAST`."
      },
      "asset_type": {
        "type": "string",
        "description": "Filter by asset type — typically `equity` or `crypto`. Matched case-insensitively against the stored value."
      },
      "exchange": {
        "type": "string",
        "description": "Filter by exchange code (uppercase). Common values: `XNYS`, `XNAS`, `BATS`."
      },
      "sector": {
        "type": "string",
        "description": "Filter by sector name. Exact match against the stored `sector` field."
      },
      "min_market_cap": {
        "type": "number",
        "description": "Minimum market cap (USD). Results are ordered by `market_cap DESC NULLS LAST` when set."
      }
    }
  }
}

tickerbot_get_ticker

GET /v2/tickers/{ticker}

{
  "name": "tickerbot_get_ticker",
  "description": "GET /v2/tickers/{ticker}. Get the full current state of one ticker, or a snapshot at a past date. Returns the entire ticker row. Every field on the schema page, including current values for all numeric signals and the current value of every boolean flag. Pass `?asof=YYYY-MM-DD` to return the row exactly as it was at the close of that day — see the dedicated [as-of snapshot](/docs/endpoints/tickers/history) page for frozen-fields and coverage semantics.",
  "input_schema": {
    "type": "object",
    "properties": {
      "ticker": {
        "type": "string",
        "description": "Ticker symbol. Case-insensitive. Equities: exchange symbol (`AAPL`). Crypto: bare symbol (`BTC`)."
      },
      "asof": {
        "type": "string",
        "description": "Optional. Returns the row as it was at the close of the given `YYYY-MM-DD` (or full ISO). Sourced from `signal_daily_state`. Every plan gets the full history window, Free included. Coverage note: price, technical indicators, and financial-statement fields reconstruct to the exact date; a few vendor-sourced fields (`forward_pe`, `short_interest`, ownership %, analyst estimates) are populated forward-only and may be `null` in older snapshots while historical backfill is in progress — a `null` there means \"not yet backfilled,\" not \"no value existed.\""
      }
    },
    "required": [
      "ticker"
    ]
  }
}

tickerbot_get_ticker_history

GET /v2/tickers/{ticker}/history

{
  "name": "tickerbot_get_ticker_history",
  "description": "GET /v2/tickers/{ticker}/history. Get the full ticker row as of a past date. Time-travel to any historical date and pull the whole wide row as it stood then, including the boolean flag values, technical indicators, and the most-recent fundamentals known on that date. Useful for \"what did we know\" reconstructions and replaying screens across history.",
  "input_schema": {
    "type": "object",
    "properties": {
      "ticker": {
        "type": "string",
        "description": "Ticker symbol."
      },
      "asof": {
        "type": "string",
        "description": "Target date as `YYYY-MM-DD` or full ISO timestamp `YYYY-MM-DDTHH:MM:SSZ`. A time component is truncated to the calendar day (`_meta.resolution: \"daily\"`). The response returns the most-recent daily snapshot on or before this date, with **no lookback limit** — it walks back as far as needed and 404s only if the ticker has no state at all on/before the date. (This differs from `/v2/scan?asof=` and `/v2/signals/{signal}?asof=`, which cap carry-forward at 14 days and return empty rather than 404.)"
      }
    },
    "required": [
      "ticker",
      "asof"
    ]
  }
}

tickerbot_get_ticker_bars

GET /v2/tickers/{ticker}/bars/{interval}

{
  "name": "tickerbot_get_ticker_bars",
  "description": "GET /v2/tickers/{ticker}/bars/{interval}. Open/high/low/close/volume bars for a symbol at a given interval. Simple OHLCV bars from 1-minute through daily, with point-in-time (`asof`) and back-paging (`before`/`limit`). `1d` and `1h` cover the full universe with full history; sub-hour intervals cover the active universe and back-fill on demand. Pass a comma-separated symbol list for a bulk response keyed by symbol.",
  "input_schema": {
    "type": "object",
    "properties": {
      "ticker": {
        "type": "string",
        "description": "Ticker symbol, or a comma-separated list (up to 50) for a bulk response keyed by symbol."
      },
      "interval": {
        "type": "string",
        "description": "Bar interval — one of `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 — back-paging."
      },
      "asof": {
        "type": "string",
        "description": "Return a single bar as of that date/timestamp (point-in-time)."
      },
      "cursor": {
        "type": "string",
        "description": "Continuation token from a prior response's `next_cursor`; sugar for `before`."
      }
    },
    "required": [
      "ticker",
      "interval"
    ]
  }
}

tickerbot_get_ticker_events

GET /v2/tickers/{ticker}/events

{
  "name": "tickerbot_get_ticker_events",
  "description": "GET /v2/tickers/{ticker}/events. Discrete event log for one ticker. Returns dividends, splits, and insider transactions for the ticker, newest-first, in a unified envelope. Each event has a `ts`, a `kind`, and a `payload` whose shape depends on the kind. (Analyst rating changes are a separate feed: `/v2/analyst/events`.)",
  "input_schema": {
    "type": "object",
    "properties": {
      "ticker": {
        "type": "string",
        "description": "Ticker symbol."
      },
      "kind": {
        "type": "string",
        "description": "Filter by event kind. Omit for all three merged into one timeline.",
        "enum": [
          "dividend",
          "split",
          "insider"
        ]
      },
      "before": {
        "type": "string",
        "description": "Return events strictly before this date (YYYY-MM-DD). Back-paging cursor; pass the previous response's `next_cursor`."
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 1000.",
        "default": 100
      }
    },
    "required": [
      "ticker"
    ]
  }
}

tickerbot_get_ticker_holdings

GET /v2/tickers/{ticker}/holdings

{
  "name": "tickerbot_get_ticker_holdings",
  "description": "GET /v2/tickers/{ticker}/holdings. Constituent holdings of an ETF, weight-ranked. Returns the ETF's constituents and their weights, heaviest first. When the ticker is not an ETF, `is_etf` is false and `holdings` is empty. The reverse lookup (\"which ETFs hold NVDA\") is a `/v2/scan` filter on the `etf_holders` column, not an endpoint.",
  "input_schema": {
    "type": "object",
    "properties": {
      "ticker": {
        "type": "string",
        "description": "ETF symbol."
      },
      "limit": {
        "type": "integer",
        "description": "Max holdings returned. Max 5000.",
        "default": 500
      }
    },
    "required": [
      "ticker"
    ]
  }
}

tickerbot_get_ticker_sectors

GET /v2/tickers/{ticker}/sectors

{
  "name": "tickerbot_get_ticker_sectors",
  "description": "GET /v2/tickers/{ticker}/sectors. Sector allocation of an ETF, weight-ranked. Returns the ETF's sector weights, heaviest first. When the ticker is not an ETF, `is_etf` is false and `sectors` is empty.",
  "input_schema": {
    "type": "object",
    "properties": {
      "ticker": {
        "type": "string",
        "description": "ETF symbol."
      }
    },
    "required": [
      "ticker"
    ]
  }
}

tickerbot_post_ticker_subscribe

POST /v2/tickers/{ticker}/subscribe

{
  "name": "tickerbot_post_ticker_subscribe",
  "description": "POST /v2/tickers/{ticker}/subscribe. Create a webhook that fires when this ticker matches a condition. Subscribes a single ticker. `condition` is a WHERE-clause fragment evaluated when this ticker is matched — you do not need to add `ticker = '…'` yourself; the endpoint scopes the predicate to the path ticker automatically. Plan-gated by webhook tier; counts against your account-wide webhook cap. Returns the same shape as `GET /v2/webhooks/{id}`, plus a `test_url` pointing at `POST /v2/webhooks/{id}/test` — subscribing does NOT auto-fire anything at your endpoint; hit the test URL to send a real-shape `webhook.fired` and validate your receiver.\n\nCustom signals referenced in `condition` are expanded and frozen into the webhook at creation — editing the signal later will not change this subscription (re-subscribe to apply). See Create a custom signal.",
  "input_schema": {
    "type": "object",
    "properties": {
      "ticker": {
        "type": "string",
        "description": "Ticker symbol (case-insensitive)."
      },
      "condition": {
        "type": "string",
        "description": "WHERE-clause fragment using signal/column names from the schema."
      },
      "target_url": {
        "type": "string",
        "description": "https:// URL to POST when the condition fires. Omit for in-app delivery (visible in the dashboard)."
      },
      "channel": {
        "type": "string",
        "description": "Delivery channel. `webhook` (POST to `target_url`), `discord` (post an embed to `discord_url`), `in_app` (dashboard only), or `mobile_push` (notify a phone signed in to the Tickerbot mobile app; requires a `device_id` from `POST /v2/devices/register`). Inferred when omitted: `webhook` if `target_url` is set, `discord` if `discord_url` is set, else `in_app`. `slack` is reserved and returns `501`. See the Delivery channels guide.",
        "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`. Stored as a posting credential and stripped from list/get responses, which set `channel_config_present: true` instead."
      },
      "cadence": {
        "type": "string",
        "description": "How often to evaluate. `realtime` (the default) is evaluated on every data refresh (~1×/min); `hourly` and `nyse_open` throttle to a batch schedule. `1m` is a deprecated alias for `realtime`.",
        "enum": [
          "realtime",
          "hourly",
          "nyse_open"
        ]
      },
      "name": {
        "type": "string",
        "description": "Human-readable label (up to 80 chars). Defaults to `<TICKER>: <condition>`."
      },
      "fields": {
        "type": "string",
        "description": "Comma-separated extra columns to include in each fired payload match row, beyond the standard set (`ticker`, `name`, `asset_type`, `price`, `day_change_pct`, `market_cap`). Each must be a real column; an unknown column is rejected at creation."
      }
    },
    "required": [
      "ticker",
      "condition"
    ]
  }
}

tickerbot_list_signals_catalog

GET /v2/signals

{
  "name": "tickerbot_list_signals_catalog",
  "description": "GET /v2/signals. Built-in signals plus your custom signals, each tagged with `kind`. Returns the unified signal catalog: every built-in column on the live schema (tagged `kind: 'builtin'`, carrying `name`, `type`, `source`) plus the caller's own custom signals (tagged `kind: 'expression'`, carrying `name`, `description`, `expr`, `created_at`, `updated_at`).\n\nPagination applies only to the custom-signal slice — built-ins are returned in their entirety on every page since they're a small fixed set.",
  "input_schema": {
    "type": "object",
    "properties": {
      "kind": {
        "type": "string",
        "description": "Filter by kind. Omit to return both.",
        "enum": [
          "builtin",
          "custom"
        ]
      },
      "limit": {
        "type": "integer",
        "description": "Page size for the custom-signal slice. Max 200.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor from a prior response."
      }
    }
  }
}

tickerbot_get_signals_match

GET /v2/signals/{signal}

{
  "name": "tickerbot_get_signals_match",
  "description": "GET /v2/signals/{signal}. Find tickers that match a signal right now, or at a past date with ?asof=. Return the set of tickers where the named signal is true (for boolean flags) or where the numeric value satisfies a single-bounded condition. Sorted by signal value descending for numerics, alphabetically by ticker for booleans. Pass `?asof=YYYY-MM-DD` to get the match set as it was at the close of that day — sourced from `signal_daily_state`.",
  "input_schema": {
    "type": "object",
    "properties": {
      "signal": {
        "type": "string",
        "description": "A column on `ticker`. Boolean flags (e.g. `golden_cross`, `above_sma_50`) are detected automatically; numerics (e.g. `rsi_14`, `market_cap`, `pe_ratio`) require a condition."
      },
      "condition": {
        "type": "string",
        "description": "Required for numeric signals. Single bound, format `<op><value>`. Operators: `>`, `>=`, `=`, `!=`, `<`, `<=`. Examples: `>70`, `<=200`, `!=0`. Ignored for boolean flags."
      },
      "asof": {
        "type": "string",
        "description": "Optional. Run the match against historical daily state at the close of the given `YYYY-MM-DD` (or full ISO). Every plan gets full history, Free included."
      },
      "universe": {
        "type": "string",
        "description": "Optional. Scope to a system or caller-owned universe slug."
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 200.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor from the previous response."
      }
    },
    "required": [
      "signal"
    ]
  }
}

tickerbot_get_signal_asof

GET /v2/signals/{signal}

{
  "name": "tickerbot_get_signal_asof",
  "description": "GET /v2/signals/{signal}. Run the match set at the close of a past date — same endpoint as live, with ?asof=. Time-travel form of `GET /v2/signals/{signal}`. Pass `?asof=YYYY-MM-DD` (or full ISO) and the server returns the set of tickers where the signal was true (booleans) or where the numeric value satisfied your `condition` at the close of that day. Sourced from `signal_daily_state`. Same response envelope as the live call, with `_meta.resolution: \"daily\"` and `_meta.frozen_fields` listing the columns whose values aren't historized.",
  "input_schema": {
    "type": "object",
    "properties": {
      "signal": {
        "type": "string",
        "description": "A column on `ticker`. Boolean flags are detected automatically; numerics require a `condition`."
      },
      "asof": {
        "type": "string",
        "description": "Target date as `YYYY-MM-DD` or full ISO timestamp. The server returns the match set as of the close of this day."
      },
      "condition": {
        "type": "string",
        "description": "Required for numeric signals. Single-bounded condition: `<op><value>`. Operators: `>`, `>=`, `=`, `!=`, `<`, `<=`. Ignored for boolean flags."
      },
      "universe": {
        "type": "string",
        "description": "Scope to a system or caller-owned universe slug."
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 200.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor from the previous response."
      }
    },
    "required": [
      "signal",
      "asof"
    ]
  }
}

tickerbot_get_signal_history

GET /v2/signals/{signal}/{ticker}/history/{interval}

{
  "name": "tickerbot_get_signal_history",
  "description": "GET /v2/signals/{signal}/{ticker}/history/{interval}. Time series of one signal for one ticker. Returns a sequence of `{t, v}` bars for the requested signal × ticker at the chosen resolution. `t` is an ISO timestamp (or `YYYY-MM-DD` for daily/weekly). `v` is the signal value at that bar (a number for numeric signals, `true`/`false` for boolean flags).",
  "input_schema": {
    "type": "object",
    "properties": {
      "signal": {
        "type": "string",
        "description": "A column on the wide-state tables (matches the schema page 1:1)."
      },
      "ticker": {
        "type": "string",
        "description": "Ticker symbol."
      },
      "interval": {
        "type": "string",
        "description": "Resolution. Daily-only signals (SMAs, RSI, MACD, fundamentals) are not stored at minute/hourly resolution; request `1d` for those. Minute (`1m`) history is computed for the ~1,000 most-liquid tickers only — a ticker outside that set returns sparse/empty; hourly (`1h`) and daily (`1d`) cover the full universe. `1q` (quarterly) is a distinct surface: it returns per-fiscal-quarter fundamentals for `eps`, `eps_estimate`, `eps_surprise`, `eps_surprise_pct`, `revenue`, `gross_profit`, and `free_cash_flow` (20+ quarters), with `fiscal_period` on each bar.",
        "enum": [
          "1m",
          "1h",
          "1d",
          "1w",
          "1q"
        ]
      },
      "from": {
        "type": "string",
        "description": "Earliest bar timestamp (inclusive). YYYY-MM-DD or ISO. Defaults to your plan's history window."
      },
      "to": {
        "type": "string",
        "description": "Latest bar timestamp (inclusive). YYYY-MM-DD or ISO. Defaults to \"now\"."
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 1000.",
        "default": 252
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor. Walks back in time page-by-page from the most-recent bar."
      }
    },
    "required": [
      "signal",
      "ticker",
      "interval"
    ]
  }
}

tickerbot_list_signal_events

GET /v2/signals/{signal}/{ticker}/events

{
  "name": "tickerbot_list_signal_events",
  "description": "GET /v2/signals/{signal}/{ticker}/events. Run-length-encoded occurrences — windows for state flags, points for event flags. Per-bar booleans (`history`) are useful for historical analysis; per-occurrence records are what you want for chart overlays and dashboards. This endpoint returns each *firing* of a signal on a ticker as a single row.\n\nFor **state**-style signals (`above_sma_50`, `in_uptrend`, `at_52w_high`, …) each row is a true-window: `started_at` is when the condition flipped false→true, `ended_at` is when it flipped back (or null if it's still true). `start_price` and `end_price` carry the price at the open/close of the window.\n\nFor **event**-style signals (`golden_cross`, `gap_up`, `breakout`, …) each row is a point firing: `started_at` is when the event fired and `ended_at` is the same value. `start_price` is the price at that bar.\n\n**This endpoint serves boolean flags only** — occurrence spans come from the precomputed `flag_events` table, which exists only for built-in boolean flags. A numeric field (`rsi_14`, `adx_14`, …) has no on/off windows: to see it over time use `…/history/{interval}`; to find the days a numeric threshold held use `/v2/scan?asof=`. Custom (expression) signals have no precomputed spans and are rejected with 400.",
  "input_schema": {
    "type": "object",
    "properties": {
      "signal": {
        "type": "string",
        "description": "Built-in boolean flag name (e.g. `above_sma_50`, `golden_cross`). Numeric fields are not valid here; custom signals return 400."
      },
      "ticker": {
        "type": "string",
        "description": "Ticker symbol (case-insensitive)."
      },
      "limit": {
        "type": "integer",
        "description": "Max rows returned, newest first. Clamped to 5000.",
        "default": 2000
      },
      "merge_gap_seconds": {
        "type": "integer",
        "description": "Interval-union: contiguous windows whose gap is ≤ N seconds collapse into one (de-overlaps the writer's per-tick fragmentation). Default 0 = no merge.",
        "default": 0
      }
    },
    "required": [
      "signal",
      "ticker"
    ]
  }
}

tickerbot_post_signal_subscribe

POST /v2/signals/{signal}/subscribe

{
  "name": "tickerbot_post_signal_subscribe",
  "description": "POST /v2/signals/{signal}/subscribe. Create a webhook that fires when any ticker matches this signal. For boolean signals (e.g. `at_52w_high`, `golden_cross`), the predicate is `signal = true` and `condition` is ignored. For numeric signals (e.g. `rsi_14`, `change_1m`), `condition` is required and takes the shape `\">70\"`, `\"<30\"`, `\">=100\"`, `\"=50\"`. Optionally restrict the scope to a single ticker or a universe. Response includes a `test_url` — subscribing does NOT auto-fire; hit it to send a real-shape `webhook.fired` and validate your receiver.\n\n`{signal}` may be one of your custom signals, resolved as a boolean signal. The subscription freezes the signal SQL at creation — editing the signal later will not change it (re-subscribe to apply). See Create a custom signal.",
  "input_schema": {
    "type": "object",
    "properties": {
      "signal": {
        "type": "string",
        "description": "Signal name from the schema (case-insensitive)."
      },
      "condition": {
        "type": "string",
        "description": "Required for numeric signals; ignored for boolean signals. Shape: `\">70\"`, `\"<30\"`, `\">=100\"`, `\"=50\"`."
      },
      "ticker": {
        "type": "string",
        "description": "Restrict to a single ticker. Default: any ticker."
      },
      "universe": {
        "type": "string",
        "description": "Restrict to a system or user-owned universe (e.g. `top_100`)."
      },
      "target_url": {
        "type": "string",
        "description": "https:// URL to POST when fired. Omit for in-app delivery."
      },
      "channel": {
        "type": "string",
        "description": "Delivery channel. `webhook` (POST to `target_url`), `discord` (post an embed to `discord_url`), `in_app` (dashboard only), or `mobile_push` (notify a phone signed in to the Tickerbot mobile app; requires a `device_id` from `POST /v2/devices/register`). Inferred when omitted: `webhook` if `target_url` is set, `discord` if `discord_url` is set, else `in_app`. `slack` is reserved and returns `501`. See the Delivery channels guide.",
        "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`. Stored as a posting credential and stripped from list/get responses, which set `channel_config_present: true` instead."
      },
      "cadence": {
        "type": "string",
        "description": "`realtime` (the default) is evaluated on every data refresh (~1×/min); `hourly` and `nyse_open` throttle to a batch schedule. `1m` is a deprecated alias for `realtime`.",
        "enum": [
          "realtime",
          "hourly",
          "nyse_open"
        ]
      },
      "name": {
        "type": "string",
        "description": "Human-readable label (up to 80 chars)."
      },
      "fields": {
        "type": "string",
        "description": "Comma-separated extra columns to include in each fired payload match row, beyond the standard set (`ticker`, `name`, `asset_type`, `price`, `day_change_pct`, `market_cap`). Each must be a real column; an unknown column is rejected at creation."
      }
    },
    "required": [
      "signal"
    ]
  }
}

tickerbot_create_custom_signal

POST /v2/signals

{
  "name": "tickerbot_create_custom_signal",
  "description": "POST /v2/signals. Save a named boolean SQL predicate you can reference like a built-in signal. Pro and above. Persists a named **boolean** predicate you can reference anywhere a built-in signal goes. A custom signal is always boolean — the expression must evaluate to true/false (a non-boolean root is rejected at compile).\n\nOnce saved, the name resolves in these contexts, expanded to its SQL before the query runs:\n\n- **Scan** — inside `/v2/scan` `q` (live and `asof`), mixed freely with columns: `q = my_squeeze AND market_cap > 1e9`.\n- **Signal** — as the signal itself on `GET /v2/signals/{name}` (live and `asof`) and `GET /v2/signals/{name}/{ticker}/history`; it behaves like a built-in boolean signal (matches where the predicate is true, `condition` is ignored).\n- **Ticker** — inside a `POST /v2/tickers/{ticker}/subscribe` `condition`.\n- **Subscribe** — as the signal on `POST /v2/signals/{name}/subscribe`, or referenced from a scan/ticker subscribe `q`/`condition`.\n- **Composition** — inside another custom signal's `expr`; nested references are inlined, and recursion is detected.\n\nIt does **not** resolve on `GET /v2/signals/{name}/{ticker}/events` — occurrence spans are precomputed for built-in flags only, so that endpoint returns `400 not_supported_for_custom_signal` and points you at `/history` or `/scan?asof=`.\n\nResolution is an exact match against your own custom signal names. A name can never collide with a built-in column (create returns 409 `name_collision`), so the two never ambiguate; any identifier that isn't one of your custom signals is left as raw SQL.\n\n**Freeze on subscribe:** the subscribe endpoints expand the custom signal into the stored webhook query at creation time, so a subscription is a snapshot. Editing or deleting the custom signal later does **not** change existing subscriptions — re-subscribe to apply a new definition. Ad-hoc reads (scan/signal GETs) always reflect the current definition.\n\nThe expression is compiled at create time against the live column whitelist; references to other custom signals you own are inlined first. Names are scoped per-user: `(user_id, name)` is the key.\n\n**Pro tier for both create AND consume.** Custom signals are a Pro-plan feature end to end: creating one requires Pro, and so does *referencing* one — in a scan `q`, a ticker `condition`, as a signal name, or via history/stats/subscribe. A request that references a custom signal on a lower tier returns `403 custom_signals_tier_required` — including a downgraded account whose custom signals still exist (they can no longer be consumed). Free/Hobby can still list the `/v2/signals` catalog (metadata only).",
  "input_schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "Slug — `^[a-z][a-z0-9_]{0,63}$`. Must not collide with any built-in column name."
      },
      "expr": {
        "type": "string",
        "description": "Boolean SQL predicate. May reference built-in columns and other custom signals you own. Must evaluate to true/false. Max 4000 chars."
      },
      "description": {
        "type": "string",
        "description": "Free-form notes. Max 500 chars."
      }
    },
    "required": [
      "name",
      "expr"
    ]
  }
}

tickerbot_patch_custom_signal

PATCH /v2/signals/{name}

{
  "name": "tickerbot_patch_custom_signal",
  "description": "PATCH /v2/signals/{name}. Edit the expression or description of one of your custom signals. Pro and above. Partial update — supply `expr`, `description`, or both. Providing `expr` recompiles against the live column whitelist (same validator as create). Built-in signals are read-only — only custom signals you own can be patched. Write access requires Pro or above; Free/Hobby return `403 custom_signals_tier_required`.\n\n**Existing subscriptions do not change.** Scan/signal/ticker `subscribe` endpoints freeze a custom signal's SQL into the webhook at creation time, so editing the signal here updates ad-hoc reads and *new* subscriptions but leaves already-created webhooks firing on the old definition. Re-subscribe to apply the change.",
  "input_schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "Custom signal slug."
      },
      "expr": {
        "type": "string",
        "description": "New SQL expression. Re-validated and re-inlined against your other custom signals."
      },
      "description": {
        "type": "string",
        "description": "New description."
      }
    },
    "required": [
      "name"
    ]
  }
}

tickerbot_delete_custom_signal

DELETE /v2/signals/{name}

{
  "name": "tickerbot_delete_custom_signal",
  "description": "DELETE /v2/signals/{name}. Remove a custom signal — refused by default if anything references it. Pro and above. By default, DELETE is cascade-safe: it refuses with 409 `signal_referenced` if any of your other custom signals reference this signal by name. The response carries a `referencing_signals` array so you can audit before deciding.\n\nPass `?force=true` to delete anyway. Existing references will compile to `unknown_column` errors next time the consumer recompiles, so use force when you've already fixed the references. Write access requires Pro or above; Free/Hobby return `403 custom_signals_tier_required`.",
  "input_schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "Custom signal slug."
      },
      "force": {
        "type": "boolean",
        "description": "When `true`, skip the reference check and delete. References will break on next recompile.",
        "default": "false"
      }
    },
    "required": [
      "name"
    ]
  }
}

tickerbot_get_scan

GET /v2/scan

{
  "name": "tickerbot_get_scan",
  "description": "GET /v2/scan. GET form of POST /v2/scan — same parameters, query string instead of a JSON body. Convenience GET that takes the same parameters as [`POST /v2/scan`](/docs/endpoints/scan/live) in the query string. Useful for short queries and quick interactive testing. For production code, long WHERE clauses, or when `q` contains characters that are awkward to URL-encode, prefer the POST form.",
  "input_schema": {
    "type": "object",
    "properties": {
      "q": {
        "type": "string",
        "description": "SQL WHERE expression. Max 4000 chars. Semicolons, comments, and write-side keywords (INSERT/UPDATE/DELETE/DROP/etc.) are rejected."
      },
      "universe": {
        "type": "string",
        "description": "Slug of a system universe (`top_10`, `top_100`) or one of your own (`/v2/universes`). Scopes the scan to those tickers. When omitted, the scan runs across all ~12,947 tracked tickers."
      },
      "asof": {
        "type": "string",
        "description": "Optional. Run the WHERE against historical daily state for the given `YYYY-MM-DD` (or full ISO). Plan history depth applies."
      },
      "order": {
        "type": "string",
        "description": "Column to sort by. Must be a valid identifier (lowercase letters, digits, underscore).",
        "default": "day_change_pct"
      },
      "dir": {
        "type": "string",
        "description": "Sort direction.",
        "enum": [
          "asc",
          "desc"
        ],
        "default": "desc"
      },
      "fields": {
        "type": "string",
        "description": "Comma-separated list of extra columns to include in each result row. Default columns are always present: ticker, name, asset_type, price, day_change_pct, gap_pct, relative_volume, market_cap."
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 100.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor from the previous response."
      }
    },
    "required": [
      "q"
    ]
  }
}

tickerbot_post_scan_asof

POST /v2/scan

{
  "name": "tickerbot_post_scan_asof",
  "description": "POST /v2/scan. Run a SQL WHERE against the close of a past trading day — same endpoint as live, with `asof` set. Time-travel form of [`POST /v2/scan`](/docs/endpoints/scan/live). Pass `\"asof\": \"YYYY-MM-DD\"` (or full ISO) in the body and the server evaluates your `q` against the daily-state snapshot of every ticker on that day. Same SQL grammar, same response envelope as live; the `_meta.resolution: \"daily\"` marker and `_meta.frozen_fields` list signal that the universe was reconstructed from historical state. The same endpoint also accepts GET with parameters in the query string — handy for short queries and quick interactive testing.",
  "input_schema": {
    "type": "object",
    "properties": {
      "q": {
        "type": "string",
        "description": "SQL WHERE expression. Same grammar as live scan. Max 4000 chars."
      },
      "asof": {
        "type": "string",
        "description": "Target date as `YYYY-MM-DD` or full ISO timestamp. The server evaluates the WHERE against the close-of-day snapshot for this date."
      },
      "universe": {
        "type": "string",
        "description": "Slug of a system or caller-owned universe. Scopes the scan to those tickers as of the same date."
      },
      "order": {
        "type": "string",
        "description": "Column to sort by. Same column whitelist as live scan.",
        "default": "day_change_pct"
      },
      "dir": {
        "type": "string",
        "description": "Sort direction.",
        "enum": [
          "asc",
          "desc"
        ],
        "default": "desc"
      },
      "fields": {
        "type": "string",
        "description": "Comma-separated list of extra columns to include in each result row. Default columns are always present."
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 100.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor from the previous response. Pass the prior `next_cursor` to get the next page."
      }
    },
    "required": [
      "q",
      "asof"
    ]
  }
}

tickerbot_get_scan_asof

GET /v2/scan

{
  "name": "tickerbot_get_scan_asof",
  "description": "GET /v2/scan. GET form of POST /v2/scan with `asof` — same parameters, query string instead of a JSON body. Convenience GET that takes the same parameters as [`POST /v2/scan` with `asof`](/docs/endpoints/scan/asof) in the query string. Useful for short queries and interactive testing — for production code or long WHERE clauses, prefer the POST form.",
  "input_schema": {
    "type": "object",
    "properties": {
      "q": {
        "type": "string",
        "description": "SQL WHERE expression. Same grammar as live scan. Max 4000 chars."
      },
      "asof": {
        "type": "string",
        "description": "Target date as `YYYY-MM-DD` or full ISO timestamp."
      },
      "universe": {
        "type": "string",
        "description": "Slug of a system or caller-owned universe."
      },
      "order": {
        "type": "string",
        "description": "Column to sort by.",
        "default": "day_change_pct"
      },
      "dir": {
        "type": "string",
        "description": "Sort direction.",
        "enum": [
          "asc",
          "desc"
        ],
        "default": "desc"
      },
      "fields": {
        "type": "string",
        "description": "Comma-separated extra columns."
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 100.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor from the previous response."
      }
    },
    "required": [
      "q",
      "asof"
    ]
  }
}

tickerbot_post_scan

POST /v2/scan

{
  "name": "tickerbot_post_scan",
  "description": "POST /v2/scan. Run a SQL WHERE against the current universe. Filter the live ticker universe with a SQL WHERE clause expressed in our SQL grammar. Send `q` and the other parameters in a JSON body — no URL-encoding, no length limits to fight, and the same shape on every paging request. Columns in `q`, `order`, and `fields` are the customer-facing names listed on /docs/schema and /docs/flags. No translation layer. The same endpoint also accepts GET with parameters in the query string — handy for short queries and quick interactive testing.",
  "input_schema": {
    "type": "object",
    "properties": {
      "q": {
        "type": "string",
        "description": "SQL WHERE expression. Max 4000 chars. Semicolons, comments, and write-side keywords (INSERT/UPDATE/DELETE/DROP/etc.) are rejected."
      },
      "universe": {
        "type": "string",
        "description": "Slug of a system universe (`top_10`, `top_100`) or one of your own (`/v2/universes`). Scopes the scan to those tickers. When omitted, the scan runs across all ~12,947 tracked tickers."
      },
      "asof": {
        "type": "string",
        "description": "Optional. Run the WHERE against historical daily state for the given `YYYY-MM-DD` (or full ISO). Plan history depth applies. See [As-of scan](/docs/endpoints/scan/asof) for the dedicated reference."
      },
      "order": {
        "type": "string",
        "description": "Column to sort by. Must be a valid identifier (lowercase letters, digits, underscore).",
        "default": "day_change_pct"
      },
      "dir": {
        "type": "string",
        "description": "Sort direction.",
        "enum": [
          "asc",
          "desc"
        ],
        "default": "desc"
      },
      "fields": {
        "type": "string",
        "description": "Comma-separated list of extra columns to include in each result row. Default columns are always present: ticker, name, asset_type, price, day_change_pct, gap_pct, relative_volume, market_cap."
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 100.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor from the previous response. Pass the prior `next_cursor` to get the next page."
      }
    },
    "required": [
      "q"
    ]
  }
}

tickerbot_post_scan_subscribe

POST /v2/scan/subscribe

{
  "name": "tickerbot_post_scan_subscribe",
  "description": "POST /v2/scan/subscribe. Create a webhook that fires when any ticker matches an arbitrary WHERE clause. Subscribes to a free-form `q` (same grammar as `POST /v2/scan`). The webhook delivers the match set every time it changes (tickers entering or leaving). Optionally scope to a `universe`. Max `q` length is 1500 chars. Response includes a `test_url` — subscribing does NOT auto-fire anything at your endpoint; call it to send a real-shape `webhook.fired` POST instantly and validate your receiver.\n\nCustom signals referenced in `q` are expanded and frozen into the webhook at creation — editing the signal later will not change this subscription (re-subscribe to apply). See Create a custom signal.",
  "input_schema": {
    "type": "object",
    "properties": {
      "q": {
        "type": "string",
        "description": "WHERE-clause expression using signal/column names. Max 1500 chars."
      },
      "universe": {
        "type": "string",
        "description": "System or user-owned universe to scope the scan (e.g. `top_100`)."
      },
      "target_url": {
        "type": "string",
        "description": "https:// URL to POST when the match set changes. Omit for in-app delivery."
      },
      "channel": {
        "type": "string",
        "description": "Delivery channel. `webhook` (POST to `target_url`), `discord` (post an embed to `discord_url`), `in_app` (dashboard only), or `mobile_push` (notify a phone signed in to the Tickerbot mobile app; requires a `device_id` from `POST /v2/devices/register`). Inferred when omitted: `webhook` if `target_url` is set, `discord` if `discord_url` is set, else `in_app`. `slack` is reserved and returns `501`. See the Delivery channels guide.",
        "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`. Stored as a posting credential and stripped from list/get responses, which set `channel_config_present: true` instead."
      },
      "cadence": {
        "type": "string",
        "description": "`realtime` (the default) is evaluated on every data refresh (~1×/min); `hourly` and `nyse_open` throttle to a batch schedule. `1m` is a deprecated alias for `realtime`.",
        "enum": [
          "realtime",
          "hourly",
          "nyse_open"
        ]
      },
      "name": {
        "type": "string",
        "description": "Human-readable label (up to 80 chars). Defaults to `scan: <q>`."
      },
      "fields": {
        "type": "string",
        "description": "Comma-separated extra columns to include in each fired payload match row, beyond the standard set (`ticker`, `name`, `asset_type`, `price`, `day_change_pct`, `market_cap`). Each must be a real column; an unknown column is rejected at creation."
      }
    },
    "required": [
      "q"
    ]
  }
}

tickerbot_list_analyst_events

GET /v2/analyst/events

{
  "name": "tickerbot_list_analyst_events",
  "description": "GET /v2/analyst/events. Per-event analyst rating history — upgrades, downgrades, initiations, and price-target changes. Filter-based reader over the analyst_events archive (per-event analyst actions back to 2012). The same feed powers the `recent_*` analyst flags on /v2/scan and the consensus snapshot columns on the ticker object — this endpoint gives you the underlying discrete events.\n\nAll filter params are AND-combined, and at least one of `ticker`, `tickers`, `since`, `until`, `firm`, or `action` is required (an unfiltered full-history scan is rejected with 400). Rows come back newest-first (timestamp DESC, then event_id ASC); page older with the `cursor` from a prior response. When a cursor is supplied, every filter except `limit` is ignored.",
  "input_schema": {
    "type": "object",
    "properties": {
      "ticker": {
        "type": "string",
        "description": "Single-ticker filter. Takes precedence over `tickers`."
      },
      "tickers": {
        "type": "string",
        "description": "Comma-separated ticker filter. Max 50 symbols."
      },
      "since": {
        "type": "string",
        "description": "Events at or after this instant. ISO date or datetime."
      },
      "until": {
        "type": "string",
        "description": "Events strictly before this instant. ISO date or datetime."
      },
      "firm": {
        "type": "string",
        "description": "Case-insensitive analyst-firm match."
      },
      "action": {
        "type": "string",
        "description": "Exact rating-action filter.",
        "enum": [
          "upgrades",
          "downgrades",
          "initiates_coverage_on",
          "maintains",
          "reiterates",
          "assumes",
          "reinstates",
          "suspends"
        ]
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 1000 on Scale/Enterprise.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque pagination cursor from a prior response's `next_cursor`. Ignores all filters except `limit`."
      }
    }
  }
}

tickerbot_list_news_scan

GET /v2/news/scan

{
  "name": "tickerbot_list_news_scan",
  "description": "GET /v2/news/scan. SQL-style scan over the news_article archive. Returns articles or aggregate rollups. One endpoint, two shapes. Without `group_by`, each result is one article (default columns: `id`, `time_published`, `title`, `source`, `tickers`, `overall_sentiment_score`, `overall_sentiment_label`). With `group_by` set, results are aggregate rollups — default SELECT becomes `<group_by cols>, COUNT(*) AS volume`. Use the `tk` alias (auto-UNNEST of `tickers`) to roll up per ticker without writing the join.\n\n\"As-of\" is just a WHERE filter on `time_published` — there is no dedicated asof parameter.\n\nBoth `GET` and `POST` are supported; `POST` body takes the same parameter names for queries too long to fit in a URL.",
  "input_schema": {
    "type": "object",
    "properties": {
      "q": {
        "type": "string",
        "description": "WHERE clause over the news_article table. Max 4000 chars."
      },
      "select": {
        "type": "string",
        "description": "Comma-separated columns/expressions to return. Defaults to article columns (no `group_by`) or `<group_by cols>, COUNT(*) AS volume` (with `group_by`)."
      },
      "group_by": {
        "type": "string",
        "description": "Comma-separated group keys. Switches the response to aggregate rows. Use `tk` to roll up per ticker."
      },
      "having": {
        "type": "string",
        "description": "HAVING clause on the aggregate. Requires `group_by`."
      },
      "order": {
        "type": "string",
        "description": "Sort column or alias. Defaults to `time_published` (article rows) or `volume` (aggregate rows)."
      },
      "dir": {
        "type": "string",
        "description": "Sort direction.",
        "enum": [
          "asc",
          "desc"
        ],
        "default": "desc"
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 1000 on Scale/Enterprise.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque pagination cursor from a prior response's `next_cursor`."
      }
    },
    "required": [
      "q"
    ]
  }
}

tickerbot_list_webhooks

GET /v2/webhooks

{
  "name": "tickerbot_list_webhooks",
  "description": "GET /v2/webhooks. List every webhook subscription on this account. Returns webhook subscriptions for the current account (user-scoped, not key-scoped — every key on the same account sees the same registry). Ordered newest-first by `created_at`. Each row carries a `subscription_origin` discriminator naming which subscribe endpoint created it (`ticker` | `signal` | `scan`). Use `?status=` to filter; pass `next_cursor` back via `cursor=` to paginate.",
  "input_schema": {
    "type": "object",
    "properties": {
      "status": {
        "type": "string",
        "description": "Filter by status.",
        "enum": [
          "active",
          "disabled"
        ]
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 100.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor from the previous response."
      }
    }
  }
}

tickerbot_get_webhook

GET /v2/webhooks/{id}

{
  "name": "tickerbot_get_webhook",
  "description": "GET /v2/webhooks/{id}. Fetch one webhook subscription. Returns the current state of a webhook subscription. Visibility is account-scoped (not key-scoped) — every API key on this account can fetch every webhook on this account. Webhooks owned by a different account return 404.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Webhook id returned by a subscribe endpoint (`POST /v2/tickers/{T}/subscribe`, etc.)."
      }
    },
    "required": [
      "id"
    ]
  }
}

tickerbot_delete_webhook

DELETE /v2/webhooks/{id}

{
  "name": "tickerbot_delete_webhook",
  "description": "DELETE /v2/webhooks/{id}. Delete a webhook subscription. Hard delete. Any pending deliveries already enqueued may still fire (they capture the target URL + signing secret at queue time). To pause-rather-than-delete, just stop accepting deliveries at your target; after enough consecutive failures the subscription flips to `disabled` and you can re-enable later via `POST /v2/webhooks/{id}/enable`.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Webhook id."
      }
    },
    "required": [
      "id"
    ]
  }
}

tickerbot_list_webhook_deliveries

GET /v2/webhooks/{id}/deliveries

{
  "name": "tickerbot_list_webhook_deliveries",
  "description": "GET /v2/webhooks/{id}/deliveries. Inspect recent deliveries for a webhook. Returns the most-recent deliveries (pings and fires) for a webhook, newest-first by `created_at`. Useful for diagnosing failures and verifying signatures. The request body sent to your target is not stored on the delivery row — only the metadata (status, attempt, response code, error). Account-scoped: any key on the same account can read any webhook's deliveries.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Webhook id."
      },
      "status": {
        "type": "string",
        "description": "Filter by delivery status.",
        "enum": [
          "pending",
          "delivered",
          "permanent_failure"
        ]
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 100.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor."
      }
    },
    "required": [
      "id"
    ]
  }
}

tickerbot_enable_webhook

POST /v2/webhooks/{id}/enable

{
  "name": "tickerbot_enable_webhook",
  "description": "POST /v2/webhooks/{id}/enable. Re-enable a disabled webhook. Flips a webhook back to `active` and clears its match-state cache so the next eval treats every currently-matching ticker as new and fires a single `webhook.fired`. Use after fixing whatever caused auto-disable. To validate your receiver before letting real fires hit it, call `POST /v2/webhooks/{id}/test`.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Webhook id."
      }
    },
    "required": [
      "id"
    ]
  }
}

tickerbot_test_fire_webhook

POST /v2/webhooks/{id}/test

{
  "name": "tickerbot_test_fire_webhook",
  "description": "POST /v2/webhooks/{id}/test. Send a real-shape `webhook.fired` POST to your endpoint, instantly. Fires a real-shape `webhook.fired` payload to the webhook's `target_url` synchronously. The request body is **byte-identical** to a real fire — same field set, same signing, same `X-Tickerbot-Signature` HMAC — so tools that introspect payload shape (n8n, Zapier, custom field mappers) lock the right schema the first time. The test marker rides in an `X-Tickerbot-Test: true` HTTP header (not the body), so receivers that don't care get a payload indistinguishable from production, and those that do can filter on the header. Use this any time to validate endpoint reachability, payload parsing, and signature verification.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Webhook id."
      }
    },
    "required": [
      "id"
    ]
  }
}

tickerbot_list_universes

GET /v2/universes

{
  "name": "tickerbot_list_universes",
  "description": "GET /v2/universes. List your universes. A universe is a named list of tickers you can reference from `/v2/scan?universe=`, `/v2/signals/{signal}?universe=`, and `/v2/tickers?universe=`, or pass as `universe_id` when subscribing on a resource. System universes (`top_10` and `top_100`, the top-N most-actively-traded tickers, rebalanced monthly) are available to every account at `GET /v2/universes/system`.",
  "input_schema": {
    "type": "object",
    "properties": {
      "limit": {
        "type": "integer",
        "description": "Page size. Max 100.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor from the previous response."
      }
    }
  }
}

tickerbot_create_universe

POST /v2/universes

{
  "name": "tickerbot_create_universe",
  "description": "POST /v2/universes. Create a universe. Create a named ticker list owned by the calling account. Body: `{ id?, name, description?, tickers }`. If `id` is omitted, a slug is generated.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Optional slug. Lowercase letters, digits, underscore. Must be unique within your account."
      },
      "name": {
        "type": "string",
        "description": "Human-readable label."
      },
      "description": {
        "type": "string",
        "description": "Free-form notes."
      },
      "tickers": {
        "type": "array",
        "description": "Ticker symbols. Validated against your plan scope + the active universe.",
        "items": {
          "type": "string"
        }
      }
    },
    "required": [
      "name",
      "tickers"
    ]
  }
}

tickerbot_list_system_universes

GET /v2/universes/system

{
  "name": "tickerbot_list_system_universes",
  "description": "GET /v2/universes/system. List system universes (top_10, top_100). System universes are read-only ticker lists rebalanced monthly by 30-day trailing dollar volume. Available to every account regardless of plan.",
  "input_schema": {
    "type": "object",
    "properties": {}
  }
}

tickerbot_get_universe

GET /v2/universes/{id}

{
  "name": "tickerbot_get_universe",
  "description": "GET /v2/universes/{id}. Fetch a single universe. Returns the universe doc. Use `top_10`/`top_100` to fetch a system universe; any other slug must be one your account owns.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Universe slug."
      }
    },
    "required": [
      "id"
    ]
  }
}

tickerbot_patch_universe

PATCH /v2/universes/{id}

{
  "name": "tickerbot_patch_universe",
  "description": "PATCH /v2/universes/{id}. Update a universe. Update `name`, `description`, or `tickers`. To add/remove ticker subsets without replacing the full list, pass `{ add: [...], remove: [...] }` instead of `tickers`.",
  "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 /v2/universes/{id}. Delete a universe. Permanently delete one of your universes. System universes (`top_10`, `top_100`) cannot be deleted — they return `403 system_universe_immutable`. Subscribed webhooks that reference the deleted universe will fail on their next eval with `unknown_universe`; clean them up first via `/v2/webhooks/{id}` and re-subscribe.",
  "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.