# Substack Public Mention Monitor (`maydit/substack-public-mention-monitor`) Actor

Monitor public Substack RSS articles for brand aliases and linked domains. Save short evidence, source dates and confirmed changes with explicit recent-feed coverage.

- **URL**: https://apify.com/maydit/substack-public-mention-monitor.md
- **Developed by:** [Brandt May](https://apify.com/maydit) (community)
- **Categories:** Social media
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.80 / 1,000 saved observations

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

## Substack Public Mention Monitor

Watch selected Substack publications for brand names, aliases and linked domains. Get short, source-linked evidence and a saved record of what changed in the public RSS feed.

### First useful result

Paste a public publication homepage or its /feed URL. The default returns five recent articles from Lenny’s Newsletter with no keyword filter. No account, API key, browser or proxy is needed. Open **Saved observations** to export the rows or **Readable run report** to review article previews, source coverage and limits.

```
{"publicationUrls":["https://www.lennysnewsletter.com"],"maxResults":5}
```

### A monitoring workflow, with evidence

Use a newsletter watchlist for PR coverage, competitor research or a regular reading brief. Optional brands, aliases and outbound domains filter the articles already available in each public feed. All configured filters combine with OR. Matching a brand or link does not establish endorsement, sponsorship, sentiment, investment performance or the truth of an article’s claims.

| What you need | Fields to use |
|---|---|
| Which publication mentioned a brand? | publicationTitle, title, mentions, publishedAt, url |
| Where is the evidence? | mentions\[].matchedText, field and short excerpt |
| Which requested domains were linked? | outboundMatches, requestedDomain and matchedHost |
| What changed since my saved check? | comparisonStatus, changedFields, previousObservedAt |
| What was actually read? | sourceUrl, sourceType, guid, observedAt, SUMMARY.sourceCoverage |

Brands and phrases match case-insensitively after Unicode/whitespace normalization, with Unicode word boundaries. “AI” does not match “chair” or “AItools.” Matching is literal, not fuzzy or semantic. An alias label also matches itself. Occurrences in visible URL text count as text mentions; HTML link destinations are checked separately by the domain filter. Counts cover occurrences across the configured brand/alias terms and both title and body; overlapping aliases can count the same text separately.

Domain filters match the requested hostname and its subdomains, never suffix lookalikes. Output links omit query strings and fragments. A redirect/tracking link is not followed or decoded to guess its destination.

### Three ready-to-run examples

**Recent product and growth articles**

```
{"publicationUrls":["https://www.lennysnewsletter.com"],"maxResults":3}
```

**OpenAI and ChatGPT in One Useful Thing**

```
{"publicationUrls":["https://www.oneusefulthing.org"],"keywords":["OpenAI"],"aliases":{"OpenAI":["ChatGPT"]},"maxResults":3}
```

**OpenAI links and mentions in Not Boring**

```
{"publicationUrls":["https://www.notboring.co"],"keywords":["OpenAI"],"outboundDomains":["openai.com"],"maxResults":3}
```

These selected feeds were tested locally on November 3, 2026. Publisher content and availability can change. They are examples, not a guarantee for every publication or hosting configuration.

### Public feed coverage

Only the supplied publication’s public RSS document is fetched, after its robots file. Substack [documents this public feed endpoint](https://support.substack.com/hc/en-us/articles/360038239391-Is-there-an-RSS-feed-for-my-publication). Custom domains are accepted when the returned RSS identifies the Substack generator and its self URL and channel match the requested publication.

The three tested feeds each contained 20 recent articles. This Actor does not extend that window, search Substack globally or fetch article pages. Paid posts may contribute only a publisher-provided public preview. Every row says **public_feed_text_may_be_preview**. No private feed, subscriber session, paywall or credentials are accepted. The Actor does not return full article bodies, comments, subscriber counts or engagement analytics.

One row is one validated article observation. Publication timestamps come from RSS pubDate; observedAt is this run’s observation time. recordId combines the public feed URL and source GUID. A publication-domain or GUID change can establish a new identity. Up to five short mention excerpts and ten unique matching outbound links are returned per article; total counts and truncation flags show when additional evidence was omitted. The public text hash detects changes in normalized public feed text only.

SUMMARY records items in each feed, matching/filtered articles, saved rows, items not processed, source failures and unreached publications. A complete fetched feed is still not a complete archive. Feed order is the publisher’s order, not a promised chronological ranking.

### Scheduled comparison

Enable comparePrevious and reuse the same publications, brand/alias filters, outbound domains and snapshotName. The first run has no baseline: matching articles become “new” only after their rows are confirmed saved. Later runs label each saved observation new, changed or unchanged. With onlyChanges enabled, unchanged articles do not create dataset rows; zero rows can be a correct successful result.

```
{"publicationUrls":["https://www.lennysnewsletter.com"],"comparePrevious":true,"onlyChanges":true,"snapshotName":"product-news-watch","maxResults":100}
```

Use a large enough result cap to establish the desired current feed baseline. Articles excluded by an earlier cap were never saved and can appear as new in a later comparison. Comparison is between your saved observations, not a historical archive of article edits. History records are never removed because an article rolls out of a feed, fails to load or stops matching a filter.

Run one writer per snapshot. A detected concurrent update is reported instead of overwriting newer history, but the storage has no atomic multi-run transaction. Use separate snapshot names for concurrent workflows. History retains at most 20,000 identities per scope; start a new snapshot after reaching that bound.

### Limits and cost

- At most five unique publications and two source requests per publication: robots.txt and /feed. Requests are spaced at least one second apart. No redirects, retries or alternate network routes.
- Public HTTPS destinations only. DNS addresses are validated and pinned per request; private/reserved networks and credentialed URLs are rejected. The public feed path is fixed; private feed paths and query strings are rejected.
- A feed response is capped at 4 MiB; robots at 256 KiB; feeds at 200 items. Malformed, ambiguous or non-Substack source documents fail visibly rather than become paid placeholder rows.
- Requests have a 15-second timeout within the remaining run budget. The default budget is 60 seconds and the platform deadline can shorten it. Maximum saved articles is 1–100, default 5.
- A result cap inside a successfully read feed is an intentionally bounded selection. A cap that prevents another requested publication from being reached produces partial coverage. Time, charge and source failures are also reported separately.
- Base event pricing is **$0.003 per saved article observation**, with all of its mention arrays included, plus **$0.00005 per run start**. Store pricing and the run’s spending limit govern actual charges. Unchanged filtered-out articles, failed sources, SUMMARY and RUN_REPORT create no paid dataset rows. History advances only for confirmed saved output.

Robots exclusions are respected for this crawler. A missing robots file is recorded as missing; it is not a license or authorization assertion. Source access may still change or be limited.

### Support

Open an Actor issue with the public publication URL, run ID and expected field. Do not send login cookies, private feed URLs or subscriber credentials. This is an independent monitoring tool, not an official Substack product.

# Actor input Schema

## `publicationUrls` (type: `array`):

Up to five public HTTPS publication homepages or exact /feed URLs. Blank uses Lenny’s Newsletter. Custom domains are supported when the feed identifies Substack. Private feeds, credentials, query strings and redirects are rejected.

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

Case-insensitive literal whole-word or whole-phrase OR matching in titles and public feed text. Empty keywords, aliases and domains return recent articles without a mention filter.

## `outboundDomains` (type: `array`):

Bare public domain names, for example openai.com. Matches that host and its subdomains in links published in the public feed. Matching a link does not establish a sponsorship.

## `maxResults` (type: `integer`):

Global cap on confirmed saved article observations. Mention arrays are included in each article row. Does not expand the publisher’s recent feed window.

## `aliases` (type: `object`):

Optional map from brand label to up to five literal aliases, for example {"OpenAI":\["ChatGPT"]}. Each brand label also matches itself. Up to ten labels; all configured filters combine with OR.

## `comparePrevious` (type: `boolean`):

First comparison run establishes a baseline from confirmed saved rows. Reuse the same publications, filters and snapshot name. One writer per snapshot.

## `onlyChanges` (type: `boolean`):

Enables comparison. Unchanged articles produce no paid data row. Missing or rolled-off articles are never labeled deleted.

## `snapshotName` (type: `string`):

1–80 letters, digits, underscores or hyphens. Named saved history is isolated by publication/filter scope. Reuse the same scope for comparisons.

## `maxRunSeconds` (type: `integer`):

All requests share the remaining deadline. At most two requests per publication; no redirects, retries, browser or proxy.

## Actor input object example

```json
{
  "publicationUrls": [
    "https://www.lennysnewsletter.com"
  ],
  "keywords": [],
  "outboundDomains": [],
  "maxResults": 5,
  "aliases": {},
  "comparePrevious": false,
  "onlyChanges": false,
  "snapshotName": "default",
  "maxRunSeconds": 60
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `report` (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 = {
    "publicationUrls": [
        "https://www.lennysnewsletter.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("maydit/substack-public-mention-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 = { "publicationUrls": ["https://www.lennysnewsletter.com"] }

# Run the Actor and wait for it to finish
run = client.actor("maydit/substack-public-mention-monitor").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 '{
  "publicationUrls": [
    "https://www.lennysnewsletter.com"
  ]
}' |
apify call maydit/substack-public-mention-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,maydit/substack-public-mention-monitor"
        }
    }
}
```

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/4g2fP7KwDlqFCfAG7/builds/veejlQOUBS9lcQ8Ir/openapi.json
