Search for the Exploding Topics affiliate program and you can still find its old landing page, offering a 30% recurring commission. Request that page today, explodingtopics.com/affiliate, and the server answers 404 Not Found (checked November 9, 2026). Any article that still sends readers there is sending them to an error page, and the site owner gets no email about it.

That is how affiliate links usually die. Nothing on your own site changes. The article still ranks, the button still looks fine, and the click goes somewhere that pays you nothing. You find out months later, when a reader mentions it or a commission report goes flat.

This guide builds a small automation that checks every outbound and affiliate link once a month and sends you a short list of the ones that need a look. It shows the same check three ways: in Make, in n8n, and as a script written by an AI coding agent and run on a free scheduler.

The short answer

  • Run the check monthly. Requesting each link once a month is enough to catch closed programs and moved pages without hammering anyone’s server.
  • Flag four things: a final status other than 200, a redirect that ends on a homepage, a final domain that differs from the merchant you expected, and a request that times out or fails.
  • Treat 403 as “check by hand”. Bot protection often blocks automated requests to pages that work fine in a browser.
  • Pick the build by where you already work. Make Free covers about 50 links for roughly 100 credits a run. n8n uses one execution per run. A script on GitHub Actions costs nothing and records every redirect hop.

There are three common ways, and only two of them show up as an error.

  1. The program closes. Perplexity’s help center states: “As of November 6, 2025, the Comet Referral Program has concluded” (checked November 9, 2026). When a program ends, the link may stop working, redirect somewhere generic, or keep resolving while paying nothing. A link checker catches the first two. Only the program’s own announcement catches the third, so keep a list of the program pages you rely on and read them before each round of updates.
  2. The merchant moves the page. A product gets renamed, a pricing page moves from /pricing to /plans, and the old URL either returns 404 or redirects to the homepage.
  3. The network changes its tracking domain. Affiliate links usually pass through a tracking domain before landing on the merchant. If that domain is retired, old links fail at the first hop.

What the checker should look at

A link check reads two things: the status code the server returns, and the redirect chain the request follows before it lands.

Status codes in plain English

Every web response starts with a three-digit number. The ones you will see in a link report:

Code What it means What to do
200 OK, the page loaded Still check where it loaded (see redirects)
301, 308 Moved permanently Normal inside affiliate links; update your link if the move is on your own content
302, 303, 307 Moved temporarily Normal for tracking links; judge by the final page
404, 410 Not found, or gone for good Broken. Replace the link
403 Forbidden Often bot protection. Open it in a browser before acting
429 Too many requests You were rate-limited. Slow down and check again later
500 to 599 Server error Recheck the next day before changing anything
No code Timeout, or the domain no longer resolves A missing domain is a strong sign the program or product is gone

Redirect chains

An affiliate link rarely goes straight to the merchant. A typical chain is your link, then the network’s tracking domain, then the merchant’s landing page, sometimes with one or two extra hops for country or language. Each hop is a 3xx response with a Location header pointing at the next URL.

Since a healthy affiliate link redirects anyway, judge it by where the chain ends. Flag a link when:

  • the final status is anything other than 200,
  • the final page is a homepage (path /) when you linked a specific product or pricing page,
  • the final domain is not the merchant you expected, which happens when a closed program’s tracking link falls back to the network’s own site,
  • the chain is longer than about ten hops, or loops.

The last check needs one piece of data you have to supply: the domain each link is supposed to end on. That goes in your link list.

You have two sources, and for affiliate links the second is better.

Crawl your sitemap. Fetch sitemap.xml, request each page, and pull every external href. This finds every outbound link, including ones you forgot. It is also slower, it re-reads your whole site every month, and it cannot know which domain each link should land on.

Keep a links list. A Google Sheet, a CSV, or a JSON file with one row per link:

Column Example Why
url your affiliate or vendor URL What to request
expectedHost make.com Lets the check catch “landed on the wrong site”
foundOn /posts/make-vs-n8n-solo-automation/ Tells you which article to fix
homepageOk true only if you meant to link a homepage Avoids false homepage alerts

