Ryan Smith
production2026-08

Who Got Pirated Overnight

Checks a course-piracy site every morning and emails a list of the newly stolen courses.

128Pirated courses logged
1 to 3New listings per day
28Creators contacted

The Problem

Course creators lose sales when their paid material shows up on piracy sites, but most never find out it happened. The sites post new stolen courses every day, and the only way to know who was hit is to check by hand. Nobody does that. Ryan wanted a daily list of which creators had just been pirated, so he could contact them while the listing was fresh and offer to get it taken down.

What It Does

Every morning at 7:07 it loads the piracy site's full course catalog, which is a single page with every listing embedded in it, newest first. It pulls out the title, link, and posting date for the newest 120 listings. It compares those against a Google Sheet of everything it has already seen and drops any it already knows. Anything new gets appended to the sheet, oldest first so the ledger stays in order. Then one email goes out listing the new titles with links, newest at the top. If nothing new appeared, no email is sent.

Anyone selling a takedown or content-protection service to course creators, or a creator who wants to know the day their own material shows up on a piracy site.

Outcome

The feed did its job: it surfaced 36 recently pirated creators, and Ryan contacted 28 of them. It did not produce a paying client. One creator verbally committed to a $2,500 one-time cleanup across several assets but never paid. Another agreed to a free demo takedown but never signed the authorization, or sent the email version Ryan offered instead. Most of the rest said takedowns feel like whack-a-mole and are not worth their time. No takedowns were performed from this run of work.

Screenshots

The morning digest as it arrived in Gmail: two newly pirated courses with links and excerpts, from the first version of the parser.
The morning digest as it arrived in Gmail: two newly pirated courses with links and excerpts, from the first version of the parser.
The morning digest as it arrived in Gmail: two newly pirated courses with links and excerpts, from the first version of the parser.

The morning digest as it arrived in Gmail: two newly pirated courses with links and excerpts, from the first version of the parser.

The Google Sheet ledger. After the 2026-09-02 rebaseline, one to three new listings land per day.
The Google Sheet ledger. After the 2026-09-02 rebaseline, one to three new listings land per day.
The Google Sheet ledger. After the 2026-09-02 rebaseline, one to three new listings land per day.

The Google Sheet ledger. After the 2026-09-02 rebaseline, one to three new listings land per day.

The n8n canvas: fetch, parse, read the ledger, filter to new, append, email.
The n8n canvas: fetch, parse, read the ledger, filter to new, append, email.
The n8n canvas: fetch, parse, read the ledger, filter to new, append, email.

The n8n canvas: fetch, parse, read the ledger, filter to new, append, email.

How It's Built

Trigger:n8n schedule trigger, daily at 07:07 America/Chicago.
Components:10

A Code node emits the single catalog URL. An HTTP Request node fetches the page as raw text with a 30 second timeout. A Parse node finds the inline JavaScript array the page uses to render its table, parses it as JSON, takes the newest 120 rows, converts dates like 'Sep 2, 2026' to ISO, stamps each row with a Central-time capture date and time, and throws a hard error if it finds zero rows. A Google Sheets node reads the full existing ledger once. A Filter node builds a set of known URLs and drops anything already present or duplicated within the run, then sorts oldest first. An If node stops the flow when nothing survived. Otherwise a Sheets append writes the new rows, a Code node builds one HTML digest, and a Gmail node sends it. Failures route to a shared error workflow.

By the numbers

unique pirated courses logged
128 (2026-08-18 to 2026-09-13)
new listings appended per run day, after the 2026-09-02 rebaseline
1 to 3
listings in the source catalog
3,900+
newest listings inspected per run
120
creators tracked in the outreach pipeline
36
creators contacted
28
largest verbal commitment
$2,500 one-time cleanup, never paid
paying clients from this outreach
0
run cadence
daily, 07:07 Central
typical run time
2.5 to 8.3 seconds
running cost
no paid APIs; effectively free on a self-hosted n8n instance

Key Decisions

Scrape the site's all-courses table page instead of paging through blog listings

The page embeds the entire 3,900-row catalog in one inline script, newest first, so one fetch covers everything and there is no pagination to break.

Parse the embedded JavaScript array rather than the rendered HTML

The data is already structured as JSON inside a script tag. Reading it directly is more reliable than pattern-matching table markup.

Dedupe on post URL against the full sheet, not on title or date

The URL is the one stable identifier. Titles get edited and the page no longer exposes a post time.

Only inspect the newest 120 rows per run

The catalog is thousands of rows and the site adds a handful a day. Dedupe catches anything missed, so there is no need to reprocess the whole list.

Fail loudly when the parser finds zero posts

Zero results almost always means the site changed its markup, not that nothing was posted. A silent empty run would hide the break.

Run at a fixed time each morning, before the workday starts

The fresh list is ready when Ryan sits down to work on outreach, and nothing else on the n8n instance runs at that hour.

Keep post_time and excerpt columns as empty strings

The new source page dropped those fields. Writing blanks keeps the sheet schema intact for the earlier rows that do have them.

What broke & how it was fixed

The first version read the site's updates feed page and captured a post time and short excerpt for each course. Six days in, that page started returning a WordPress 'Page not found' response, so the parser found zero posts and the run failed with 'The resource you are requesting could not be found'. The ledger shows the gap: nothing appended from 2026-08-24 to 2026-09-02. The fix was to switch the source to the site's all-courses table page, which ships the whole catalog as an inline JavaScript array, and rewrite the parser around that. Post time and excerpt are no longer available from the new page, so those columns are written blank. The rewrite rebaselined on 2026-09-02 and has run clean daily since.

Where else this applies

The shape is a daily diff against a ledger: fetch a public list, keep only what you have not seen, log it, tell someone. It fits any site that publishes a rolling list without alerts, such as new court filings, permit issuances, marketplace listings, competitor price pages, or job boards. The interesting part is not the scraping. It is knowing the day something appears so you can act while it still matters.