Category: Field notes

First-hand write-ups of what we ran, what broke, and the numbers behind it.

  • We wrote a second checker for the space between articles. It caught two orphans on day one.

    We wrote a second checker for the space between articles. It caught two orphans on day one.

    Every automated check we had looked inside a single article. Links resolve, disclosures are present, no duplicate of anything published before. Nine checks, all of them scoped to one piece of writing at a time.

    Our three worst failures were all outside that scope. A sitemap frozen for ninety-three days. An article that went twenty-seven days without a crawl. A site with no internal links at all. Each individual article was fine. The site was not.

    So we wrote a second checker, at a different scale. This is what it looks at, what it found immediately, and where it runs.

    A diagram of three article boxes. The nine per-article gates wrap each box. The dashed lines running between the boxes mark the space where our worst failures lived.
    Nine checks guard each box. Nothing was watching the dashed lines.

    The six checks and where each one came from

    None of these are clever. Each exists because we lived through the thing it now catches.

    Is the sitemap current? Ours had been frozen since June. It was still advertising eleven pages we had deleted while listing none of the ones we had published since.

    Do the URLs in the sitemap resolve? Those eleven returned 404 to anything that followed them.

    How many article-to-article links exist? Across our English site the answer was six. On the Japanese site it was two, and both of those were between the same pair of posts.

    Which articles have nothing pointing at them? These are dead ends for a crawler. A reader who lands there has nowhere to go except back.

    Which articles point at nothing? The same problem from the other side.

    How old is the newest article? A site that has not published in a month is a different kind of broken, and nothing reports it.

    Six checks, a few hundred lines, standard library only. Writing it took about an hour, most of which was spent on the link graph.

    What it found

    The Japanese site had four articles with no article-to-article links in either direction. They had been that way since publication. All four were still linking correctly to legal pages, which is why a visual check had passed them for months — the links were there, they just went nowhere useful.

    Six site-level checks listed against the real failure each one came from. A sitemap frozen ninety-three days. Eleven URLs returning 404. Six article-to-article links. Four articles with nothing pointing at them. A newest article thirty-two days old.
    A checklist built from scars again, which is the only way these ever get written.

    Then it caught something while we were still working.

    We publish through a script. That script now runs the site checker immediately after a successful publish. On the first article we shipped after building it, the output read:

    Gates passed, article published, then a failure line: one article with nothing pointing at it. The article it was complaining about was the one we had just published, forty seconds earlier.

    We fixed it before the page had been crawled. The same thing happened again a few hours later with an article on the Japanese site. Two catches on the first day, both of which would otherwise have sat there until somebody noticed.

    Terminal output showing an article publishing successfully, then the site health check immediately reporting one article with nothing pointing at it. The article it flagged was the one just published.
    The first article we shipped after building it. The check was complaining about that very article.

    Where it runs

    Two places, for two different reasons.

    Immediately after every publish. A new article starts life with nothing pointing at it. That is the default state, not an unusual one, so the moment of publishing is exactly when the check is worth running. The site check does not block the publish and cannot undo it — the article is already live, and the check is telling you what to do next.

    Every morning at nine. Rot is not an event. The sitemap did not break on a particular Tuesday; it stopped updating and then stayed stopped for three months. Only a repeated check finds that kind of failure.

    The part that decides whether any of this works

    The checker writes a dated report to a folder. If that were the whole design, it would be worthless.

    A report nobody opens is not monitoring. It is a second copy of the original problem, because the entire failure mode we are trying to fix is information that existed and was never read. The sitemap was visible in a browser the whole time. Search Console would have answered the crawl question for anyone who asked it. Nobody asked.

    So the check raises a system notification when something fails. That notification is the actual product; the report is just where the detail lives.

    We verified that the notification appears. This sounds like an unnecessary step until you remember that the command to display one returns success whether or not the operating system shows anything, which is the same shape as a publishing API that returns 201 for an article that reaches readers as raw markup. An exit code describes the call. It does not describe what a person saw.

    What it cannot do

    It does not know whether an internal link is any good. A link exists or it does not; whether it was worth placing where it sits is a judgment. Nothing here stops you from satisfying every check with a block of related links at the bottom of each article, which passes the counter while helping nobody.

    It also has no opinion about whether the articles are worth reading. That was true of the per-article gates as well. Both checkers clear the floor so the attention goes somewhere that needs it.

    If your site is small

    You can run the first three checks by hand in under two minutes, today, without writing anything.

    Open your sitemap in a browser. Look at the date on it, then click two or three of the URLs inside.

    Take your newest article and inspect its URL in Search Console. Not the ranking. Whether it has been crawled at all.

    Open any article and try to reach a different one by clicking. If you cannot, a crawler cannot either.

    If all three pass, you do not need a checker yet. If any of them fails, you have learned something more useful than a month of traffic data would have told you.


    Editorial note. Written and edited by the operator of this site. The counts, the dates, and the day-one results are from our own two sites and our own tooling, recorded on 2026-09-15.

    Disclosure. This article contains no affiliate or sponsored links. This site may earn commissions from links in other articles, and those articles carry their own disclosure at the top. Nothing here is professional advice.

  • We chose an affiliate programme by its commission rate. The rejection arrived in six minutes.

    We chose an affiliate programme by its commission rate. The rejection arrived in six minutes.

    We applied to an affiliate programme at 02:49 and were turned down at 02:55. Six minutes. Whatever happened in that window, it was not a careful reading of our site.

    The rejection was the right outcome. What is worth writing down is that we had picked the programme for a reason that could not survive contact with the question the reviewer was actually asking.

    A timeline showing the application sent at 02:49 and the rejection received at 02:55, six minutes later.
    The entire review, start to finish. The verdict was still right.

    How we chose

    We were looking for recurring commission, because a payout that resets every month means the work never accumulates. We had already done that arithmetic on our own numbers and concluded the volume model was unreachable for a site our size.

    So we ranked the candidates by commission rate and duration, picked the highest, and applied.

    That is the whole selection process. Written down like that, the gap is obvious. We never asked whether their customers were our readers.

    What the rejection said

    The email listed four possible reasons, in the generic way these templates do. Only one of them mattered:

    Your audience focuses on a different niche than ours — we partner with creators who help other creators earn a living online

    That is the programme describing its own customer in one sentence. Our articles are about publication gates, firewalls blocking their own owner, and instruments that were never connected. Our readers run small publishing operations. Those are adjacent worlds that share almost no purchase intent.

    No amount of rewriting the application would close that gap. We were the wrong applicant, and a better-written application would only have made us a more convincing wrong applicant.

    The part that was our fault

    One of the other four reasons did apply, and it was ours:

    Your application didn’t include enough detail about how you’d promote

    The first draft of our application described how we would promote the product in concrete terms. Then we shortened it, and the specifics were what got cut. What survived was a statement of editorial policy with no plan attached.

    Shortening was the right instinct. Choosing which parts to cut is a separate decision, and we made it badly.

    The arithmetic nobody does before applying

    Here is what we should have computed first. The programme paid 50% for twelve months, then a reduced rate only if you keep referring ten or more customers a year.

    Their customers pay roughly $29 a month. So one referral is worth about $174, spread over a year, and then it stops.

    To reach a modest monthly target from that programme alone, we would have needed about 46 new referrals every month, forever, because nothing carries over. Compare that with a programme paying 30% for as long as the customer stays: the same target needs a base of active referrals that you build once and keep.

    Two commission models side by side at the same customer price. The programme paying 50 percent for twelve months requires 46 new referrals every month. The programme paying 30 percent for as long as the customer stays requires a base of 77 active referrals built once.
    Two programmes that sit next to each other in any comparison table, doing completely different work.

    One of those is a treadmill. The other is a staircase. They look similar in a comparison table, and the commission rate is not what tells them apart.

    What we changed

    We wrote down selection criteria before looking at any more programmes. Four of them, in order.

    Do we actually use it? We published a policy that we do not write about products we have not used. Applying to a programme for a product we had never opened meant that approval would have left us unable to write anything. We had put the steps in the wrong order.

    Does the commission accumulate? A rate that ends after twelve months is a one-time payout wearing a subscription costume.

    Are their customers our readers? Not adjacent. The same people, with the same problem, at the same moment.

    Would they approve a site this size? Five articles and almost no traffic is a real constraint, and pretending otherwise wastes everyone’s time.

    Four selection criteria applied to the programme we chose. Three are clear failures: we had never used the product, the commission stops after twelve months, their customers are not our readers.
    Scored after the fact. Every answer was available before we applied.

    Applying that list to the programme we had chosen, it fails three of four. We could have known that before applying, from published information, in about twenty minutes.

    If you are monetising a small site

    The commission rate is the most visible number and the least useful one. It is the last thing to compare, after you have established that the product is something you use, that the payment continues, and that the audience is genuinely the same.

    There is also a quieter cost to getting this wrong that has nothing to do with the rejection. We spent two weeks building toward a monetisation path that could not have worked, and none of that showed up as a failure until an automated email arrived six minutes after we asked. Most wrong turns in a small business are like that: they do not announce themselves, and the first real signal is much later than the decision.


    Editorial note. Written and edited by the operator of this site. The timestamps, the quoted lines, and the figures come from our own application and the rejection email we received on 2026-09-14. We have not named the individual who replied.

    Disclosure. This article contains no affiliate or sponsored links. We were not accepted into the programme discussed. This site may earn commissions from links in other articles, and those articles carry their own disclosure at the top. Nothing here is professional advice.

  • Eight automated checks passed. The reader saw asterisks.

    Eight automated checks passed. The reader saw asterisks.

    For about a day, three of the four articles on this site displayed their own markup. Headings arrived as ## What the dashboard said, with the hashes sitting in the text. Bold arrived wrapped in asterisks. Links arrived as square brackets followed by a naked URL in parentheses.

    Every one of those articles had passed an eight-gate publication checker with no failures. The checker was working exactly as designed. That is the part worth writing down.

    What the reader actually got

    The left panel is a live article as published. The right is the same passage after the fix.

    Two screenshots of the same passage from our own published article. On the left, as it was live for about a day, the heading appears as hash characters and the bold text is wrapped in asterisks. On the right, after the fix, the heading renders as a heading.
    Our own published page, before the fix on the left. The reader met the markup before they met the sentence.

    How it happened

    Our publishing pipeline reads a file, runs the gates, then posts the body to the WordPress REST API. The articles were written in Markdown. WordPress does not render Markdown. It stores what you send it.

    The assumption was sitting in our own source code, in a comment on the data model:

    body: HTML or Markdown

    Nobody had ever tested the second half of that sentence. The first article on this site happened to be written in raw HTML, so it rendered correctly. When we switched to writing in Markdown, everything after it broke, quietly, in a way that looked fine from every angle we were checking.

    Why the checker did not catch it

    This is the useful part. Here is what the eight gates ask:

    Does every link resolve? Do commercial links point only to approved programmes? Are there banned expressions? Is this a near-duplicate of something already published? Is the disclosure above the fold? Are the editorial note and disclaimer present? Are there generation artifacts in the text? Are the dates internally consistent?

    Read those eight as a set. Every single one is a question about what the article says. Not one of them is a question about whether the article has a form.

    The nine questions our publication gates ask. The first eight are all questions about what the article says. Only the ninth asks whether the article has a rendered form.
    Written out as a set, the shape of the blind spot is obvious. Added one at a time after incidents, it never was.

    That was not an oversight in the sense of forgetting an item. Each gate was written in response to a specific failure we had already lived through: a dead link, a duplicate, an income claim in a headline. The list was a list of scars. You cannot write a scar for an injury you have not had yet, which is precisely why a checklist built from experience has a predictable shape: it is dense where you have been hurt and empty everywhere else.

    The gap was not in the list. It was in the space the list did not cover.

    Why a human did not catch it either

    We publish, then read the result of the publish. The API returned 201. The gate report printed eight passes. A person read both of those and moved on.

    Both of those signals were true. Both of them were about the system. Neither of them was about the page.

    Nobody opened the public URL. Not once, across three articles. The information was one click away for a day and a half, sitting in a browser tab nobody opened, because the dashboard had already said the thing was fine.

    This is the same failure we wrote about two weeks ago, where a site produced no revenue for fifteen months while the instrument measuring it was disconnected. Same shape. We read the report instead of the thing the report described. Knowing about the pattern in the abstract did nothing to stop us repeating it eleven days later.

    Three signals after publishing. An HTTP 201 means the request was accepted. Eight of eight passed means the body satisfied eight content rules. Only opening the public URL tells you what the reader sees.
    Two of these three signals were green for three articles running. Neither was about the page.

    What we changed

    A converter, so the input is always valid output. Markdown now becomes HTML before it reaches the API. A few hundred lines, standard library only, handling exactly the notation our articles actually use and nothing else.

    A ninth gate that fails closed. G-9 scans the body that is about to be posted for surviving markup notation. If it finds any, nothing publishes. Writing the gate took twenty minutes. It exists now because the failure happened, which is the same way the other eight got here.

    A second checker, at a different scale. The nine gates all look inside one article. We later built a set of checks for the space between articles, which is where our worst failures had been living.

    A rule that is not automated at all. After publishing, open the public URL and look at the page. Not the API response. Not the gate report. The page, as a reader loads it.

    That last one is the only part of this that generalises. The first two fix this specific bug. A machine check can confirm that a page contains no leftover asterisks. It cannot tell you that a paragraph is confusing, that a figure landed in the wrong place, or that the article now opens with two identical headlines because the cover image repeats the title. Every one of those was also true of our pages this week, and the only thing that found them was a person looking at the page.

    If you publish through an API

    Three things, in order of how much time they save.

    Load the public URL after every publish. The response code tells you the request succeeded. It tells you nothing about what the reader gets.

    Assume every comment in your own code is untested. The word or in a code comment is where bugs live. Ours said Markdown would work. It had never worked. It had simply never been tried, and the comment made it look like a decision rather than an assumption.

    When you add a check, ask what category it belongs to. Our eight gates were all one category. That is invisible while you are adding them one at a time in response to incidents, and obvious the moment you write them out as a list and read them together. If every check you have asks the same kind of question, you have a blind spot the exact size of the questions you are not asking.

    None of this made the articles better. It made them legible, which is the floor rather than the ceiling. But a reader who sees asterisks in the second paragraph does not reach the argument, so the floor is where the work has to start.


    Editorial note. Written and edited by the operator of this site. The screenshots are of our own pages, taken before and after the fix on 2026-09-14. The affected articles were live in the broken state for roughly one day.

    Disclosure. This article contains no affiliate or sponsored links. This site may earn commissions from links in other articles, and those articles carry their own disclosure at the top. Nothing here is professional advice.

  • Fifteen months. Zero revenue. The measurement was never actually running.

    Fifteen months. Zero revenue. The measurement was never actually running.

    Here is the whole of it. Between June 2025 and August 2026 this site earned nothing. Not a small amount. Zero confirmed commissions, zero conversions, across fifteen months of an affiliate programme we had been approved for and had linked from live articles the entire time.

    That number is easy to explain away. New site, hard niche, not enough articles. All of those were true, and all of them were beside the point, because when we finally audited the site properly in September we found something worse than failure. The experiment had never been running.

    What the dashboard said

    Search Console, for the five weeks to September 10: fourteen impressions. Zero clicks. Average position 52.6, which is page five or six of the results. Nine pages indexed, twenty not.

    Read quickly, that is a story about quality. The articles are not good enough to rank, so they sit on page six, so nobody clicks. Write better articles, wait longer.

    We read it that way for months.

    What was actually true

    Three things, all found in a single afternoon of looking at the plumbing rather than the numbers.

    The sitemap had been frozen since June 9. Three months. It was still advertising pages we had deleted — eleven URLs returning 404 — while listing none of the articles published since. Of the twenty pages Google had not indexed, eleven were pages that no longer existed. We had been feeding a crawler a map of a site that was not there.

    An article published on August 14 had never been crawled. Not ranked badly. Not crawled at all. URL inspection returned no record of a visit, three and a half weeks after publication.

    There were no internal links. Zero, across every article on the site. No path from any page to any other page. Each article sat alone with nothing pointing at it and nothing leading away.

    Any one of these would depress the numbers. Together they mean the fourteen impressions were not a measurement of anything. They were the reading you get from an instrument that is unplugged.

    Two columns comparing what the dashboard reported with what was true. The dashboard showed 14 impressions with zero clicks. The site meanwhile had a sitemap frozen since June 9 advertising 11 dead URLs. No tool raised an alert.
    The same five weeks, read two ways. Only the right-hand column explains the left.

    The part that is uncomfortable to write

    All three failures were silent. No error, no alert, no red number on a dashboard. The sitemap plugin did not announce that it had stopped. Search Console does not tell you that an article is uncrawled unless you inspect that specific URL by hand. Nothing anywhere reports “this site has no internal links.”

    Three site failures shown as bars by how long each went undetected. The sitemap was frozen for 93 days. An article went uncrawled for 27 days. Internal links were missing for the life of the site.
    How long each failure ran before anyone noticed. No tool reported any of them.

    And all three were visible to anyone who looked. Not one required special tooling. Opening the sitemap in a browser takes four seconds and would have shown a file with a June date and eleven dead URLs in it.

    Nobody looked, because the numbers were being reported every week and the numbers looked like a story we already understood. A bad number that fits your theory is more dangerous than no number at all. It closes the question.

    The arithmetic we should have done first

    Once the plumbing was fixed, we finally did the sum we had been avoiding for over a year.

    The affiliate programme pays roughly ¥100 per click on the products we were approved for, measured from our own account. Conversion runs near 0.8%. So one confirmed sale needs about 125 clicks on an affiliate link. At a 7% click-through from article to link, that is about 1,800 sessions for a single sale.

    The original goal was ¥1,000,000 a month. Run the same numbers forward and that requires roughly 135,000 sessions a month, every month, for a payout model that resets to zero on the first of each month. We had six articles.

    The calculation chain from earnings per click of 100 yen through a 0.8 percent conversion rate to 1800 sessions per sale. A target of one million yen a month requires 135000 sessions every month.
    Fifteen months of work could not tell us the plan was unreachable. Twenty minutes with a calculator did.

    This is not a stretch target. It is a different business, and no amount of writing better articles was going to close a gap of that size.

    The useful thing about doing the arithmetic is that it is cheap and it is final. Fifteen months of effort could not tell us the plan was unreachable. Twenty minutes with a calculator did.

    What changed as a result

    We stopped chasing the volume model. A payout that resets monthly means every month starts at zero no matter how much you published last month; the only way up is more traffic, forever. The alternative is recurring commission, where each referral keeps paying while the customer stays subscribed, so the work accumulates instead of evaporating.

    Under the recurring model the traffic requirement drops by more than an order of magnitude, because you are adding to a base rather than replacing it. Choosing which recurring programme turned out to be its own problem: our first application was rejected six minutes after we sent it. That is the business we are now building, in English, on this site. Whether it works is an open question. What is no longer open is whether the previous plan could have worked.

    We also changed how we measure. Two rules now:

    Check the instrument before reading it. Before any monthly number goes into a report, someone confirms the sitemap is current, spot-checks that recent articles have been crawled, and confirms internal links exist. Under a minute. It would have caught all three failures in June.

    A bad number gets audited, not explained. The temptation with a disappointing metric is to reach for a reason. The reason is usually available and usually plausible. The rule now is that an unexpected number is treated as a possible instrument fault until the instrument has been checked.

    If you run a small site

    The failure here was not strategic. We did not pick the wrong niche and lose fifteen months to it, although we did also pick a hard niche. The failure was that we spent fifteen months collecting readings from equipment nobody had verified was connected, and then reasoning carefully about those readings.

    Three checks, none longer than a minute:

    Open your sitemap in a browser. Look at the date. Look at whether the URLs in it still resolve.

    Take your most recent article and inspect the URL in Search Console. Not “how is it ranking” — has it been crawled at all.

    Pick any article and try to click your way to another one. If you cannot, neither can a crawler.

    We have described the gates that now run before anything publishes. Those catch problems inside an article. None of them would have caught any of the three failures above, which lived between the articles rather than inside them. That gap is the next thing we are building for. It turned out to be wider than we thought: the same gates later passed three articles that reached readers as raw markup.


    Editorial note. Written and edited by the operator of this site. Every figure here is from our own affiliate account, our own Search Console property, or our own audit notes of 2026-09-10. The revenue figure is not rounded.

    Disclosure. This article contains no affiliate or sponsored links. This site may earn commissions from links in other articles, and those articles carry their own disclosure at the top. Nothing here is professional advice.

  • Three 403s in one week. We named the wrong cause twice, for the same reason.

    Three 403s in one week. We named the wrong cause twice, for the same reason.

    Every 403 we hit last week came from our own infrastructure. Not an attacker, not an outage — our own web application firewall, refusing our own requests, on our own site. That part is ordinary; anyone running a managed host with a WAF in front of WordPress will meet it. What is worth writing down is that we diagnosed two of the three wrong before we diagnosed them right, and both times the mistake had the same shape.

    We run a small English-language site on a Japanese managed host. The publishing pipeline is automated: a checker runs eight gates against a draft and posts it through the REST API if all eight pass. So when the firewall says no, it says no to a script, with no browser and no human watching. You get a 403 and an HTML page that tells you nothing.

    Table of three 403 errors from one week, showing what we concluded, what was actually true, and the one-minute check that would have shown it. Two of the three conclusions were wrong.
    Three 403s in one week. The column we would rather not publish is the second one.

    The first one: a login that worked everywhere else

    A login POST returned 403 while a GET of the same page returned 200. We concluded the site had a block specific to itself — it is the newer of our two properties, and it had an overseas-access restriction switched on that the older site did not. We turned that restriction off.

    It made no difference, because it was never the cause. The real cause was a separate setting in the host’s control panel, a login restriction on the admin screen, sitting one menu below the thing we had changed.

    What would have caught it in a minute: sending the same request to our other site. We did that eventually and got the same 403 there — from a site that was working perfectly well. The 403 was simply what that endpoint returns to a request with no session cookie. It was never evidence of anything.

    We had changed a security setting on a live site based on a difference we never confirmed was a difference.

    The second one: three paragraphs the firewall would not accept

    An article refused to publish. The gates passed; the POST returned 403. This one we got right, and the reason we got it right is worth naming: we did not reason about it at all. We posted the article one paragraph at a time. Forty-seven went through and three did not.

    The three had nothing in common thematically. They had something in common structurally: each contained a comma-separated list inside quotation marks. The firewall rule was a SQL injection filter that treats a comma followed shortly after by the word and as an attack signature — the shape of any ordinary three-item list written with an Oxford comma.

    We would print the example here, but the firewall will not carry it. This article is published through the same rule set, and an example is precisely what it refuses. This paragraph took four attempts.

    Bisecting is not a clever technique. It is just a comparison baseline applied repeatedly, which is why it works when reasoning does not.

    The third one: an upload limit that did not exist

    Uploading images to the media library, three succeeded and the fourth failed with 403. Every attempt after that failed too. The obvious reading was a rate limit — three per window, then a cooldown.

    So we waited, and retried eight times over ten minutes. All 403.

    Five diagnostic steps for a blocked image upload. A ten minute wait returned 403. A new filename returned 403. A re-render at two different sizes returned 403. Re-sending a file that had already uploaded returned 200, which ended the investigation.
    Ten minutes spent on a theory that three earlier successes had made look obvious.

    Then we did the thing we should have done first: took an image that had already uploaded successfully, gave it a new filename, and sent it in the same batch as one that kept failing.

    The known-good image returned 200. The failing one returned 403. There was no rate limit and there never had been. The block was a property of the file, not of the sequence — certain PNG byte sequences match a firewall signature, and re-rendering the same figure at a different size produced the same bytes in the same place. Converting it to JPEG uploaded on the first try.

    “Three succeeded, then everything failed” felt like strong evidence for a limit. It was a coincidence of ordering, and we spent ten minutes acting on it.

    The same mistake twice

    Two of the three had the identical structure. We observed a sequence of outcomes, inferred a cause that fit the sequence, and acted on the inference — without once running the case we already knew worked down the same path at the same time.

    Two requests sent to the same endpoint in the same minute: a file known to upload successfully returns 200, while the failing figure returns 403, showing the block belongs to the file rather than to the sequence.
    The control request. Same endpoint, same credentials, same minute — and an answer that no amount of reasoning about the failing case could produce.

    A comparison baseline costs almost nothing. In the first incident it was one request to our other site. In the third it was one file we had already uploaded. Both were under a minute. Both would have removed the wrong answer immediately, and in the first case they would have saved us from weakening a security setting for no reason.

    The reason this bites harder with a WAF than with ordinary bugs is that a firewall gives you a block page instead of an error. There is no stack trace, no rule name, no line number. The absence of information invites you to fill the gap with a theory, and a theory that fits three data points feels like knowledge.

    What we actually changed

    Not the firewall. That was the other instinct worth resisting.

    The same firewall had blocked over two hundred real scans of the site that month. Switching it off to make our own pipeline work would have traded a known ongoing risk for a few minutes of convenience. The host’s WAF log lists every block with the rule that fired and an option to exclude that single rule. We excluded one rule — the SQL injection filter that cannot tell an English list from an attack — and left everything else running.

    For the image uploads we changed nothing on the server at all. The pipeline now converts to JPEG and retries when a PNG comes back 403.

    If you run the same setup

    Three things, in the order they save you time.

    Go to the WAF log before you form a theory. It names the rule. Everything we worked out by bisecting was sitting in that log the whole time, with the matched string highlighted.

    Send the known-good case down the same path, at the same moment. Not yesterday’s result, not the other environment’s result — the same path, now. If it also fails, your theory about what makes the failing case special is already wrong.

    Exclude rules, don’t disable the firewall. A single-rule exclusion is auditable and reversible. “We turned it off during the incident” is neither, and it tends to stay off.

    None of this is sophisticated. It is the discipline of confirming a difference before building on it, which is exactly the discipline that is hardest to hold when you are three hours in and the deploy is blocked.

    We have written up the gate system that sits in front of all of this. We have also written up what broke when we handed the work to scheduled agents. The pattern is consistent across all three write-ups: the failures were not caused by the automation being wrong. They were caused by us being confident about something we had not checked.


    Editorial note. Written and edited by the operator of this site. Every 403, timing, and retry count described here is from our own logs for the week of 2026-09-08. Where we describe a mistake, we made it.

    Disclosure. This article contains no affiliate or sponsored links. This site may earn commissions from links in other articles, and those articles carry their own disclosure at the top. Nothing here is professional advice.

  • We built an 8-gate checker for AI-drafted articles. The first run failed our own live site.

    We built an 8-gate checker for AI-drafted articles. The first run failed our own live site.

    Google’s guidance on scaled content abuse draws the line in a place a lot of people misread. It is not about whether a machine wrote the draft. It is about whether a person actually exercised judgment before it went live. That distinction sounds reassuring until you try to prove it. What does “a person reviewed this” look like six months later, when the only evidence is that someone remembers skimming it?

    We run a small publisher where the drafts are machine-written and the decisions are not. We have written before about what breaks when you hand the work to agents, and this is the control we built afterward. In September we stopped relying on memory and built a checker: eight gates, every article, fail-closed. Nothing publishes unless all eight pass.

    Then we pointed it at our own live site, which had seven published articles on it. It failed six of them.

    What the eight gates check

    The gates are deliberately boring. Each one exists because something specific went wrong, either to us or to someone whose postmortem we read.

    G-1: every outbound link resolves. A HEAD request to each URL in the body. Link rot is slow and invisible, and a dead link in a comparison article is the reader’s last impression of you.

    G-2: every commercial link points to an approved program. The article can only link to advertisers we have an actual approved relationship with. On our English site, where we have no approved programs yet, this gate blocks commercial links entirely rather than trusting a future editor to remember that.

    G-3: no banned expressions. A pattern list: income promises, absolutes, manufactured urgency. More on how badly this one behaved in a moment.

    G-4: not a near-duplicate of anything already published. Sequence similarity against the live corpus, pulled from the CMS at check time rather than from a local index that drifts.

    G-5: commercial relationships are disclosed above the fold. Only fires when G-2 found a commercial link, and then it requires the disclosure inside the first 400 characters, not in a footer.

    G-6: editorial responsibility and disclaimer are both present. Who stands behind this, and what it is not.

    G-7: no generation artifacts. Stray model chatter that survived into the draft.

    G-8: dates are sane. A surprisingly common failure: an article published in 2026 that confidently discusses “this year’s” pricing from 2024.

    Terminal output of the publication gate run for this article, showing all eight checks passing and the post being published as post_id 70.
    The gate run that published this article. A single failure writes the post as a draft instead; there is no flag to skip a check.

    What the first run found

    Six of seven live articles failed, and the failures were not subtle.

    One article carried an income figure in its own title, a specific monthly yen amount presented as an outcome. It had been sitting on the front page for weeks. Nobody had decided to publish that claim; a draft had simply never been argued with.

    Three articles were near-duplicates of each other. Same topic, same structure, marginally different phrasing, all three indexed and competing with each other for the same query. That is the exact pattern the scaled-content guidance describes, and we had produced it without noticing, because each one looked fine on the day it was written.

    Seven of seven were missing any link to our own disclosure and editorial pages. Those pages had existed since June. Not one article pointed at them. Every individual article had been reviewed by a person; the property as a whole had been reviewed by nobody.

    Separately, and outside the gates, we found the sitemap had been frozen since June 9 and was serving eleven URLs that no longer existed.

    None of this required sophistication to find. It required something that looks at every article the same way every time, which is precisely the thing a human reviewer stops doing around article four.

    The gates were wrong before they were right

    The first version of G-3 was close to useless, and this is the part most write-ups skip.

    It flagged a Japanese headline meaning roughly “the fundamentals, so you don’t fail” as a no-risk claim. It is not a claim about risk; it is an ordinary way to title an explainer. It flagged an article about selling AI prompts because the word “prompt” appeared in our list of generation artifacts. It flagged the phrase “right now” in a sentence where nothing was being sold.

    On the English side the pattern list had the same disease in a different accent. Early drafts of the rules would have flagged a sentence stating there is real risk in unattended automation, because it contained the word risk near a negation. They would have flagged “the server costs twelve dollars a month” as a price claim and “the site earned zero” as an income claim. Those are the sentences we most want to be able to write.

    Table of three real false positives from the first rule set: a headline about fundamentals flagged as a no-risk claim, an article about selling AI prompts flagged as a model artifact, and a quoted reader sentiment flagged as urgency.
    Three real false positives from the first rule set. Every one of them is a sentence we wanted to be able to write.

    So we narrowed the rules and wrote tests that assert non-detection. Fifty tests now, and a meaningful share of them exist to prove the checker stays quiet on honest sentences. The fixtures are real headlines from our own deleted drafts, not invented examples.

    This matters more than the detections do. A checker with a high false-positive rate does not get tuned. It gets bypassed, and then it is worse than nothing, because the bypass becomes the habit and the green checkmark becomes a lie.

    Three test strings posted to the site: a plain greeting returns 201, while a comma-separated list inside quotation marks returns 403, blocked by an SQL injection firewall rule.
    A separate failure, found the same week: the host firewall read an ordinary English list as an SQL injection and refused three paragraphs of this article. Bisecting the post paragraph by paragraph isolated it. The full account of that week is here.

    What it does not do

    The gates cannot tell you whether an article is worth reading. They cannot detect a confident factual error, an outdated recommendation, or a comparison that quietly favors the advertiser paying the most. Every one of those still requires a person who knows the subject.

    What the gates do is clear the floor, so that the human review is spent on judgment instead of on checking whether the links work. That is the whole argument. Automated checks are not a substitute for editorial oversight; they are what makes the scarce oversight land somewhere useful. We learned how narrow that floor really is a few days later, when all eight gates passed three articles that reached readers as raw markup.

    If you want to build one

    You do not need a framework. Ours is a few hundred lines of Python using only the standard library, and it runs in under a minute against the live CMS.

    Start with the failure you have already had. If you have shipped a dead link, write G-1 first. Run it against your existing published articles before you write another word, because the archive is where the evidence is, and it will tell you which gates you actually need.

    Then make it fail closed, and resist the urge to add an override flag. The moment there is a documented way to skip the gates under deadline pressure, the gates describe your intentions rather than your output.

    We are about seven weeks into this and the checker has caught things a human review already passed, more than once. That is not an argument that the humans were careless. It is an argument that reading an article and auditing an article are different tasks, and only one of them gets harder the more articles you have.


    Editorial note. Written and edited by the operator of this site. Every figure here comes from our own systems and is reproducible from our logs. Where we describe a failure, we found it on our own property, not someone else’s.

    Disclosure. This article contains no affiliate or sponsored links. This site may earn commissions from links in other articles, and those articles carry their own disclosure at the top. Nothing here is professional advice.

  • We ran a business on seven AI agents for three months. Every failure had the same root cause.

    We ran a business on seven AI agents for three months. Every failure had the same root cause.

    We gave seven scheduled AI agents real jobs inside a small media business. One wrote articles. One logged KPIs every night. One produced a weekly board report. They ran on cron. Nobody watched them closely.

    Three months later the business had published 21 drafts we could not use, served Google 11 dead URLs, and earned zero revenue.

    This is the incident log, with the numbers. If you are putting agents into an operational loop, the specific failures matter more than the general advice, so they are all here.

    Failure 1: the article factory produced the same article five times

    The writing agent ran every morning at 9:00 and published directly. For weeks nobody read the output. When we finally audited it:

    • One title — a fiber-internet comparison — appeared five times, published on five different days
    • A second title appeared three times
    • Several posts linked to affiliate programs we had never been approved for
    • One post claimed an earnings figure with no basis behind it
    • One post’s title was literally the model’s own error message asking the operator a clarifying question

    Of 21 drafts sitting in the queue, zero were usable.

    The pipeline itself never broke. It ran on schedule every single day. Only the output was broken — which is precisely why nobody noticed.

    Failure 2: the morning agents ran two hours late, every day

    Two agents were scheduled for 6:10 and 6:45. We measured when they actually executed: 8:18 and 8:20.

    The cause was mundane. The scheduler only fires while the host application is running, and the machine was not awake at 6:00. Every morning the jobs queued and executed whenever the app opened.

    One of those agents was supposed to prepare a 7:30 posting slot. It never once made it in time. That slot stayed empty for three weeks.

    The same cause corrupted our measurement. The nightly KPI logger was scheduled for 22:00 — an hour when the machine was reliably awake — and it mostly ran. But across a 20-day window we had 12 days of data. Forty percent missing, and no alert, because a job that never runs produces no error.

    The lesson we took: you can only schedule automation for hours when the host is reliably available. For us that was night, not morning. Check your actual execution timestamps against your intended schedule. They may not match.

    Failure 3: two agents wrote to the same output

    One agent generated social posts. Another scheduled them. Both had been given permission to publish to the same channel.

    Neither malfunctioned. Each did its job correctly. The result was 1.25× the intended daily volume and the same link going out twice in one day.

    We fixed it by making the output single-writer: one agent owns the channel, every other agent may only deposit material for it. That rule has held since.

    Failure 4: four weeks of publishing that search engines never saw

    This one cost us the most and we found it last.

    We published an article and watched for a month. No impressions. We assumed it was ranking slowly — new sites take time. Then we inspected the URL directly in Search Console:

    Last crawl: N/A
    Referring sitemaps: none detected
    Referring pages: none detected

    Google had never crawled it. Not ranked low — never fetched.

    Three things had gone wrong at once, and none of them were visible from the admin dashboard:

    1. The sitemap had frozen three months earlier. It still advertised pages we had deleted — 11 URLs returning 404 — and listed none of the new ones.
    2. The new article had zero internal links pointing at it. No sitemap entry, no inbound link, no path in.
    3. The URL used non-ASCII characters, so every share turned into a wall of percent-encoding.

    Index coverage at that point: 9 pages indexed, 20 not indexed. The largest single reason for exclusion was “Not found (404)” — the dead URLs our own sitemap was handing to the crawler.

    All four had the same root cause

    Nobody looked at the output for long enough.

    That is the uncomfortable part. Automation removes the work, and it quietly removes the checking along with it. “It ran” and “it ran correctly” are different claims, and only the first one is self-reporting. The second requires a human to look.

    Failure 1 needed someone to read one published article. Failure 2 needed someone to compare a timestamp. Failure 3 was visible on the channel itself. Failure 4 needed one URL inspection. None of them required expertise. All of them required attention that the automation had implicitly retired.

    What we changed

    Checkpoint long jobs. An agent told to “research, write, and generate an image” in one run would die partway and leave nothing. We now save a skeleton first and append to it. A failed run leaves partial work instead of none.

    One writer per output. Any shared destination is owned by exactly one agent. Others deposit inputs. This single rule eliminated an entire class of collision.

    Machine checks before publication. We wrote a gate that runs eight checks before anything goes live: every link resolves, affiliate links point only to approved programs, no unsupported earnings claims, no near-duplicate of existing posts, required disclosures present, no model artifacts in the text, and dates that are internally consistent. It fails closed — a failed check means the post becomes a draft, not a publication. We have since written up each of those eight checks in detail, including the ones that were wrong when we first wrote them.

    We ran that gate against our existing published posts as a test. Every one of them failed. Problems the authors had not noticed were still sitting live on the site.

    A human still reads it. This is the one we were most tempted to skip, and the one that matters most. A machine check catches broken links and duplicates. It cannot judge whether a piece is worth reading. Google’s 2026 guidance draws the line in the same place: the question is not whether AI drafted the content, but whether a knowledgeable person reviewed it. Sites publishing unreviewed AI output at volume have reported traffic losses of 50–80%.

    The gate’s job, we decided, is not to approve content. It is to make sure only things worth a human’s attention reach a human.

    The volume question

    One more data point, because it cuts against the instinct.

    On a separate property we increased published articles from 13 to 23 in a single push. In the days immediately after, followers went down, from 18 to 17. What moved the number later was not volume — it was fixing the profile and the navigation, and publishing one piece on a topic nobody else was covering.

    Our most-read piece across everything we shipped was not the highest-volume output. It was the one thing we knew that others did not.

    If you are about to do this

    Decide your review cadence before you decide your automation scope. We did it in the other order and paid for three months.

    Concretely, from our own logs:

    • Compare intended schedule against actual execution timestamps, once
    • Open your sitemap in a browser and confirm your most recent post is listed
    • Inspect one published URL and check the “last crawl” field
    • Read one full piece of output per week, start to finish

    The fourth item is the one we skipped, and it was the one that would have caught everything.

    We are running the same system again with those changes in place. We will publish what happens, including the parts that do not work.


    Editorial note: written and fact-checked against our own operational logs and analytics. All figures are measured from our own properties, not estimates. Reviewed before publication by the site operator.

    Disclosure: this article contains no affiliate links. Where we recommend tools in future articles, we will state clearly whether we have a commercial relationship with them.