If your site is built from Markdown files, as LeverSolo’s Astro site is, a coding agent can generate this list from the links in your posts and their frontmatter in one prompt. Regenerate it whenever you publish.

Version A: Make

Make runs each link through the modules one at a time, which suits a list of 30 to 100 links.

The scenario:

  1. Schedule: monthly, on a fixed day.
  2. Google Sheets: Search Rows. Reads your links list. One bundle per row.
  3. HTTP: Make a request. Method GET, the link’s url. In the advanced settings, keep Allow redirects on (Make follows up to 10) and set Return error if HTTP request fails to No, so a 404 is recorded instead of stopping the run. Set a timeout around 15 seconds (Make accepts 1 to 300).
  4. HTTP: Resolve URL. Make’s docs say it “follows all redirects and returns the final resolved URL.” Use GET, since Make’s docs note HEAD “might not be supported by some servers.”
  5. Filter (free): pass only rows where the status is not 200, or the resolved URL does not contain expectedHost, or the resolved URL’s path is / and homepageOk is false.
  6. Tools: Text aggregator. Builds one line per failing link: URL, status, final URL, article.
  7. Email (or Slack): sends the report. Add a filter before it so an all-clear month sends nothing, or keep it to confirm the check ran.

Credit math (Make’s “How features use credits” help page and pricing page, checked November 9, 2026). Search modules cost 1 credit per run however many rows they return. Action modules cost 1 credit per bundle they process. Routers and filters are free. For 50 links:

Module Credits per run
Search Rows 1
Make a request × 50 50
Resolve URL × 50 50
Filter 0
Text aggregator 1
Email 1
Total about 103

Once a month, that is about 10% of the Free plan’s 1,000 credits. Weekly, it is about 450 credits a month, still inside Free. If you add a Sleep module between links for politeness, add 1 credit per link.

One limit to plan around is run length. Make caps how long a single scenario run may last; the official pages we checked do not state the cap per plan, and users on Make’s community forum report 5 minutes on the Free plan. Fifty quick requests fit easily. With several hundred links, split the sheet into batches of 50 and run the scenario once per batch.

Version B: n8n

n8n counts one execution per workflow run however many links it checks, so the monthly check uses 1 execution a month.

The workflow:

  1. Schedule Trigger: monthly.
  2. Read the list: a Google Sheets node, or an n8n data table if you keep the list inside n8n.
  3. HTTP Request. In Options, turn on Include Response Headers and Status and Never Error, so 404s come through as data. Leave Redirects on. Under Batching, set 1 item per batch and a Batch Interval of 1,500 ms, which spaces the requests out. Set Response Format to Text.
  4. HTML node, Extract HTML Content. Read the href attribute of link[rel=canonical]. Most merchant pages declare their own URL there, which tells you where the chain ended.
  5. Filter or IF: status not 200, canonical domain not equal to expectedHost, or canonical path is /.
  6. Aggregate, then Send Email.

Two n8n-specific notes (n8n docs, checked November 9, 2026). The HTTP Request node does not document a “final URL” output, which is why step 4 reads the canonical tag; pages without one need a manual look. And set an error workflow with the Error Trigger node, so a failed run messages you instead of skipping a month in silence.

On cost, n8n Cloud Starter is €20/month billed annually for 2,500 executions (pricing page, checked November 9, 2026). That price is worth paying only if you already run n8n for other work. On a self-hosted Community edition, the check is free.

Version C: a script on a free scheduler

A short script is the most precise version, because it can record every hop of the redirect chain. You don’t have to write it yourself. Paste the spec further down into a coding agent and review what comes back. If you are weighing this route against Make and n8n in general, read code it yourself vs Make and n8n.

