← All articles Agent infra

Your agent is verifying the wrong artifact

20 August 2026 · 6 min read

An agent that writes something and then reads it back has not verified the write. It has read a copy that other systems were free to edit on the way out.

This is a routine step in any autonomous loop. Deploy the page, fetch the URL, confirm it looks right, report success. The step is doing less than it appears to, and the limit is the number of places the agent is able to look.

Three addresses, one page

A single page exists in three forms at once. There is what the agent wrote, which lives in its own context and is the only copy it fully trusts. There is what is stored, which is what the platform actually kept. And there is what is served, which is what arrives after the edge, the cache and whatever turns markup into text for the model.

Most verification loops hold the first and the third. Everything that happens between them is compressed into one HTTP response, and a mismatch in that response has at least two causes that look identical: the write failed, or a layer in the middle changed something. An agent with two observations cannot separate them. It can only retry, which is the wrong move for one cause and harmless for the other, so it retries.

The extractor is not a window

The routine health check that opens this job fetched the OBTO homepage through a text extraction, the same shape of read an agent gets from most fetch tooling. It returned the canonical tag, every Open Graph and Twitter property, the full navigation, the pricing table, the FAQ and the footer.

It returned neither of the page’s two application/ld+json blocks. Both are in the source. Between them they carry four entities, including the Organization record with the company’s name, URL, logo and social profiles.

An agent asking “did my structured data deploy” through that path reads nothing and has no way to tell nothing-is-there from nothing-came-through. The extraction is not lying. It was built to return readable content, and a JSON-LD block is not readable content. It is simply not the instrument for the question being asked, and nothing in the response says so.

What the edge adds on the way out

The second read is the served HTML itself, and it is also not the stored page. A draft staged last week from a live article URL carries this, sitting just before the closing body tag:

<script>(function(){ ... window.__CF$cv$params={r:'a2b031acde2e811f', ... }; var a=document.createElement('script'); a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js'; ...})();</script></body>

That block appears in no source record. It is injected at the edge. The same page carries a second, quieter version of the same effect. The stored source links out to a web font with three ordinary <link> tags. A copy taken from the served page last week carried an inlined @font-face block pointing at a rewritten path instead, with those three tags gone, and stripping it back out took roughly nine kilobytes off the file.

Neither transformation is a bug. Both are the edge doing its job. They matter because an agent that templates a new page off a served URL, which is the obvious thing to do when the live page is right there, writes those artifacts back into storage permanently. The source then contains a challenge bootstrap and a rewritten font path that no author wrote, and the next agent to read that record inherits them as intent.

The count that only the record has

The OBTO blog index is one stored record. Read directly and counted, it contains 43 article cards in the grid and 42 BlogPosting entries in its structured-data feed. One article is present to a reader and absent to a crawler.

grep "Read article" → matchCount: 43 grep "@type": "BlogPosting" → matchCount: 42

Nothing about that page renders wrong. The grid lays out 43 cards. The JSON-LD parses cleanly and validates. Fetch the URL and every check an agent would plausibly run comes back green, because the defect is not inside either representation. It is in the relationship between two representations that are each internally fine.

Counting one against the other is the only way to see it, and that requires reading the artifact rather than the page. It had been sitting there for roughly three weeks.

What the third address has to be

What closes the gap is a third place to look, one that sits on neither the write path nor the serve path: an address for the stored artifact, resolvable at runtime, returning source rather than output.

On OBTO a page is a record with an id, so that address already exists. A read against it returns the source, the line count and the record id, and it does not traverse the edge. That is enough for an agent to hold all three observations at once and say which pair disagrees.

It is worth being precise about the claim, because the uninteresting version of it is that the page is a database record. That is a storage detail. The part that changes an agent’s behaviour is that the stored form is separately addressable from the served form, which is what turns “the fetch looked wrong” from a dead end into a diff with a named side.

The same reasoning runs through the rest of the loop. A write is safe to retry when the platform can confirm the content by hash rather than by the agent’s recollection of sending it. A failure is actionable when the response says what to do differently instead of only that something went wrong. And under a stateless contract, where no call inherits context from the last one, verification has to be re-derivable from addresses rather than remembered, which makes having a stable one for stored state load-bearing rather than convenient.

If you own the write path, the cheapest version of this is an id on the stored record and a read that bypasses the edge. The rest is one call away once that exists.

Frequently asked questions

Why isn’t fetching the published URL enough to verify a write?

A fetch measures the whole delivery chain, not the write. Between storage and the response sit the cache, the CDN or edge worker, and whatever converts markup into text for the model. Each can add, remove or rewrite content. When the fetch disagrees with intent, the cause is either a failed write or a transformation in the middle, and the response alone does not distinguish them.

What is the difference between the stored artifact and the served page?

The stored artifact is the source a platform kept. The served page is that source after every layer in the delivery path has had a turn. Common differences include injected security or analytics scripts, rewritten asset URLs, inlined font declarations, and minification. All are legitimate; none are in the source.

Why did a text extraction drop the JSON-LD?

Extraction tools return readable content, and a structured-data block is markup meant for machines rather than prose meant for people. It is usually discarded along with scripts and styles. The result reads as absence, which is indistinguishable from a block that failed to deploy unless the agent can also read the source.

What happens if an agent templates a new page off a live URL?

It copies the edge’s modifications into storage as though they were authored. A challenge bootstrap or a rewritten font path becomes part of the source record permanently, and the original markup it replaced is lost. If a served URL has to be used as a template, the extracted artifacts should be stripped and their absence asserted before anything is written back.

How do you catch a defect that renders correctly in every view?

By counting one representation against another rather than inspecting either alone. A grid of 43 cards and a structured-data feed of 42 entries are both valid on their own terms; only the comparison shows that one article is invisible to crawlers. Cross-representation checks of this kind need the source, because the served page contains just one side of the comparison.

More from the OBTO blog