# Substack Post Monitor: RSS & Paywall Alert (`accountable_eel/substack-post-monitor`) Actor

Monitor Substack publication posts via RSS feeds with XML parsing, paywall detection, and new-post delta state tracking in Apify KV store.

- **URL**: https://apify.com/accountable_eel/substack-post-monitor.md
- **Developed by:** [Adriana Voss](https://apify.com/accountable_eel) (community)
- **Categories:** News, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 substack post returneds

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 Post Monitor: New Newsletter Posts From the Public RSS Feed

List the Substack publications you care about. Each run reads their public RSS feeds and returns one row per post: title, link, author, publication date and a paywall flag. With monitoring on, a scheduled run returns only posts that were not in the previous run.

### Who it's for

**The curator of a weekly links digest.** You read thirty newsletters and summarise the best pieces on Fridays. Instead of opening thirty inboxes, you want one list of everything published since last Friday, with links.

**A venture analyst following a newsletter category.** You are mapping which independent writers in a niche publish weekly and which have slowed down. A dated list of posts per publication, collected on a schedule, is the raw material for that chart.

**A communications team watching a few outspoken writers.** When a critic or a customer-turned-commentator publishes about your company, you want to see it the same morning, not when it is forwarded to you.

### Why this one

- **No login and no key.** Substack publishes an RSS feed for every public publication, and that is the only thing read.
- **Custom domains work.** A publication on its own domain is read as well as one on `substack.com`, because the actor appends `/feed` to whatever address you give it. We checked one of each.
- **A paywall flag on every post.** `isPaywalled` tells you whether the feed text carries a paywall marker.
- **Monitoring with a memory.** Each publication's posts already returned are remembered in your own Apify account.

### What you get

One row per post.

| Field | Meaning |
| --- | --- |
| `publicationUrl`, `publicationTitle` | The address you gave and the feed's channel title. |
| `title` | The post title. |
| `postUrl` | The link to the post on Substack. |
| `guid` | The feed's unique id for the post, used for monitoring. |
| `pubDate` | Publication time as an ISO timestamp. |
| `author` | The creator name from the feed. |
| `description` | The feed's short description, cut at 500 characters. |
| `isPaywalled` | `true` if paywall markers were found for the post, otherwise `false`. |
| `isNew`, `firstSeenAt` | Monitoring flags, empty when monitoring is off. |

Every row also carries `postCount`, the number of posts produced for that publication in the run.

### What the RSS feed can and cannot see

Everything here comes from one file per publication, so what that file holds sets the limits.

**It can see** the newest posts. On 2026-10-08 Astral Codex Ten's feed held 20 posts, from 2026-09-07 to 2026-10-07, and Lenny's Newsletter, on its own domain, held 20 from 2026-09-15 to 2026-10-05. We saw a first monitored run on the first publication report "20 new of 20 fetched".

**It cannot see** anything older than the feed's window. Twenty is the number we measured on both active publications, so treat roughly the 20 newest posts as the ceiling. A publication that posts daily fills the window in 20 days, so a monitor that runs less often than that can miss posts. Some feeds are shorter: one publication we tried held a single post.

**It does not carry the body.** The actor reads the feed's short description, and the full text is discarded. On our two samples the longest description was 39 characters on one publication and 246 on the other. Follow `postUrl` for the article.

**It does not carry engagement.** There are no likes, comments, subscriber counts or restacks in the feed.

**The paywall flag means "a marker was seen".** It matches phrases and CSS names that Substack puts in paywalled posts' feed text. On the 40 posts above, none was flagged. A `false` therefore means no marker was found in the feed, not that the post is free to read. Set `fetchFullArticle` to true and, for every post not flagged by the feed, the actor also requests the post page and looks for the same markers there. That is one extra request per post, so runs get slower.

### Pricing

- **Substack post returned**: $2 per 1,000 posts

Plus a $0.0015 start fee per run. Each event above is billed independently, only when it actually returns data — misses (`found:false`) are never charged.

Empty results are not billed. This was a billing bug in earlier builds, fixed and confirmed with local runs on 2026-10-08:

- A monitored run in which no post is new returns one row with `found: true` and `postCount: 0`, and that row is not charged. You pay only the start fee.
- A publication address that does not exist, such as a mistyped name, now returns a `found: false` row with status `NOT_FOUND`, which is never charged.

A first full run of a 20-post feed costs $0.04.

### How to use

1. **In the Apify Console.** Open the actor page and click **Start** — the `publication_url` field is already pre-filled with a working example. Results land in the run's dataset as soon as each item is found.
2. **Via the API.** Call it directly with a POST request — no Console needed once you have an API token:
   ```bash
   curl "https://api.apify.com/v2/acts/accountable_eel~substack-post-monitor/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
     -X POST \
     -H "Content-Type: application/json" \
     -d '{"publication_url":["https://astralcodexten.substack.com"]}'
   ```
3. **On a schedule.** Save this actor as an Apify **Task** with the input you want, then add a **Schedule** (hourly, daily, weekly) so it runs on its own — no server of your own required.

### Input

```json
{
  "publication_url": [
    "https://astralcodexten.substack.com"
  ]
}
```

Substack publication URL (e.g., https://astralcodexten.substack.com). Accepted formats: https://astralcodexten.substack.com, astralcodexten.substack.com.

Other settings:

- `deltaMode` switches on monitoring, and `deltaName` names the watchlist.
- `maxResults` caps posts per publication. It defaults to 50, which is above what a feed holds.
- `fetchFullArticle` adds the paywall check described above.
- Two optional word lists, `includeKeywords` and `excludeKeywords`, trim the rows after they are fetched. In a monitored run they act before the watch records a post, so a post they hide is not remembered and appears as new if you loosen the lists later.
- `testRun` runs only the first five publications, and `onlyFound` hides rows with no result.

### Sample output

| query | found | status | postCount | publicationUrl | publicationTitle | title | postUrl | guid | pubDate | author | description | isPaywalled | isNew | firstSeenAt | scrapedAt |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| https://astralcodexten.substack.com | true | OK | <posts returned> | <publication url> | <publication title> | <title> | <post url> | <guid> | <publication date> | <author> | <description> | <is paywalled> | <is new> | <first seen> | 1970-01-01T00:00:00.000Z |

### Use it from Clay, n8n, Make, or an AI agent

This actor runs synchronously over plain HTTP — call it directly from a script, a workflow tool, or an AI agent, no Apify Console needed once you have an API token.

```bash
curl "https://api.apify.com/v2/acts/accountable_eel~substack-post-monitor/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"publication_url":["https://astralcodexten.substack.com"]}'
```

**n8n.** Add an HTTP Request node: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~substack-post-monitor/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body Content Type `JSON`, JSON Body `{"publication_url":["https://astralcodexten.substack.com"]}` (swap in an expression from an earlier node for a real value).

**Clay.** Add an "HTTP API" column: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~substack-post-monitor/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body `{"publication_url":["{{publication url}}"]}`, mapping the row's publication url into the `publication_url` array.

**MCP.** In Claude, Cursor, or any MCP client with the Apify MCP server, ask for "Substack Post Monitor | Apify" — the agent will find and run this actor.

### Tips

- **Set `deltaName` once and keep it.** Without it, the monitoring record is keyed on a hash of your publication list, so adding one publication changes the key and restarts every baseline.
- **Schedule often enough for your busiest writer.** With a 20-post window, a writer who publishes 20 posts between two runs pushes the oldest of them out of the feed unseen.
- **A recorded `deltaName` without `deltaMode` still bills every post.** It marks `isNew` but drops nothing. Turn `deltaMode` on to receive only new posts.
- **Use the bare domain if you like.** `astralcodexten.substack.com` works without `https://`.
- **Test an address in a browser.** Open `<address>/feed`. If it shows XML, the actor will read it; if it shows an error page, fix the address before it costs you a post.

### vs. alternatives

| | What it costs | What you get | Trade-off |
| --- | --- | --- | --- |
| **This actor** (`substack-post-monitor`) | $0.002 per post returned, $0.0015 per run | A rolling list of the newest posts per publication, with a monitoring memory and a paywall flag | About 20 posts at most, no body text, and no engagement numbers |
| **Substack Publication Lookup** (`substack-publication-lookup`) | Billed per publication; see its page for the current rate | Name, tagline, posting frequency, the paid-to-free ratio, topics and the last ten posts with reactions and comments | For researching a publication rather than tailing it, this is the better choice. It does not give you a recurring new-post feed. |

### FAQ

#### How many posts can it see?

About the 20 newest, because that is what the feed holds. We measured 20 on two active publications.

#### Does it read comments or Notes?

No. Only the publication's post feed is read.

#### Is a post marked `isPaywalled: false` free?

Not necessarily. It means the feed showed no paywall marker.

#### Why was I billed for a run that returned nothing?

You should not be. Earlier builds charged one post for an empty monitored run or an unreachable address; that is fixed, and neither is billed now (see Pricing). If you still see such a charge, check that the run used the latest build.

### Related actors

- [Substack Publication Lookup](https://apify.com/accountable_eel/substack-publication-lookup) profiles a publication: posting frequency, paid-to-free ratio and recent posts with reactions.

# Actor input Schema

## `publication_url` (type: `array`):

Substack publication URL (e.g., https://astralcodexten.substack.com). Accepted formats: https://astralcodexten.substack.com, astralcodexten.substack.com. You're only charged for the ones we actually find — a miss costs nothing.

## `testRun` (type: `boolean`):

Turn this on to test your input on a small sample before running the full list. Turn it off to process everything.

## `onlyFound` (type: `boolean`):

Only keep rows where something was actually found. Misses are always free, whether or not you show them here.

## `includeKeywords` (type: `array`):

Optional. Only keep results that mention at least one of these words (e.g. a job title, a city, a product name). Leave empty to keep everything.

## `excludeKeywords` (type: `array`):

Optional. Drop any result that mentions one of these words. Leave empty to skip nothing.

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

Caps how many recent feed posts to retrieve per publication.

## `deltaMode` (type: `boolean`):

Stores latest guid and pubDate in KV store and returns only new posts.

## `deltaName` (type: `string`):

Custom key for stored state in the Apify KV store.

## `fetchFullArticle` (type: `boolean`):

Inspect full HTML DOM for Substack paywall classes (.paywall-subscribe) when RSS markup is inconclusive.

## `columns` (type: `array`):

Choose which pieces of information to include in each result row. All are included by default.

## `expandRows` (type: `boolean`):

When on, each post found gets its own row instead of being grouped under its publication. You're still only charged once per publication, no matter how many rows it produces.

## `maxConcurrency` (type: `integer`):

Parallel requests. Keep conservative — this target has no browser fallback, so getting blocked costs more than slow-and-steady.

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

Apify Proxy config. Residential recommended for anti-bot-sensitive targets.

## Actor input object example

```json
{
  "publication_url": [
    "https://astralcodexten.substack.com"
  ],
  "testRun": false,
  "onlyFound": false,
  "includeKeywords": [],
  "excludeKeywords": [],
  "maxResults": 50,
  "deltaMode": false,
  "deltaName": "",
  "fetchFullArticle": false,
  "columns": [
    "publicationUrl",
    "publicationTitle",
    "title",
    "postUrl",
    "pubDate",
    "guid",
    "author",
    "isPaywalled",
    "description",
    "isNew",
    "firstSeenAt"
  ],
  "expandRows": true,
  "maxConcurrency": 2,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (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 = {
    "publication_url": [
        "https://astralcodexten.substack.com"
    ],
    "includeKeywords": [],
    "excludeKeywords": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("accountable_eel/substack-post-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 = {
    "publication_url": ["https://astralcodexten.substack.com"],
    "includeKeywords": [],
    "excludeKeywords": [],
}

# Run the Actor and wait for it to finish
run = client.actor("accountable_eel/substack-post-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 '{
  "publication_url": [
    "https://astralcodexten.substack.com"
  ],
  "includeKeywords": [],
  "excludeKeywords": []
}' |
apify call accountable_eel/substack-post-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,accountable_eel/substack-post-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/d4DcmlnqoM0jq5l9a/builds/Soy3bbeJSlVECcqdT/openapi.json
