# Subreddit Pain Miner for Validation and Outreach (`isotonic/subreddit-pain-miner`) Actor

Mines public Reddit JSON endpoints for customer pain signals, with a deterministic mock mode for testing.

- **URL**: https://apify.com/isotonic/subreddit-pain-miner.md
- **Developed by:** [Brian Keefe](https://apify.com/isotonic) (community)
- **Categories:** Social media, Lead generation, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

## Subreddit Pain Miner for Validation and Outreach

Production-ready Apify Actor that mines public Reddit JSON endpoints for customer pain signals. It is designed for validation research and respectful outreach preparation, not automated posting.

### What It Does

- Searches public Reddit JSON endpoints for matching posts in selected subreddits.
- Fetches public comments JSON for matched posts.
- Deduplicates posts and comments.
- Scores each item for `painIntensity`, `buyerFit`, and `recency`.
- Emits structured records with quotes, vocabulary, urgency, validation questions, and a non-spammy reply draft.
- Supports deterministic `mockItems` input so tests and smoke runs do not depend on live Reddit.

### Output Fields

Each output item includes:

- `painQuote`
- `sourceUrl`
- `subreddit`
- `author` when public
- `suggestedValidationQuestion`
- `softReplyDraft`
- `competitorMentions`
- `urgency`
- `buyerVocabulary`
- `itemType`
- `createdAt`
- `painIntensity`
- `buyerFit`
- `recency`

### Input

Required production inputs:

- `subreddits`: array of subreddit names without `r/`
- `queries`: array of search terms

Optional tuning inputs:

- `maxPosts`
- `maxCommentsPerPost`
- `timeWindowDays`
- `sort`

Deterministic testing input:

- `mockItems`: optional array of fixture items. When present, the actor skips live Reddit requests entirely.

See `examples/sample-input.json`.

### Local Run

Install:

```bash
npm install
````

Run deterministic smoke path:

```bash
npm run smoke
```

Run tests:

```bash
npm test
```

Run the actor locally with the sample input file:

```bash
node src/main.js --input examples/sample-input.json --output examples/generated-local-output.json
```

Run against live Reddit by removing `mockItems` from your input JSON and passing that file to `src/main.js`, or by using `apify run`.

### Apify Run

Local Apify run:

```bash
apify run
```

Typical Apify input:

```json
{
  "subreddits": ["SaaS", "smallbusiness"],
  "queries": ["frustrated with", "looking for a tool"],
  "maxPosts": 10,
  "maxCommentsPerPost": 5,
  "timeWindowDays": 90,
  "sort": "relevance"
}
```

### Anti-Spam Stance

- This actor does not post, comment, message, or automate outreach on Reddit.
- `softReplyDraft` is for human review only.
- The output is intended for research, validation interviews, and respectful manual follow-up.

### Error Handling

- Uses only public Reddit JSON endpoints.
- Gracefully handles `403`, `429`, and network errors by logging warnings and continuing.
- Includes `sourceUrl` for every record so each finding can be reviewed manually.

### Limitations

- Public Reddit JSON can rate-limit or block requests.
- Search quality depends on Reddit indexing and the chosen queries.
- Scores are heuristic and should be reviewed by a human before making business decisions.
- Deleted users and deleted comment bodies are omitted or normalized.

### Files

- Actor config: `.actor/actor.json`
- Input schema: `.actor/input_schema.json`
- Runtime: `src/main.js`, `src/lib.js`
- Examples: `examples/sample-input.json`, `examples/sample-output.json`
- Tests: `tests/smoke.test.mjs`

### Publish Commands

Do not publish automatically from this repository. When ready, use:

```bash
apify login
apify push
```

Then publish from the Apify Console after final review.

# Actor input Schema

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

Subreddits to search, without the r/ prefix.

## `queries` (type: `array`):

Search terms that suggest pains, blockers, or urgent jobs-to-be-done.

## `maxPosts` (type: `integer`):

Maximum posts to process across all subreddit/query combinations.

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

Maximum matching comments to keep per post.

## `timeWindowDays` (type: `integer`):

Ignore posts and comments older than this many days.

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

Reddit search sort mode.

## `mockItems` (type: `array`):

Optional deterministic fixture input for tests and smoke runs. When present, no live Reddit requests are made.

## Actor input object example

```json
{
  "subreddits": [
    "SaaS",
    "smallbusiness"
  ],
  "queries": [
    "frustrated with",
    "looking for a tool"
  ],
  "maxPosts": 10,
  "maxCommentsPerPost": 5,
  "timeWindowDays": 90,
  "sort": "relevance"
}
```

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("isotonic/subreddit-pain-miner").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("isotonic/subreddit-pain-miner").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{}' |
apify call isotonic/subreddit-pain-miner --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=isotonic/subreddit-pain-miner",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Subreddit Pain Miner for Validation and Outreach",
        "description": "Mines public Reddit JSON endpoints for customer pain signals, with a deterministic mock mode for testing.",
        "version": "0.1",
        "x-build-id": "sRF1ClcFQz9uVJ0IW"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/isotonic~subreddit-pain-miner/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-isotonic-subreddit-pain-miner",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/isotonic~subreddit-pain-miner/runs": {
            "post": {
                "operationId": "runs-sync-isotonic-subreddit-pain-miner",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/isotonic~subreddit-pain-miner/run-sync": {
            "post": {
                "operationId": "run-sync-isotonic-subreddit-pain-miner",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "subreddits",
                    "queries"
                ],
                "properties": {
                    "subreddits": {
                        "title": "Subreddits",
                        "type": "array",
                        "description": "Subreddits to search, without the r/ prefix.",
                        "items": {
                            "type": "string"
                        },
                        "default": [
                            "SaaS",
                            "smallbusiness"
                        ]
                    },
                    "queries": {
                        "title": "Queries",
                        "type": "array",
                        "description": "Search terms that suggest pains, blockers, or urgent jobs-to-be-done.",
                        "items": {
                            "type": "string"
                        },
                        "default": [
                            "frustrated with",
                            "looking for a tool"
                        ]
                    },
                    "maxPosts": {
                        "title": "Max Posts",
                        "minimum": 1,
                        "maximum": 100,
                        "type": "integer",
                        "description": "Maximum posts to process across all subreddit/query combinations.",
                        "default": 10
                    },
                    "maxCommentsPerPost": {
                        "title": "Max Comments Per Post",
                        "minimum": 0,
                        "maximum": 100,
                        "type": "integer",
                        "description": "Maximum matching comments to keep per post.",
                        "default": 5
                    },
                    "timeWindowDays": {
                        "title": "Time Window Days",
                        "minimum": 1,
                        "maximum": 3650,
                        "type": "integer",
                        "description": "Ignore posts and comments older than this many days.",
                        "default": 90
                    },
                    "sort": {
                        "title": "Sort",
                        "enum": [
                            "relevance",
                            "new",
                            "top",
                            "comments"
                        ],
                        "type": "string",
                        "description": "Reddit search sort mode.",
                        "default": "relevance"
                    },
                    "mockItems": {
                        "title": "Mock Items",
                        "type": "array",
                        "description": "Optional deterministic fixture input for tests and smoke runs. When present, no live Reddit requests are made.",
                        "items": {
                            "type": "object"
                        }
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
