# Brand Mention Scraper API — News, Reddit & Hacker News (`quarry/brand-monitor`) Actor

Scrape brand and competitor mentions across Google News, Reddit, Hacker News, Product Hunt and YouTube. Blocked sources are reported as partial coverage, never as silence. Unofficial — not affiliated with any source named.

- **URL**: https://apify.com/quarry/brand-monitor.md
- **Developed by:** [Quarry](https://apify.com/quarry) (community)
- **Categories:** Social media, News, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $8.00 / 1,000 mention founds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — 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

## Brand Monitor — mentions across news, Reddit and Hacker News

Track any brand, product, competitor or topic. Schedule it daily and **it only ever shows you what's new** — it remembers everything it has already reported.

### Sample output

```json
{
  "id": "googlenews:CBMi...",
  "source": "googlenews",
  "keyword": "apify",
  "title": "AI Agents Gain Financial Independence: Apify Adds 20,000 Tools",
  "url": "https://ffnews.com/newsarticle/...",
  "snippet": "Apify announced support for agent payments, adding…",
  "origin": "FF News",
  "author": null,
  "publishedAt": "2026-07-28T12:55:17.000Z",
  "matchedIn": "both"
}
```

Every field is plain text. `snippet` is decoded and stripped of markup, so it is readable in a spreadsheet without a second cleaning pass.

### Why this one

- **It's a monitor, not a search.** State persists between runs, so a daily schedule returns only genuinely new mentions. No re-reading yesterday's results, and **you are not charged for anything you've already been shown.**
- **Three sources, one schema.** Google News, Reddit and Hacker News in identical shape — `origin` means origin whether it's a publisher, a subreddit or HN.
- **`matchedIn` tells you why it matched** — `title`, `body`, `both`, `url`, `author`, or `channel`. The fastest way to triage false positives without reading every row. A mention we cannot prove contains your keyword is dropped and never billed.
- **`excludeTerms` kills recurring noise** before you're billed for it.
- **It tells you when it could not see.** A source that refuses a request is reported as a coverage gap, never as "no mentions".

### Input

| Field | What it does |
|---|---|
| `keywords` | **Required.** Brands, competitors, products, topics. |
| `sources` | `googlenews`, `reddit`, `hackernews` (default), plus `producthunt` and `youtube`. |
| `onlyNew` | Suppress already-reported mentions. **On** for monitoring; off for a one-time backfill. |
| `maxAgeHours` | Ignore older mentions. Default 168 (one week). 0 = no limit. |
| `excludeTerms` | Drop mentions containing any of these. |
| `youtubeChannels` | Channel IDs to watch when the `youtube` source is on. |

Daily competitor watch:

```json
{
  "keywords": ["your brand", "competitor a", "competitor b"],
  "sources": ["googlenews", "reddit", "hackernews"],
  "onlyNew": true,
  "maxAgeHours": 48,
  "excludeTerms": ["job", "hiring"]
}
```

### Use cases

- **Brand and reputation monitoring** — know within hours when your name appears in news or on Reddit.
- **Competitor tracking** — watch launches, funding, outages and complaints across several rivals at once.
- **Crisis detection** — schedule hourly with a short `maxAgeHours` and a narrow keyword set.
- **PR measurement** — a dated, exportable record of coverage rather than a dashboard screenshot.

### Reliability

Every run writes `RUN_STATS`: `ok`, `empty`, `drift`, `transport`, `rate_limited`, `not_found`, plus `newMentions`, `suppressedAsAlreadySeen`, `droppedAsUnmatched`, `trackedIds`, and the coverage triple `pairsAttempted` / `pairsAnswered` / `pairsFailed`.

**Silence is never presented as safety.**

- `drift` means a source returned feed items but the parser produced none — a format change. Firehose sources that legitimately match nothing report `empty` instead, so you never get a false alarm from a quiet day.
- **`PARTIAL COVERAGE`** appears in the log whenever fewer keyword/source pairs answered than were attempted, and names the pairs that did not. Reddit in particular rate-limits hard (see Limits), and a blocked request is booked as `rate_limited` — it is *never* counted as "your brand was not mentioned".
- **A run in which no source answered fails loudly** rather than finishing green with an empty result. A mistyped YouTube channel or a total block is an error you will see, not a quiet week you will misread.
- **A run that finds nothing new still writes one row**: `{"type": "no_changes", …}` with the run's counters. It is never charged. Mention rows carry no `type` field, so `type == "no_changes"` is an unambiguous filter for "checked, nothing new".

### Limits

- Public sources only. No private groups, no logged-in content.
- Google News returns roughly the last week per query — for older coverage, lower `maxAgeHours` and schedule more often rather than reaching further back.
- **Reddit answers roughly one request per minute per address.** Requests to it are serialised, but on a run with several keywords some Reddit pairs will still come back rate-limited; they are reported under `PARTIAL COVERAGE`. Split large keyword sets across scheduled runs rather than raising `concurrency`.
- **Hacker News yield is brand-dependent.** Its index is queried as an exact phrase with typo tolerance disabled, which is what makes it usable at all — the default index matched "Notion" against 80 stories about a *nation*. Even so, a brand that simply is not discussed on HN returns few or no rows, and rows that do not provably contain your keyword are dropped rather than billed. Expect HN to be a strong source for technology brands and a thin one for consumer brands.
- `producthunt` is a firehose filtered locally against the daily feed, so it matches far less often than the search-backed sources.
- `youtube` is a **channel watch, not a search**: it returns every new video from the channels in `youtubeChannels`, with `matchedIn: "channel"`, because the channel itself is what you asked to watch. It does not search YouTube for your keywords.
- Rows carry no `language` field. None of these sources reports one reliably, and a field that is null on every row is worse than no field at all. The `language` **input** still selects which Google News edition is queried.
- State is capped at 50,000 remembered mentions per store; beyond that the oldest are forgotten.

### Pricing

Pay per **new** mention. Re-runs that find nothing new cost nothing, and the `no_changes` row is never charged.

# Actor input Schema

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

Brands, products, competitors or topics. Each is searched across every enabled source.

## `sources` (type: `array`):

Which surfaces to watch. Each enabled source is searched for every keyword, so adding one multiplies the mentions found — and the cost.

## `onlyNew` (type: `boolean`):

Suppress anything reported in a previous run. Leave on for scheduled monitoring; turn off for a one-time backfill.

## `monitorId` (type: `string`):

Names the history this run remembers. Leave empty and each distinct keyword list gets its own, so your separate brand monitors never suppress each other. Set it to keep one history across a changing keyword list, or change it to start clean.

## `maxAgeHours` (type: `integer`):

Ignore mentions older than this. 0 = no limit.

## `excludeTerms` (type: `array`):

Drop mentions containing any of these — the cheapest way to kill recurring false positives.

## `youtubeChannels` (type: `array`):

Channel IDs (UC…) to watch, when the youtube source is enabled.

## `language` (type: `string`):

Two-letter language code used for news search, e.g. en, de, fr.

## `region` (type: `string`):

Two-letter country code used for news search, e.g. US, GB, DE. Changes which outlets are surfaced.

## `maxPerSource` (type: `integer`):

Cap on mentions taken from each source, per keyword. Bounds cost when a keyword trends.

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

Hard ceiling on billable mentions for the whole run. 0 = no limit.

## `concurrency` (type: `integer`):

How many sources to fetch in parallel. Raise for speed, lower if a source rate-limits you.

## Actor input object example

```json
{
  "keywords": [
    "OpenAI",
    "Anthropic"
  ],
  "sources": [
    "googlenews",
    "reddit",
    "hackernews"
  ],
  "onlyNew": true,
  "maxAgeHours": 168,
  "language": "en",
  "region": "US",
  "maxPerSource": 100,
  "maxItems": 0,
  "concurrency": 4
}
```

# 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 = {
    "keywords": [
        "OpenAI",
        "Anthropic"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("quarry/brand-monitor").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 = { "keywords": [
        "OpenAI",
        "Anthropic",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("quarry/brand-monitor").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 '{
  "keywords": [
    "OpenAI",
    "Anthropic"
  ]
}' |
apify call quarry/brand-monitor --silent --output-dataset

```

## MCP server setup

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

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/4JUraE1j3XcVi7TV4/builds/YKuQl4sutrxLiPbMK/openapi.json
