sentisense 0.45.0 → 0.46.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/README.md CHANGED
@@ -3,7 +3,25 @@
3
3
  [![npm version](https://img.shields.io/npm/v/sentisense.svg)](https://www.npmjs.com/package/sentisense)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- Official JavaScript/TypeScript SDK for the [SentiSense](https://sentisense.ai) market intelligence API.
6
+ Official JavaScript/TypeScript SDK and CLI for the [SentiSense](https://sentisense.ai) market intelligence API: stock prices, news and social sentiment, the SentiSense Score, insider and congressional trading, institutional 13F flows, options positioning, analyst ratings, earnings analysis, and a cross-signal screener.
7
+
8
+ - Full TypeScript support with detailed type definitions
9
+ - Works in Node.js 18+, Deno, Bun, and browsers
10
+ - Zero runtime dependencies (native `fetch`)
11
+ - Namespaced resources (`stocks`, `documents`, `institutional`, ...) and a typed error hierarchy
12
+ - A complete command line interface in the same package, runnable via `npx` with nothing to install
13
+
14
+ Get a free API key at [app.sentisense.ai/get-api-key](https://app.sentisense.ai/get-api-key). Full API docs at [sentisense.ai/docs/api](https://sentisense.ai/docs/api).
15
+
16
+ ## Contents
17
+
18
+ - [Install](#install)
19
+ - [Quick start](#quick-start)
20
+ - [The CLI](#the-cli)
21
+ - [Configuration](#configuration)
22
+ - [Response shapes](#response-shapes)
23
+ - [API reference](#api-reference)
24
+ - [Error handling](#error-handling)
7
25
 
8
26
  ## Install
9
27
 
@@ -11,7 +29,7 @@ Official JavaScript/TypeScript SDK for the [SentiSense](https://sentisense.ai) m
11
29
  npm install sentisense
12
30
  ```
13
31
 
14
- ## Quick Start
32
+ ## Quick start
15
33
 
16
34
  ```typescript
17
35
  import SentiSense from "sentisense";
@@ -26,7 +44,7 @@ console.log(price.currentPrice);
26
44
  const flows = await client.institutional.getFlows();
27
45
  ```
28
46
 
29
- ## CLI
47
+ ## The CLI
30
48
 
31
49
  The same package ships a command line tool. Nothing to install:
32
50
 
@@ -44,8 +62,6 @@ npx -y sentisense@latest auth <your key>
44
62
  npx -y sentisense@latest health
45
63
  ```
46
64
 
47
- Get a key at [app.sentisense.ai/get-api-key](https://app.sentisense.ai/get-api-key).
48
-
49
65
  ### Commands
50
66
 
51
67
  | Command | What you get |
@@ -67,7 +83,7 @@ Get a key at [app.sentisense.ai/get-api-key](https://app.sentisense.ai/get-api-k
67
83
 
68
84
  Run `sentisense help <command>` for its flags and examples.
69
85
 
70
- ### Output
86
+ ### Output modes
71
87
 
72
88
  Readable in a terminal, plain text when piped, and exact API JSON on request:
73
89
 
@@ -77,40 +93,13 @@ npx -y sentisense@latest quote NVDA | cat # plain text, no escape codes
77
93
  npx -y sentisense@latest quote NVDA --json | jq # the API response untouched
78
94
  ```
79
95
 
80
- `--json` prints what the API returned, envelope and all, so `isPreview` and `totalCount` stay
81
- visible. For `quote` that is the exact quote response for one ticker, and an object keyed by
82
- ticker for several. `--full` widens any command. `--no-color` and `NO_COLOR` drop the colour,
83
- `--plain` and `--pretty` force a layout, and `--debug` prints stack traces.
84
-
85
- Commands spend requests on the answer, not on decoration: `quote` looks up the company name
86
- only for the terminal layout, so piped and `--json` output cost one request per ticker. When
87
- something supplementary does not come back, such as the Score history behind a sparkline, the
88
- command still prints its answer and exits 0 with a `note:` line on stderr, so stdout stays
89
- clean for a pipe and the gap is never silent.
90
-
91
- ### Saying who is calling
92
-
93
- If you set `SENTISENSE_AGENT_NAME` (what your agent is called) and `SENTISENSE_SKILL` (the
94
- slug of the skill driving it), requests carry that identity, so usage can be understood and
95
- the tools improved. Both are optional, never required, and nothing is inferred when they are
96
- absent.
97
-
98
- ```bash
99
- export SENTISENSE_AGENT_NAME=research-desk
100
- export SENTISENSE_SKILL=stock-analysis
101
- npx -y sentisense@latest quote NVDA
102
- # User-Agent: sentisense-node/0.44.0 sentisense-cli/0.44.0 (stock-analysis; agent/research-desk)
103
- ```
96
+ `--json` prints what the API returned, envelope and all, so `isPreview` and `totalCount` stay visible. For `quote` that is the exact quote response for one ticker, and an object keyed by ticker for several. `--full` widens any command. `--no-color` and `NO_COLOR` drop the colour, `--plain` and `--pretty` force a layout, and `--debug` prints stack traces.
104
97
 
105
- Either can also be a flag (`--agent`, `--skill`) or a stored setting
106
- (`sentisense auth --agent research-desk --skill stock-analysis`), resolved flag first, then
107
- environment, then config. Values are reduced to letters, digits, dot, underscore and hyphen,
108
- and capped at 32 characters, so nothing you set can reshape the header.
98
+ Commands spend requests on the answer, not on decoration: `quote` looks up the company name only for the terminal layout, so piped and `--json` output cost one request per ticker. When something supplementary does not come back, such as the Score history behind a sparkline, the command still prints its answer and exits 0 with a `note:` line on stderr, so stdout stays clean for a pipe and the gap is never silent.
109
99
 
110
100
  ### Exit codes
111
101
 
112
- Failures print two lines to stderr, what went wrong and what to do about it, and exit with a
113
- code you can branch on. The CLI does not retry, so a 5 is yours to handle.
102
+ Failures print two lines to stderr, what went wrong and what to do about it, and exit with a code you can branch on. The CLI does not retry, so a 5 is yours to handle.
114
103
 
115
104
  | Code | Meaning |
116
105
  |------|---------|
@@ -122,30 +111,52 @@ code you can branch on. The CLI does not retry, so a 5 is yours to handle.
122
111
  | 5 | Rate limited |
123
112
  | 6 | Network failure or timeout |
124
113
 
114
+ ### Saying who is calling
115
+
116
+ If you set `SENTISENSE_AGENT_NAME` (what your agent is called) and `SENTISENSE_SKILL` (the slug of the skill driving it), requests carry that identity, so usage can be understood and the tools improved. Both are optional, never required, and nothing is inferred when they are absent.
117
+
118
+ ```bash
119
+ export SENTISENSE_AGENT_NAME=research-desk
120
+ export SENTISENSE_SKILL=stock-analysis
121
+ npx -y sentisense@latest quote NVDA
122
+ # User-Agent: sentisense-node/{version} sentisense-cli/{version} (stock-analysis; agent/research-desk)
123
+ ```
124
+
125
+ Either can also be a flag (`--agent`, `--skill`) or a stored setting (`sentisense auth --agent research-desk --skill stock-analysis`), resolved flag first, then environment, then config. Values are reduced to letters, digits, dot, underscore and hyphen, and capped at 32 characters, so nothing you set can reshape the header.
126
+
125
127
  Research data, not investment advice.
126
128
 
127
- ## Features
129
+ ## Configuration
128
130
 
129
- - Full TypeScript support with detailed type definitions
130
- - Works in Node.js 18+, Deno, Bun, and browsers
131
- - Zero runtime dependencies (uses native `fetch`)
132
- - Namespaced API resources (stocks, documents, institutional, etc.)
133
- - Typed error hierarchy for clean error handling
131
+ ```typescript
132
+ const client = new SentiSense({
133
+ apiKey: process.env.SENTISENSE_API_KEY, // Get yours at app.sentisense.ai/get-api-key
134
+ baseUrl: "https://...", // Default: https://app.sentisense.ai
135
+ timeout: 30000, // Default: 30s (in milliseconds)
136
+ maxRetries: 3, // Default: 3
137
+ userAgentSuffix: "my-bot/1.4", // Default: none
138
+ });
139
+ ```
140
+
141
+ | Option | Default | What it does |
142
+ |--------|---------|--------------|
143
+ | `apiKey` | none | Sent as `X-SentiSense-API-Key`. Required by every endpoint. |
144
+ | `baseUrl` | `https://app.sentisense.ai` | Override for a non-production host. |
145
+ | `timeout` | `30000` | Per-request timeout in milliseconds. |
146
+ | `maxRetries` | `3` | Retries on 429 and 5xx, honouring `Retry-After`. Set `0` to fail fast. |
147
+ | `userAgentSuffix` | none | Appended to the User-Agent, after `sentisense-node/{version}`. |
148
+
149
+ `userAgentSuffix` is how you say what is calling on top of the SDK, so your traffic is legible in your own logs and in ours. A tool name and version works (`"my-bot/1.4"`), optionally with an agent label (`"my-bot/1.4 agent/research-desk"`). Node only, since browsers set the header themselves. Newlines are collapsed and an empty value is ignored.
150
+
151
+ Keep the key in the environment rather than in source. Committing a literal key leaks it into git history and into every registry security scan that reads your repo.
134
152
 
135
153
  ## Response shapes
136
154
 
137
- Most methods resolve to the payload directly, but two families wrap it. The return types
138
- describe the wrapper, so `.data` / `.documents` type-check natively, no cast.
155
+ Most methods resolve to the payload directly, but two families wrap it. The return types describe the wrapper, so `.data` / `.documents` type-check natively, no cast.
139
156
 
140
- **1. Tier-gated endpoints return a preview envelope.** The payload is in `data`, and
141
- `isPreview` tells you whether it was truncated for your tier. `totalCount` carries the
142
- untruncated size whenever the server knows it: on a truncated response, so you can render
143
- "showing N of M", and on a paged endpoint such as `politicians.getActivity`, where it is
144
- the full match count on every tier including PRO. A missing `totalCount` means "count
145
- `data` yourself", never "zero results".
157
+ **1. Tier-gated endpoints return a preview envelope.** The payload is in `data`, and `isPreview` tells you whether it was truncated for your tier. `totalCount` carries the untruncated size whenever the server knows it: on a truncated response, so you can render "showing N of M", and on a paged endpoint such as `politicians.getActivity`, where it is the full match count on every tier including PRO. A missing `totalCount` means "count `data` yourself", never "zero results".
146
158
 
147
- Affected: `institutional.getFlows` / `getHolders` / `getActivists`, and all five
148
- `insights` methods.
159
+ Affected: `institutional.getFlows` / `getHolders` / `getActivists`, and all five `insights` methods.
149
160
 
150
161
  ```typescript
151
162
  const flows = await client.institutional.getFlows();
@@ -168,11 +179,9 @@ for (const insight of insights.data) {
168
179
  }
169
180
  ```
170
181
 
171
- **2. Document endpoints return a search wrapper.** This is not the preview envelope:
172
- the rows are in `documents` and there is no `isPreview`.
182
+ **2. Document endpoints return a search wrapper.** This is not the preview envelope: the rows are in `documents` and there is no `isPreview`.
173
183
 
174
- Affected: `documents.getByTicker` / `getByTickerRange` / `getByEntity` / `search` /
175
- `getBySource`. Also `stocks.getFundamentalsPeriods`, whose periods are in `periods`.
184
+ Affected: `documents.getByTicker` / `getByTickerRange` / `getByEntity` / `search` / `getBySource`. Also `stocks.getFundamentalsPeriods`, whose periods are in `periods`.
176
185
 
177
186
  ```typescript
178
187
  const results = await client.documents.search("NVDA earnings", { days: 7 });
@@ -182,34 +191,32 @@ for (const doc of results.documents) {
182
191
  }
183
192
  ```
184
193
 
185
- Everything else, including `stocks.getPrice()`, `documents.getStories()`,
186
- `insights.types()` and `institutional.getQuarters()`, resolves to the value itself with no
187
- wrapper.
194
+ Everything else, including `stocks.getPrice()`, `documents.getStories()`, `insights.types()` and `institutional.getQuarters()`, resolves to the value itself with no wrapper.
188
195
 
189
- > **Upgrading from 0.28.x or earlier?** These return types were corrected in 0.29.0. If your
190
- > code read the flat shape (`flows.inflows`, `holders.filter(...)`), it was returning
191
- > `undefined` / throwing at runtime already; switch to `flows.data.inflows` /
192
- > `holders.data.holders`. See [CHANGELOG.md](./CHANGELOG.md) for the full mapping.
196
+ > **Upgrading from 0.28.x or earlier?** These return types were corrected in 0.29.0. If your code read the flat shape (`flows.inflows`, `holders.filter(...)`), it was returning `undefined` / throwing at runtime already; switch to `flows.data.inflows` / `holders.data.holders`. See [CHANGELOG.md](./CHANGELOG.md) for the full mapping.
193
197
 
194
- ## API Reference
198
+ ## API reference
195
199
 
196
200
  ### Stocks
197
201
 
198
202
  ```typescript
199
203
  client.stocks.list() // All ticker symbols
200
204
  client.stocks.listDetailed() // All stocks with details
201
- client.stocks.getPrice("AAPL") // Real-time price
205
+ client.stocks.getPrice("AAPL") // Latest price
202
206
  client.stocks.getPrices(["AAPL", "NVDA"]) // Batch prices
207
+ client.stocks.getQuote("AAPL") // Fuller quote: ranges, market cap, P/E
203
208
  client.stocks.getProfile("AAPL") // Company profile
204
209
  client.stocks.getChart("AAPL", { timeframe: "6M" }) // OHLCV chart data
205
210
  client.stocks.getMarketStatus() // Market open/closed
206
211
  client.stocks.getFundamentals("AAPL") // Financial data
207
212
  client.stocks.getShortInterest("GME") // Short interest
208
- client.stocks.getOptionsSummary("NVDA") // End-of-day options dossier
213
+ client.stocks.getOptionsSummary("NVDA") // End-of-day options dossier
209
214
  client.stocks.getAISummary("AAPL", { depth: "deep" }) // AI report (PRO)
210
215
  ```
211
216
 
212
- ### Documents & News
217
+ Price fields carry `priceAsOf` (Unix milliseconds) for the age of the market data; read that for freshness rather than `timestamp`, which is when the response was served.
218
+
219
+ ### Documents & news
213
220
 
214
221
  ```typescript
215
222
  client.documents.getByTicker("AAPL", { source: "news", days: 3 })
@@ -218,7 +225,7 @@ client.documents.getStories({ limit: 10 })
218
225
  client.documents.getStoryDetail("cluster_abc123")
219
226
  ```
220
227
 
221
- ### Institutional Flows (13F)
228
+ ### Institutional flows (13F)
222
229
 
223
230
  ```typescript
224
231
  client.institutional.getQuarters()
@@ -227,12 +234,7 @@ client.institutional.getHolders("AAPL", "2025-02-14")
227
234
  client.institutional.getActivists("2025-02-14")
228
235
  ```
229
236
 
230
- #### Paging the holder list
231
-
232
- A widely held ticker returns thousands of rows: a megacap quarter is roughly 6,000
233
- holders and 1.5 MB on the wire. Pass `limit` unless you really want the whole list.
234
- Omitting the options object sends the original unbounded request, so existing code
235
- keeps working.
237
+ **Paging the holder list.** A widely held ticker returns thousands of rows: a megacap quarter is roughly 6,000 holders and 1.5 MB on the wire. Pass `limit` unless you really want the whole list; omitting the options object sends the original unbounded request, so existing code keeps working.
236
238
 
237
239
  | Option | Values |
238
240
  |--------|--------|
@@ -241,14 +243,9 @@ keeps working.
241
243
  | `sortBy` | `"shares"` (server default), `"valueUsd"`, or `"sharesChangePct"`. Requires `limit`. |
242
244
  | `sortDir` | `"desc"` (server default) or `"asc"`. Requires `limit`. |
243
245
 
244
- `limit` is the switch for the whole set. Send `offset`, `sortBy`, or `sortDir` without it
245
- and the server ignores them, returning the full unsorted list with a 200 and no warning.
246
+ `limit` is the switch for the whole set: send `offset`, `sortBy`, or `sortDir` without it and the server ignores them, returning the full unsorted list with a 200 and no warning.
246
247
 
247
248
  ```typescript
248
- import SentiSense from "sentisense";
249
-
250
- const client = new SentiSense({ apiKey: process.env.SENTISENSE_API_KEY });
251
-
252
249
  // Top 10 holders by position value, largest first
253
250
  const top = await client.institutional.getHolders("AAPL", "2026-03-31", {
254
251
  limit: 10,
@@ -267,39 +264,19 @@ const page = await client.institutional.getHolders("AAPL", "2026-03-31", {
267
264
  console.log(`${page.data.holders.length} rows of ${page.data.holderCount}`);
268
265
  ```
269
266
 
270
- A response to a request carrying `limit` also has three fields the unbounded response does
271
- not: `returnedCount` (rows on this page, smaller than your `limit` on the last one),
272
- `offset` (echoed back), and `notableChanges`, a ticker-wide summary of the quarter's biggest
273
- position moves so you do not have to scan every page to find them.
274
-
275
- ```typescript
276
- const page = await client.institutional.getHolders("AAPL", "2026-03-31", { limit: 100 });
277
- console.log(`${page.data.returnedCount} of ${page.data.holderCount} holders`);
278
- for (const mover of page.data.notableChanges?.top ?? []) {
279
- console.log(mover.filerName, mover.changeType, mover.sharesChangePct);
280
- }
281
- ```
282
-
283
- Each holder row also carries `entitySlug`, which you can hand straight to
284
- `institutional.getInstitutionDetail()`, and `cikCount` when the row rolls up several SEC
285
- filers under one manager. Both are null for filers we have not matched to an institution
286
- page, so check before building a link.
267
+ A response to a request carrying `limit` also has three fields the unbounded response does not: `returnedCount` (rows on this page, smaller than your `limit` on the last one), `offset` (echoed back), and `notableChanges`, a ticker-wide summary of the quarter's biggest position moves so you do not have to scan every page to find them. Each holder row also carries `entitySlug`, which you can hand straight to `institutional.getInstitutionDetail()`, and `cikCount` when the row rolls up several SEC filers under one manager; both are null for filers not matched to an institution page, so check before building a link.
287
268
 
288
- ### Congressional Trading
269
+ ### Congressional trading
289
270
 
290
271
  ```typescript
291
272
  client.politicians.getActivity({ lookbackDays: 90 }) // Market-wide STOCK Act feed
292
273
  client.politicians.getFilings("NVDA") // Trades in one stock
293
274
  client.politicians.getMembers() // Tracked members + trade stats
294
275
  client.politicians.getMember("nancy-pelosi") // One member's profile and trades
276
+ client.politicians.getDirectory({ q: "tex" }) // Discover slugs, including former members
295
277
  ```
296
278
 
297
- #### Paging the activity feed
298
-
299
- A 90-day window is routinely well over a thousand disclosures, and without `limit` the
300
- server returns the first 200 with nothing in the payload to say it stopped. `totalCount` on
301
- the envelope is the real size on every tier, so size the walk from that rather than from
302
- `data.length`.
279
+ **Paging the activity feed.** A 90-day window is routinely well over a thousand disclosures, and without `limit` the server returns the first 200 with nothing in the payload to say it stopped. `totalCount` on the envelope is the real size on every tier, so size the walk from that rather than from `data.length`.
303
280
 
304
281
  | Option | Values |
305
282
  |--------|--------|
@@ -319,63 +296,37 @@ for (let offset = 100; offset < (first.totalCount ?? 0); offset += 100) {
319
296
  }
320
297
  ```
321
298
 
322
- ### Entity Metrics
299
+ ### Insider trading (Form 4)
323
300
 
324
301
  ```typescript
325
- // Time-series metrics (v2 API)
326
- client.entityMetrics.getMetrics("AAPL", { metricType: "sentiment" })
327
- client.entityMetrics.getMetrics("AAPL", {
328
- metricType: "mentions",
329
- startTime: Date.now() - 7 * 86400000,
330
- endTime: Date.now(),
331
- maxDataPoints: 100,
332
- })
333
-
334
- // Distribution by source
335
- client.entityMetrics.getDistribution("AAPL", "sentiment")
336
- client.entityMetrics.getDistribution("AAPL", "mentions", { dimension: "source" })
302
+ client.insider.getActivity({ lookbackDays: 30 }) // Market-wide buys and sells by ticker
303
+ client.insider.getTrades("NVDA", { lookbackDays: 90 }) // Individual filed transactions
304
+ client.insider.getClusterBuys({ lookbackDays: 90 }) // 3+ distinct insiders buying the same stock
337
305
  ```
338
306
 
339
- Available metric types: `mentions`, `sentiment`, `sentisense`, `social_dominance`, `creators`.
307
+ Each trade row carries both the raw SEC `transactionCode` and a simplified `transactionType`. Only codes `P` and `S` are open-market trades; awards, gifts, exercises, and code `F` (shares withheld to cover taxes at vest, served as `SELL`) are corporate mechanics, so read `transactionCode` when you tally discretionary buying or selling. The market-wide activity endpoint's sells already exclude code `F` server-side.
340
308
 
341
- ### Knowledge Base
309
+ ### Analyst ratings
342
310
 
343
- ```typescript
344
- client.kb.getPopularEntities()
345
- ```
346
-
347
- ### Analyst Ratings
348
-
349
- The **price target cone** (mean, high, low, upside %) and consensus are **free for everyone, full data via API**: we give it away. Upgrade/downgrade feeds and forward EPS estimates are limited on free, unlimited on PRO.
311
+ The price target cone (mean, high, low, upside %) and consensus are free for everyone with full data. Upgrade/downgrade feeds and forward EPS estimates are limited on free, unlimited on PRO.
350
312
 
351
313
  ```typescript
352
- client.analyst.consensus("AAPL") // Price target cone + consensus. Free for everyone, full data.
353
- client.analyst.actions("AAPL", { lookbackDays: 30 }) // Upgrade/downgrade feed. Free: 3 most recent. PRO: unlimited.
354
- client.analyst.estimates("AAPL") // Forward EPS + earnings surprises. Free: 1 quarter. PRO: full history.
314
+ client.analyst.consensus("AAPL") // Price target cone + consensus. Free, full data.
315
+ client.analyst.actions("AAPL", { lookbackDays: 30 }) // Upgrade/downgrade feed. Free: 3 most recent.
316
+ client.analyst.estimates("AAPL") // Forward EPS + surprises. Free: 1 quarter.
355
317
  client.analyst.marketActivity({ lookbackDays: 7 }) // Market-wide analyst actions (PRO).
356
318
  ```
357
319
 
358
- ### Company KPIs (PRO)
359
-
360
- ```typescript
361
- client.stocks.getKpis("AAPL") // Product metrics and segment revenue time-series. Free returns metadata only (empty kpis array); PRO returns full series.
362
- client.stocks.listKpiCoverage() // All tickers with curated KPI data (free, no quota cost)
363
- ```
364
-
365
320
  ### Earnings
366
321
 
367
322
  The earnings analysis report is the assembled version of a quarter: one object per fiscal period carrying the editorial headline, the KPI cards with year-over-year deltas, the guidance language as management phrased it, and a summary of the earnings call. Pair it with the recent-reporters feed to drive a post-earnings sweep. Both return the preview envelope.
368
323
 
369
324
  ```typescript
370
- client.earnings.getSummaries("AAPL", { limit: 4 }) // Per-quarter analysis report, newest first. FREE: latest quarter, shaped. PRO: every hydrated quarter in full.
325
+ client.earnings.getSummaries("AAPL", { limit: 4 }) // Per-quarter analysis, newest first. Free: latest quarter, shaped.
371
326
  client.earnings.getRecent({ days: 7, limit: 25 }) // Who reported in the last N days. Full window on every key.
372
327
  ```
373
328
 
374
329
  ```typescript
375
- import SentiSense from "sentisense";
376
-
377
- const client = new SentiSense({ apiKey: process.env.SENTISENSE_API_KEY });
378
-
379
330
  const res = await client.earnings.getSummaries("AAPL", { limit: 1 });
380
331
  const quarter = res.data[0];
381
332
 
@@ -397,22 +348,49 @@ if (quarter) {
397
348
 
398
349
  The forward-looking half of the family is `client.calendar.getEarnings()`, which covers scheduled dates and consensus EPS rather than results.
399
350
 
351
+ ### Company KPIs (PRO)
352
+
353
+ ```typescript
354
+ client.stocks.getKpis("AAPL") // Product metrics and segment revenue. Free: metadata only. PRO: full series.
355
+ client.stocks.listKpiCoverage() // All tickers with curated KPI data (free, no quota cost)
356
+ ```
357
+
400
358
  ### ETFs (beta)
401
359
 
402
- Composition data is public; the holdings-weighted aggregate views follow the same PRO-with-preview pattern as Analyst/Insider. Aggregates synthesize fund-level views from each constituent's per-stock data, weighted by allocation, with a `coverage` block on every response.
360
+ Composition data is public; the holdings-weighted aggregate views follow the same PRO-with-preview pattern as analyst and insider data. Aggregates synthesize fund-level views from each constituent's per-stock data, weighted by allocation, with a `coverage` block on every response.
361
+
362
+ ```typescript
363
+ client.etfs.list() // Every ETF tracked
364
+ client.etfs.holdings("QQQ") // Full composition + freshness metadata
365
+ client.etfs.analystAggregate("QQQ") // Holdings-weighted analyst consensus
366
+ client.etfs.insiderAggregate("ARKK", { lookbackDays: 90 }) // Holdings-weighted Form 4 net flow
367
+ client.etfs.sentimentAggregate("QQQ") // Constituent-weighted vs direct Score
368
+ ```
369
+
370
+ ### Entity metrics
403
371
 
404
372
  ```typescript
405
- client.etfs.list() // Every ETF tracked. Returns ticker, name, issuer, tracked index, asset class.
406
- client.etfs.holdings("QQQ") // Full composition: per-holding weights + freshness metadata.
407
- client.etfs.analystAggregate("QQQ") // Holdings-weighted analyst consensus. Free: headline + coverage. PRO: + topContributors.
408
- client.etfs.insiderAggregate("ARKK", { lookbackDays: 90 }) // Holdings-weighted Form 4 net flow. Free: headline. PRO: + topContributors.
409
- client.etfs.sentimentAggregate("QQQ") // SentiSense readings side-by-side: constituent-weighted vs direct.
373
+ // Time-series metrics (v2 API)
374
+ client.entityMetrics.getMetrics("AAPL", { metricType: "sentiment" })
375
+ client.entityMetrics.getMetrics("AAPL", {
376
+ metricType: "mentions",
377
+ startTime: Date.now() - 7 * 86400000,
378
+ endTime: Date.now(),
379
+ maxDataPoints: 100,
380
+ })
381
+
382
+ // Distribution by source
383
+ client.entityMetrics.getDistribution("AAPL", "sentiment")
384
+ client.entityMetrics.getDistribution("AAPL", "mentions", { dimension: "source" })
410
385
  ```
411
386
 
412
- ### Market Mood
387
+ Available metric types: `mentions`, `sentiment`, `sentisense`, `social_dominance`, `creators`.
388
+
389
+ ### Market mood & knowledge base
413
390
 
414
391
  ```typescript
415
- client.marketMood.get()
392
+ client.marketMood.get() // Composite market sentiment with sub-signals
393
+ client.kb.getPopularEntities() // Most-tracked entities
416
394
  ```
417
395
 
418
396
  ### Screener
@@ -456,15 +434,15 @@ Three field semantics are worth stating outright, because guessing them wrong pr
456
434
 
457
435
  - **`ANALYST_RATING_MEAN` is inverted.** It is the vendor's 1-to-5 scale where **1.0 is strong buy**, so bullish is `LTE 2.5`. Prefer `ANALYST_BUY_RATIO_PCT`, which runs the intuitive direction.
458
436
  - **`MA_CROSS_STATE` is ordinal**, not a percentage: `1` golden cross, `-1` death cross, `0` neither. Use `EQ`.
459
- - **`SENTIMENT_DIRECTION` is the sign of the 7-day SentiSense Score** (`1` / `0` / `-1`) with a neutral band of plus-or-minus 5. Despite the name it is not sentiment polarity, and `0` matches only an exact zero.
437
+ - **`SENTIMENT_DIRECTION` is the sign of the 7-day SentiSense Score** (`1` / `0` / `-1`) with a neutral band of plus-or-minus 5. Despite the name it is not sentiment polarity.
460
438
 
461
439
  The Score fields (`SENTI_SCORE_7D`, `SENTI_SCORE_1M`, `SCORE_CHANGE_7D`) are the SentiSense Score, not polarity: unbounded, banded at 5 / 13 / 23 either side of zero. Filter on those band edges, not on values like `0.5`, which behave as "any positive score". Nulls never match in either direction, so `RETURN_1Y >= 0` and `RETURN_1Y < 0` do not partition the universe: a stock listed four months ago is in neither result. If a screen returns fewer rows than you expect, check coverage before you check your thresholds.
462
440
 
463
441
  On the ETF side, `CONSTITUENTS_WEIGHTED_SENTISENSE` is the holdings-weighted Score across what the fund owns and is usually the one you want; `DIRECT_SENTISENSE` is the Score from chatter about the fund ticker itself. `WEIGHT_COVERED_PCT` tells you how much of the fund's weight had constituent data behind the weighted number.
464
442
 
465
- Screens read a snapshot that refreshes every 20 minutes, so this is not a quote feed. Use `client.stocks.getQuote()` for live prices.
443
+ Screens read a snapshot that refreshes every 20 minutes, so this is not a quote feed. Use `client.stocks.getQuote()` for current quotes.
466
444
 
467
- ## Error Handling
445
+ ## Error handling
468
446
 
469
447
  ```typescript
470
448
  import SentiSense, { AuthenticationError, RateLimitError } from "sentisense";
@@ -480,7 +458,7 @@ try {
480
458
  }
481
459
  ```
482
460
 
483
- | Error Class | HTTP Status | When |
461
+ | Error class | HTTP status | When |
484
462
  |------------|-------------|------|
485
463
  | `AuthenticationError` | 401, 403 | Invalid API key or insufficient tier |
486
464
  | `NotFoundError` | 404 | Resource not found |
@@ -489,39 +467,13 @@ try {
489
467
 
490
468
  All errors extend `SentiSenseError` and include `status`, `code`, and `message` properties.
491
469
 
492
- ## Configuration
493
-
494
- ```typescript
495
- const client = new SentiSense({
496
- apiKey: process.env.SENTISENSE_API_KEY, // Get yours at app.sentisense.ai/settings/developer
497
- baseUrl: "https://...", // Default: https://app.sentisense.ai
498
- timeout: 30000, // Default: 30s (in milliseconds)
499
- maxRetries: 3, // Default: 3
500
- userAgentSuffix: "my-bot/1.4", // Default: none
501
- });
502
- ```
503
-
504
- | Option | Default | What it does |
505
- |--------|---------|--------------|
506
- | `apiKey` | none | Sent as `X-SentiSense-API-Key`. Required by every endpoint. |
507
- | `baseUrl` | `https://app.sentisense.ai` | Override for a non-production host. |
508
- | `timeout` | `30000` | Per-request timeout in milliseconds. |
509
- | `maxRetries` | `3` | Retries on 429 and 5xx, honouring `Retry-After`. Set `0` to fail fast. |
510
- | `userAgentSuffix` | none | Appended to the User-Agent, after `sentisense-node/{version}`. |
511
-
512
- `userAgentSuffix` is how you say what is calling on top of the SDK, so your traffic is legible
513
- in your own logs and in ours. A tool name and version works (`"my-bot/1.4"`), optionally with
514
- an agent label (`"my-bot/1.4 agent/research-desk"`). Node only, since browsers set the header
515
- themselves. Newlines are collapsed and an empty value is ignored.
516
-
517
- Keep the key in the environment rather than in source. Committing a literal key leaks it
518
- into git history and into every registry security scan that reads your repo.
519
-
520
- ## Get an API Key
470
+ ## Links
521
471
 
522
- Generate your API key from the [Developer Console](https://app.sentisense.ai/settings/developer).
472
+ - Get a free API key: [app.sentisense.ai/get-api-key](https://app.sentisense.ai/get-api-key)
473
+ - API documentation: [sentisense.ai/docs/api](https://sentisense.ai/docs/api)
474
+ - Changelog: [CHANGELOG.md](./CHANGELOG.md)
523
475
 
524
- For full API documentation, see [sentisense.ai/docs/api](https://sentisense.ai/docs/api).
476
+ SentiSense provides research data for informational and educational purposes, not investment advice.
525
477
 
526
478
  ## License
527
479
 
package/dist/cli.cjs CHANGED
@@ -1038,7 +1038,7 @@ var flowsCommand = {
1038
1038
  };
1039
1039
 
1040
1040
  // src/version.ts
1041
- var VERSION = "0.45.0";
1041
+ var VERSION = "0.46.0";
1042
1042
 
1043
1043
  // src/resources/analyst.ts
1044
1044
  var Analyst = class {
@@ -2498,6 +2498,9 @@ var insidersCommand = {
2498
2498
  ],
2499
2499
  notes: [
2500
2500
  "Rows are individual filed transactions, newest first, not a net total.",
2501
+ "The bought/sold figures count open-market rows only. Form 4 code F rows (shares withheld",
2502
+ "to cover taxes on vesting) arrive typed SELL but are mechanical withholding, not a decision",
2503
+ "to sell, so they are excluded from sold and shown separately as withheld.",
2501
2504
  "The plan column says whether the trade was under a confirmed pre-arranged 10b5-1 plan,",
2502
2505
  "which is the difference between a scheduled sale and a discretionary one.",
2503
2506
  "A free key sees the top few transactions; a PRO key sees the window you asked for.",
@@ -2522,18 +2525,22 @@ var insidersCommand = {
2522
2525
  if (note) notes.push(note);
2523
2526
  }
2524
2527
  const shown = full ? trades : trades.slice(0, 15);
2528
+ const isWithholding = (row) => row.transactionCode === "F";
2525
2529
  const buys = trades.filter((trade) => trade.transactionType === "BUY");
2526
- const sells = trades.filter((trade) => trade.transactionType === "SELL");
2530
+ const sells = trades.filter((trade) => trade.transactionType === "SELL" && !isWithholding(trade));
2531
+ const withheld = trades.filter(isWithholding);
2527
2532
  const sum = (rows) => rows.reduce((total, row) => total + (row.totalValue || 0), 0);
2533
+ const headline = [
2534
+ field("trades", String(trades.length)),
2535
+ field("bought", humanize(sum(buys)), buys.length > 0 ? "up" : void 0),
2536
+ field("sold", humanize(sum(sells)), sells.length > 0 ? "down" : void 0)
2537
+ ];
2538
+ if (withheld.length > 0) headline.push(field("withheld", humanize(sum(withheld))));
2528
2539
  const blocks = [
2529
2540
  {
2530
2541
  kind: "head",
2531
2542
  title: field("ticker", ticker),
2532
- right: fields(
2533
- field("trades", String(trades.length)),
2534
- field("bought", humanize(sum(buys)), buys.length > 0 ? "up" : void 0),
2535
- field("sold", humanize(sum(sells)), sells.length > 0 ? "down" : void 0)
2536
- )
2543
+ right: fields(...headline)
2537
2544
  }
2538
2545
  ];
2539
2546
  if (shown.length === 0) {
@@ -2548,8 +2555,8 @@ var insidersCommand = {
2548
2555
  cell(truncate(trade.insiderName, full ? 40 : 22)),
2549
2556
  cell(truncate(trade.insiderTitle ?? "", full ? 40 : 18)),
2550
2557
  cell(
2551
- trade.transactionType,
2552
- trade.transactionType === "BUY" ? "up" : trade.transactionType === "SELL" ? "down" : void 0
2558
+ isWithholding(trade) ? "TAX-W" : trade.transactionType,
2559
+ !isWithholding(trade) && trade.transactionType === "BUY" ? "up" : !isWithholding(trade) && trade.transactionType === "SELL" ? "down" : void 0
2553
2560
  ),
2554
2561
  cell(humanize(trade.sharesTransacted, 1)),
2555
2562
  cell(humanize(trade.totalValue)),
package/dist/index.cjs CHANGED
@@ -1036,7 +1036,7 @@ var Trackers = class {
1036
1036
  };
1037
1037
 
1038
1038
  // src/version.ts
1039
- var VERSION = "0.45.0";
1039
+ var VERSION = "0.46.0";
1040
1040
 
1041
1041
  // src/client.ts
1042
1042
  var DEFAULT_BASE_URL = "https://app.sentisense.ai";