Where to run it. GitHub Actions is the best fit for a monthly job.

  • Standard runners in public repositories are free. On GitHub Free, private repositories get $12 a month of included Actions usage, and a standard Linux runner is billed at $0.006 a minute (GitHub billing docs, checked November 9, 2026). That works out to about 2,000 Linux minutes; a 50-link check takes a couple of minutes.
  • The schedule trigger runs only on the default branch, can be delayed when GitHub is busy, and in a public repository scheduled workflows are “automatically disabled when no repository activity has occurred in 60 days” (GitHub docs). A monthly link check in a quiet public repo will switch itself off. Use a private repo, or add a manual run button and watch for the warning email.

Cloudflare Workers Cron Triggers look tempting, but the Free plan allows 50 subrequests per invocation and every redirect hop counts toward it (Workers limits page, checked November 9, 2026). Twenty affiliate links with two hops each already exceed it.

The script (Node 20 or newer, no dependencies; we ran it against a sample list on November 9, 2026):

// check-links.mjs: monthly link check. Node 20+, no dependencies.
import { readFile, writeFile } from 'node:fs/promises';

const UA = 'MySiteLinkCheck/1.0 (+https://example.com/contact)'; // use your own site
const DELAY_MS = 1500;   // pause between links
const TIMEOUT_MS = 15000;
const MAX_HOPS = 10;

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function trace(start) {
  const hops = [];
  let url = start;
  for (let i = 0; i <= MAX_HOPS; i++) {
    const res = await fetch(url, {
      redirect: 'manual',
      headers: { 'user-agent': UA },
      signal: AbortSignal.timeout(TIMEOUT_MS),
    });
    await res.body?.cancel();
    hops.push(`${res.status} ${url}`);
    const next = res.headers.get('location');
    if (res.status >= 300 && res.status < 400 && next) {
      url = new URL(next, url).href;
      continue;
    }
    return { hops, finalUrl: url, status: res.status };
  }
  return { hops, finalUrl: url, status: 'too many redirects' };
}

function problems(link, r) {
  if (typeof r.status !== 'number') return [r.status];
  const out = [];
  const final = new URL(r.finalUrl);
  const host = link.expectedHost;
  if (r.status === 403 || r.status === 429) out.push(`${r.status}: blocked or rate-limited, check by hand`);
  else if (r.status !== 200) out.push(`final status ${r.status}`);
  if (host && final.hostname !== host && !final.hostname.endsWith('.' + host)) out.push(`landed on ${final.hostname}, expected ${host}`);
  if (final.pathname === '/' && !link.homepageOk) out.push('landed on a homepage');
  if (/404|not-found|page-not-found/i.test(final.pathname)) out.push('URL looks like an error page');
  return out;
}

const links = JSON.parse(await readFile('links.json', 'utf8'));
const rows = [];
for (const link of links) {
  let r;
  try {
    r = await trace(link.url);
  } catch (err) {
    r = { hops: [], finalUrl: link.url, status: `request failed (${err.name})` };
  }
  const found = problems(link, r);
  if (found.length) rows.push(`- ${link.url} (on ${link.foundOn})\n  - ${found.join('; ')}\n  - chain: ${r.hops.join(' -> ') || 'none'}`);
  await sleep(DELAY_MS);
}

const report = rows.length
  ? `# Link check: ${rows.length} of ${links.length} links need a look\n\n${rows.join('\n')}\n`
  : '';
await writeFile('report.md', report);
console.log(report || `All ${links.length} links passed.`);

The schedule (.github/workflows/link-check.yml). It opens a GitHub issue only when something failed, and GitHub emails you about new issues in your own repo:

name: Monthly link check
on:
  schedule:
    - cron: '0 6 1 * *'   # 06:00 UTC on the 1st of each month
  workflow_dispatch:       # adds a manual "Run workflow" button
permissions:
  contents: read
  issues: write
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
      - run: node check-links.mjs
      - name: Open an issue if any link failed
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          if [ -s report.md ]; then
            gh issue create --title "Link check $(date +%F)" --body-file report.md
          fi

Be polite, and expect false positives

