# Stocktwits Scraper - Messages, Sentiment & Trending (`abotapi/stocktwits-com-scraper`) Actor

Scrape Stocktwits message streams with bull/bear sentiment labels, bodies, likes, replies, cashtags and author stats for any symbol. Trending symbols, user profiles and full reply threads. Incremental monitoring, resume and MCP export.

- **URL**: https://apify.com/abotapi/stocktwits-com-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 message or profile records

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Stocktwits Scraper

Pull the retail-trader conversation from Stocktwits (stocktwits.com): symbol
message streams with the crowd's own Bullish / Bearish sentiment labels, full
reply threads, user profiles and the trending symbols list. Every message
record carries the body, the author, the sentiment tag the author chose,
likes, reply counts and the cashtags it mentions. Recurring runs can report
only what changed.

### Why This Scraper?

- **Ready-made sentiment dataset**: authors tag their own messages Bullish or
  Bearish; every record ships that label (or None), no inference required.
- **Any surface, one schema**: a single record shape covers symbol streams,
  user messages, thread replies, profiles and trending symbols, filtered by
  the `kind` field.
- **Full reply threads**: open any message and get every public reply with
  its own author, sentiment and likes (the reviews-analog surface).
- **Trending and profiles**: the current trending symbols with watchlist
  counts, and user profiles with follower, following and idea counts.
- **Incremental monitoring**: recurring runs emit only NEW, UPDATED,
  REAPPEARED or EXPIRED records against the same input, with first-seen and
  last-seen timestamps.
- **Resume support**: continue an interrupted run without returning records
  already collected.

### Data You Get

> Sample shape, values are illustrative placeholders, not from a live listing.

| Field | Example value |
|---|---|
| `kind` | `message` |
| `recordId` | `m:00000000` |
| `messageId` | `00000000` |
| `url` | `https://stocktwits.com/sampletrader/messages/00000000` |
| `body` | `Sample trading note about the sample company. $SAMPLE looking ready.` |
| `createdAt` | `2027-01-15T19:30:24Z` |
| `sentiment` | `Bullish` (or `Bearish`, or `None` for untagged) |
| `likes` | `0` |
| `repliesCount` | `0` |
| `reshares` | `0` |
| `cashtags` | `["SAMPLE"]` |
| `symbols` | `[{"symbol": "SAMPLE", "title": "Sample Company Inc", "exchange": "NYSE"}]` |
| `mentionedUsers` | `[]` |
| `authorUsername` / `authorName` | `sampletrader` / `Sample Trader` |
| `authorFollowers` | `0` |
| `authorIdentity` / `authorOfficial` | `User` / `false` |
| `symbolContext` | `SAMPLE` (the stream the message was read from) |
| `kind` | `reply` (thread replies add `parentMessageId`, `inReplyToMessageId`) |
| `kind` | `symbol` (trending: `symbol`, `symbolTitle`, `exchange`, `region`, `watchlistCount`, `trendingRank`) |
| `kind` | `user` (profiles: `userId`, `username`, `profileUrl`, `joinDate`, `followers`, `following`, `ideas`) |
| `changeType` / `changedFields` / `firstSeenAt` / `lastSeenAt` | incremental mode only |

### Modes

| Mode | Input | What you get |
|---|---|---|
| `symbols` | `symbols` (tickers, `$` optional) | each ticker's message stream walked newest-first |
| `trending` | none | the current trending symbols with ranks |
| `users` | `usernames` (`@` optional) | profile record + the user's recent messages |
| `threads` | `messageUrls` (links or bare message IDs) | the message + every public reply |
| `url` | `startUrls` | symbol pages walked as streams, profile pages as user reads, message links as threads |

### Filters

