A competitor snapshot is NOT a competitor diff

A competitor snapshot is NOT a competitor diff

Martin Shein · · 7 min read

I pulled 30 of a competitor's live ads last week. Not one of them disclosed what it cost to run. Then I checked the field that looked like it held the answer, and found it filled in on all 30, with the value negative one.

That is the whole problem with competitor reporting in one number.

The report that is confidently, invisibly wrong

Here is the failure I built this skill to stop, and it is not the one people expect.

The usual assumption is that competitor reports fail because they miss something. A pricing change slips past. An ad campaign runs for three weeks before anyone notices. Real, but fixable with more sources.

The failure that actually costs you is different. Someone writes a true description of what a competitor's pricing page says today. It is accurate. It is well sourced. And it is written in change-flavoured language, so the reader closes it believing something moved. Nothing moved. The page has said that since March.

Here is why that matters: nothing inside the report tells you which one you are holding. A re-snapshot and a real diff look identical on the page. Same headings, same confidence, same citations. You cannot catch it by reading harder, and neither can the person who wrote it.

So writing discipline is the wrong fix. If a claim can only be checked by the person making it, it will drift.

Make "changed" something the server says, not something you write

The reframe is small and it changes everything: stop asking a writer to assert that something changed, and make the substrate answer instead.

I store one page per competitor per surface in AgentDocs. Every week the scan writes the current state to that page. AgentDocs is content-addressed, so writing the exact same bytes back creates no new revision at all. Not a duplicate revision. Not a revision with an empty diff. Nothing.

A dark chalkboard showing two rows. The top row reads SAME BYTES with an arrow pointing to NO NEW REVISION. The bottom row reads REAL EDIT with an arrow pointing to NEW REVISION inside a solid gold box.

Let me show you exactly what that means in practice. I proved the sequence live before I built anything on it:

  • Week one, create the page. One revision.

  • Write byte-identical content again. The revision id does not move. updated_at does not move. The history still shows one revision.

  • Write content with three real edits. A new revision appears, carrying the parent revision's id.

  • Ask for the diff between them. Real unified diff, additions: 4, deletions: 3.

Now for the good part. "Nothing changed this week" stops being a judgement call and becomes a server-side fact, and it is free. There is no revision to point at, so there is no change to report. And when something did move, changed has to carry a revision pair and the add and delete counts with it. You cannot write that sentence without the evidence, because the evidence is what generates it.

That is the difference between a snapshot and a diff, and it is structural rather than editorial.

The numbers that are not numbers

But there is a catch, and it lives one layer down. Some of the values these sources return look like numbers and are not.

I ran 32 live reads across real ad libraries before authoring a line of the skill. Four things came back that would each put a confident wrong figure into a report:

Ad spend does not exist. Meta discloses spend only for political and social-issue ads. Across 30 commercial ads, the field was empty 30 times. So the skill never emits a spend figure, on any platform. Not estimated, not prefixed with a tilde. The honest answer is that nobody publishes it.

A filled field can still be empty. impressions_with_index was non-null on all 30 ads, which means a naive completeness check scores it fully available. Open it and every one contains an impressions index of negative one. A tool that trusts non-nullness reports minus one impressions as a real measurement.

A dark chalkboard showing a large empty white chalk rectangle labelled 30 slash 30 FILLED, with an arrow pointing right to a small solid gold box containing minus one.

Missing is not the same as zero. LinkedIn publishes flight dates and impressions only for ads targeted into the EU, under the DSA. Personio disclosed on 19 of 24 ads. Zapier disclosed on 0 of 24. So did n8n, which is a Berlin company, so it is not about where the advertiser is headquartered but about where each individual ad points. Read those blanks as zero and you have just reported that a company running 24 ads stopped advertising.

Page one is not the library. Meta returned 30 ads out of 377. Google returned 40 out of roughly 2,000. Both are recency-ordered samples, 8% and 2%. Subtract last week's page one from this week's page one and you manufacture ads disappearing that never stopped running. Either paginate to the end and pay for it, or label the output a sample and only claim appearances.

A dark chalkboard showing a small box labelled PAGE 1 nested inside a much larger box labelled 30 OF 377, with an arrow pointing right to a gold box reading PHANTOM CHURN.

Every one of those is a place where a report prints a number that reads as authoritative and is wrong. That is why the skill ships four enforcement scripts rather than four paragraphs of good intentions.

The competitor your ad reports cannot see

Here is the part I did not plan for.

Searching for "zapier alternative" costs one credit and returns ten results. Five of the top eight were other automation companies, publishing "best Zapier alternatives" articles to intercept exactly that search. Gumloop, Vellum, Composio, Lindy, Outfunnel. Second place went to a Reddit thread of customers explaining why they were leaving.

A dark chalkboard showing a row of exactly eight small squares, five of them filled solid gold and three left as empty white outlines, under the heading THEM ALTERNATIVE and above the caption 5 OF 8 ARE RIVALS.

No ad library will ever show you that. An ad library shows what a competitor pays for. A website diff shows what they say about themselves. Neither answers the question that actually decides your quarter, which is who is hunting their unhappy customers, and whether anyone is hunting yours. Companies that buy no ads at all are invisible to every ad-library read and fully visible here, for one credit.

The false negative I nearly logged myself

Bottom line, I should show you the mistake rather than the polish.

While probing Reddit, I asked for the comments on a thread using a permalink I had assembled from the post title. It returned HTTP 200, one credit charged, and an empty comment list. That is indistinguishable from a thread with no comments, and I very nearly wrote down that the endpoint returns nothing useful.

I re-ran it with the permalink the search response had already handed me. Ten comments, naming the exact products people switch to when they leave.

A 200 with an empty collection is the most convincing way to be wrong that I know of, and I walked into it inside the very tool built to prevent it. So it became a hard rule in the skill and one of its seven recorded learnings: never synthesise an identifier the API already gave you, and never record an absence without first proving you asked the right question.

What you get

@di-atomic/competitor-watch is live on the OPVS marketplace. It is guidance-only, so there is no backend to provision and nothing to wire up. Your agent reads it and runs the decision tree: a declared competitor set, weekly cross-surface diffs, five verdicts instead of two, and four scripts that print pass or fail with numbers you cannot argue with.

It will tell you what changed. Just as importantly, it will tell you when it could not look, instead of quietly calling that "no change".