A link checker is a small bot. Run it like one you would be happy to receive.

  • Space the requests. 1 to 2 seconds between links keeps you well clear of rate limits, especially when many of your links share one tracking domain.
  • Identify yourself. Send a user agent that names your site and a contact page, as the script does.
  • Use GET. Some servers answer HEAD requests wrongly, so a “faster” HEAD check produces errors that are not real.
  • Expect 403s that aren’t real. On November 9, 2026, both Make’s pricing page and Perplexity’s help-center article returned 403 to a plain command-line request, and both load normally in a browser. Make’s response was a bot-challenge page titled “Just a moment…”. The fix is a human look. Do not try to disguise the checker as a browser to get past the challenge; the site has asked bots to stay out.
  • Recheck before acting. A 5xx or a timeout on one day is often gone the next. Act on a failure that shows up twice.
  • Mind your click stats. Requesting your own tracking links passes through the network’s click tracking, so the check may show up as clicks in your affiliate dashboard. Once a month for 50 links is small noise; running it daily is not.

Which version to build

Your situation Build it in
You already use Make and have under about 100 links Make Free (about 103 credits per monthly run for 50 links)
You already run n8n, Cloud or self-hosted n8n (1 execution per run)
Your site lives in Git and its links are in Markdown Script on GitHub Actions
You want the full redirect chain in the report Script
Several hundred links Script, or Make in batches of 50
You never want to read code Make

The spec to paste into a coding agent

Copy this, fill in the bracketed parts, and give it to the coding agent you use. Ask it to explain anything you don’t understand before you run it.

Build a monthly link checker for my website.

Input: links.json, an array of objects with fields
  url (string), expectedHost (string), foundOn (string), homepageOk (boolean, optional).
[Optional: generate links.json from the Markdown files in <path>, collecting every
external link and every affiliate URL in frontmatter. Ask me for each link's expectedHost.]

For each link:
- Send a GET request with the user agent "<MySite>LinkCheck/1.0 (+<contact URL>)".
- Do not follow redirects automatically. Follow them yourself, up to 10 hops,
  recording the status code and URL of every hop.
- 15-second timeout per request. Wait 1.5 seconds between links.

Flag a link when:
- the final status is not 200 (label 403 and 429 as "check by hand", not "broken"),
- the final hostname is not expectedHost or a subdomain of it,
- the final path is "/" and homepageOk is not true,
- the chain exceeds 10 hops, or the request fails (timeout, DNS error).

Output: write report.md listing only flagged links, with the reason, the article
(foundOn) and the full redirect chain. Write an empty report.md if nothing failed.

Constraints: Node 20+, no dependencies. Never disguise the user agent as a browser.
Then write a GitHub Actions workflow that runs it at 06:00 UTC on the 1st of each
month, plus a manual trigger, and opens a GitHub issue with report.md as the body
only when report.md is not empty. Minimal permissions: contents read, issues write.
  1. Open it in a normal browser. If it works there and failed with 403, mark it as a false positive and move on.
  2. Check the program page. If the program has closed, remove the affiliate link and link the vendor’s own page instead, or recommend an alternative.
  3. Find the new URL. For a moved page, update the link and the row in your list.
  4. Update the article the same day, with an updated date. A price or program change can also be worth a short news post of its own, since readers searching for that tool want to know what changed.

Who this is for, and who can skip it

Build it if you have more than a handful of affiliate links, your articles are more than a few months old, or you link to tools whose programs you haven’t checked lately.

Skip it for now if your site has fewer than ten outbound links. Open them by hand on the first of each month; it takes five minutes.

Quick answers

How often should I check affiliate links? Monthly suits most small sites. Add a manual check whenever a program emails you about changed terms, and check right after a site migration.

Why does a link work in my browser but fail in the report? Usually bot protection: the server returns 403 to automated requests. Mark the link as checked by hand. A 429 means the check went too fast for that server; raise the delay.

Can a link pass the check and still earn nothing? Yes. A closed program’s link can keep resolving to the merchant. Only the program’s own page or announcement tells you it stopped paying, so read the program pages you depend on each quarter.


More builds like this live in the Automation Workflows pillar. To choose a platform first, see Make vs n8n for solo builders. For the companion check on prices, read how to watch vendor pricing pages automatically.