- **Stream order** (symbols mode): `all` (newest first, the default) or `top`
  (the site's engagement-ranked order).
- **Sentiment filter**: keep only `Bullish`, `Bearish` or untagged (`None`)
  messages. Runs on every fetched message in every mode that returns them.

### Reply threads (opt-in, surcharge per 10 replies)

Turn on **Fetch reply threads** to read each returned message's conversation
(one extra read per message) and emit every public reply as its own `reply`
record. There is no reply cap: the full public thread is returned. Threads
mode needs no toggle: the thread is the surface you asked for. Large,
talkative symbols can make this heavy, so:

- the thread read bills one `detail-enrichment` event, plus one more per
  started block of 10 replies returned (a 25-reply thread bills 3),
- a run-level breaker stops thread fetching for the rest of the run when
  threads keep failing,
- a suppressed incremental record never triggers a thread read or the
  surcharge.

### Limits

**Max items** is the one cap: it defaults to 20, and `0` means no limit (the
run then stops when the streams run out). Large, active symbol streams
effectively never run out, so set a limit when you do not need the full
history.

### Resume and recurring updates

- **Resume** (`resumeFromRunId`): paste a previous run or dataset ID to skip
  records already collected there.
- **Incremental mode** (`incrementalMode`): for scheduled monitoring. The
  first run reports every record NEW; later runs report NEW, UPDATED and
  REAPPEARED. Like and reply counts move constantly, so they never mark a
  message UPDATED; a real body or sentiment change does.
- `emitUnchanged` / `emitExpired`: off by default; turning them on returns
  (and bills) extra rows. EXPIRED detection only fires after a complete,
  uninterrupted scan, so a capped or partial run never tombstones live
  records.

### Send results into your apps (MCP connectors)

You can optionally pipe results into apps you already use via Model Context
Protocol (MCP) connectors. Authorize a connector under Apify, Settings, API &
Integrations, then select it in the `mcpConnectors` input field. For Notion,
set `notionParentPageUrl` to the page under which item pages are created; the
Notion export writes a page per record with a readable summary. Other
connectors get a best-effort write or digest. Cap the export with
`maxNotifyListings`. Leave `mcpConnectors` empty to skip the export entirely;
the dataset output is never changed by the connector surface.

### Connection and limits

- Runs on the plain Apify proxy by default; every plan works. The source
  answered identically on datacenter and residential exits when probed, so
  there is no need to pay for residential unless you want to.
- Per-request timeout 15 s, at most 5 attempts per read, short backoff, and a
  fresh exit on transport errors: the run fails fast rather than grinding.
- If every read is refused, the run fails loudly instead of reporting an
  empty success; a genuinely empty result succeeds with a warning.

### Usage notes

- Tickertime note: symbol streams cover stocks, ETFs, crypto pairs and
  futures tickers that exist on Stocktwits. An unknown ticker is reported and
  skipped, and a run where nothing could be read fails loudly.
- One dataset, mixed kinds: filter on `kind` (`message`, `reply`, `symbol`,
  `user`) in the dataset views.

# Actor input Schema

## `mode` (type: `string`):

Symbols reads the message stream of one or more tickers. Trending returns the current trending symbols. Users reads a profile and its recent messages. Threads reads the full reply thread of specific messages. URL mode reads the Stocktwits pages you paste.

## `symbols` (type: `array`):

Symbols mode: one ticker per line, with or without the $ prefix. Each symbol's message stream is walked from newest to oldest. Multi-value supported.

## `sort` (type: `string`):

Symbols mode: All returns the newest messages first. Top returns the stream ordered by engagement, matching the site's Top view.

## `usernames` (type: `array`):

Users mode: one Stocktwits username per line, with or without the @ prefix. Returns the profile record plus the user's recent messages. Multi-value supported.

## `messageUrls` (type: `array`):

Threads mode: one message per line, as a full link (stocktwits.com/user/messages/123456) or a bare numeric message ID. Returns the message and its full reply thread. Multi-value supported.

## `startUrls` (type: `array`):

URL mode: Stocktwits pages to read, one per line. Symbol pages (stocktwits.com/symbol/AAPL) are walked as message streams, profile pages as user reads, and message links as reply threads. Multi-value supported.

## `sentiment` (type: `string`):

Keep only messages with a chosen crowd sentiment. All keeps every message; the other values keep only messages the authors tagged Bullish or Bearish. Untagged messages are labelled None. Applies to every mode that returns messages.

## `fetchReplies` (type: `boolean`):

For each returned message, also read its full reply thread and return every public reply as its own record. Adds a surcharge billed per thread read: one event for the read itself plus one per started block of 10 replies returned (see the Reply threads section of the README). Turning this on can be slow and heavy on large, talkative symbols: each thread is one extra read per message.

## `maxItems` (type: `integer`):

Stop after this many records (0 = no limit; the run then stops when the streams run out). Large, active symbol streams never run out on their own. Defaults to 20.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a large pull without returning messages already collected there.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. The first run returns every matching record as NEW. Later runs normally return only NEW, UPDATED and REAPPEARED records. Turn on Emit unchanged or Emit expired only when you also want those rows returned (and billed). State is kept separately for each input setup; use State key to name or deliberately share a monitoring campaign.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or deliberately share state across differently configured runs. Leave empty to let the actor derive a key automatically from the mode and input settings.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return records that have not changed since the last run, marked UNCHANGED. This returns, and bills, extra rows you already have.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return records that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked input without stopping early.

## `mcpConnectors` (type: `array`):

Optionally send results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize one under Apify, Settings, API & Integrations, then select it here. Notion gets a rich page-per-item export; other connectors get a best-effort write or digest. Leave empty to skip; never changes the dataset output.

## `notionParentPageUrl` (type: `string`):

URL or id of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## `proxyConfiguration` (type: `object`):

Apify Proxy settings. The prefilled default works on every Apify plan.

## Actor input object example

```json
{
  "mode": "symbols",
  "symbols": [
    "AAPL"
  ],
  "sort": "all",
  "usernames": [
    "Stocktwits"
  ],
  "startUrls": [
    "https://stocktwits.com/symbol/AAPL"
  ],
  "sentiment": "all",
  "fetchReplies": false,
  "maxItems": 20,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `messages` (type: `string`):

No description

## `symbols` (type: `string`):

No description

## `users` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "mode": "symbols",
    "symbols": [
        "AAPL"
    ],
    "sort": "all",
    "usernames": [
        "Stocktwits"
    ],
    "startUrls": [
        "https://stocktwits.com/symbol/AAPL"
    ],
    "sentiment": "all",
    "fetchReplies": false,
    "maxItems": 20,
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "maxNotifyListings": 50,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/stocktwits-com-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "mode": "symbols",
    "symbols": ["AAPL"],
    "sort": "all",
    "usernames": ["Stocktwits"],
    "startUrls": ["https://stocktwits.com/symbol/AAPL"],
    "sentiment": "all",
    "fetchReplies": False,
    "maxItems": 20,
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "maxNotifyListings": 50,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/stocktwits-com-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "mode": "symbols",
  "symbols": [
    "AAPL"
  ],
  "sort": "all",
  "usernames": [
    "Stocktwits"
  ],
  "startUrls": [
    "https://stocktwits.com/symbol/AAPL"
  ],
  "sentiment": "all",
  "fetchReplies": false,
  "maxItems": 20,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call abotapi/stocktwits-com-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/stocktwits-com-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/2EAqJtOShzOYXebkx/builds/zN5LyzapNwtthCEMR/openapi.json
