# Reddit Scraper - Posts, Comments, Users & Keywords, No Login (`ntriqpro/reddit-user-subreddit-osint`) Actor

Reddit scraper that works without login: posts, comments, full user history and keyword search from subreddit, user, search and domain feeds, plus user and subreddit profiles, extracted indicators (CVE, IP, hash, wallet, domain), related subreddits and a rate-limit report.

- **URL**: https://apify.com/ntriqpro/reddit-user-subreddit-osint.md
- **Developed by:** [daehwan kim](https://apify.com/ntriqpro) (community)
- **Categories:** Social media, Lead generation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

## Reddit Scraper - Posts, Comments, Users & Keywords, No Login

**Give it subreddits, usernames, keywords, domains or post links and get back the posts and comments — plus a profile of each user and subreddit, the indicators inside the text (CVE, IP, hash, wallet, domain, u/ and r/ mentions), keyword result counts, related subreddits, optional rule-based signals (intent, sentiment, your own labels) and a report of what Reddit's limits let the run read.** No login and no cookies: it reads Reddit's public feeds. On paid plans you can add your own Reddit API key for scores and comment trees (optional, beta).

| | Free plan (preview) | Paid Apify plan (Starter and up) |
|---|---|---|
| Sources per run (subreddits, users, keywords, domains and post URLs together) | 2 | 500 |
| Post and comment rows per source | the 10 newest | as far as Reddit lists them (1,000 per listing, about 250 per search) — and everything after that with monitoring |
| Text | first 280 characters (`textTruncated: true`); links from the shown text only | full |
| More items, date and start point (`maxItemsPerSource` above 10, `since`, `after`) | locked | yes |
| New items only (`incremental`) | locked | yes |
| Posts and comment stream of a subreddit | one listing per subreddit: its posts (its comments when `include` leaves posts out), 10 newest | both |
| Comments of one post (`postUrls`) | the post and its 9 newest comments | about 500 per request; a post that reaches that is read again under 5 more sort orders and merged (measured: 497 → 1,700 and 492 → 1,288 comments) |
| Posts of a user whose own address lists nothing | the fact, and how many posts Reddit search lists | those posts, through Reddit search (posts only) |
| Your own Reddit API key (scores, comment counts, comment tree) | locked | yes (optional, beta) |
| User and subreddit profile rows | summary numbers; the top 3 subreddits + how many more | everything: every subreddit with its share, active hours, top authors, domains and terms |
| Indicator rows | 1 of each kind + how many more were found | all |
| Related subreddit rows | 3 + how many more were found | all |
| Keywords | 1 | all |
| Signals (intent, sentiment, sales flags, your own labels — rule-based, no LLM) | on the 10 rows shown; 1 custom label | every row; every custom label |
| AI layer (a language model: analysis, your own questions, translation, summaries) | not available — a free run calls no model | every layer you switch on, up to `aiMaxItems` posts and comments per run |
| Limits report (`limits` and `source` rows) | complete | complete |
| Price per event | list price | collection rows and rule-based signals: BRONZE −20%, SILVER −35%, GOLD and up −48%; AI events: the same price on every plan |

**How big is the difference?** Measured on 2026-10-07: for `r/cybersecurity` a free run returns 10 posts, a paid run 593 posts for the last 30 days (59×) plus 494 comments per day. For `u/kn0thing`: 10 items and 3 subreddits against 940 posts, 973 comments and the 286 subreddits he is active in (191×). For the keyword `data breach`: 10 results against 242 results from 185 subreddits (24×).

A free run never fails because of a limit: it reads the first page of each listing (up to 100 items), shows the 10 newest, tells you in the dataset what stayed locked as counted numbers (`1 of 19 shown`), and ignores paid-only inputs with a notice instead of an error.

> **Unofficial.** This Actor is **not affiliated with, endorsed by, or sponsored by** Reddit. It reads **public** feeds only, without login. You are responsible for how you use the input and the results, for complying with Reddit's terms, and with GDPR, PIPA, CCPA and other applicable laws.

### Why use this Reddit scraper?

- **It keeps working without an account.** Every request goes out on its own proxy IP, because Reddit allows one request per IP per minute on its public feeds. Nothing to log in, nothing to get banned. In the measurement behind this Actor (126 requests, 2026-10-07) 117 of 118 requests to open sources succeeded; the other 8 were two users whose feeds answer 403.
- **Empty is explained.** Every source gets a `source` row: did Reddit answer, how many items were read, how far back, what stopped the run, did a listing reach Reddit's own limit. A hidden profile or a missing subreddit is an answer, not a failed run.
- **A user as one row.** `userProfile`: posts and comments counted, first and last activity, every subreddit with its share of the activity, active hours (UTC), most linked domains, most used terms — counted from the items the run read, with the count stated.
- **A subreddit as one row.** `subredditProfile`: posts in the last 7 and 30 days, authors, comments per day, top authors, top domains, and the ranked posts when you read a ranked listing.
- **Keyword answers, including "nothing found".** One `keywordSummary` row per keyword and source: results, last 7 days, first and last, which subreddits, 5 sample posts. Rows carry `matchedKeywords` and `matchedIn` (title, body, url); `strictMatch` drops search results that only matched somewhere the feed does not show.
- **Indicators as rows, not as a blob.** CVE IDs, IPv4 addresses, MD5/SHA-1/SHA-256 hashes, BTC/ETH/TRON wallet addresses, emails, phone numbers, domains, mentioned users (u/…) and mentioned subreddits (r/…), deduplicated per source with how often and where each was seen.
- **Related subreddits with the reason.** Where a user is active, which subreddits the items mention, and what Reddit's subreddit search returns for the topic.
- **Signals without a model.** Switch on `signals` and every post and comment row gets an intent (question, seeking_recommendation, complaint, praise, purchase_intent, comparison, self_promotion, discussion), a sentiment label and score, flags (`wantsHelp`, `mentionsPrice`, `mentionsCompetitor`, `hasQuestion`) and your own labels — rule-based, no LLM: word lists and patterns counted inside the Actor, with the words that fired each rule in `signalEvidence`.
- **AI layer, when you want one.** Optional and separate: with `ai` set, a language model reads the posts and comments (any language) and adds an analysis, answers to your own questions, an English translation and written summaries whose every statement cites the posts it rests on. Paid plans; see "AI layer" for what is sent where.
- **Long threads, further than one request.** One request returns about 500 comments of a post. A post that reaches that is read again under the other sort orders and merged; every comment is delivered and charged once.
- **Honest about the fields.** The public feed has no scores, comment counts or comment depth. Those columns are `null` and every row lists them in `fieldAvailability`; with your own Reddit API key (paid plans, beta) they are filled — see "Add your own Reddit API key".

Built for OSINT and threat-intelligence work, brand and leak monitoring, and community research. Runs on the Apify platform with API access, scheduling, integrations and monitoring built in.

### How to use it

1. Fill in at least one of **subreddits** (`netsec`, `r/netsec`, `netsec+osint`), **users** (`spez`, `u/spez`), **keywords**, **domains** or **post URLs**.
2. Optionally choose the listing (`sort`, `time`), how many items to read per listing, filter keywords and the indicator kinds you care about.
3. Click **Start** and read the rows in the **Output** tab. Filter by `recordType`.
4. For monitoring turn on **New items only** with a **monitor key** and put the Actor on a schedule.

### Input

| Field | Type | Description |
|---|---|---|
| `subreddits` | array | Name, `r/name` or link; `a+b` reads several as one listing |
| `users` | array | Name, `u/name` or profile link |
| `keywords` | array | Reddit-wide searches; operators `subreddit:`, `title:`, `author:`, `url:` work |
| `domains` | array | Posts that link to this domain (`viaDomain` on the rows) |
| `postUrls` | array | One post and the comments its feed carries |
| `include` | array | `posts`, `comments`, `userProfile`, `subredditProfile`, `keywordSummary`, `indicators`, `relatedSubreddits` (all when omitted) |
| `sort` / `time` | string | `new`, `hot`, `top`, `rising`, `controversial` / `hour` … `all` |
| `maxItemsPerSource` | integer | Items to read per listing, newest first, 1–1,000. When omitted: 40 (above 10: paid plans) |
| `maxCommentsPerPost` + `deepComments` | integer + boolean | Post URLs: items to read from one post, 1–3,000 (default 500 — about what one request carries; raise it to get more); above 500, a post whose first request returned 480 or more comments is read again under other sort orders unless `deepComments` is `false`. Each comment is one `comment` event (paid plans) |
| `since` / `after` | date / string | Stop at older items / start after an item id (paid plans) |
| `filterKeywords` + `onlyMatching` | array + boolean | Match the items read; optionally deliver only the matching ones |
| `strictMatch` | boolean | Keyword searches: deliver only results that contain every word |
| `indicatorKinds` | array | `cve`, `ipv4`, `hash`, `wallet`, `email`, `phone`, `domain`, `mentioned_user`, `mentioned_subreddit` (all when omitted) |
| `incremental` + `monitorKey` | boolean + string | Only items newer than the last run with the same key; any text works as a key and each distinct name keeps its own position (paid plans) |
| `outputFormat` | string | `full`, `minimal` or `rag` |
| `signals` | boolean | Add rule-based signals to every post and comment row (see "Signals") |
| `competitorNames` | array | Names that set `flags.mentionsCompetitor` when signals are on |
| `customLabels` | array | Your own labels: `[{ "label": "pricing", "anyOf": ["too expensive", "price increase*", "$#/mo"] }]` — words and phrases, with `*` and `#` as wildcards; no regular expressions (free plan: the first label) |
| `ai` | array | AI layers to switch on: `analysis`, `labels`, `translation`, `threadSummary`, `userBrief`, `subredditDigest`, `keywordInsight` (paid plans; empty = no text is sent to any model) — see "AI layer" |
| `aiLabels` | array | Your own questions for the `labels` layer: `[{ "label": "churn_risk", "question": "Does the author say they will stop using a product?" }]`, optionally with `"options": [...]` |
| `aiMaxItems` | integer | The most posts and comments one run sends to the model (1–20,000, default 2,000) |
| `redditClientId` + `redditClientSecret` + `redditUserAgent` | secret strings + string | Your own Reddit API app key and User-Agent (paid plans, optional, beta) — see "Add your own Reddit API key" |
| `apiRequestsPerMinute` / `apiMaxRequests` | integer | Pace (1–100, default 80) and cap for requests sent with your key |
| `proxyConfiguration` | object | Apify Proxy; keep it on |

```json
{
  "subreddits": ["netsec"],
  "users": ["spez"],
  "keywords": ["CVE-2026"],
  "domains": ["bleepingcomputer.com"],
  "maxItemsPerSource": 150
}
```

`maxItemsPerSource` counts the items the run reads from each listing (a subreddit and a user have two: posts and comments); `filterKeywords` with `onlyMatching`, `strictMatch` and `since` then decide which of those become rows. With `include` set to profiles or indicators only, the items are read and counted but no post or comment row is delivered or charged.

### Output

Every row has a `recordType`: `post`, `comment`, `source`, `userProfile`, `subredditProfile`, `keywordSummary`, `indicator`, `relatedSubreddit` or `limits` — and `threadSummary` when that AI layer is on. With `signals` on, post and comment rows also carry a `signals` object; with an AI layer on, an `ai` object. Rows that carry a `noticeCode` instead are **messages, not results** (`free-plan-preview`, `free-plan-locked`, `charge-limit-reached`, `charge-unavailable`, `lookup-failed`, `upstream-blocked`, `run-summary`, `invalid-input`, `no-input`, `ledger-unreadable`, `ledger-unwritable`, `delivery-unconfirmed`, `restart-unconfirmed`) — they are never charged; skip them when parsing. Source rows, the limits row and notices carry the responsibility line in `disclaimer`.

```json
{
  "recordType": "post",
  "platform": "reddit",
  "accessLayer": "rss",
  "sourceKind": "subreddit",
  "sourceName": "r/netsec",
  "postId": "1wzpmfz",
  "fullname": "t3_1wzpmfz",
  "postTitle": "One Copilot model refused. Another leaked secrets about half the time.",
  "postUrl": "https://www.reddit.com/r/netsec/comments/1wzpmfz/one_copilot_model_refused_another_leaked_secrets/",
  "subreddit": "netsec",
  "author": "Haunting_Ganache_850",
  "postedAt": "2026-10-07T06:45:54.000Z",
  "text": "This one gets stranger the more you look at it.…",
  "textTruncated": false,
  "externalUrl": "https://adversa.ai/blog/cryptographic-context-injection-github-copilot/",
  "externalDomain": "adversa.ai",
  "links": ["https://adversa.ai/blog/cryptographic-context-injection-github-copilot/"],
  "matchedKeywords": [],
  "matchedIn": [],
  "rankInFeed": null,
  "score": null,
  "commentCount": null,
  "fieldAvailability": {
    "accessLayer": "rss",
    "unavailable": ["score", "upvoteRatio", "commentCount", "flair", "isStickied", "isLocked", "over18"],
    "reason": "Reddit's public feed does not carry these fields; they need Reddit API access with an API key."
  }
}
```

```json
{
  "recordType": "userProfile",
  "username": "spez",
  "postCount": 23,
  "commentCount": 150,
  "historyComplete": false,
  "firstActivityAt": "2015-07-11T17:02:59.000Z",
  "lastActivityAt": "2026-08-05T18:31:50.000Z",
  "subredditCount": 15,
  "subreddits": [{ "subreddit": "RDDT", "posts": 0, "comments": 47, "items": 47, "share": 0.272 }],
  "activeHoursUtc": [15, 3, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 4, 18, 11, 19, 16, 11, 22, 25, 18],
  "countBasis": "counted in the 173 items this run read (23 posts, 150 comments)"
}
```

```json
{
  "recordType": "limits",
  "layers": [
    {
      "accessLayer": "rss",
      "requests": 12,
      "http429": 0,
      "retriedOnNewIp": 0,
      "failedRequests": 0,
      "rateLimit": { "used": 1, "remaining": 0, "resetSeconds": 36, "responsesWithHeaders": 12 },
      "perIpWindow": { "requestsPerWindow": 1, "secondsUntilReset": 36 }
    }
  ],
  "caps": [],
  "advice": "This run made 12 requests to Reddit's public feeds, each on its own proxy IP; 0 were answered 429 and 0 were retried on a new IP. Reddit's headers allowed 1 request(s) per IP per window (0 left, reset in 36 s), which is why no IP is used twice."
}
```

A run also writes `RUN-SUMMARY` to its key-value store: rows per kind, per-source numbers, requests and the listings that reached a limit.

You can download the dataset in various formats such as JSON, HTML, CSV, or Excel.

### Data table

| Row | Key fields |
|---|---|
| `post` | `postId`, `fullname`, `postTitle`, `postUrl`, `subreddit`, `author`, `postedAt`, `text`, `textTruncated`, `markdown`, `externalUrl`, `externalDomain`, `isSelfPost`, `thumbnailUrl`, `links`, `linkCount`, `mentionedUsers`, `mentionedSubreddits`, `feedSort`, `rankInFeed`, `viaDomain`, `matchedKeywords`, `matchedIn`, `signals` (when switched on); `null` on the public feed and filled with your own API key: `score`, `upvoteRatio`, `commentCount`, `flair`, `isStickied`, `isLocked`, `over18` |
| `comment` | `commentId`, `fullname`, `commentUrl`, `postId`, `postTitle`, `postUrl`, `subreddit`, `author`, `postedAt`, `text`, `textTruncated`, `markdown`, `links`, `linkCount`, `matchedKeywords`, `matchedIn`, `isSubmitter` (post URL sources), `signals` (when switched on); `null` on the public feed and filled with your own API key: `score`, `parentId`, `depth` |
| `source` | `sourceKind`, `sourceName`, `status` (`ok`, `empty`, `forbidden`, `not_found`, `partial`, `blocked`), `statusNote`, `itemsRead`, `postsDelivered`, `commentsDelivered`, `duplicatesSkipped`, `newestItemAt`, `oldestReachedAt`, `stopReason`, `capReached`, `collectionError`, `feeds` (per listing: address, HTTP status, requests, items, stop reason, `capLimit`, `capReached`, `capAdvice`, `incrementalMode`, `incrementalNote`, `savedPosition`, `accessLayers`, and for a post URL `thread`: comments received, sort orders used, whether one request came back full), `limits`; for users `profileVisibility` (`public`, `hidden-or-empty`, `unavailable`), `recoveredVia`, `recoverablePosts`; with a key `apiLayer` |
| `userProfile` | `username`, `postCount`, `commentCount`, `itemsCounted`, `historyComplete`, `firstActivityAt`, `lastActivityAt`, `subredditCount`, `subreddits` (posts, comments, share), `activeHoursUtc`, `topDomains`, `topTerms`, `countBasis`, `profileVisibility`, `recoveredVia`, `recoveredNote`, `signalSummary` (signals on); with your API key `karma`, `postKarma`, `commentKarma`, `accountCreatedAt` |
| `subredditProfile` | `subreddit`, `postsCounted`, `commentsCounted`, `postsLast7Days`, `postsLast30Days`, `uniqueAuthors`, `commentsPerDay`, `daysCovered`, `topAuthors`, `topDomains`, `topTerms`, `rankedPosts`, `countBasis`, `signalSummary` (signals on); with your API key `subscriberCount`, `activeUserCount` |
| `keywordSummary` | `keyword`, `keywordOrigin`, `itemsScanned`, `matchCount`, `strictMatchCount`, `matchesLast7Days`, `firstMatchAt`, `lastMatchAt`, `subredditCount`, `subredditBreakdown`, `samplePosts` (5 newest), `signalSummary` (signals on) |
| `indicator` | `indicatorKind`, `value`, `occurrences`, `firstSeenAt`, `lastSeenAt`, `itemUrls` (up to 5), `hashAlgorithm` (hashes), `walletNetwork` (wallets) |
| `relatedSubreddit` | `relatedSubreddit`, `relatedUrl`, `relationKinds` (`user_active`, `mentioned`, `topic_search`), `relationBasis`, `occurrences`, `shareOfActivity`, `searchRank`, `relatedTitle`, `relatedAbout`, `relatedCreatedAt`, `itemUrls` |
| `threadSummary` (AI layer) | `postId`, `postUrl`, `postTitle`, `subreddit`, `summary`, `themes`, `issues`, `viewsFor`, `viewsAgainst`, `solutions`, `productsMentioned` (each a statement `{ text, evidenceUrls }` or a list of them), `itemsRead`, `commentsRead`, `generatedBy`, `countBasis` |
| `ai` on other rows (AI layer) | post and comment rows: `analyzed`, `language`, `sentiment`, `intent`, `emotion`, `flags`, `category`, `labels`, `translation`, `note`; `userProfile`, `subredditProfile` and `keywordSummary` rows: `summarized`, `summary` and the lists of that summary, `itemsRead`, `note` |
| `limits` | `layers` (per access layer: requests, 429s, retries, Reddit's rate-limit headers; for your API key also the pace, rests and whether it stopped early), `caps` (listings that reached Reddit's limit, with advice), `threads` (per post URL: comments received, sort orders used; the post's total comment count is known only with an API key), `layerSwitch`, `unconfirmedCharges` (charge calls that failed; their rows were delivered and are not charged again), `rowsDeliveryUnconfirmed` (charged rows whose write failed and could not be checked), `rowsUnconfirmedAfterRestart` (rows a restarted run found under way — not charged or written again), `ledgerUnwritable`, `runWideDedupe` (whether an item reached through two sources was still handed over once — see "Restarts and repeat charges"), `advice` |

All rows also carry `recordType`, `platform`, `accessLayer` (`rss` for the public feed, `api` for your own key), `scrapedAt` and `sourceUrl`. Every number in a profile or summary row is counted from items the run really read; nothing is estimated.

**Run summary.** At the end of a run the default key-value store holds a `RUN-SUMMARY` record (JSON, not charged): rows per kind and per source, the period covered, requests per access layer with Reddit's rate-limit values, sources that reached a limit, the signal distribution, the AI layer's counts (calls, tokens, items analysed, why it stopped) and the number of charges per event. It holds counts and dates only — no input values.

### Signals (rule-based, no LLM)

With `signals: true` every post and comment row carries:

```json
"signals": {
  "evaluated": true,
  "method": "rules",
  "intent": "seeking_recommendation",
  "sentimentLabel": "negative",
  "sentimentScore": -0.5,
  "flags": { "wantsHelp": true, "mentionsPrice": true, "mentionsCompetitor": true, "hasQuestion": true },
  "competitorsMentioned": ["Splunk"],
  "labels": { "pricing": true, "churn": false },
  "signalEvidence": [
    { "rule": "label:pricing", "matched": "too expensive" },
    { "rule": "intent:seeking_recommendation:looking-for", "matched": "looking for an" },
    { "rule": "sentiment:negative", "matched": "expensive" }
  ],
  "note": null
}
```

- **How it is computed.** Word lists and patterns, counted inside the Actor. No model is called and no text leaves the run. `signalEvidence` names the rule and the words that fired, so you can check every verdict.
- **Intent** is one of `question`, `seeking_recommendation`, `complaint`, `praise`, `purchase_intent`, `comparison`, `self_promotion`, `discussion`. When several fire, the order is self_promotion, purchase_intent, seeking_recommendation, comparison, complaint, praise, question.
- **Sentiment** is `positive`, `negative`, `mixed`, `neutral` or `uncertain`, with a score from −1 to 1 (positive words minus negative words over both; a negation right before a word flips it).
- **Quoted and negated text.** Quoted lines (`>`), quoted spans and links are left out of intent and sentiment; "not looking for" does not count as looking for. Your labels and competitor names are searched in the whole text — a mention is a mention.
- **English rules.** The script is checked whatever the length of the text. A text that does not look English — or that has too few letters to tell (emoji, digits or punctuation only) — gets `evaluated: false`, `intent: "discussion"` and `sentimentLabel: "uncertain"`, and no `signals-evaluated` charge; your labels, competitor names, prices and question marks are still checked and filled in, and `note` says so. The optional AI layer reads any language.
- **Not evaluated, not charged.** Deleted, removed or empty text, a bot's standard message and a text in a language the rules do not cover get `evaluated: false` with `sentimentLabel: "uncertain"` and no `signals-evaluated` charge.
- **Your labels.** Each `anyOf` entry is a word or a phrase, matched whole (not inside a longer word) and case-insensitively. Two wildcards: `*` stands for any letters or digits inside one word (`vuln*` finds "vulnerability", `*ware` finds "malware"), `#` for one or more digits (`CVE-#-#` finds "CVE-2026-12345"); a phrase such as `price increase` has to stand on consecutive words. Regular expressions are not accepted: an entry written as `/…/` is refused and reported in an `invalid-input` notice row, and the label's other entries still apply. Limits: 50 labels (free plan: 1), 20 entries per label, 80 characters per entry; labels read the first 5,000 characters of a text and skip any single word longer than 80 characters (a long link, a hash). The matching work per item is bounded; a set of labels built to be slow leaves the remaining labels of that item unchecked (`false`), and the row's `note` says how many.
- **Summaries.** Profile and keyword rows carry `signalSummary` (counts and shares per intent and sentiment, counts per flag and label) over the rows of that source that carry signals.
- These are heuristics. They are right often enough to sort and filter thousands of rows, and wrong often enough that a single verdict should be read with its evidence.

### AI layer (optional, paid plans)

AI analysis is a separate, optional layer on top of the rule-based signals. Nothing in it runs unless you put a layer name in `ai`. You can run the signals, the AI layer, or both: they fill different fields (`signals` and `ai`) and are charged separately.

**What is sent where.** When an AI layer is on, the text of the public posts and comments this run reads (title and body, cut to 2,000 characters per item) is sent to DeepSeek's API — DeepSeek is a company based in China — and the answer comes back to the run. Author names are not sent. Every request of this layer goes to that one address, `https://api.deepseek.com` — no input changes it. If you do not want text sent to DeepSeek, do not switch an AI layer on: leave `ai` empty and no text leaves the run (the rule-based `signals` layer sends nothing anywhere).

| Layer in `ai` | What you get | Where |
|---|---|---|
| `analysis` | `sentiment` (positive, negative, mixed, neutral), `intent` (the same eight values as the signals), `emotion`, `flags` (`wantsHelp`, `purchaseIntent`, `mentionsPrice`, `isVendor`, `hasQuestion`), `category`, `language` | `ai` on post and comment rows |
| `labels` | the answer to each of your own questions in `aiLabels` — true/false, or one of your `options`; comes on top of `analysis`, which it switches on | `ai.labels.<label>` on post and comment rows |
| `translation` | an English translation of items that are not in English — the first 800 characters of each item's text are translated | `ai.translation` on post and comment rows |
| `threadSummary` | the answers, experiences or themes that come up most, the points at issue and views for and against (when the post is a debate), recurring solutions, products and companies named | one `threadSummary` row per post URL |
| `userBrief` | interests, topics, apparent expertise, tone | `ai` on the `userProfile` row |
| `subredditDigest` | main themes, recurring complaints, frequent questions | `ai` on the `subredditProfile` row |
| `keywordInsight` | the contexts a keyword is used in | `ai` on `keywordSummary` rows |

- **What is rule-based and what is a model.** `signals`, `signalEvidence` and `signalSummary` are counted by rules inside the Actor and call no model. Everything under `ai`, and the `threadSummary` row, is written by a language model.
- **Labels on top of the analysis.** AI labels come on top of the item analysis: `labels` switches `analysis` on for the same posts and comments, also when `analysis` is not in `ai`, and each item is charged `ai-analyzed` ($0.0005) plus `ai-label` ($0.0001) per label — with 3 questions, $0.0008 per item. One request to the model does both; reading the item's text is what the analysis event pays for. Each part is charged only when its own part of the answer passed the checks. A run that picked `labels` without `analysis` says so in `limits.ai.note`. With `labels` picked and no usable question in `aiLabels`, neither runs.
- **Any language.** The rules behind `signals` are English only; the model reads other languages too. That is one reason to switch `analysis` on for a non-English subreddit.
- **Charged after the answer, and only for what passed.** A post or comment is charged its AI event once the model has answered and the answer passed the checks: the expected fields, values from the lists above, and the id of an item that was sent. An answer that is empty, malformed or outside the lists is dropped — nothing is shown and nothing is charged for it, and `ai.note` says why. Rule-based `signals` on the same row are not affected.
- **Summaries cite their sources.** Every statement of a summary carries `evidenceUrls`, and only addresses of posts and comments this run sent to the model are accepted; a statement that cites anything else is dropped, and a summary left without a cited summary sentence is not delivered or charged. A summary is written from at most 100 items (1,000 characters each) and needs at least 3. It holds at most 5 statements in each list and 3 addresses per statement. One summary is at most 2 requests to the model: when the first answer is empty the same question is asked once more, and when it was cut off at the model's output limit a shorter answer is asked for. The second answer is the last — if it is empty, cut off or does not pass the checks, the summary is not delivered or charged. A failed request (an HTTP error, no answer in time) is not asked again. The 2 requests are counted for the whole run, restarts included (see "Restarts" below). The run keeps that count for 2,000 summaries; a summary past that number is not asked for, delivered or charged (`summary-uncounted`) — only a run with many keyword rows gets there.
- **Text is treated as data.** Posts are written by strangers, and some contain instructions aimed at language models. The text is passed as data, the model is told not to follow anything inside it, and whatever comes back is read through the fixed lists above — a value outside them never reaches a row.
- **Limits.** `aiMaxItems` (default 2,000, at most 20,000) caps the posts and comments one run sends. `aiLabels` takes up to 10 questions, each cut at 200 characters, with up to 8 options of 40 characters. The run also stops using the AI layer — and goes on without it — after 5 failed model calls in a row, when the model refuses the key or the balance is used up, when the run's own ceiling on model spend is reached, or when your maximum charge per run is reached. The `ai` entry of the `limits` row gives the calls, tokens, items sent and analysed, answers dropped, requests that were not made (`requestsNotMadeLedgerSave`, `requestsNotMadeSpendHeld`) and the reason it stopped. A row whose summary was not written says why at the end of its `ai.note`.
- **Why an answer was not used.** `limits.ai.discardReasons` (also in `RUN-SUMMARY`) counts, per code, how often an answer of the model — or a part of one — was thrown away. Whole answers count per request (an answer asked a second time counts twice), `summary-*` per summary, `item-*` per post or comment. Only the code is recorded, never the answer itself. None of these is charged.

  | Code | What happened |
  |---|---|
  | `no-answer` | no answer came back from the model (a timeout or a network error) |
  | `http-error` | the model's API answered with an HTTP error |
  | `empty-answer` | the model's answer was empty |
  | `cut-off` | the model's answer was cut off at the output limit |
  | `not-json` | the model's answer was not the JSON that was asked for |
  | `key-in-answer` | the model's answer held a secret and was thrown away unread |
  | `summary-missing` | the model's answer had no summary sentence |
  | `summary-uncited` | the model's summary sentence cited no post or comment of this run |
  | `summary-alone` | no statement besides the summary sentence cited a post or comment of this run |
  | `summary-attempts-used` | the requests this summary may make were already used (a summary is asked at most twice in a run, restarts included) |
  | `summary-uncounted` | the run's record of summary requests was full, so the model was not asked for this summary |
  | `item-missing` | the model's answer had no entry for this item |
  | `item-id-mismatch` | an entry of the model's answer had an id that was not asked, or one given twice |
  | `item-invalid-values` | an entry of the model's answer held values outside the allowed lists |
- **The ceiling on model spend.** It is our cost, not yours — you pay the AI events, never the model's bill. Once the spend known to the run reaches the ceiling, no new request is made. Requests already out are not taken back, and what they really cost can be above the estimate held for them. Far from the ceiling up to 4 requests are out at once; once fewer than 4 large requests fit in what is left of it (about the last $0.08 of the $5), a request goes out only when no other is out. So the spend can end above the ceiling by what one request cost above what was left — the ceiling plus the excess of one request, not of four. Requests that were out together far from the ceiling pass it only if they cost, on average, more than is held for the largest request (about $0.019 each). The first request of a run goes out alone; when an answer costs more than was held for it (`limits.ai.estimateExceeded`), requests go out one at a time for the rest of the run.
- **Restarts.** What the layer sent and spent is saved in the run's ledger before each request, so a run that Apify moves to another server counts on from there: `aiMaxItems` and the spend ceiling hold for the whole run, not per restart. The posts and comments of a request that was out at the moment the run was moved (at most 20 per request) count as sent and are not sent a second time: after the restart their rows go out with `ai.analyzed: false`, a note in `ai.note` and no AI event charged, and `limits.ai.itemsNotResent` counts them. Items whose answer had already come back but whose rows were not yet handed over are sent again after the restart — you are charged once per item either way. A summary's 2 requests are counted in the ledger before each one goes out, so they hold across a restart: a summary whose first request was out when the run was told to stop, or whose first answer was empty or cut off and whose second request had not gone out, is asked once more (with the shorter question), and one whose second request was already made, or whose answer had come back before the run was told to stop — a summary whose row was not yet handed over, or an answer that was thrown away — is not asked again: it is not delivered and not charged, the row it would ride on says so in `ai.note`, and `limits.ai.discardReasons` counts it as `summary-attempts-used`. That an answer came back is saved in the ledger before the summary's row goes out; if that one save fails the run goes on, and after a restart that summary may be asked for once more — still at most 2 requests in all. A restarted run that cannot read its ledger ends without collecting (see "Restarts and repeat charges"). A first start that cannot read it at first collects once its store answers again (a row is charged only after it is saved), but calls no model for that run (`limits.ai.stopped: "ledger-unreadable"`): rows go out without `ai` verdicts and without AI charges.
- **No second pass.** A post or comment that went out without an AI verdict — a limit was reached, an answer was dropped, a restart — is not analysed in a later run: with `incremental` on, the next run starts after it.
- **Same price on every plan.** The seven AI events have no plan tier discount: BRONZE, SILVER, GOLD and higher plans pay the list price for them. The discount applies to the collection rows and to rule-based `signals`.
- **Free plan.** A free run calls no model: `ai` is ignored and a `free-plan-locked` row says so.
- **Not measured yet.** How often the model's verdicts agree with a person's has not been measured for this Actor. Use them to sort and filter, and read the text before acting on a single one.
- **Your responsibility.** You decide whether this text may be sent to a model provider and what the output is used for; check Reddit's terms on using its content with language models before you do.

### Pricing

Pay per event. The compute, proxy and storage a run uses are included in the event prices; Apify platform usage is not billed on top of them.

| Event | Charged when | Price (list) | Paid plan tiers | Not charged |
|---|---|---|---|---|
| `apify-actor-start` | the run starts — once per run (memory is limited to 1 GB) | $0.003 | same on every plan | — |
| `source-checked` | one source's lookup is completed — including the answer that a profile is hidden or a subreddit does not exist | $0.003 | discounted | our errors (timeouts, failed retries, parser errors, an answer that is not a feed) and sources cut short by them; the same source entered twice; sources Reddit refused before anything could be read |
| `post` | one post row is written | $0.002 | discounted | duplicates (a post reached through two sources of one run is delivered once); notice rows |
| `comment` | one comment row is written | $0.002 | discounted | duplicates; notice rows |
| `user-profiled` | one user profile row is written | $0.02 | discounted | profiles cut short by our error; hidden or missing profiles |
| `subreddit-profiled` | one subreddit profile row is written | $0.01 | discounted | profiles cut short by our error |
| `keyword-checked` | one keyword × source result row is written, including zero matches | $0.004 | discounted | sources that failed on our side |
| `indicator` | one indicator row, deduplicated per source, is written | $0.002 | discounted | duplicates; notice rows |
| `related-subreddit` | one related subreddit row is written | $0.002 | discounted | duplicates; notice rows |
| `signals-evaluated` | `signals` is on and the signals of one post or comment row were evaluated — on top of that row's own event | $0.0005 | discounted | runs with `signals` off; deleted, removed or empty text; a bot's standard message; items in a language the rules do not cover (English only) or with too few letters to tell; duplicates; notice rows |
| `ai-analyzed` | `analysis` is on — picked, or switched on by `labels` — and a model's analysis of one post or comment was written to its `ai` field — on top of that row's own event | $0.0005 | same on every plan | free plan runs (no model is called); answers that were missing or did not pass the checks; failed model calls; English items (translation); duplicates; notice rows |
| `ai-label` | `labels` is on and one of your questions was answered for one post or comment — per label and item (all the labels of a row in one charge), on top of that row's own event and of its `ai-analyzed` | $0.0001 | same on every plan | free plan runs (no model is called); answers that were missing or did not pass the checks; failed model calls; English items (translation); duplicates; notice rows |
| `translated` | `translation` is on and the English translation of one non-English post or comment was written to its `ai.translation` (the first 800 characters of its text) | $0.002 | same on every plan | free plan runs (no model is called); answers that were missing or did not pass the checks; failed model calls; English items (translation); duplicates; notice rows |
| `thread-summarized` | `threadSummary` is on and one `threadSummary` row is written | $0.02 | same on every plan | free plan runs (no model is called); answers that were missing or did not pass the checks; failed model calls; English items (translation); duplicates; notice rows |
| `user-briefed` | `userBrief` is on and a brief was written to a user profile row — on top of that row's own event | $0.03 | same on every plan | free plan runs (no model is called); answers that were missing or did not pass the checks; failed model calls; English items (translation); duplicates; notice rows |
| `subreddit-digested` | `subredditDigest` is on and a digest was written to a subreddit profile row — on top of that row's own event | $0.05 | same on every plan | free plan runs (no model is called); answers that were missing or did not pass the checks; failed model calls; English items (translation); duplicates; notice rows |
| `keyword-insight` | `keywordInsight` is on and an insight was written to a keyword result row — on top of that row's own event | $0.02 | same on every plan | free plan runs (no model is called); answers that were missing or did not pass the checks; failed model calls; English items (translation); duplicates; notice rows |

`apify-actor-start`: Charged once when a run starts. This Actor's memory is limited to 1 GB, so that is one charge per run (the platform adds one more charge for every 1 GB of memory above 1 GB). It costs $0.003. The start fee is the same on every plan tier.

Paid plan tiers pay less per event for collection rows and rule-based signals (`source-checked` to `signals-evaluated` in the table): BRONZE −20%, SILVER −35%, GOLD and up −48%. The seven AI events cost the same price on every plan, and so does the start fee: an AI event pays for a call to a language model, and that does not get cheaper with your plan.

Example: `r/netsec` with 40 posts + 40 comments + 48 indicators + 24 related subreddits + 1 subreddit profile + 1 source check + start (once) = $0.32 at list price; with `signals` on, the 80 post and comment rows add up to $0.04; with `ai: ["analysis"]`, the same 80 rows add another $0.04 and a `subredditDigest` $0.05; with `ai: ["labels"]` and 3 questions in `aiLabels`, the same 80 rows add 80 × ($0.0005 + 3 × $0.0001) = $0.064 — the analysis and the three answers of each row. The AI amounts are the same on every plan; the tier discount applies to the other amounts. Rows read with your own Reddit API key cost the same as rows read from the public feed. Use `include` to leave out the row kinds you do not need, and set a maximum charge per run in Run options to cap spending; when the cap is reached the run stops normally and says so in a `charge-limit-reached` row.

**Restarts and repeat charges.** When Apify moves a run to another server, or you abort a run and resurrect it, the run gets a signal, saves its own ledger and continues from it: rows already delivered are neither written nor charged again, and finished sources are not read again. A run that dies without such a signal (for example killed at its timeout) and is then resurrected continues from its ledger as well: a row is charged only after the ledger holding it was saved, so no row is charged a second time. A row that was being handed over at the very moment the run died (charged or not, written or not — the run cannot tell) is neither charged nor written again; `rowsUnconfirmedAfterRestart` in the limits row and a `restart-unconfirmed` notice give the count. If the ledger cannot be saved (3 tries), the run charges no further row or AI event and says so in a `ledger-unwritable` notice. If a run that starts again cannot read its ledger (three reads in a row fail, or the record is broken), it cannot tell what it already delivered, so it delivers nothing more and charges no row and no AI event more: it makes no request to Reddit and calls no model, writes one `ledger-unreadable` notice row (not charged) that says how many rows were in the dataset, and ends as a normal run. The run's start fee ($0.003) is charged as for every run — "nothing more" is about rows and AI events, not about the run as a whole. The rows delivered before the restart stay valid. Run it again with the same input to get the rest; with `incremental` on, the next run continues after the last saved position, without it the next run starts from the beginning and charges what it delivers. A run is taken as started again when its dataset already holds a row and its ledger cannot be read or is not there — or when the dataset cannot be counted; a first start has an empty dataset and collects as usual (a store that never answers lets it charge nothing: see `ledger-unwritable` above). The ledger that could not be read is not written over — every save reads it again first, and once that works the old record and the new one are saved together, so the next restart skips the rows of both. If the dataset refuses a row that was already charged, the run reads the end of the dataset back and writes the row once more only when it is not there; if that fails too, the row is missing from the dataset, is not charged again, and the count is reported in the source row's `collectionError` (or in a `run-summary` row). When the dataset cannot be read back, the row is not written a second time (to avoid a duplicate) and a `delivery-unconfirmed` notice gives the count. From that row on the run charges no further row or AI event: sources it had not read yet are not requested. If the platform's charge call itself fails, that row is still delivered and the call is not made again, so a row is never charged twice because of it; the number of such calls is `unconfirmedCharges` in the limits row. When the call fails 3 times in a row, or 10 times in one run, the run stops charging and requesting, and a `charge-unavailable` notice says how many charges could not be confirmed and how many sources were not requested. In `incremental` runs the saved position is written one write at a time; a write that fails is repeated with the next one, on the next save signal and once more when the run ends, and only if every one of those fails does the next run return (and charge) those items again. A post or comment reached through two sources of one run is delivered and charged once. For the first 400,000 items of a run that also holds across a restart; past that number the run only remembers the items (it no longer saves them), so it still holds inside the run, but after a restart an item first delivered past that number and reached again through another source may be delivered and charged by both — the limits row says so (`runWideDedupe.savedForRestart: false`). Past 1,900,000 items the run cannot remember more, and such an item may be delivered once per source (`runWideDedupe.active: false`).

**Interrupted monitoring runs.** With `incremental: true`, items are handed over oldest first, and the run saves the last item it delivered when it gets a migration or abort signal and with every periodic state save of the platform. The next scheduled run starts after that item, so it does not charge those items again even when the interrupted run is never resurrected. Two things remain. A run that dies without a signal leaves the items it delivered after its last periodic save to be delivered and charged again by the next run. And the indicator, related-subreddit, profile and keyword rows of a source are written after its items, so for a source that was interrupted they come only if that run is resurrected; a later run does not produce them for items an earlier run delivered.

### Limits Reddit sets, and how the run reports them

Measured on 2026-10-07 on Reddit's public feeds (no login). The `limits` row and every `source` row report the values of your own run.

| Limit | Value | How it was established |
|---|---|---|
| Requests per IP | 1 per window of about a minute (`x-ratelimit-used: 1`, `x-ratelimit-remaining: 0.0`, `x-ratelimit-reset: <seconds>`) | read from Reddit's response headers on every answer; the same IP answered 200 again after the window |
| Items per listing | the newest 1,000 | `r/cybersecurity` new: 999 posts in 10 pages; `r/sysadmin` comments: 984, which was 13.5 hours |
| Results per search | about 250 | 5 keywords: 224 to 249 results |
| Comments of one post per request | about 500 (`limit=500`) | 4 posts: 497, 492, 305 and 14 comments. Other sort orders return other comments: 6 sort orders merged gave 1,700 and 1,288 comments for the two large posts; `after=` does not page a post's comments (0 new) |
| Posts of a user whose address lists nothing | through Reddit search (`author:name`), posts only | 5 users: one address answered 200 with nothing and search listed 17 posts; for an open profile search listed 38 posts against 23 at the address; a user whose address answers 403 had 0 in search |
| Fields in the feed | id, title, body, author, time, subreddit, link, outside link | no score, upvote ratio, comment count, comment depth or flair |

What the Actor does about them: every request uses a new proxy session, at most 4 requests run at the same time, and requests to one source are at least 1 s apart. Only 429, 5xx and dropped connections are retried (after 1, 2 and 4 s, each time on a new IP; a `Retry-After` that asks for longer is honoured up to 30 s). A 403 or 404 is the source's answer and is not retried. When 3 sources in a row cannot be read — or answer 403 before the run has read anything at all — the run stops asking, says so in an `upstream-blocked` row and ends normally; nothing is charged for what was not read. For a post URL the `source` row and the `limits` row say how many comments came, under which sort orders, and whether one request came back full; the public feed does not say how many comments the post has in total (the API key layer does). When a listing reaches the 1,000-item limit the `source` row sets `capReached` and `capAdvice` tells you, from the time span your run actually covered, how often to schedule a monitoring run so that nothing is missed.

### Add your own Reddit API key (optional, paid plans, beta)

The public feed has no scores. If you need them, register an app at reddit.com/prefs/apps and give the run its **client ID**, **client secret** and a **User-Agent**. The run then reads through Reddit's API with your key and switches back to the public feed whenever the key cannot be used.

- **What you get in addition:** `score`, `upvoteRatio`, `commentCount`, `flair`, `isStickied`, `isLocked`, `over18` on posts; `score`, `parentId`, `depth` on comments (a post URL returns the comment tree of one request, flattened with depth); `karma`, `postKarma`, `commentKarma`, `accountCreatedAt` on user profiles; `subscriberCount` on subreddit profiles; the post's own comment count in the `limits` row. Rows read this way say `accessLayer: "api"`.
- **No login as you.** The Actor uses app-only access (`grant_type=client_credentials`). It takes no Reddit password, no refresh token and no cookie, so there is no session of yours to log out. It only reads: the single POST it sends is the token request.
- **Your User-Agent, not ours.** Reddit asks API clients for `<platform>:<app ID>:<version> (by /u/<your username>)`, for example `web:com.example.myapp:v1.0 (by /u/yourname)`. Without it — or with one not in this form — the key layer stays off and the `limits` row says why. The Actor never makes one up.
- **Your key is kept out of the output.** The client ID and secret are secret inputs (stored encrypted by Apify). They and the access token are not written to the log, the dataset, the run's key-value store or any error message.
- **Same price.** Rows read with your key are charged with the same events at the same prices. An item is delivered once, whichever layer read it.
- **Free plan:** the key inputs are ignored and a `free-plan-locked` notice says so; nothing is sent with your key.

**How not to get your Reddit key throttled.** Limits as Reddit publishes them (documents read on 2026-10-07):

| Limit | Value | Source |
|---|---|---|
| Requests per key | 100 queries per minute per OAuth client id, averaged over a window of 10 minutes | [Reddit Data API Wiki](https://support.reddithelp.com/hc/en-us/articles/16160319875092-Reddit-Data-API-Wiki) |
| What Reddit reports on every answer | `X-Ratelimit-Used`, `X-Ratelimit-Remaining`, `X-Ratelimit-Reset` | same document |
| Authentication | OAuth is required; a unique User-Agent in the form above | same document |
| App-only access | `client_credentials` at `https://www.reddit.com/api/v1/access_token`, requests to `https://oauth.reddit.com`, a token lasts 1 hour | [OAuth2 wiki, "Application Only OAuth"](https://github.com/reddit-archive/reddit/wiki/OAuth2) |

What the Actor does with your key: one request at a time, at `apiRequestsPerMinute` (default 80, that is 80% of the limit; lower it when the key is used elsewhere too). It reads the three headers on every answer and, when less than 10% of the window is left, rests until Reddit's reset. On a 429 it waits what `Retry-After` asks (60 s at most) and tries once; a second 429 in a row stops the key layer for the rest of the run. On a 401 or 403 it checks once with a new token; if that is refused too, the key layer stops (this also happens when one of your sources is private — the rest of the run then reads the public feed). `apiMaxRequests` caps how many requests a run may send with the key, the token request included. A stopped key layer is never started again in that run: the run goes on over the public feed from the same item, and the `source` and `limits` rows say where and why it switched (`apiLayer.switchedToRss`, `layerSwitch`, `stopReason`). The `limits` row reports the requests sent with your key, Reddit's last header values, the pace, the rests and the share of the 10-minute budget used.

**Beta — what is and is not verified.** This layer was built from the two Reddit documents above. Its tests run against synthetic API answers (made up in the documented shape), because we have no Reddit API app of our own. **A check with a live key has not been done yet (status 2026-10-07: not verified; planned before the layer leaves beta).** If your key returns something the Actor does not understand, the run stops using it, continues over the public feed and says so in the `limits` row — please tell us in the Issues tab.

### What it does not do

- **No scores, upvote ratios, comment counts or comment depth without your own API key.** Reddit's public feed does not carry them. The columns are present and `null`, and `fieldAvailability` on each row says so. With your own Reddit API key (paid plans, beta) they are filled.
- **No login of any kind.** It never asks for a Reddit password or session cookie, and the optional API key is app-only, so there is no account to get logged out or banned — and nothing that needs an account can be read: private subreddits, age-restricted content, the comments of a hidden profile.
- **No complete history of a busy listing in one run.** Reddit lists the newest 1,000 items; use `incremental` on a schedule for the rest.
- **No guaranteed complete comment tree of a post.** One request returns about 500 comments; reading under 6 sort orders reaches more (1,700 and 1,288 in two large posts) but not provably all, and the public feed has no reply structure. With your API key the comments of one request come with depth and parent; replies Reddit leaves out of that answer are counted, not fetched.
- **No comments of a hidden profile.** When a user's own address lists nothing, Reddit search still lists their posts; it does not list their comments.
- **No user search.** Reddit's public feeds do not offer one; give usernames.
- **No language model unless you switch one on.** Signals are rule-based (word lists and patterns, English) and come with their evidence; they are not a model's reading of the text. The AI layer is off by default, and its accuracy has not been measured yet: treat a verdict as a model's opinion, not as a fact.
- **No media downloads.** Image and video addresses are delivered as links.

### Tips

- Start with the default (`maxItemsPerSource: 40`) to see the shape of the data, then raise it.
- For monitoring, schedule the Actor with `incremental: true` and one `monitorKey` per schedule. The first run takes a baseline (the newest items); every later run returns only what is newer, oldest first. When more new items are waiting than `maxItemsPerSource`, the oldest are delivered and the rest come with the next run — nothing is skipped. A run cut short by an error or by your charge limit saves the last item it delivered and the next run continues from there.
- A busy subreddit's comments pass 1,000 within hours. Read the `capAdvice` of the first run and schedule accordingly.
- `include: ["userProfile", "relatedSubreddits"]` profiles a list of usernames without paying for every post and comment row.
- `mentioned_user` indicator values are plain usernames — you can pass them back into `users`, or to a username-lookup Actor in the Apify Store, as a next step.
- A `forbidden` or `not_found` source is a fact about that source, not a failed run.
- A post URL returns up to 500 items by default (one request, each comment one `comment` event). For a long thread set `maxCommentsPerPost` to 3000: only above 500 is the post read again under the other sort orders.
- Lead and support triage: `signals: true` with `customLabels` and `competitorNames`, then filter the dataset on `signals.intent` and `signals.flags`.

### FAQ, disclaimers, and support

**Is this legal?** The Actor reads feeds that Reddit serves publicly to anyone without an account. Whether a particular use of that data is lawful depends on what you collect and what you do with it. You are responsible for how you use the results. Do not use this tool for harassment, stalking, or any unlawful purpose.

**Reddit's terms.** Your use of the data is subject to Reddit's own rules: the [User Agreement](https://www.redditinc.com/policies/user-agreement) and, where you build on the data, the [Developer Terms](https://www.redditinc.com/policies/developer-terms) and [Data API Terms](https://www.redditinc.com/policies/data-api-terms). Among other things they expect you to respect deletions: when a post, a comment or an account is deleted on Reddit, delete it from your copies too. The Developer Terms say "as soon as possible"; Reddit's [Data API Wiki](https://support.reddithelp.com/hc/en-us/articles/16160319875092-Reddit-Data-API-Wiki) strongly recommends routinely deleting stored user data and content within 48 hours (both read on 2026-10-07). Read the current texts — they change, and they are the authority, not this summary.

**Privacy & data responsibility.** **You are the data controller** for every lookup you run with this Actor; ntriqpro is a **data processor** acting solely on your instructions. You are responsible for having a lawful basis and a legitimate, purpose-limited reason for each lookup, and for complying with GDPR, PIPA, CCPA, and other applicable privacy laws. **We do not store your results** — output is written only to your own run's dataset. With `incremental`, the last item id per listing is kept in a key-value store in your own account.

**Why did a source return nothing?** Look at its `source` row: `forbidden` means Reddit answered 403 (for a user: the profile is hidden, suspended or does not exist — the feed does not say which); `not_found` means there is nothing at that address; `empty` means the feed is open and lists no items. For a user, `profileVisibility` is `hidden-or-empty` when the user's own addresses answer 200 with nothing — the profile is hidden or has no public activity, the feed does not say which — and `unavailable` when they answer 403. In the first case a paid run reads the user's posts through Reddit search (`recoveredVia: "search"`); a free run says how many it found (`recoverablePosts`).

**Trademarks.** Reddit is a trademark of its owner. This Actor is an independent tool and uses the name only to describe what it reads.

**Found a bug or want a feature?** Use the **Issues** tab.

*Used this Actor? A sentence in the **Reviews** tab about what you used it for helps the next person decide.*

# Actor input Schema

## `subreddits` (type: `array`):

Subreddits to read: a name, r/name or a reddit.com link. Join several with + to read them as one listing (netsec+osint). Each gives post rows, comment rows, a subreddit profile, indicators and related subreddits. Paid Apify plans read up to 500 sources per run, as far back as Reddit lists them (1,000 posts and 1,000 comments per subreddit). Free plan runs are a preview: 2 sources, the 10 newest items of each (text cut at 280 characters), 1 indicator of each kind, 3 related subreddits and 1 keyword. Charged per source checked ($0.003) and per post or comment row ($0.002).

## `users` (type: `array`):

Reddit users to read: a name, u/name or a profile link. Each gives the user's posts and comments, a user profile row (activity by subreddit, active hours, top domains and terms) and one row per subreddit the user is active in. A hidden, suspended or missing profile is answered with a source row saying so.

## `keywords` (type: `array`):

Searches over all of Reddit, one per entry. Search operators work: subreddit:netsec, title:CVE, author:name, url:example.com. Each keyword gives the posts found, one keyword result row (count, last 7 days, first and last, which subreddits, sample posts — also when nothing was found) and related subreddits. Reddit search stops near 250 results per query. Free plan runs search 1 keyword.

## `domains` (type: `array`):

Posts that link to a domain, for example bleepingcomputer.com. Delivered as post rows with viaDomain set.

## `postUrls` (type: `array`):

Single posts by address, for example https://www.reddit.com/r/netsec/comments/abc123/title/. Gives the post and the comments its public feed carries (the feed of one post returns only part of a long thread).

## `include` (type: `array`):

Which kinds of rows to return (all when empty). With neither posts nor comments selected the items are still read and counted for the profiles and indicators, but no post or comment row is delivered or charged. Free plan runs read one listing per subreddit: its posts, or its comments when posts are left out (10 newest).

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

Listing to read for subreddits and searches: new, hot, top, rising or controversial. Ranked listings add rankInFeed to each post. Users and domains are always read newest first, and so is every listing when New items only is on.

## `time` (type: `string`):

Time range for top and controversial listings and for searches.

## `maxItemsPerSource` (type: `integer`):

How many items to read from each listing of a source, newest first (1 to 1,000 — Reddit lists no more). A subreddit and a user have two listings, posts and comments. Filters below apply to the items read. Free plan runs return up to 10 newest items per source. Values above 10 (paid Apify plans only — free plan runs ignore this and say so)

## `maxCommentsPerPost` (type: `integer`):

For post URLs: how many items (the post and its comments) to read from one post, 1 to 3,000. Default 500: one request returns about 500 comments. Raise it to get more — above 500 the post is read again under other sort orders (see Deep comments). Each comment is one "comment" event. (paid Apify plans only — free plan runs ignore this and say so)

## `deepComments` (type: `boolean`):

For post URLs: when one request comes back with 480 or more comments, read the post again under the sort orders new, old, top, controversial and Q\&A (at most 6 requests) and merge the comments. Each comment is delivered and charged once. Measured 2026-10-07 on two large posts: 497 → 1,700 and 492 → 1,288 comments. Only happens when "Max comments per post URL" is above 500. (paid Apify plans only — free plan runs ignore this and say so)

## `since` (type: `string`):

Stop at items older than this date (YYYY-MM-DD). (paid Apify plans only — free plan runs ignore this and say so)

## `after` (type: `string`):

Start a listing after this Reddit item id instead of at its newest item: t3\_… for posts, t1\_… for comments (the fullname field of a row). (paid Apify plans only — free plan runs ignore this and say so)

## `filterKeywords` (type: `array`):

Words or phrases to look for in the items read from every source (case-insensitive; every word must be present in the title, the body or the link). Each source × keyword gives one keyword result row; matching rows carry matchedKeywords and matchedIn. Free plan runs check the first one.

## `onlyMatching` (type: `boolean`):

Deliver only posts and comments that match a filter keyword. The others are read and counted but not delivered or charged.

## `strictMatch` (type: `boolean`):

For keyword searches: deliver only results whose title, body or link really contain every word of the keyword. Reddit search also returns posts that match elsewhere (in a comment, for example); those are counted in the keyword result row but not delivered.

## `indicatorKinds` (type: `array`):

Which indicators to extract from the items (all when empty). One row per distinct value per source.

## `incremental` (type: `boolean`):

Remember the last item of each listing and return only newer items on the next run with the same monitor key. Because Reddit lists only the newest 1,000 items, a schedule with this switch is how a busy subreddit is collected completely; the source row says when a listing reached that limit before the saved position and how often to run. (paid Apify plans only — free plan runs ignore this and say so)

## `monitorKey` (type: `string`):

Name of the saved position used by "New items only". Use a different key for each schedule that should keep its own position: any text works; each distinct name keeps its own position. (paid Apify plans only — free plan runs ignore this and say so)

## `outputFormat` (type: `string`):

Shape of the post and comment rows: full (every field), minimal (ids, address, date, author, text) or rag (Markdown text and links for LLM pipelines).

## `signals` (type: `boolean`):

Add a signals object to every post and comment row: intent (question, seeking_recommendation, complaint, praise, purchase_intent, comparison, self_promotion, discussion), sentiment label and score, flags (wantsHelp, mentionsPrice, mentionsCompetitor, hasQuestion), your own labels, and the words that fired each rule. Computed by word lists and patterns inside the Actor — no model is called. English rules; other languages keep intent "discussion" and sentiment "uncertain". Adds the signals-evaluated event ($0.0005) per evaluated row. Free plan: on the 10 rows shown per source.

## `competitorNames` (type: `array`):

Names to look for when signals are on (case-insensitive, whole words). A row that mentions one gets flags.mentionsCompetitor: true and the names in competitorsMentioned. Up to 200 names of at most 80 characters each; a longer name is left out.

## `customLabels` (type: `array`):

Your own labels when signals are on, for example \[{"label": "pricing", "anyOf": \["too expensive", "price increase\*", "CVE-#-#"]}]. Each entry of anyOf is a word or a phrase, matched whole and case-insensitively, with two wildcards: \* for any letters or digits inside one word (vuln\* finds vulnerability) and # for one or more digits (CVE-#-# finds CVE-2026-12345). A row gets labels.<label>: true when any entry is found. Regular expressions are not accepted: an entry written as /…/ is refused and reported in a notice row. Up to 50 labels, 20 entries per label, 80 characters per entry; labels read the first 5,000 characters of a text. Free plan: the first label only; paid Apify plans apply every label.

## `ai` (type: `array`):

A separate, optional layer on top of the rule-based signals: a language model reads the text, in any language. analysis adds ai.sentiment, ai.intent, ai.emotion, ai.flags, ai.category and ai.language to post and comment rows ($0.0005 each); labels answers your own questions below — AI labels come on top of the item analysis: picking labels switches analysis on for the same posts and comments, and each item is charged ai-analyzed ($0.0005) plus ai-label ($0.0001) per label; translation adds an English translation of non-English items ($0.002 each, the first 800 characters of the text); threadSummary writes one summary row per post URL ($0.02); userBrief ($0.03), subredditDigest ($0.05) and keywordInsight ($0.02) add a written summary to the profile and keyword rows, each statement with the addresses of the posts it rests on. Charged only after the model answered and the answer passed the checks. Turning a layer on sends the text of the public posts and comments read by this run to DeepSeek's API (DeepSeek is a company based in China) and nowhere else. Leave empty to send nothing. (paid Apify plans only — free plan runs ignore this and say so)

## `aiLabels` (type: `array`):

Questions a model answers for every post and comment when "labels" is among the AI layers, for example \[{"label": "churn_risk", "question": "Does the author say they will stop using a product?"}, {"label": "stage", "question": "Where is the author in buying?", "options": \["researching", "comparing", "decided", "not_buying"]}]. Without options the answer is true or false; with options it is one of them. The answer is in ai.labels.<label>. AI labels come on top of the item analysis: with "labels" picked, "analysis" runs for the same posts and comments too, and each item is charged "ai-analyzed" ($0.0005) plus "ai-label" ($0.0001) per label. Up to 10 labels; a label name is letters, digits, \_ or -; a question is cut at 200 characters; up to 8 options of 40 characters each. (paid Apify plans only — free plan runs ignore this and say so)

## `aiMaxItems` (type: `integer`):

The most posts and comments one run sends to the model (1 to 20,000, default 2,000). Past it the run goes on without the AI layers for single items, and the limits row says so. (paid Apify plans only — free plan runs ignore this and say so)

## `redditClientId` (type: `string`):

Client ID of your own Reddit app (reddit.com/prefs/apps, type "script" or "web app"). With the client secret and the User-Agent below, the run reads through Reddit's API with your key and fills score, upvote ratio, comment count, flair, comment depth and parent, author karma and subscriber count. App-only access (client_credentials): the Actor never logs in as you, takes no password and only reads. Beta: built from Reddit's published documentation; not yet verified with a live key. (paid Apify plans only — free plan runs ignore this and say so)

## `redditClientSecret` (type: `string`):

Client secret of the same Reddit app. Stored encrypted by Apify; never written to the log, the dataset or the run's key-value store. (paid Apify plans only — free plan runs ignore this and say so)

## `redditUserAgent` (type: `string`):

The User-Agent Reddit asks API clients to send, in its form: <platform>:<app ID>:<version> (by /u/<your Reddit username>), for example web:com.example.myapp:v1.0 (by /u/yourname). Required for the API key layer: the Actor does not make one up. When it is missing or not in this form, the run reads the public feeds instead and the limits row says why. (paid Apify plans only — free plan runs ignore this and say so)

## `apiRequestsPerMinute` (type: `integer`):

Pace for requests sent with your Reddit API key (1 to 100, default 80). Reddit allows 100 per minute per key, averaged over 10 minutes. Lower it when the same key is also used elsewhere. (paid Apify plans only — free plan runs ignore this and say so)

## `apiMaxRequests` (type: `integer`):

The most requests this run may send with your Reddit API key, the token request included (no limit when empty). When it is reached the run continues over the public feeds. (paid Apify plans only — free plan runs ignore this and say so)

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

Apify Proxy settings. Reddit allows one request per IP per minute on its public feeds, so every request of this Actor goes out on a new proxy session; keep Apify Proxy on. The default datacenter proxy is what the Actor was measured with.

## Actor input object example

```json
{
  "subreddits": [
    "netsec"
  ],
  "users": [],
  "keywords": [],
  "domains": [],
  "postUrls": [],
  "include": [
    "posts",
    "comments",
    "userProfile",
    "subredditProfile",
    "keywordSummary",
    "indicators",
    "relatedSubreddits"
  ],
  "sort": "new",
  "time": "all",
  "maxItemsPerSource": 40,
  "deepComments": true,
  "filterKeywords": [],
  "indicatorKinds": [
    "cve",
    "ipv4",
    "hash",
    "wallet",
    "email",
    "phone",
    "domain",
    "mentioned_user",
    "mentioned_subreddit"
  ],
  "outputFormat": "full",
  "competitorNames": [],
  "customLabels": [],
  "ai": [],
  "aiLabels": [],
  "aiMaxItems": 2000,
  "apiRequestsPerMinute": 80,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `dataset` (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 = {
    "subreddits": [
        "netsec"
    ],
    "users": [],
    "keywords": [],
    "domains": [],
    "postUrls": [],
    "include": [
        "posts",
        "comments",
        "userProfile",
        "subredditProfile",
        "keywordSummary",
        "indicators",
        "relatedSubreddits"
    ],
    "sort": "new",
    "time": "all",
    "maxItemsPerSource": 40,
    "deepComments": true,
    "filterKeywords": [],
    "indicatorKinds": [
        "cve",
        "ipv4",
        "hash",
        "wallet",
        "email",
        "phone",
        "domain",
        "mentioned_user",
        "mentioned_subreddit"
    ],
    "outputFormat": "full",
    "competitorNames": [],
    "customLabels": [],
    "ai": [],
    "aiLabels": [],
    "aiMaxItems": 2000,
    "apiRequestsPerMinute": 80,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("ntriqpro/reddit-user-subreddit-osint").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 = {
    "subreddits": ["netsec"],
    "users": [],
    "keywords": [],
    "domains": [],
    "postUrls": [],
    "include": [
        "posts",
        "comments",
        "userProfile",
        "subredditProfile",
        "keywordSummary",
        "indicators",
        "relatedSubreddits",
    ],
    "sort": "new",
    "time": "all",
    "maxItemsPerSource": 40,
    "deepComments": True,
    "filterKeywords": [],
    "indicatorKinds": [
        "cve",
        "ipv4",
        "hash",
        "wallet",
        "email",
        "phone",
        "domain",
        "mentioned_user",
        "mentioned_subreddit",
    ],
    "outputFormat": "full",
    "competitorNames": [],
    "customLabels": [],
    "ai": [],
    "aiLabels": [],
    "aiMaxItems": 2000,
    "apiRequestsPerMinute": 80,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("ntriqpro/reddit-user-subreddit-osint").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 '{
  "subreddits": [
    "netsec"
  ],
  "users": [],
  "keywords": [],
  "domains": [],
  "postUrls": [],
  "include": [
    "posts",
    "comments",
    "userProfile",
    "subredditProfile",
    "keywordSummary",
    "indicators",
    "relatedSubreddits"
  ],
  "sort": "new",
  "time": "all",
  "maxItemsPerSource": 40,
  "deepComments": true,
  "filterKeywords": [],
  "indicatorKinds": [
    "cve",
    "ipv4",
    "hash",
    "wallet",
    "email",
    "phone",
    "domain",
    "mentioned_user",
    "mentioned_subreddit"
  ],
  "outputFormat": "full",
  "competitorNames": [],
  "customLabels": [],
  "ai": [],
  "aiLabels": [],
  "aiMaxItems": 2000,
  "apiRequestsPerMinute": 80,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call ntriqpro/reddit-user-subreddit-osint --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ntriqpro/reddit-user-subreddit-osint"
        }
    }
}
```

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/Ez3atjn9CmTDKtBkc/builds/6hZaeA9FpyZABcI7l/openapi.json
