@tickerbot/mcp-server 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tools.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // OFFLINE FALLBACK tool registry for the Tickerbot MCP server.
2
2
  //
3
+ // GENERATED by scripts/regen-tools.mjs — do not edit by hand.
4
+ //
3
5
  // The canonical tool list lives in main_service/mcp/tools.js on the
4
6
  // server. At startup, this package fetches `${baseUrl}/mcp/tools` and
5
7
  // uses the live catalog — see src/index.ts. This file is the snapshot
@@ -9,388 +11,1642 @@
9
11
  //
10
12
  // Staleness here is fine. Updating this file does NOT change behavior
11
13
  // for online users — they always see whatever main_service is serving.
12
- // Refresh this snapshot by running the regen script when the live
13
- // catalog has drifted enough that you want offline users to see the
14
- // newer version. No need to republish for every catalog tweak.
14
+ // Refresh with: node scripts/regen-tools.mjs (or --live)
15
15
  export const tools = [
16
- // ── Tickers ──────────────────────────────────────────────────────────
17
- {
18
- name: 'tickerbot_list_tickers',
19
- description: 'List active tickers from the Tickerbot universe (~12,000 US equities + top 100 crypto). Use `tickers` for bulk lookup of named symbols (returns full rows); otherwise walks the universe alphabetically with `cursor` pagination. Supports filters: search, asset_type, exchange, sector, min_market_cap.',
20
- inputSchema: {
21
- type: 'object',
22
- properties: {
23
- tickers: { type: 'string', description: 'Comma-separated symbols (max 50). When set, returns full rows for these symbols and pagination params are ignored.' },
24
- limit: { type: 'integer', description: 'Page size. Max 1000. Default 50.' },
25
- cursor: { type: 'string', description: 'Opaque cursor from a prior response.' },
26
- search: { type: 'string', description: 'Case-insensitive substring filter on ticker/name. Orders results by market_cap desc.' },
27
- asset_type: { type: 'string', description: 'Filter by asset type (e.g. "equity", "crypto").' },
28
- exchange: { type: 'string', description: 'Filter by exchange (e.g. "XNYS", "XNAS", "BATS").' },
29
- sector: { type: 'string', description: 'Exact-match sector filter (e.g. "Technology").' },
30
- min_market_cap: { type: 'number', description: 'Minimum market cap in USD. Orders results by market_cap desc.' },
16
+ {
17
+ "name": "tickerbot_list_tickers",
18
+ "description": "List active tickers from the Tickerbot universe (~14,500 US equities, plus the US Treasury curve and Fed policy rates under `R:`, and major FX pairs, spot metals and crypto under `X:`). Use `tickers` for bulk lookup of named symbols (returns full rows); otherwise walks the universe alphabetically with `cursor` pagination. Supports filters: search, asset_type, exchange, sector, min_market_cap.",
19
+ "inputSchema": {
20
+ "type": "object",
21
+ "properties": {
22
+ "tickers": {
23
+ "type": "string",
24
+ "description": "Comma-separated symbols (max 50). When set, returns full rows for these symbols and pagination params are ignored."
25
+ },
26
+ "limit": {
27
+ "type": "integer",
28
+ "description": "Page size. Max 1000. Default 50."
29
+ },
30
+ "cursor": {
31
+ "type": "string",
32
+ "description": "Opaque cursor from a prior response."
33
+ },
34
+ "search": {
35
+ "type": "string",
36
+ "description": "Case-insensitive substring filter on ticker/name. Orders results by market_cap desc."
37
+ },
38
+ "asset_type": {
39
+ "type": "string",
40
+ "description": "Filter by INSTRUMENT TYPE within equities: the stored value (CS, ETF, ADRC, PFD, FUND, UNIT, SP, ETS, WARRANT, RIGHT, ETN, ETV), or \"equity\" for the equity-like set. This is not an asset class — `asset_type=crypto` is rejected; crypto is in the main list under its X: symbols."
41
+ },
42
+ "exchange": {
43
+ "type": "string",
44
+ "description": "Filter by exchange (e.g. \"XNYS\", \"XNAS\", \"BATS\")."
45
+ },
46
+ "sector": {
47
+ "type": "string",
48
+ "description": "Exact-match sector filter (e.g. \"Technology\")."
49
+ },
50
+ "min_market_cap": {
51
+ "type": "number",
52
+ "description": "Minimum market cap in USD. Orders results by market_cap desc."
53
+ },
54
+ "universe": {
55
+ "type": "string",
56
+ "description": "Restrict to a universe slug (top_10, top_100, or a saved one)."
57
+ },
58
+ "asset_class": {
59
+ "type": "string",
60
+ "description": "Filter by asset class — `stocks`, `rates`, `crypto`, `fx`, or a comma-separated list. Omit for every class. Distinct from asset_type (the instrument type within equities)."
61
+ }
62
+ }
63
+ },
64
+ "endpoint": {
65
+ "method": "GET",
66
+ "path": "/v2/tickers",
67
+ "paramLocation": {
68
+ "tickers": "query",
69
+ "limit": "query",
70
+ "cursor": "query",
71
+ "search": "query",
72
+ "asset_type": "query",
73
+ "exchange": "query",
74
+ "sector": "query",
75
+ "min_market_cap": "query",
76
+ "universe": "query",
77
+ "asset_class": "query"
78
+ }
79
+ }
80
+ },
81
+ {
82
+ "name": "tickerbot_get_ticker",
83
+ "description": "Get the full current row for one ticker — every column on the schema (price, change, indicators like rsi_14, every boolean flag like above_sma_50, fundamentals like pe_ratio). Pass `asof` (YYYY-MM-DD or ISO timestamp like 2026-07-20T15:30:00Z) for the row as it stood at that past moment — date-only gives the close of that day.",
84
+ "inputSchema": {
85
+ "type": "object",
86
+ "properties": {
87
+ "ticker": {
88
+ "type": "string",
89
+ "description": "Symbol. Case-insensitive. Equities: bare symbol (AAPL). Crypto: X-prefixed pair (X:BTCUSD) — bare BTC/ETH are US-listed ETFs, not spot crypto."
90
+ },
91
+ "asof": {
92
+ "type": "string",
93
+ "description": "Optional YYYY-MM-DD or ISO timestamp. Date-only returns the row at close of that day; a timestamp returns the row as of that moment (finest tier covering each column)."
94
+ }
31
95
  },
96
+ "required": [
97
+ "ticker"
98
+ ]
32
99
  },
33
- endpoint: {
34
- method: 'GET',
35
- path: '/v2/tickers',
36
- paramLocation: {
37
- tickers: 'query', limit: 'query', cursor: 'query', search: 'query',
38
- asset_type: 'query', exchange: 'query', sector: 'query', min_market_cap: 'query',
100
+ "endpoint": {
101
+ "method": "GET",
102
+ "path": "/v2/tickers/{ticker}",
103
+ "paramLocation": {
104
+ "ticker": "path",
105
+ "asof": "query"
106
+ }
107
+ }
108
+ },
109
+ {
110
+ "name": "tickerbot_get_ticker_history",
111
+ "description": "Time-travel SNAPSHOT: the full wide row for one ticker as it stood at a past date (one row, not a series — for a multi-column time SERIES use tickerbot_get_series). Returns indicators, boolean flags, and the most-recent fundamentals known on that date. Unlimited depth on every plan.",
112
+ "inputSchema": {
113
+ "type": "object",
114
+ "properties": {
115
+ "ticker": {
116
+ "type": "string",
117
+ "description": "Symbol."
118
+ },
119
+ "asof": {
120
+ "type": "string",
121
+ "description": "Target date as YYYY-MM-DD or full ISO timestamp."
122
+ }
39
123
  },
124
+ "required": [
125
+ "ticker",
126
+ "asof"
127
+ ]
40
128
  },
129
+ "endpoint": {
130
+ "method": "GET",
131
+ "path": "/v2/tickers/{ticker}/history",
132
+ "paramLocation": {
133
+ "ticker": "path",
134
+ "asof": "query"
135
+ }
136
+ }
41
137
  },
42
138
  {
43
- name: 'tickerbot_get_ticker',
44
- description: 'Get the full current row for one ticker — every column on the schema (price, change, indicators like rsi_14, every boolean flag like above_sma_50, fundamentals like pe_ratio). Pass `asof` (YYYY-MM-DD) for the row as it stood at the close of a past trading day.',
45
- inputSchema: {
46
- type: 'object',
47
- properties: {
48
- ticker: { type: 'string', description: 'Symbol. Case-insensitive. Equities: bare symbol (AAPL). Crypto: bare symbol (BTC).' },
49
- asof: { type: 'string', description: 'Optional YYYY-MM-DD. Returns the row at close of that day.' },
139
+ "name": "tickerbot_get_series",
140
+ "description": "THE series primitive: cross-ticker, multi-column time series on one aligned grid. Pick up to 25 columns (price, OHLCV, indicators like rsi_14, boolean flags, custom signals) and up to 50 tickers; get one flat row per ticker per interval step ({ticker, t, price, rsi_14, …}), cursor-paged backward. `transitions_only=true` with boolean columns returns only the rows where a flag CHANGED — \"every golden_cross flip this year\" in one call. All-time on every plan. Replaces looping asof snapshots per date, and replaces the sunset per-ticker history routes.",
141
+ "inputSchema": {
142
+ "type": "object",
143
+ "properties": {
144
+ "ticker": {
145
+ "type": "string",
146
+ "description": "Single symbol (alias of `tickers`, wins when both are set). One of ticker/tickers is required."
147
+ },
148
+ "tickers": {
149
+ "type": "string",
150
+ "description": "Comma-separated symbols, max 50, all sharing one time grid. One of ticker/tickers is required."
151
+ },
152
+ "columns": {
153
+ "type": "string",
154
+ "description": "Comma list of columns (max 25). `fields` is a permanent alias. Defaults to a small set intersected with the interval's schema (intraday tiers carry fewer columns than daily — e.g. market_cap is daily-only)."
155
+ },
156
+ "interval": {
157
+ "type": "string",
158
+ "description": "Grid granularity. `1w` weekly, `1q` fiscal-quarterly (fundamentals).",
159
+ "enum": [
160
+ "1m",
161
+ "1h",
162
+ "1d",
163
+ "1w",
164
+ "1q"
165
+ ]
166
+ },
167
+ "from": {
168
+ "type": "string",
169
+ "description": "Earliest timestamp (inclusive), YYYY-MM-DD or ISO."
170
+ },
171
+ "to": {
172
+ "type": "string",
173
+ "description": "Latest timestamp (inclusive; a bare date means through that day)."
174
+ },
175
+ "transitions_only": {
176
+ "type": "boolean",
177
+ "description": "Only rows where a boolean column changed value (requires at least one boolean column). Each row carries `transition_drivers` naming the flags that flipped."
178
+ },
179
+ "limit": {
180
+ "type": "integer",
181
+ "description": "Rows per page. Max 1000. Default 252."
182
+ },
183
+ "cursor": {
184
+ "type": "string",
185
+ "description": "Opaque cursor from a prior response — pages older."
186
+ }
50
187
  },
51
- required: ['ticker'],
188
+ "required": []
52
189
  },
53
- endpoint: {
54
- method: 'GET',
55
- path: '/v2/tickers/{ticker}',
56
- paramLocation: { ticker: 'path', asof: 'query' },
190
+ "endpoint": {
191
+ "method": "GET",
192
+ "path": "/v2/series",
193
+ "paramLocation": {
194
+ "ticker": "query",
195
+ "tickers": "query",
196
+ "columns": "query",
197
+ "interval": "query",
198
+ "from": "query",
199
+ "to": "query",
200
+ "transitions_only": "query",
201
+ "limit": "query",
202
+ "cursor": "query"
203
+ }
204
+ }
205
+ },
206
+ {
207
+ "name": "tickerbot_get_ticker_coverage",
208
+ "description": "Data-coverage report for one ticker — which intervals and columns exist, from when, at what depth. The honesty endpoint: ask this before assuming a gap in bars/series is a data outage vs. genuinely-never-covered (e.g. sub-hour bars outside the active universe, fundamentals on non-equities).",
209
+ "inputSchema": {
210
+ "type": "object",
211
+ "properties": {
212
+ "ticker": {
213
+ "type": "string",
214
+ "description": "Symbol."
215
+ }
216
+ },
217
+ "required": [
218
+ "ticker"
219
+ ]
57
220
  },
221
+ "endpoint": {
222
+ "method": "GET",
223
+ "path": "/v2/tickers/{ticker}/coverage",
224
+ "paramLocation": {
225
+ "ticker": "path"
226
+ }
227
+ }
58
228
  },
59
229
  {
60
- name: 'tickerbot_get_ticker_history',
61
- description: 'Time-travel: get the full wide row for one ticker as it stood at a past date. Returns indicators, boolean flags, and the most-recent fundamentals known on that date. Use for backtests and reconstructions.',
62
- inputSchema: {
63
- type: 'object',
64
- properties: {
65
- ticker: { type: 'string', description: 'Symbol.' },
66
- asof: { type: 'string', description: 'Target date as YYYY-MM-DD or full ISO timestamp. Returns the most-recent daily snapshot on or before this date.' },
230
+ "name": "tickerbot_get_ticker_bars",
231
+ "description": "Get OHLCV (open/high/low/close/volume) bars for one or more symbols at a given interval, oldest-first. Pass a comma-separated `ticker` list (up to 50) for a bulk response keyed by symbol. Use `asof` for a single point-in-time bar, or `before`+`limit` to back-page. `1d`/`1h` cover the full universe with full history; sub-hour intervals back-fill on demand.",
232
+ "inputSchema": {
233
+ "type": "object",
234
+ "properties": {
235
+ "ticker": {
236
+ "type": "string",
237
+ "description": "Symbol, or comma-separated list (up to 50) for a bulk response keyed by symbol."
238
+ },
239
+ "interval": {
240
+ "type": "string",
241
+ "description": "Bar interval.",
242
+ "enum": [
243
+ "1s",
244
+ "1m",
245
+ "5m",
246
+ "15m",
247
+ "30m",
248
+ "1h",
249
+ "1d"
250
+ ]
251
+ },
252
+ "limit": {
253
+ "type": "integer",
254
+ "description": "Most-recent N bars. Default 100."
255
+ },
256
+ "before": {
257
+ "type": "string",
258
+ "description": "Return the N bars ending strictly before this date/timestamp (YYYY-MM-DD or epoch-ms) — back-paging."
259
+ },
260
+ "from": {
261
+ "type": "string",
262
+ "description": "Window start (inclusive), YYYY-MM-DD / ISO / epoch-ms. Combines with `to`; page inside the window with `cursor`. Mutually exclusive with `asof` and `before` (400)."
263
+ },
264
+ "to": {
265
+ "type": "string",
266
+ "description": "Window end (inclusive; a bare YYYY-MM-DD means through the end of that day). Mutually exclusive with `asof` and `before` (400)."
267
+ },
268
+ "asof": {
269
+ "type": "string",
270
+ "description": "Return a single bar as of this date/timestamp (point-in-time)."
271
+ },
272
+ "cursor": {
273
+ "type": "string",
274
+ "description": "Continuation token from a prior response's `next_cursor` (sugar for `before`, and the way to page inside a from/to window)."
275
+ }
67
276
  },
68
- required: ['ticker', 'asof'],
69
- },
70
- endpoint: {
71
- method: 'GET',
72
- path: '/v2/tickers/{ticker}/history',
73
- paramLocation: { ticker: 'path', asof: 'query' },
74
- },
75
- },
76
- {
77
- name: 'tickerbot_get_ticker_events',
78
- description: 'Get the discrete event log for one ticker (splits, dividends, analyst rating changes), newest-first. Optional date window and event-kind filter.',
79
- inputSchema: {
80
- type: 'object',
81
- properties: {
82
- ticker: { type: 'string', description: 'Symbol.' },
83
- from: { type: 'string', description: 'Earliest event timestamp (inclusive). YYYY-MM-DD or ISO.' },
84
- to: { type: 'string', description: 'Latest event timestamp (inclusive). YYYY-MM-DD or ISO.' },
85
- kind: { type: 'string', description: 'Filter by event kind.', enum: ['split', 'dividend', 'rating_change'] },
86
- limit: { type: 'integer', description: 'Page size. Max 1000. Default 100.' },
87
- cursor: { type: 'string', description: 'Opaque cursor.' },
277
+ "required": [
278
+ "ticker",
279
+ "interval"
280
+ ]
281
+ },
282
+ "endpoint": {
283
+ "method": "GET",
284
+ "path": "/v2/tickers/{ticker}/bars/{interval}",
285
+ "paramLocation": {
286
+ "ticker": "path",
287
+ "interval": "path",
288
+ "limit": "query",
289
+ "before": "query",
290
+ "from": "query",
291
+ "to": "query",
292
+ "asof": "query",
293
+ "cursor": "query"
294
+ }
295
+ }
296
+ },
297
+ {
298
+ "name": "tickerbot_get_ticker_holdings",
299
+ "description": "Get an ETF's constituent holdings and their weights, heaviest first. When the ticker is not an ETF, `is_etf` is false and `holdings` is empty. (The reverse lookup \"which ETFs hold NVDA\" is a scan filter on the `etf_holders` column, not this tool.)",
300
+ "inputSchema": {
301
+ "type": "object",
302
+ "properties": {
303
+ "ticker": {
304
+ "type": "string",
305
+ "description": "ETF symbol."
306
+ },
307
+ "limit": {
308
+ "type": "integer",
309
+ "description": "Max holdings returned. Max 5000. Default 500."
310
+ }
88
311
  },
89
- required: ['ticker'],
312
+ "required": [
313
+ "ticker"
314
+ ]
90
315
  },
91
- endpoint: {
92
- method: 'GET',
93
- path: '/v2/tickers/{ticker}/events',
94
- paramLocation: { ticker: 'path', from: 'query', to: 'query', kind: 'query', limit: 'query', cursor: 'query' },
316
+ "endpoint": {
317
+ "method": "GET",
318
+ "path": "/v2/tickers/{ticker}/holdings",
319
+ "paramLocation": {
320
+ "ticker": "path",
321
+ "limit": "query"
322
+ }
323
+ }
324
+ },
325
+ {
326
+ "name": "tickerbot_get_ticker_sectors",
327
+ "description": "Get an ETF's sector allocation (sector weights, heaviest first). When the ticker is not an ETF, `is_etf` is false and `sectors` is empty.",
328
+ "inputSchema": {
329
+ "type": "object",
330
+ "properties": {
331
+ "ticker": {
332
+ "type": "string",
333
+ "description": "ETF symbol."
334
+ }
335
+ },
336
+ "required": [
337
+ "ticker"
338
+ ]
95
339
  },
340
+ "endpoint": {
341
+ "method": "GET",
342
+ "path": "/v2/tickers/{ticker}/sectors",
343
+ "paramLocation": {
344
+ "ticker": "path"
345
+ }
346
+ }
96
347
  },
97
- // ── Signals catalog ──────────────────────────────────────────────────
98
348
  {
99
- name: 'tickerbot_list_signals_catalog',
100
- description: 'List the unified signal catalog: every built-in column on the schema (`kind: builtin`) plus the caller\'s custom signals (`kind: expression`). Use to discover what `q=` clauses and signal names are available before composing a scan.',
101
- inputSchema: {
102
- type: 'object',
103
- properties: {
104
- kind: { type: 'string', description: 'Filter by kind. Omit for both.', enum: ['builtin', 'custom'] },
105
- limit: { type: 'integer', description: 'Page size for custom slice. Max 200. Default 50.' },
106
- cursor: { type: 'string', description: 'Opaque cursor.' },
349
+ "name": "tickerbot_list_signals_catalog",
350
+ "description": "List the unified signal catalog: every built-in column on the schema (`kind: builtin`) plus the caller's custom signals (`kind: expression`). Built-in rows carry the audited spec metadata — description, category, update cadence, ticker coverage (`ticker_scope`), history depth (`history`/`history_since`), queryable resolutions, and asset classes. Use to discover what `q=` clauses and signal names are available before composing a scan.",
351
+ "inputSchema": {
352
+ "type": "object",
353
+ "properties": {
354
+ "kind": {
355
+ "type": "string",
356
+ "description": "Filter by kind. Omit for both. (`custom` is accepted as a legacy alias of `expression`.)",
357
+ "enum": [
358
+ "builtin",
359
+ "expression"
360
+ ]
361
+ },
362
+ "limit": {
363
+ "type": "integer",
364
+ "description": "Page size for custom slice. Max 200. Default 50."
365
+ },
366
+ "cursor": {
367
+ "type": "string",
368
+ "description": "Opaque cursor."
369
+ }
370
+ }
371
+ },
372
+ "endpoint": {
373
+ "method": "GET",
374
+ "path": "/v2/signals",
375
+ "paramLocation": {
376
+ "kind": "query",
377
+ "limit": "query",
378
+ "cursor": "query"
379
+ }
380
+ }
381
+ },
382
+ {
383
+ "name": "tickerbot_get_signals_match",
384
+ "description": "Find tickers that match a single signal right now (or at a past moment with `asof`). Booleans need no condition. Numerics need a `condition` like \">70\" or \"<=200\". Sorted by signal value desc for numerics.",
385
+ "inputSchema": {
386
+ "type": "object",
387
+ "properties": {
388
+ "signal": {
389
+ "type": "string",
390
+ "description": "Column name on ticker (e.g. golden_cross_today, rsi_14, market_cap)."
391
+ },
392
+ "condition": {
393
+ "type": "string",
394
+ "description": "Required for numerics. Single bound: <op><value>, ops in (>, >=, =, !=, <, <=)."
395
+ },
396
+ "asof": {
397
+ "type": "string",
398
+ "description": "Optional YYYY-MM-DD or ISO timestamp. Date-only matches daily state; a timestamp matches the finest intraday state covering the query."
399
+ },
400
+ "universe": {
401
+ "type": "string",
402
+ "description": "Optional universe slug."
403
+ },
404
+ "sort_by": {
405
+ "type": "string",
406
+ "description": "Row order: `default` (alphabetic for booleans, highest-value-first for numerics) or `market_cap` (desc, adds market_cap to each row).",
407
+ "enum": [
408
+ "default",
409
+ "market_cap"
410
+ ]
411
+ },
412
+ "include_active_since": {
413
+ "type": "boolean",
414
+ "description": "Built-in booleans only: adds `active_since` per row — when the flag last flipped true (from the spans archive)."
415
+ },
416
+ "limit": {
417
+ "type": "integer",
418
+ "description": "Page size. Max 200. Default 50."
419
+ },
420
+ "cursor": {
421
+ "type": "string",
422
+ "description": "Opaque cursor."
423
+ }
107
424
  },
425
+ "required": [
426
+ "signal"
427
+ ]
108
428
  },
109
- endpoint: {
110
- method: 'GET',
111
- path: '/v2/signals',
112
- paramLocation: { kind: 'query', limit: 'query', cursor: 'query' },
429
+ "endpoint": {
430
+ "method": "GET",
431
+ "path": "/v2/signals/{signal}",
432
+ "paramLocation": {
433
+ "signal": "path",
434
+ "condition": "query",
435
+ "asof": "query",
436
+ "universe": "query",
437
+ "sort_by": "query",
438
+ "include_active_since": "query",
439
+ "limit": "query",
440
+ "cursor": "query"
441
+ }
442
+ }
443
+ },
444
+ {
445
+ "name": "tickerbot_list_signal_events",
446
+ "description": "Occurrence SPANS of a boolean signal for one ticker, newest-first. For STATE flags (above_sma_50, in_uptrend) each row is a true-WINDOW: started_at when it flipped true, ended_at when it flipped back (null while still true), with prices at both ends. For EVENT flags (golden_cross, gap_up) each row is a point firing (started_at = ended_at). \"Golden crosses in June\" is from=2026-06-01&to=2026-06-30. Built-in booleans only — numerics and custom signals have no precomputed spans (use tickerbot_get_series).",
447
+ "inputSchema": {
448
+ "type": "object",
449
+ "properties": {
450
+ "signal": {
451
+ "type": "string",
452
+ "description": "Built-in boolean flag name."
453
+ },
454
+ "ticker": {
455
+ "type": "string",
456
+ "description": "Symbol."
457
+ },
458
+ "from": {
459
+ "type": "string",
460
+ "description": "Window start (inclusive) on each span's started_at — YYYY-MM-DD, ISO, or epoch-ms."
461
+ },
462
+ "to": {
463
+ "type": "string",
464
+ "description": "Window end (inclusive) on started_at; a bare YYYY-MM-DD means through the end of that day."
465
+ },
466
+ "merge_gap_seconds": {
467
+ "type": "integer",
468
+ "description": "Interval-union: contiguous windows whose gap is ≤ N seconds collapse into one — de-fragments flags with thousands of per-tick rows (e.g. 3600 for daily flags). Default 0 = no merge. A cursor pins this; resend it unchanged when paging."
469
+ },
470
+ "limit": {
471
+ "type": "integer",
472
+ "description": "Page size. Max 1000, newest first."
473
+ },
474
+ "cursor": {
475
+ "type": "string",
476
+ "description": "Opaque cursor — pages older, inside the from/to window if one is set."
477
+ }
478
+ },
479
+ "required": [
480
+ "signal",
481
+ "ticker"
482
+ ]
113
483
  },
484
+ "endpoint": {
485
+ "method": "GET",
486
+ "path": "/v2/signals/{signal}/{ticker}/events",
487
+ "paramLocation": {
488
+ "signal": "path",
489
+ "ticker": "path",
490
+ "from": "query",
491
+ "to": "query",
492
+ "merge_gap_seconds": "query",
493
+ "limit": "query",
494
+ "cursor": "query"
495
+ }
496
+ }
114
497
  },
115
498
  {
116
- name: 'tickerbot_get_signals_match',
117
- description: 'Find tickers that match a single signal right now (or at a past date with `asof`). Booleans need no condition (returns tickers where flag is true). Numerics need a `condition` like ">70" or "<=200". Sorted by signal value desc for numerics.',
118
- inputSchema: {
119
- type: 'object',
120
- properties: {
121
- signal: { type: 'string', description: 'Column name on ticker. E.g. golden_cross_today, above_sma_50, rsi_14, market_cap, pe_ratio.' },
122
- condition: { type: 'string', description: 'Required for numerics. Single bound: <op><value>, ops in (>, >=, =, !=, <, <=). E.g. ">70".' },
123
- asof: { type: 'string', description: 'Optional YYYY-MM-DD. Match against historical daily state.' },
124
- universe: { type: 'string', description: 'Optional universe slug.' },
125
- limit: { type: 'integer', description: 'Page size. Max 200. Default 50.' },
126
- cursor: { type: 'string', description: 'Opaque cursor.' },
499
+ "name": "tickerbot_create_custom_signal",
500
+ "description": "Save a SQL WHERE expression as a named custom signal the caller can reference by name in future scans.",
501
+ "inputSchema": {
502
+ "type": "object",
503
+ "properties": {
504
+ "name": {
505
+ "type": "string",
506
+ "description": "Snake_case identifier."
507
+ },
508
+ "expr": {
509
+ "type": "string",
510
+ "description": "SQL WHERE expression. Same grammar as scan `q`."
511
+ },
512
+ "description": {
513
+ "type": "string",
514
+ "description": "Optional human description."
515
+ }
127
516
  },
128
- required: ['signal'],
129
- },
130
- endpoint: {
131
- method: 'GET',
132
- path: '/v2/signals/{signal}',
133
- paramLocation: { signal: 'path', condition: 'query', asof: 'query', universe: 'query', limit: 'query', cursor: 'query' },
134
- },
135
- },
136
- {
137
- name: 'tickerbot_get_signal_history',
138
- description: 'Get the time series of one signal for one ticker at a chosen interval (1m, 1h, 1d, 1w). Use for charting a numeric signal\'s evolution or seeing when a boolean flag was on/off across time.',
139
- inputSchema: {
140
- type: 'object',
141
- properties: {
142
- signal: { type: 'string', description: 'Column name.' },
143
- ticker: { type: 'string', description: 'Symbol.' },
144
- interval: { type: 'string', description: 'Bar interval.', enum: ['1m', '1h', '1d', '1w'] },
145
- from: { type: 'string', description: 'Earliest bar timestamp. YYYY-MM-DD or ISO.' },
146
- to: { type: 'string', description: 'Latest bar timestamp. YYYY-MM-DD or ISO.' },
147
- limit: { type: 'integer', description: 'Page size.' },
148
- cursor: { type: 'string', description: 'Opaque cursor.' },
517
+ "required": [
518
+ "name",
519
+ "expr"
520
+ ]
521
+ },
522
+ "endpoint": {
523
+ "method": "POST",
524
+ "path": "/v2/signals",
525
+ "paramLocation": {
526
+ "name": "body",
527
+ "expr": "body",
528
+ "description": "body"
529
+ }
530
+ }
531
+ },
532
+ {
533
+ "name": "tickerbot_update_custom_signal",
534
+ "description": "Edit one of the caller's custom signals — supply `expr`, `description`, `new_name`, or any combination. Providing `expr` recompiles it against the live column whitelist. Renaming is refused (409) while other custom signals reference the current name. Built-in signals are read-only; only custom signals the caller owns can be patched. Available on every plan.",
535
+ "inputSchema": {
536
+ "type": "object",
537
+ "properties": {
538
+ "name": {
539
+ "type": "string",
540
+ "description": "CURRENT slug — identifies which signal to edit."
541
+ },
542
+ "new_name": {
543
+ "type": "string",
544
+ "description": "Rename the signal to this slug (snake_case, must not collide with a built-in column or another of your signals)."
545
+ },
546
+ "expr": {
547
+ "type": "string",
548
+ "description": "New SQL WHERE expression. Re-validated on save."
549
+ },
550
+ "description": {
551
+ "type": "string",
552
+ "description": "New description."
553
+ }
149
554
  },
150
- required: ['signal', 'ticker', 'interval'],
151
- },
152
- endpoint: {
153
- method: 'GET',
154
- path: '/v2/signals/{signal}/{ticker}/history/{interval}',
155
- paramLocation: { signal: 'path', ticker: 'path', interval: 'path', from: 'query', to: 'query', limit: 'query', cursor: 'query' },
156
- },
157
- },
158
- {
159
- name: 'tickerbot_list_signal_events',
160
- description: 'Get the list of discrete firings of a boolean signal for one ticker (each time the flag went true). Newest-first, paginated.',
161
- inputSchema: {
162
- type: 'object',
163
- properties: {
164
- signal: { type: 'string', description: 'Boolean signal column.' },
165
- ticker: { type: 'string', description: 'Symbol.' },
166
- from: { type: 'string', description: 'Earliest event timestamp.' },
167
- to: { type: 'string', description: 'Latest event timestamp.' },
168
- limit: { type: 'integer', description: 'Page size.' },
169
- cursor: { type: 'string', description: 'Opaque cursor.' },
555
+ "required": [
556
+ "name"
557
+ ]
558
+ },
559
+ "endpoint": {
560
+ "method": "PATCH",
561
+ "path": "/v2/signals/{name}",
562
+ "paramLocation": {
563
+ "name": "path",
564
+ "new_name": "body",
565
+ "expr": "body",
566
+ "description": "body"
567
+ }
568
+ }
569
+ },
570
+ {
571
+ "name": "tickerbot_delete_custom_signal",
572
+ "description": "Delete one of the caller's custom signals. Cascade-safe by default: refused with 409 if another custom signal references it (the error lists the referencing signals). Pass `force: true` to delete anyway — existing references will break on next recompile. Available on every plan.",
573
+ "inputSchema": {
574
+ "type": "object",
575
+ "properties": {
576
+ "name": {
577
+ "type": "string",
578
+ "description": "Custom signal slug."
579
+ },
580
+ "force": {
581
+ "type": "boolean",
582
+ "description": "When true, skip the reference check and delete anyway. Default false."
583
+ }
170
584
  },
171
- required: ['signal', 'ticker'],
172
- },
173
- endpoint: {
174
- method: 'GET',
175
- path: '/v2/signals/{signal}/{ticker}/events',
176
- paramLocation: { signal: 'path', ticker: 'path', from: 'query', to: 'query', limit: 'query', cursor: 'query' },
177
- },
178
- },
179
- // ── Scan ─────────────────────────────────────────────────────────────
180
- {
181
- name: 'tickerbot_scan',
182
- description: 'Run a SQL WHERE clause against the live ticker universe (or against a past trading day with `asof`). Returns matching tickers sorted by chosen column. The `q` grammar is a flat WHERE: column names from the schema, AND/OR/NOT, comparison operators, numeric/string literals. No JOIN, GROUP BY, or subqueries. Example: `gap_up AND market_cap < 2000000000 AND NOT earnings_this_week`.',
183
- inputSchema: {
184
- type: 'object',
185
- properties: {
186
- q: { type: 'string', description: 'SQL WHERE expression. Max 4000 chars. Identifiers are bare column names from the schema (e.g. above_sma_50, rsi_14, market_cap). Numeric literals fully specified (no 1.5B shorthand).' },
187
- universe: { type: 'string', description: 'Optional universe slug (top_10, top_100, or your own).' },
188
- asof: { type: 'string', description: 'Optional YYYY-MM-DD. Run the WHERE against historical close-of-day snapshot for this date.' },
189
- order: { type: 'string', description: 'Sort column. Default day_change_pct.' },
190
- dir: { type: 'string', description: 'Sort direction.', enum: ['asc', 'desc'] },
191
- fields: { type: 'string', description: 'Comma-separated extra columns to include in each result row beyond the default set.' },
192
- limit: { type: 'integer', description: 'Page size. Max 100. Default 50.' },
193
- cursor: { type: 'string', description: 'Opaque cursor.' },
585
+ "required": [
586
+ "name"
587
+ ]
588
+ },
589
+ "endpoint": {
590
+ "method": "DELETE",
591
+ "path": "/v2/signals/{name}",
592
+ "paramLocation": {
593
+ "name": "path",
594
+ "force": "query"
595
+ }
596
+ }
597
+ },
598
+ {
599
+ "name": "tickerbot_scan",
600
+ "description": "Run a SQL WHERE clause against the live ticker universe (or against a past moment with `asof` — unlimited depth on every plan). Returns matching tickers sorted by chosen column, OR — with `group_by` — aggregate rollups instead of rows (breadth stats: \"count of tickers above their 200dma by sector\", \"median RSI by sector on 2026-03-03\"). The `q` grammar is a flat WHERE: column names from the schema, AND/OR/NOT, comparison operators, numeric/string literals. No JOIN or subqueries. Example: `gap_up AND market_cap < 2000000000 AND NOT earnings_this_week`.",
601
+ "inputSchema": {
602
+ "type": "object",
603
+ "properties": {
604
+ "q": {
605
+ "type": "string",
606
+ "description": "SQL WHERE expression. Max 4000 chars."
607
+ },
608
+ "universe": {
609
+ "type": "string",
610
+ "description": "Optional universe slug."
611
+ },
612
+ "asof": {
613
+ "type": "string",
614
+ "description": "Optional YYYY-MM-DD or ISO timestamp for a historical scan. Unlimited depth on every plan."
615
+ },
616
+ "order": {
617
+ "type": "string",
618
+ "description": "Sort column. Default day_change_pct (rows) / tickers (aggregate)."
619
+ },
620
+ "dir": {
621
+ "type": "string",
622
+ "description": "Sort direction.",
623
+ "enum": [
624
+ "asc",
625
+ "desc"
626
+ ]
627
+ },
628
+ "fields": {
629
+ "type": "string",
630
+ "description": "Comma-separated extra columns to include (row mode only)."
631
+ },
632
+ "group_by": {
633
+ "type": "string",
634
+ "description": "AGGREGATE MODE: 1–6 comma-separated group keys (columns or expressions, e.g. `sector`). Results become rollup rows instead of tickers."
635
+ },
636
+ "select": {
637
+ "type": "string",
638
+ "description": "Aggregate output items (requires group_by). Default: group keys + COUNT(*) AS tickers. Aggregates: count/avg/sum/min/max/stddev/string_agg + FILTER (WHERE …). Alias items with AS. Example: `sector, COUNT(*) AS n, AVG(rsi_14) AS avg_rsi`."
639
+ },
640
+ "having": {
641
+ "type": "string",
642
+ "description": "Aggregate filter (requires group_by). Example: `COUNT(*) >= 10`."
643
+ },
644
+ "limit": {
645
+ "type": "integer",
646
+ "description": "Page size. Max 100. Default 50. Aggregate mode does not paginate — response sets `truncated: true` when groups were cut."
647
+ },
648
+ "cursor": {
649
+ "type": "string",
650
+ "description": "Opaque cursor (row mode only)."
651
+ },
652
+ "full": {
653
+ "type": "boolean",
654
+ "description": "Row mode: return the FULL wide row for each match (every column) instead of the slim default projection."
655
+ }
194
656
  },
195
- required: ['q'],
196
- },
197
- endpoint: {
198
- method: 'GET',
199
- path: '/v2/scan',
200
- paramLocation: {
201
- q: 'query', universe: 'query', asof: 'query',
202
- order: 'query', dir: 'query', fields: 'query',
203
- limit: 'query', cursor: 'query',
657
+ "required": [
658
+ "q"
659
+ ]
660
+ },
661
+ "endpoint": {
662
+ "method": "GET",
663
+ "path": "/v2/scan",
664
+ "paramLocation": {
665
+ "q": "query",
666
+ "universe": "query",
667
+ "asof": "query",
668
+ "order": "query",
669
+ "dir": "query",
670
+ "fields": "query",
671
+ "group_by": "query",
672
+ "select": "query",
673
+ "having": "query",
674
+ "limit": "query",
675
+ "cursor": "query",
676
+ "full": "query"
677
+ }
678
+ }
679
+ },
680
+ {
681
+ "name": "tickerbot_list_universes",
682
+ "description": "List universes — your saved ones and/or the built-in system universes (top_10, top_100). Filter with `owner` (like the signals catalog's `kind`): `me` (default, your own), `system` (built-ins), or `all` (both). Each row carries `system: true|false`.",
683
+ "inputSchema": {
684
+ "type": "object",
685
+ "properties": {
686
+ "owner": {
687
+ "type": "string",
688
+ "description": "Which universes to list.",
689
+ "enum": [
690
+ "me",
691
+ "system",
692
+ "all"
693
+ ]
694
+ },
695
+ "limit": {
696
+ "type": "integer",
697
+ "description": "Page size (applies to your own; system universes are a small fixed set returned in full on the first page)."
698
+ },
699
+ "cursor": {
700
+ "type": "string",
701
+ "description": "Opaque cursor."
702
+ }
703
+ }
704
+ },
705
+ "endpoint": {
706
+ "method": "GET",
707
+ "path": "/v2/universes",
708
+ "paramLocation": {
709
+ "owner": "query",
710
+ "limit": "query",
711
+ "cursor": "query"
712
+ }
713
+ }
714
+ },
715
+ {
716
+ "name": "tickerbot_list_system_universes",
717
+ "description": "List the built-in system universes (`top_10`, `top_100` — the most-actively-traded tickers by 30-day trailing dollar volume, rebalanced monthly). Available to every account regardless of plan. Use these slugs as `universe` in scans/signals or `universe_id` when subscribing.",
718
+ "inputSchema": {
719
+ "type": "object",
720
+ "properties": {}
721
+ },
722
+ "endpoint": {
723
+ "method": "GET",
724
+ "path": "/v2/universes/system",
725
+ "paramLocation": {}
726
+ }
727
+ },
728
+ {
729
+ "name": "tickerbot_get_universe",
730
+ "description": "Get one universe by slug, including its ticker list.",
731
+ "inputSchema": {
732
+ "type": "object",
733
+ "properties": {
734
+ "id": {
735
+ "type": "string",
736
+ "description": "Universe slug."
737
+ }
204
738
  },
739
+ "required": [
740
+ "id"
741
+ ]
205
742
  },
743
+ "endpoint": {
744
+ "method": "GET",
745
+ "path": "/v2/universes/{id}",
746
+ "paramLocation": {
747
+ "id": "path"
748
+ }
749
+ }
206
750
  },
207
- // ── Universes ────────────────────────────────────────────────────────
208
751
  {
209
- name: 'tickerbot_list_universes',
210
- description: 'List the caller\'s saved universes (named ticker sets to scope scans). Each universe has a slug, name, and ticker count.',
211
- inputSchema: {
212
- type: 'object',
213
- properties: {
214
- limit: { type: 'integer', description: 'Page size.' },
215
- cursor: { type: 'string', description: 'Opaque cursor.' },
752
+ "name": "tickerbot_create_universe",
753
+ "description": "Create a new universe (named set of tickers) for scoping future scans.",
754
+ "inputSchema": {
755
+ "type": "object",
756
+ "properties": {
757
+ "id": {
758
+ "type": "string",
759
+ "description": "Optional slug (lowercase letters, digits, underscore). Auto-generated from name if omitted. Must be unique within the account."
760
+ },
761
+ "name": {
762
+ "type": "string",
763
+ "description": "Human-readable name."
764
+ },
765
+ "description": {
766
+ "type": "string",
767
+ "description": "Optional free-form notes."
768
+ },
769
+ "tickers": {
770
+ "type": "array",
771
+ "description": "List of ticker symbols.",
772
+ "items": {
773
+ "type": "string"
774
+ }
775
+ }
216
776
  },
777
+ "required": [
778
+ "name",
779
+ "tickers"
780
+ ]
217
781
  },
218
- endpoint: {
219
- method: 'GET',
220
- path: '/v2/universes',
221
- paramLocation: { limit: 'query', cursor: 'query' },
782
+ "endpoint": {
783
+ "method": "POST",
784
+ "path": "/v2/universes",
785
+ "paramLocation": {
786
+ "id": "body",
787
+ "name": "body",
788
+ "description": "body",
789
+ "tickers": "body"
790
+ }
791
+ }
792
+ },
793
+ {
794
+ "name": "tickerbot_update_universe",
795
+ "description": "Update one of the caller's universes. Pass `name`/`description` to relabel, `tickers` to replace the whole list, or `add`/`remove` to adjust subsets without replacing. System universes (`top_10`/`top_100`) cannot be edited.",
796
+ "inputSchema": {
797
+ "type": "object",
798
+ "properties": {
799
+ "id": {
800
+ "type": "string",
801
+ "description": "Universe slug."
802
+ },
803
+ "name": {
804
+ "type": "string",
805
+ "description": "New label."
806
+ },
807
+ "description": {
808
+ "type": "string",
809
+ "description": "New notes."
810
+ },
811
+ "tickers": {
812
+ "type": "array",
813
+ "description": "Replace the full ticker list.",
814
+ "items": {
815
+ "type": "string"
816
+ }
817
+ },
818
+ "add": {
819
+ "type": "array",
820
+ "description": "Add these tickers (deduplicated).",
821
+ "items": {
822
+ "type": "string"
823
+ }
824
+ },
825
+ "remove": {
826
+ "type": "array",
827
+ "description": "Remove these tickers.",
828
+ "items": {
829
+ "type": "string"
830
+ }
831
+ }
832
+ },
833
+ "required": [
834
+ "id"
835
+ ]
222
836
  },
837
+ "endpoint": {
838
+ "method": "PATCH",
839
+ "path": "/v2/universes/{id}",
840
+ "paramLocation": {
841
+ "id": "path",
842
+ "name": "body",
843
+ "description": "body",
844
+ "tickers": "body",
845
+ "add": "body",
846
+ "remove": "body"
847
+ }
848
+ }
223
849
  },
224
850
  {
225
- name: 'tickerbot_get_universe',
226
- description: 'Get one universe by slug, including its ticker list.',
227
- inputSchema: {
228
- type: 'object',
229
- properties: {
230
- id: { type: 'string', description: 'Universe slug.' },
851
+ "name": "tickerbot_delete_universe",
852
+ "description": "Delete one of the caller's universes. System universes (`top_10`/`top_100`) cannot be deleted. Webhooks that reference the deleted universe will fail on their next eval, so clean those up first.",
853
+ "inputSchema": {
854
+ "type": "object",
855
+ "properties": {
856
+ "id": {
857
+ "type": "string",
858
+ "description": "Universe slug."
859
+ }
231
860
  },
232
- required: ['id'],
861
+ "required": [
862
+ "id"
863
+ ]
233
864
  },
234
- endpoint: {
235
- method: 'GET',
236
- path: '/v2/universes/{id}',
237
- paramLocation: { id: 'path' },
865
+ "endpoint": {
866
+ "method": "DELETE",
867
+ "path": "/v2/universes/{id}",
868
+ "paramLocation": {
869
+ "id": "path"
870
+ }
871
+ }
872
+ },
873
+ {
874
+ "name": "tickerbot_list_events",
875
+ "description": "THE events primitive — one timeline over every event log, cross-ticker: dividends, splits, insider transactions, and analyst actions (\"all splits this month\", \"every analyst action in my universe this week\", \"AAPL's full corporate history\" via `ticker`), plus two opt-in kinds that join only when named: `signal` (boolean-flag firings) and `news`. Rows are { ticker, ts, kind, payload }, newest first. Analyst payloads (history to 2012) carry firm/analyst/action/rating/price_target; filter them with `q` (payload->>'firm' = 'Goldman Sachs' AND payload->>'action' = 'downgrades') — actions: upgrades, downgrades, initiates_coverage_on, maintains, reiterates, assumes, reinstates, suspends, terminates_coverage_on. Speaks the SQL grammar over exactly (ticker, ts, kind, payload jsonb); `group_by`/`select`/`having` roll the stream up (aggregates return `truncated: true` instead of paginating); `join=state` attaches the ticker's state as of each event. Requires at least one bound: a ticker scope (ticker/tickers/universe) or a time window (from/to) — q alone is not a bound.",
876
+ "inputSchema": {
877
+ "type": "object",
878
+ "properties": {
879
+ "kind": {
880
+ "type": "string",
881
+ "description": "Comma list of kinds to include. Default is the four corporate kinds: dividend, split, insider, analyst. Two more are opt-in and join only when named: `signal` (boolean-flag firings) and `news`."
882
+ },
883
+ "signal": {
884
+ "type": "string",
885
+ "description": "kind=signal only. One built-in boolean flag (e.g. golden_cross). REQUIRED to use `q` or `join` on kind=signal — naming the signal is what keeps the query on an index; optional otherwise."
886
+ },
887
+ "transition": {
888
+ "type": "string",
889
+ "description": "kind=signal only. `enter` (false->true) or `exit` (true->false). Always optional — an ordinary filter.",
890
+ "enum": [
891
+ "enter",
892
+ "exit"
893
+ ]
894
+ },
895
+ "ticker": {
896
+ "type": "string",
897
+ "description": "Single-ticker filter, e.g. AAPL."
898
+ },
899
+ "tickers": {
900
+ "type": "string",
901
+ "description": "Comma-separated tickers, max 50. Mutually exclusive with `universe`."
902
+ },
903
+ "universe": {
904
+ "type": "string",
905
+ "description": "Universe slug (top_10, top_100, or a saved one) to scope the stream. Mutually exclusive with `tickers`."
906
+ },
907
+ "from": {
908
+ "type": "string",
909
+ "description": "Events at or after this ISO date/datetime (inclusive). `since` accepted as alias."
910
+ },
911
+ "to": {
912
+ "type": "string",
913
+ "description": "Events strictly before this ISO date/datetime (exclusive). `until` accepted as alias."
914
+ },
915
+ "join": {
916
+ "type": "string",
917
+ "description": "join=state attaches each event's ticker STATE as of that event's moment (the replay join) under a `state` key — \"downgrades where rsi_14 was already under 40\" composes with q. Free on every plan.",
918
+ "enum": [
919
+ "state"
920
+ ]
921
+ },
922
+ "interval": {
923
+ "type": "string",
924
+ "description": "Grain for join=state replay (finest covering tier by default).",
925
+ "enum": [
926
+ "1m",
927
+ "1h",
928
+ "1d"
929
+ ]
930
+ },
931
+ "q": {
932
+ "type": "string",
933
+ "description": "SQL WHERE over (ticker, ts, kind, payload jsonb) — ONLY those four identifiers. Payload fields via jsonb operators: payload->>'firm' = 'Goldman Sachs', (payload->>'shares')::numeric > 1e6. On kind=signal this requires `signal` (the firing log is ~175M rows); kind=signal takes no group_by."
934
+ },
935
+ "select": {
936
+ "type": "string",
937
+ "description": "Aggregate-mode output columns (requires group_by). Default: group keys + COUNT(*) AS events."
938
+ },
939
+ "group_by": {
940
+ "type": "string",
941
+ "description": "Comma list of rollup keys — switches to aggregate rows, e.g. payload->>'firm' AS firm, or kind."
942
+ },
943
+ "having": {
944
+ "type": "string",
945
+ "description": "Post-aggregation filter (requires group_by), e.g. COUNT(*) > 5."
946
+ },
947
+ "order": {
948
+ "type": "string",
949
+ "description": "Aggregate-mode sort column/alias. Default: events."
950
+ },
951
+ "dir": {
952
+ "type": "string",
953
+ "description": "Aggregate-mode sort direction.",
954
+ "enum": [
955
+ "asc",
956
+ "desc"
957
+ ]
958
+ },
959
+ "limit": {
960
+ "type": "integer",
961
+ "description": "Page size. Max 1000."
962
+ },
963
+ "cursor": {
964
+ "type": "string",
965
+ "description": "Opaque cursor from a prior response — carries the original filters, pass it alone (long q values must be resent alongside it)."
966
+ }
967
+ }
238
968
  },
969
+ "endpoint": {
970
+ "method": "GET",
971
+ "path": "/v2/events",
972
+ "paramLocation": {
973
+ "kind": "query",
974
+ "ticker": "query",
975
+ "tickers": "query",
976
+ "universe": "query",
977
+ "from": "query",
978
+ "to": "query",
979
+ "join": "query",
980
+ "interval": "query",
981
+ "limit": "query",
982
+ "cursor": "query",
983
+ "q": "query",
984
+ "select": "query",
985
+ "group_by": "query",
986
+ "having": "query",
987
+ "order": "query",
988
+ "dir": "query",
989
+ "signal": "query",
990
+ "transition": "query"
991
+ }
992
+ }
239
993
  },
240
994
  {
241
- name: 'tickerbot_create_universe',
242
- description: 'Create a new universe (named set of tickers) for scoping future scans. Returns the new universe with its assigned slug.',
243
- inputSchema: {
244
- type: 'object',
245
- properties: {
246
- name: { type: 'string', description: 'Human-readable name.' },
247
- tickers: { type: 'array', description: 'List of ticker symbols.', items: { type: 'string' } },
995
+ "name": "tickerbot_subscribe_events",
996
+ "description": "Create an event-trigger webhook: fires when NEW events land — dividends, splits, insider filings, analyst actions (\"every split in my universe\", \"Goldman downgrades on large caps\"). TWO composable filters: `q` filters the event's TICKER STATE (market_cap > 1e10); `event_q` filters the EVENT CONTENT in the /v2/events grammar (payload->>'firm' = 'Goldman Sachs'). Paid plans (webhook slots). Deliveries carry event: \"events.fired\" with an events array. Latency = ingest cadence: analyst ≤1h, corporate kinds daily — NOT sub-minute like state webhooks.",
997
+ "inputSchema": {
998
+ "type": "object",
999
+ "properties": {
1000
+ "kinds": {
1001
+ "type": "string",
1002
+ "description": "Comma list of kinds to fire on — any of: dividend, split, insider, analyst (e.g. \"split,analyst\"). NOTE: no enum here on purpose — a scalar enum would reject multi-kind values."
1003
+ },
1004
+ "tickers": {
1005
+ "type": "string",
1006
+ "description": "Scope to specific tickers (comma list, max 50). Mutually exclusive with universe; omit both for all tickers."
1007
+ },
1008
+ "universe": {
1009
+ "type": "string",
1010
+ "description": "Scope to a universe slug (top_10, top_100, or a saved one)."
1011
+ },
1012
+ "q": {
1013
+ "type": "string",
1014
+ "description": "Optional row-STATE filter evaluated against the event's ticker at fire time, e.g. market_cap > 1e10."
1015
+ },
1016
+ "event_q": {
1017
+ "type": "string",
1018
+ "description": "Optional event-CONTENT filter over (ticker, ts, kind, payload jsonb) — only those four identifiers, e.g. payload->>'firm' = 'Goldman Sachs' AND payload->>'action' = 'downgrades'."
1019
+ },
1020
+ "target_url": {
1021
+ "type": "string",
1022
+ "description": "HTTPS delivery URL. Omit for in-app delivery."
1023
+ },
1024
+ "channel": {
1025
+ "type": "string",
1026
+ "description": "Delivery channel.",
1027
+ "enum": [
1028
+ "webhook",
1029
+ "discord",
1030
+ "in_app",
1031
+ "mobile_push"
1032
+ ]
1033
+ },
1034
+ "discord_url": {
1035
+ "type": "string",
1036
+ "description": "Discord incoming-webhook URL (channel discord)."
1037
+ },
1038
+ "device_id": {
1039
+ "type": "string",
1040
+ "description": "Registered device id from the mobile app (channel mobile_push)."
1041
+ },
1042
+ "cadence": {
1043
+ "type": "string",
1044
+ "description": "Evaluation cadence. Default realtime; hourly/nyse_open throttle. (`1m` accepted as a deprecated alias of realtime.)",
1045
+ "enum": [
1046
+ "realtime",
1047
+ "hourly",
1048
+ "nyse_open"
1049
+ ]
1050
+ },
1051
+ "name": {
1052
+ "type": "string",
1053
+ "description": "Display name."
1054
+ }
248
1055
  },
249
- required: ['name', 'tickers'],
1056
+ "required": [
1057
+ "kinds"
1058
+ ]
250
1059
  },
251
- endpoint: {
252
- method: 'POST',
253
- path: '/v2/universes',
254
- paramLocation: { name: 'body', tickers: 'body' },
1060
+ "endpoint": {
1061
+ "method": "POST",
1062
+ "path": "/v2/events/subscribe",
1063
+ "paramLocation": {
1064
+ "kinds": "body",
1065
+ "tickers": "body",
1066
+ "universe": "body",
1067
+ "q": "body",
1068
+ "event_q": "body",
1069
+ "target_url": "body",
1070
+ "channel": "body",
1071
+ "discord_url": "body",
1072
+ "device_id": "body",
1073
+ "cadence": "body",
1074
+ "name": "body"
1075
+ }
1076
+ }
1077
+ },
1078
+ {
1079
+ "name": "tickerbot_search_news",
1080
+ "description": "Search the news archive (back to 2015) with a SQL WHERE clause. Available on every plan. Columns on news_article include `time_published`, `title`, `summary`, `source`, `source_domain`, `category`, `authors`, `topics`, `tickers` (array), `overall_sentiment_score`, `overall_sentiment_label`, `url`. To filter to one ticker use `'NVDA' = ANY(tickers)` or the auto-unnest alias `tk = 'NVDA'`. Example: `q=tk='NVDA' AND time_published >= NOW() - INTERVAL '1 day'`. Supports group_by + having for aggregation (e.g. count of articles per day).",
1081
+ "inputSchema": {
1082
+ "type": "object",
1083
+ "properties": {
1084
+ "q": {
1085
+ "type": "string",
1086
+ "description": "SQL WHERE on news_article. Required."
1087
+ },
1088
+ "select": {
1089
+ "type": "string",
1090
+ "description": "Comma-separated columns to include. Defaults to a slim set."
1091
+ },
1092
+ "group_by": {
1093
+ "type": "string",
1094
+ "description": "Comma-separated columns for aggregation."
1095
+ },
1096
+ "having": {
1097
+ "type": "string",
1098
+ "description": "WHERE-style filter on aggregates. Requires group_by."
1099
+ },
1100
+ "order": {
1101
+ "type": "string",
1102
+ "description": "Sort column or SELECT alias. Default time_published (non-aggregate) or volume (aggregate)."
1103
+ },
1104
+ "dir": {
1105
+ "type": "string",
1106
+ "description": "Sort direction.",
1107
+ "enum": [
1108
+ "asc",
1109
+ "desc"
1110
+ ]
1111
+ },
1112
+ "limit": {
1113
+ "type": "integer",
1114
+ "description": "Page size."
1115
+ },
1116
+ "cursor": {
1117
+ "type": "string",
1118
+ "description": "Opaque cursor."
1119
+ }
1120
+ },
1121
+ "required": [
1122
+ "q"
1123
+ ]
255
1124
  },
1125
+ "endpoint": {
1126
+ "method": "GET",
1127
+ "path": "/v2/news/scan",
1128
+ "paramLocation": {
1129
+ "q": "query",
1130
+ "select": "query",
1131
+ "group_by": "query",
1132
+ "having": "query",
1133
+ "order": "query",
1134
+ "dir": "query",
1135
+ "limit": "query",
1136
+ "cursor": "query"
1137
+ }
1138
+ }
256
1139
  },
257
- // ── Custom signals (write) ───────────────────────────────────────────
258
1140
  {
259
- name: 'tickerbot_create_custom_signal',
260
- description: 'Save a SQL WHERE expression as a named custom signal the caller can reference by name in future scans. E.g. name="oversold_with_volume", expr="rsi_14 < 30 AND volume_ratio_20d > 2".',
261
- inputSchema: {
262
- type: 'object',
263
- properties: {
264
- name: { type: 'string', description: 'Snake_case identifier for the signal (lowercase letters, digits, underscore).' },
265
- expr: { type: 'string', description: 'SQL WHERE expression. Same grammar as scan `q`.' },
266
- description: { type: 'string', description: 'Optional human description.' },
1141
+ "name": "tickerbot_subscribe_ticker",
1142
+ "description": "Register a webhook that fires when one ticker matches a condition. `condition` is a SQL WHERE-clause fragment scoped to that ticker (e.g. \"rsi_14 > 70 AND relative_volume > 2\"). Pass `target_url` for an https POST, or `channel:\"discord\"` + `discord_url` to post to Discord; omit for in-app.",
1143
+ "inputSchema": {
1144
+ "type": "object",
1145
+ "properties": {
1146
+ "ticker": {
1147
+ "type": "string",
1148
+ "description": "Symbol."
1149
+ },
1150
+ "condition": {
1151
+ "type": "string",
1152
+ "description": "SQL WHERE fragment evaluated for this ticker."
1153
+ },
1154
+ "name": {
1155
+ "type": "string",
1156
+ "description": "Human-readable label."
1157
+ },
1158
+ "target_url": {
1159
+ "type": "string",
1160
+ "description": "Optional https URL for the `webhook` channel; omit for in-app."
1161
+ },
1162
+ "channel": {
1163
+ "type": "string",
1164
+ "description": "Delivery channel: `webhook` (POST to target_url), `discord` (embed to discord_url), `mobile_push` (to a registered device), or `in_app` (dashboard only). Inferred from the URL you pass if omitted.",
1165
+ "enum": [
1166
+ "webhook",
1167
+ "discord",
1168
+ "in_app",
1169
+ "mobile_push"
1170
+ ]
1171
+ },
1172
+ "discord_url": {
1173
+ "type": "string",
1174
+ "description": "Discord incoming-webhook URL (https://discord.com/api/webhooks/…). Required when channel is \"discord\"."
1175
+ },
1176
+ "device_id": {
1177
+ "type": "string",
1178
+ "description": "Registered device id from the mobile app. Required when channel is \"mobile_push\"."
1179
+ },
1180
+ "cadence": {
1181
+ "type": "string",
1182
+ "description": "Evaluation cadence. Default realtime; hourly/nyse_open throttle. (`1m` accepted as a deprecated alias of realtime.)",
1183
+ "enum": [
1184
+ "realtime",
1185
+ "hourly",
1186
+ "nyse_open"
1187
+ ]
1188
+ }
267
1189
  },
268
- required: ['name', 'expr'],
269
- },
270
- endpoint: {
271
- method: 'POST',
272
- path: '/v2/signals',
273
- paramLocation: { name: 'body', expr: 'body', description: 'body' },
274
- },
275
- },
276
- // ── News ─────────────────────────────────────────────────────────────
277
- {
278
- name: 'tickerbot_search_news',
279
- description: 'Search the news archive (back to 2015) with a SQL WHERE clause. Plan-gated: Scale+ required for the archive. Columns on news_article include `time_published`, `title`, `summary`, `source`, `source_domain`, `category`, `authors`, `topics`, `tickers` (array), `overall_sentiment_score`, `overall_sentiment_label`, `url`. To filter to one ticker use `\'NVDA\' = ANY(tickers)` or the auto-unnest alias `tk = \'NVDA\'`. Example: `q=tk=\'NVDA\' AND time_published >= NOW() - INTERVAL \'1 day\'`. Supports group_by + having for aggregation (e.g. count of articles per day).',
280
- inputSchema: {
281
- type: 'object',
282
- properties: {
283
- q: { type: 'string', description: 'SQL WHERE on news_article. Required.' },
284
- select: { type: 'string', description: 'Comma-separated columns to include. Defaults to a slim set.' },
285
- group_by: { type: 'string', description: 'Comma-separated columns for aggregation (e.g. `date_trunc(\'day\', time_published)`).' },
286
- having: { type: 'string', description: 'WHERE-style filter on aggregates. Requires group_by.' },
287
- order: { type: 'string', description: 'Sort column or SELECT alias. Default time_published (non-aggregate) or volume (aggregate).' },
288
- dir: { type: 'string', description: 'Sort direction.', enum: ['asc', 'desc'] },
289
- limit: { type: 'integer', description: 'Page size.' },
290
- cursor: { type: 'string', description: 'Opaque cursor.' },
1190
+ "required": [
1191
+ "ticker",
1192
+ "condition"
1193
+ ]
1194
+ },
1195
+ "endpoint": {
1196
+ "method": "POST",
1197
+ "path": "/v2/tickers/{ticker}/subscribe",
1198
+ "paramLocation": {
1199
+ "ticker": "path",
1200
+ "condition": "body",
1201
+ "name": "body",
1202
+ "target_url": "body",
1203
+ "channel": "body",
1204
+ "discord_url": "body",
1205
+ "device_id": "body",
1206
+ "cadence": "body"
1207
+ }
1208
+ }
1209
+ },
1210
+ {
1211
+ "name": "tickerbot_subscribe_signal",
1212
+ "description": "Register a webhook that fires when a signal turns true (booleans) or its value crosses a condition (numerics). Optional `ticker` restricts to one symbol; omit to watch the whole universe. Pass `target_url` for an https POST, or `channel:\"discord\"` + `discord_url` to post to Discord; omit for in-app.",
1213
+ "inputSchema": {
1214
+ "type": "object",
1215
+ "properties": {
1216
+ "signal": {
1217
+ "type": "string",
1218
+ "description": "Column name (e.g. golden_cross_today, rsi_14)."
1219
+ },
1220
+ "ticker": {
1221
+ "type": "string",
1222
+ "description": "Optional ticker to restrict the watch to one symbol."
1223
+ },
1224
+ "universe": {
1225
+ "type": "string",
1226
+ "description": "Optional universe slug."
1227
+ },
1228
+ "condition": {
1229
+ "type": "string",
1230
+ "description": "Required for numerics: single bound like \">70\" or \"<=200\". Ignored for booleans."
1231
+ },
1232
+ "name": {
1233
+ "type": "string",
1234
+ "description": "Human-readable label."
1235
+ },
1236
+ "target_url": {
1237
+ "type": "string",
1238
+ "description": "Optional https URL for the `webhook` channel; omit for in-app."
1239
+ },
1240
+ "channel": {
1241
+ "type": "string",
1242
+ "description": "Delivery channel: `webhook` (POST to target_url), `discord` (embed to discord_url), `mobile_push` (to a registered device), or `in_app` (dashboard only). Inferred from the URL you pass if omitted.",
1243
+ "enum": [
1244
+ "webhook",
1245
+ "discord",
1246
+ "in_app",
1247
+ "mobile_push"
1248
+ ]
1249
+ },
1250
+ "discord_url": {
1251
+ "type": "string",
1252
+ "description": "Discord incoming-webhook URL (https://discord.com/api/webhooks/…). Required when channel is \"discord\"."
1253
+ },
1254
+ "device_id": {
1255
+ "type": "string",
1256
+ "description": "Registered device id from the mobile app. Required when channel is \"mobile_push\"."
1257
+ },
1258
+ "cadence": {
1259
+ "type": "string",
1260
+ "description": "Evaluation cadence. Default realtime; hourly/nyse_open throttle. (`1m` accepted as a deprecated alias of realtime.)",
1261
+ "enum": [
1262
+ "realtime",
1263
+ "hourly",
1264
+ "nyse_open"
1265
+ ]
1266
+ }
291
1267
  },
292
- required: ['q'],
293
- },
294
- endpoint: {
295
- method: 'GET',
296
- path: '/v2/news/scan',
297
- paramLocation: {
298
- q: 'query', select: 'query', group_by: 'query', having: 'query',
299
- order: 'query', dir: 'query', limit: 'query', cursor: 'query',
1268
+ "required": [
1269
+ "signal"
1270
+ ]
1271
+ },
1272
+ "endpoint": {
1273
+ "method": "POST",
1274
+ "path": "/v2/signals/{signal}/subscribe",
1275
+ "paramLocation": {
1276
+ "signal": "path",
1277
+ "ticker": "body",
1278
+ "universe": "body",
1279
+ "condition": "body",
1280
+ "name": "body",
1281
+ "target_url": "body",
1282
+ "channel": "body",
1283
+ "discord_url": "body",
1284
+ "device_id": "body",
1285
+ "cadence": "body"
1286
+ }
1287
+ }
1288
+ },
1289
+ {
1290
+ "name": "tickerbot_subscribe_scan",
1291
+ "description": "Register a webhook that fires when matches for a scan query change. Pass `target_url` for an https POST, or `channel:\"discord\"` + `discord_url` to post an embed to Discord; omit for in-app delivery in the dashboard. `cadence` is real-time (1m) by default; throttle to hourly or nyse_open. Use to satisfy \"alert me when this happens\" prompts.",
1292
+ "inputSchema": {
1293
+ "type": "object",
1294
+ "properties": {
1295
+ "q": {
1296
+ "type": "string",
1297
+ "description": "SQL WHERE expression — same grammar as scan."
1298
+ },
1299
+ "name": {
1300
+ "type": "string",
1301
+ "description": "Human-readable label. Defaults to a truncated version of the query."
1302
+ },
1303
+ "universe": {
1304
+ "type": "string",
1305
+ "description": "Optional universe slug to scope the watch."
1306
+ },
1307
+ "target_url": {
1308
+ "type": "string",
1309
+ "description": "Optional https URL to POST matches to (the `webhook` channel). Omit for in-app delivery."
1310
+ },
1311
+ "channel": {
1312
+ "type": "string",
1313
+ "description": "Delivery channel: `webhook` (POST to target_url), `discord` (embed to discord_url), `mobile_push` (to a registered device), or `in_app` (dashboard only). Inferred from the URL you pass if omitted.",
1314
+ "enum": [
1315
+ "webhook",
1316
+ "discord",
1317
+ "in_app",
1318
+ "mobile_push"
1319
+ ]
1320
+ },
1321
+ "discord_url": {
1322
+ "type": "string",
1323
+ "description": "Discord incoming-webhook URL (https://discord.com/api/webhooks/…). Required when channel is \"discord\"."
1324
+ },
1325
+ "device_id": {
1326
+ "type": "string",
1327
+ "description": "Registered device id from the mobile app. Required when channel is \"mobile_push\"."
1328
+ },
1329
+ "cadence": {
1330
+ "type": "string",
1331
+ "description": "Evaluation cadence. Default realtime; hourly/nyse_open throttle. (`1m` accepted as a deprecated alias of realtime.)",
1332
+ "enum": [
1333
+ "realtime",
1334
+ "hourly",
1335
+ "nyse_open"
1336
+ ]
1337
+ }
300
1338
  },
1339
+ "required": [
1340
+ "q"
1341
+ ]
301
1342
  },
1343
+ "endpoint": {
1344
+ "method": "POST",
1345
+ "path": "/v2/scan/subscribe",
1346
+ "paramLocation": {
1347
+ "q": "body",
1348
+ "name": "body",
1349
+ "universe": "body",
1350
+ "target_url": "body",
1351
+ "channel": "body",
1352
+ "discord_url": "body",
1353
+ "device_id": "body",
1354
+ "cadence": "body"
1355
+ }
1356
+ }
302
1357
  },
303
- // ── Webhooks / subscribe (alert provisioning) ───────────────────────
304
- // Subscribe tools let the caller wire a webhook end-to-end in chat:
305
- // pass `target_url` to deliver to Discord/Slack/their own server;
306
- // omit it for in-app delivery (visible in the Tickerbot dashboard).
307
- // `cadence` is plan-gated — Hobby caps at 5m, Pro+ at 1m.
308
- {
309
- name: 'tickerbot_subscribe_scan',
310
- description: 'Register a webhook that fires when matches for a scan query change. Pass `target_url` to deliver to your own URL (Discord, Slack, server endpoint); omit for in-app delivery in the dashboard. `cadence` is plan-gated (Hobby max 5m, Pro+ 1m). Use to satisfy "alert me when this happens" prompts.',
311
- inputSchema: {
312
- type: 'object',
313
- properties: {
314
- q: { type: 'string', description: 'SQL WHERE expression — same grammar as scan.' },
315
- name: { type: 'string', description: 'Human-readable label. Defaults to a truncated version of the query.' },
316
- universe: { type: 'string', description: 'Optional universe slug to scope the watch.' },
317
- target_url: { type: 'string', description: 'Optional https URL to POST matches to. Omit for in-app delivery.' },
318
- cadence: { type: 'string', description: 'Evaluation cadence.', enum: ['1m', '5m', '15m', 'hourly', 'nyse_open'] },
1358
+ {
1359
+ "name": "tickerbot_create_webhook",
1360
+ "description": "The canonical webhook create — POST /v2/webhooks with an explicit `trigger` object: { type: \"scan\" | \"ticker\" | \"signal\" | \"event\", … } plus delivery fields. The subscribe tools above are flat sugar over exactly this; use this form when composing the trigger programmatically or when a sugar door doesn't fit. Trigger shapes: scan {type:\"scan\", q, universe?}; ticker {type:\"ticker\", ticker, condition}; signal {type:\"signal\", signal, ticker?, universe?, condition?}; event {type:\"event\", kinds, tickers?, universe?, event_q?}.",
1361
+ "inputSchema": {
1362
+ "type": "object",
1363
+ "properties": {
1364
+ "trigger": {
1365
+ "type": "object",
1366
+ "description": "What fires the webhook: { type: \"scan\" | \"ticker\" | \"signal\" | \"event\", … } — see the tool description for each shape."
1367
+ },
1368
+ "name": {
1369
+ "type": "string",
1370
+ "description": "Display name. Defaults from the trigger."
1371
+ },
1372
+ "target_url": {
1373
+ "type": "string",
1374
+ "description": "HTTPS delivery URL (`webhook` channel). Omit for in-app."
1375
+ },
1376
+ "channel": {
1377
+ "type": "string",
1378
+ "description": "Delivery channel. Inferred from the URL you pass if omitted.",
1379
+ "enum": [
1380
+ "webhook",
1381
+ "discord",
1382
+ "in_app",
1383
+ "mobile_push"
1384
+ ]
1385
+ },
1386
+ "discord_url": {
1387
+ "type": "string",
1388
+ "description": "Discord incoming-webhook URL (channel discord)."
1389
+ },
1390
+ "device_id": {
1391
+ "type": "string",
1392
+ "description": "Registered device id from the mobile app (channel mobile_push)."
1393
+ },
1394
+ "cadence": {
1395
+ "type": "string",
1396
+ "description": "Evaluation cadence. Default realtime. (`1m` accepted as a deprecated alias of realtime.)",
1397
+ "enum": [
1398
+ "realtime",
1399
+ "hourly",
1400
+ "nyse_open"
1401
+ ]
1402
+ },
1403
+ "columns": {
1404
+ "type": "string",
1405
+ "description": "Comma list of extra columns to include with each delivered match (`fields` accepted as alias)."
1406
+ }
319
1407
  },
320
- required: ['q'],
321
- },
322
- endpoint: {
323
- method: 'POST',
324
- path: '/v2/scan/subscribe',
325
- paramLocation: { q: 'body', name: 'body', universe: 'body', target_url: 'body', cadence: 'body' },
326
- },
327
- },
328
- {
329
- name: 'tickerbot_subscribe_signal',
330
- description: 'Register a webhook that fires when a signal turns true (booleans) or its value crosses a condition (numerics). Optional `ticker` restricts to one symbol; omit to watch the whole universe. Pass `target_url` for outbound delivery or omit for in-app.',
331
- inputSchema: {
332
- type: 'object',
333
- properties: {
334
- signal: { type: 'string', description: 'Column name (e.g. golden_cross_today, rsi_14).' },
335
- ticker: { type: 'string', description: 'Optional ticker to restrict the watch to one symbol.' },
336
- universe: { type: 'string', description: 'Optional universe slug.' },
337
- condition: { type: 'string', description: 'Required for numerics: single bound like ">70" or "<=200". Ignored for booleans.' },
338
- name: { type: 'string', description: 'Human-readable label.' },
339
- target_url: { type: 'string', description: 'Optional https URL for delivery; omit for in-app.' },
340
- cadence: { type: 'string', description: 'Evaluation cadence.', enum: ['1m', '5m', '15m', 'hourly', 'nyse_open'] },
1408
+ "required": [
1409
+ "trigger"
1410
+ ]
1411
+ },
1412
+ "endpoint": {
1413
+ "method": "POST",
1414
+ "path": "/v2/webhooks",
1415
+ "paramLocation": {
1416
+ "trigger": "body",
1417
+ "name": "body",
1418
+ "target_url": "body",
1419
+ "channel": "body",
1420
+ "discord_url": "body",
1421
+ "device_id": "body",
1422
+ "cadence": "body",
1423
+ "columns": "body"
1424
+ }
1425
+ }
1426
+ },
1427
+ {
1428
+ "name": "tickerbot_patch_webhook",
1429
+ "description": "Edit a webhook in place: `name`, `cadence`, `target_url`, `enabled` (pausing/resuming without losing match-state). The trigger (q) and channel are immutable by design — delete and re-create to change what fires or where it delivers. Unknown fields are a 400, never silently ignored.",
1430
+ "inputSchema": {
1431
+ "type": "object",
1432
+ "properties": {
1433
+ "id": {
1434
+ "type": "string",
1435
+ "description": "Webhook id (looks like `wh_…`)."
1436
+ },
1437
+ "name": {
1438
+ "type": "string",
1439
+ "description": "New display name."
1440
+ },
1441
+ "cadence": {
1442
+ "type": "string",
1443
+ "description": "New evaluation cadence.",
1444
+ "enum": [
1445
+ "realtime",
1446
+ "hourly",
1447
+ "nyse_open"
1448
+ ]
1449
+ },
1450
+ "target_url": {
1451
+ "type": "string",
1452
+ "description": "New HTTPS delivery URL."
1453
+ },
1454
+ "enabled": {
1455
+ "type": "boolean",
1456
+ "description": "false pauses deliveries; true resumes. (Re-enabling after auto-disable also works via tickerbot_enable_webhook, which additionally clears match-state.)"
1457
+ }
341
1458
  },
342
- required: ['signal'],
1459
+ "required": [
1460
+ "id"
1461
+ ]
343
1462
  },
344
- endpoint: {
345
- method: 'POST',
346
- path: '/v2/signals/{signal}/subscribe',
347
- paramLocation: { signal: 'path', ticker: 'body', universe: 'body', condition: 'body', name: 'body', target_url: 'body', cadence: 'body' },
1463
+ "endpoint": {
1464
+ "method": "PATCH",
1465
+ "path": "/v2/webhooks/{id}",
1466
+ "paramLocation": {
1467
+ "id": "path",
1468
+ "name": "body",
1469
+ "cadence": "body",
1470
+ "target_url": "body",
1471
+ "enabled": "body"
1472
+ }
1473
+ }
1474
+ },
1475
+ {
1476
+ "name": "tickerbot_list_webhooks",
1477
+ "description": "List the caller's webhook subscriptions (rules created via the subscribe tools), newest-first. Use `status` to filter to active or disabled rules.",
1478
+ "inputSchema": {
1479
+ "type": "object",
1480
+ "properties": {
1481
+ "status": {
1482
+ "type": "string",
1483
+ "description": "Filter by status. Omit for all. (`pending_verification` = created but never successfully pinged.)",
1484
+ "enum": [
1485
+ "active",
1486
+ "pending_verification",
1487
+ "disabled"
1488
+ ]
1489
+ },
1490
+ "limit": {
1491
+ "type": "integer",
1492
+ "description": "Page size. Max 100. Default 50."
1493
+ },
1494
+ "cursor": {
1495
+ "type": "string",
1496
+ "description": "Opaque cursor."
1497
+ }
1498
+ }
348
1499
  },
1500
+ "endpoint": {
1501
+ "method": "GET",
1502
+ "path": "/v2/webhooks",
1503
+ "paramLocation": {
1504
+ "status": "query",
1505
+ "limit": "query",
1506
+ "cursor": "query"
1507
+ }
1508
+ }
349
1509
  },
350
1510
  {
351
- name: 'tickerbot_subscribe_ticker',
352
- description: 'Register a webhook that fires when one ticker matches a condition. `condition` is a SQL WHERE-clause fragment scoped to that ticker (e.g. "rsi_14 > 70 AND relative_volume > 2"). Pass `target_url` for outbound delivery or omit for in-app.',
353
- inputSchema: {
354
- type: 'object',
355
- properties: {
356
- ticker: { type: 'string', description: 'Symbol.' },
357
- condition: { type: 'string', description: 'SQL WHERE fragment evaluated for this ticker (e.g. "rsi_14 > 70 AND gap_up").' },
358
- name: { type: 'string', description: 'Human-readable label.' },
359
- target_url: { type: 'string', description: 'Optional https URL for delivery; omit for in-app.' },
360
- cadence: { type: 'string', description: 'Evaluation cadence.', enum: ['1m', '5m', '15m', 'hourly', 'nyse_open'] },
1511
+ "name": "tickerbot_get_webhook",
1512
+ "description": "Fetch one webhook subscription by id (current state, match-set, schedule). Account-scoped: any key on the account can read any of the account's webhooks.",
1513
+ "inputSchema": {
1514
+ "type": "object",
1515
+ "properties": {
1516
+ "id": {
1517
+ "type": "string",
1518
+ "description": "Webhook id (looks like `wh_…`)."
1519
+ }
361
1520
  },
362
- required: ['ticker', 'condition'],
1521
+ "required": [
1522
+ "id"
1523
+ ]
363
1524
  },
364
- endpoint: {
365
- method: 'POST',
366
- path: '/v2/tickers/{ticker}/subscribe',
367
- paramLocation: { ticker: 'path', condition: 'body', name: 'body', target_url: 'body', cadence: 'body' },
1525
+ "endpoint": {
1526
+ "method": "GET",
1527
+ "path": "/v2/webhooks/{id}",
1528
+ "paramLocation": {
1529
+ "id": "path"
1530
+ }
1531
+ }
1532
+ },
1533
+ {
1534
+ "name": "tickerbot_list_webhook_deliveries",
1535
+ "description": "List recent deliveries (pings and fires) for a webhook, newest-first — for diagnosing failures. Returns metadata only (status, attempt, response code, error); the POST body is not stored.",
1536
+ "inputSchema": {
1537
+ "type": "object",
1538
+ "properties": {
1539
+ "id": {
1540
+ "type": "string",
1541
+ "description": "Webhook id."
1542
+ },
1543
+ "status": {
1544
+ "type": "string",
1545
+ "description": "Filter by delivery status.",
1546
+ "enum": [
1547
+ "pending",
1548
+ "delivered",
1549
+ "permanent_failure"
1550
+ ]
1551
+ },
1552
+ "from": {
1553
+ "type": "string",
1554
+ "description": "Only deliveries created at/after this moment — epoch seconds or ISO datetime. (90-day retention on every plan.)"
1555
+ },
1556
+ "limit": {
1557
+ "type": "integer",
1558
+ "description": "Page size. Max 100. Default 50."
1559
+ },
1560
+ "cursor": {
1561
+ "type": "string",
1562
+ "description": "Opaque cursor."
1563
+ }
1564
+ },
1565
+ "required": [
1566
+ "id"
1567
+ ]
368
1568
  },
1569
+ "endpoint": {
1570
+ "method": "GET",
1571
+ "path": "/v2/webhooks/{id}/deliveries",
1572
+ "paramLocation": {
1573
+ "id": "path",
1574
+ "status": "query",
1575
+ "from": "query",
1576
+ "limit": "query",
1577
+ "cursor": "query"
1578
+ }
1579
+ }
369
1580
  },
370
1581
  {
371
- name: 'tickerbot_list_webhooks',
372
- description: 'List the caller\'s active webhook subscriptions (rules created via the subscribe tools).',
373
- inputSchema: {
374
- type: 'object',
375
- properties: {
376
- limit: { type: 'integer', description: 'Page size.' },
377
- cursor: { type: 'string', description: 'Opaque cursor.' },
1582
+ "name": "tickerbot_test_webhook",
1583
+ "description": "Send a real-shape `webhook.fired` POST to the webhook's target_url synchronously, right now. The body is byte-identical to a real fire (same signing); the test marker rides in an `X-Tickerbot-Test: true` header. Returns the inline outcome (`delivered`, `http_status`, `elapsed_ms`, `error`). One-shot — a failed test never retries and never auto-disables the webhook. Fails with 400 if the webhook has no target_url (in-app deliveries have nothing to fire over the wire).",
1584
+ "inputSchema": {
1585
+ "type": "object",
1586
+ "properties": {
1587
+ "id": {
1588
+ "type": "string",
1589
+ "description": "Webhook id."
1590
+ }
378
1591
  },
1592
+ "required": [
1593
+ "id"
1594
+ ]
379
1595
  },
380
- endpoint: { method: 'GET', path: '/v2/webhooks', paramLocation: { limit: 'query', cursor: 'query' } },
1596
+ "endpoint": {
1597
+ "method": "POST",
1598
+ "path": "/v2/webhooks/{id}/test",
1599
+ "paramLocation": {
1600
+ "id": "path"
1601
+ }
1602
+ }
381
1603
  },
382
1604
  {
383
- name: 'tickerbot_delete_webhook',
384
- description: 'Delete a webhook subscription by id. Use after listing webhooks when the user wants to remove an alert.',
385
- inputSchema: {
386
- type: 'object',
387
- properties: {
388
- id: { type: 'string', description: 'Webhook id (looks like `wh_…`).' },
1605
+ "name": "tickerbot_enable_webhook",
1606
+ "description": "Re-enable a disabled webhook — flips it back to `active` and clears its match-state so the next eval treats every currently-matching ticker as new. Use after fixing whatever caused auto-disable. No-op on an already-active webhook.",
1607
+ "inputSchema": {
1608
+ "type": "object",
1609
+ "properties": {
1610
+ "id": {
1611
+ "type": "string",
1612
+ "description": "Webhook id."
1613
+ }
389
1614
  },
390
- required: ['id'],
1615
+ "required": [
1616
+ "id"
1617
+ ]
391
1618
  },
392
- endpoint: { method: 'DELETE', path: '/v2/webhooks/{id}', paramLocation: { id: 'path' } },
1619
+ "endpoint": {
1620
+ "method": "POST",
1621
+ "path": "/v2/webhooks/{id}/enable",
1622
+ "paramLocation": {
1623
+ "id": "path"
1624
+ }
1625
+ }
393
1626
  },
1627
+ {
1628
+ "name": "tickerbot_delete_webhook",
1629
+ "description": "Delete a webhook subscription by id. Use after listing webhooks when the user wants to remove an alert.",
1630
+ "inputSchema": {
1631
+ "type": "object",
1632
+ "properties": {
1633
+ "id": {
1634
+ "type": "string",
1635
+ "description": "Webhook id (looks like `wh_…`)."
1636
+ }
1637
+ },
1638
+ "required": [
1639
+ "id"
1640
+ ]
1641
+ },
1642
+ "endpoint": {
1643
+ "method": "DELETE",
1644
+ "path": "/v2/webhooks/{id}",
1645
+ "paramLocation": {
1646
+ "id": "path"
1647
+ }
1648
+ }
1649
+ }
394
1650
  ];
395
1651
  export function findTool(name) {
396
1652
  return tools.find((t) => t.name === name);