<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Erik Rekola</title>
    <description>The latest articles on DEV Community by Erik Rekola (@erekola).</description>
    <link>https://dev.to/erekola</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3994164%2F3ba59e95-e771-458f-80bf-209fc1acd1b0.jpg</url>
      <title>DEV Community: Erik Rekola</title>
      <link>https://dev.to/erekola</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/erekola"/>
    <language>en</language>
    <item>
      <title>Agent commerce discovery: A2A, AP2, and ACP</title>
      <dc:creator>Erik Rekola</dc:creator>
      <pubDate>Thu, 30 Jul 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/turva-dev/agent-commerce-discovery-a2a-ap2-and-acp-5ddg</link>
      <guid>https://dev.to/turva-dev/agent-commerce-discovery-a2a-ap2-and-acp-5ddg</guid>
      <description>&lt;p&gt;Before an AI agent can transact with a site, it has to discover what the site supports and how to reach it. Three machine-readable surfaces carry that information: an A2A Agent Card, an AP2 declaration, and an ACP discovery document. Each answers a different question, and an agent reads them before it sends a single commerce request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The A2A Agent Card
&lt;/h2&gt;

&lt;p&gt;An A2A Agent Card is a JSON file, usually at /.well-known/agent-card.json, that describes an agent interface. It states the agent's name, version, and description, the interfaces it exposes, each with a service URL and a protocol binding, the capabilities it declares, and the skills it offers, each skill carrying an id, a name, and a description. The Agent2Agent protocol uses the card so one agent can discover another and know how to reach it.&lt;/p&gt;

&lt;p&gt;The card is most useful when its skills mirror surfaces an agent can already reach, such as a service catalog or contact information. A skill that points nowhere is worse than no skill at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  AP2 and the version that matters
&lt;/h2&gt;

&lt;p&gt;AP2 is the Agent Payments Protocol. Under the v0.1 specification, which is what deployed sites and scanners still validate against, a merchant declares support not as a separate file but as an extension entry inside the A2A Agent Card. The entry carries the extension URI, a role such as merchant, and a flag marking it required.&lt;/p&gt;

&lt;p&gt;The detail that trips people up is the URI. Some helper guides write it as github.com/google-agentic-commerce/AP2/tree/v0.1.0, with an uppercase name and a three-part version. The v0.1 specification uses github.com/google-agentic-commerce/ap2/tree/v0.1, lowercase, version v0.1. A scanner that validates against that specification rejects the uppercase form even when everything else is correct. Copy the URI from the spec, not from a fix message.&lt;/p&gt;

&lt;p&gt;Note that the current AP2 specification, v0.2 from April 2026, restructures the protocol around checkout and payment mandates and drops the Agent Card extension entirely. The deployed discovery convention and the scanners still follow v0.1, so publish the v0.1 declaration for discoverability today and expect this surface to change as v0.2 adoption arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  ACP discovery and checkout
&lt;/h2&gt;

&lt;p&gt;ACP is the Agentic Commerce Protocol, and it has two parts that are easy to confuse. The first is a discovery document at /.well-known/acp.json, which started as a proposal-stage RFC and entered the released specification with the 2026-04-17 snapshot. The second is the checkout API the document points to.&lt;/p&gt;

&lt;p&gt;The discovery document is small and strict. It states the protocol name acp and a version, the api_base_url, a transports array, and a capabilities.services array. The services value is a closed set of strings such as checkout, not a list of product objects. Sending the wrong type is the most common reason an otherwise complete document fails validation.&lt;/p&gt;

&lt;p&gt;A discovery check usually reads only the document, not the checkout endpoint behind it. That makes it tempting to declare a service the site does not implement, because the check passes either way. An agent that trusts the document and calls the checkout URL would then reach nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal honest checkout
&lt;/h2&gt;

&lt;p&gt;A checkout endpoint does not have to support instant payment to be real. The ACP checkout session carries a status field, and one of its values is not_ready_for_payment. A site that sells through a written quote can create a genuine session, return it in that state, and attach a message that the engagement is confirmed in writing first. The agent receives a well-formed session that reflects how the business actually works, and the discovery claim holds because the endpoint behind it answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publish what is true
&lt;/h2&gt;

&lt;p&gt;These surfaces exist so an agent can act without guessing, which only holds when every claim resolves to something real. A card whose skills lead nowhere breaks the same way a checkout that never responds does, because the agent follows the signal and finds nothing. Publish what is true, and back each declaration with a surface that answers.&lt;/p&gt;

&lt;p&gt;turva.dev publishes an A2A Agent Card, an AP2 merchant declaration, and an ACP discovery document with a working checkout endpoint, verified by an independent scanner. For an audit of a site's agent commerce surface, contact &lt;a href="mailto:info@turva.dev"&gt;info@turva.dev&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/x402-agent-payments" rel="noopener noreferrer"&gt;x402 and agent payments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/well-known-for-agents" rel="noopener noreferrer"&gt;The /.well-known directory for agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://turva.dev/guides/agent-commerce-discovery" rel="noopener noreferrer"&gt;https://turva.dev/guides/agent-commerce-discovery&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>ecommerce</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A red reading that measured my own client</title>
      <dc:creator>Erik Rekola</dc:creator>
      <pubDate>Thu, 30 Jul 2026 05:52:00 +0000</pubDate>
      <link>https://dev.to/turva-dev/a-red-reading-that-measured-my-own-client-1le5</link>
      <guid>https://dev.to/turva-dev/a-red-reading-that-measured-my-own-client-1le5</guid>
      <description>&lt;p&gt;I posted yesterday about checks that keep passing after they have stopped measuring the right thing. The next reading I took was the same defect turned around. A request against my own MCP server came back red, and the red was about my request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two responses that agreed on the wrong story
&lt;/h2&gt;

&lt;p&gt;I was working on the README for that server and sent a request at the endpoint to see what it was serving. The first was server/discover, which the 2026-07-28 revision of the Model Context Protocol requires every server to implement. It answered -32601 Method not found. So I sent initialize with protocol version 2025-06-18, the handshake that revision removes, and it answered 200 with the server's identity.&lt;/p&gt;

&lt;p&gt;Taken at face value that is a server that never migrated. The required new request is missing and the removed handshake still works. The migration had gone out that morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The server was right and my request was not
&lt;/h2&gt;

&lt;p&gt;Reaching the new lane takes three things at once, and I had none of them. The request has to carry the header MCP-Protocol-Version: 2026-07-28. It has to carry an Mcp-Method header naming the same method as the body. And params._meta has to carry the keys io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities. Send none of that and the request is not malformed. It is a legacy request, and the library's compatibility lane answers it correctly, because answering it correctly is what the lane exists to do.&lt;/p&gt;

&lt;p&gt;That is the part worth stopping on. A compatibility lane is built to be invisible, which is right for a client and wrong for anyone taking a measurement. When the old lane answers politely, "this server does not implement the new revision" and "my client did not ask for the new revision" arrive looking the same. Neither response carries the thing that would separate them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cheap test
&lt;/h2&gt;

&lt;p&gt;There is one and it costs a single request. If initialize answers, you are on the legacy lane, and whatever you conclude next is a statement about your client. A server serving 2026-07-28 does not offer initialize on the new lane at all, so a working handshake is evidence about the caller.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the four requests returned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Without the version header, server/discover returns HTTP 200, framed as an event stream, carrying -32601 Method not found. The same request sent as initialize with protocol version 2025-06-18 returns 200 and the server's identity.&lt;/li&gt;
&lt;li&gt;With the version header and no _meta, HTTP 400 and -32602, and the message says the header names revision 2026-07-28 while the request is missing the required per-request envelope key _meta.&lt;/li&gt;
&lt;li&gt;With the header and a _meta whose keys are spelled protocol-version and client-capabilities, HTTP 400 and -32602 again, and this time the message lists the two names the server does want.&lt;/li&gt;
&lt;li&gt;With all three in place, HTTP 200, supportedVersions holding 2026-07-28 alone, capabilities.tools.listChanged true, resultType complete, ttlMs 3600000, cacheScope public, and the server identity in result._meta.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;p&gt;This is not a defect in the SDK and not a gap in the spec. The compatibility lane is a deliberate decision and the right one, and being invisible is the whole point of it. The trap is downstream, in what a reader does with a response that arrives clean.&lt;/p&gt;

&lt;p&gt;It is also not a story where the information was missing. Read the second and third rows again. The server named the envelope key I had left out, then the two keys I had misspelled. The correct request was written inside the failures from the moment the version header went on, and I read the first response as a verdict instead of reading the rest as instructions.&lt;/p&gt;

&lt;p&gt;Rechecking all of this turned up one more of my own. A note of mine says this endpoint refuses anything that does not accept both JSON and an event stream. That holds on the legacy lane. On the new one a plain JSON Accept is answered, and so is a request with no Accept header at all. I had written down the behaviour of the lane I happened to be standing in and filed it as the rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape, both times
&lt;/h2&gt;

&lt;p&gt;Yesterday it was a green check reading a lane the server no longer serves. Today it was a red reading of a lane my client never asked for. A response tells you what happened and you supply what it means, and the supplying is where it goes wrong. It goes wrong quietly, because a clean status code and a well-formed error object look like a measurement.&lt;/p&gt;

&lt;p&gt;So the habit, for anything with a compatibility layer beneath it. Before reading the result, work out which lane answered. If the protocol will not tell you, put the discriminator in the request yourself and check that the answer moves.&lt;/p&gt;

&lt;p&gt;None of this is visible to an agent-readiness scanner. Scanners do not speak MCP, so a passing readiness level says nothing about whether an MCP endpoint serves the current revision. That one belongs to whoever runs the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://turva.dev/blog/checks-that-pass-for-the-wrong-reason" rel="noopener noreferrer"&gt;The checks that pass for the wrong reason&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/mcp-server-card" rel="noopener noreferrer"&gt;MCP server card&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/measurement-led-agent-readiness" rel="noopener noreferrer"&gt;Measurement-led agent readiness&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Serving markdown to agents</title>
      <dc:creator>Erik Rekola</dc:creator>
      <pubDate>Wed, 29 Jul 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/turva-dev/serving-markdown-to-agents-5ak</link>
      <guid>https://dev.to/turva-dev/serving-markdown-to-agents-5ak</guid>
      <description>&lt;p&gt;An HTML page is built for a browser, and an agent that reads it pays for all the markup, scripts, and layout it does not need. Serving a markdown version of the same page gives an agent the content without the wrapper, which is both cheaper and less error-prone.&lt;/p&gt;

&lt;p&gt;The mechanism is content negotiation. An agent sends an Accept header asking for text/markdown, and the server returns the markdown form of the page at the same URL. A site can also publish llms-full.txt, a single file that bundles the whole site as text, so an agent can read everything in one request instead of fetching many pages.&lt;/p&gt;

&lt;p&gt;The saving is large. On turva.dev the markdown form of a page costs a fraction of the tokens the HTML would, and the difference decides whether an agent reads a page in full or truncates it halfway. A model that runs out of budget on markup is a model that answers from a partial reading.&lt;/p&gt;

&lt;p&gt;Markdown delivery is not a separate site, it is the same content offered in a second form. The page stays as it is for people, and an agent that asks for text gets text. Paired with a clear llms.txt that lists where the content lives, it makes a site fast and reliable to read at machine speed.&lt;/p&gt;

&lt;p&gt;turva.dev serves markdown on request and publishes llms.txt and llms-full.txt. For an audit of a site's content surface for agents, contact &lt;a href="mailto:info@turva.dev"&gt;info@turva.dev&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/llms-txt" rel="noopener noreferrer"&gt;llms.txt explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/response-headers-for-agents" rel="noopener noreferrer"&gt;Response headers that help agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/prerendering-for-agents" rel="noopener noreferrer"&gt;Prerendering and why agents see empty pages&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://turva.dev/guides/markdown-for-agents" rel="noopener noreferrer"&gt;https://turva.dev/guides/markdown-for-agents&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>The checks that pass for the wrong reason</title>
      <dc:creator>Erik Rekola</dc:creator>
      <pubDate>Wed, 29 Jul 2026 05:50:43 +0000</pubDate>
      <link>https://dev.to/turva-dev/the-checks-that-pass-for-the-wrong-reason-2ckl</link>
      <guid>https://dev.to/turva-dev/the-checks-that-pass-for-the-wrong-reason-2ckl</guid>
      <description>&lt;p&gt;The Model Context Protocol cut revision 2026-07-28 on 28 July. I read the released tree that evening, the way I read any spec I am about to depend on, and the finding was not in the prose. It was in the step that produced the release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thirteen links that resolved
&lt;/h2&gt;

&lt;p&gt;Cutting a revision copies the living draft tree into a dated one, and every reference inside the copy has to be dated with it. That is the whole promise a dated snapshot makes. Reading the 2026-07-28 tree a year from now should give what it gives today.&lt;/p&gt;

&lt;p&gt;The promote step rewrote one link pattern when it copied the spec tree, and that part worked. Two other patterns were not covered. Three links still pointed at the draft schema, including the one whose own next line calls it the source of truth for all protocol messages and structures. Nine pointed at the draft docs tier, eight of those at the security best practices page. The changelog's compare link was the thirteenth.&lt;/p&gt;

&lt;p&gt;Not one of them was broken. Every one resolved, just to the living tier rather than the frozen one, which is presumably why a preview check did not catch them. The reader who follows a link labelled 2026-07-28 in a year gets whatever the draft has become by then, and nothing anywhere reports a problem.&lt;/p&gt;

&lt;p&gt;I filed it with the line numbers and the two greps I had run to find them. It was closed the same day. The fixed release carries thirteen corrected links, and the promote step now runs all three rewrites over all three copied trees with the changelog compare pinned to the new tag. The second half is the half that matters. A file fix is good for one revision and a workflow fix is good for every revision after it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same shape, one day later, in my own gate
&lt;/h2&gt;

&lt;p&gt;The revision is a large breaking one. Sessions are gone, the initialize handshake is gone, the protocol is stateless, and there is a new server discovery request that servers must implement. Both dependencies my own MCP server needed went stable within two days of the cut, so I moved it over.&lt;/p&gt;

&lt;p&gt;My site has a verification script that proves the signed MCP server card describes the server that is actually running. It exists because a card once declared capabilities the server did not implement, for weeks, invisibly. The way it proved parity was to run the initialize handshake against the live server and compare what came back.&lt;/p&gt;

&lt;p&gt;The new revision has no initialize. The library keeps a compatibility lane that still answers it.&lt;/p&gt;

&lt;p&gt;So the gate would have kept passing. It would have reported the same clean count it always reports, and it would have been measuring the old lane while the new one went untested. Not a failure, not a warning, just a number that no longer meant what it said. I rewrote the parity block around the new discovery request before the migration shipped rather than after, and added two checks the old transport had no equivalent for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the two have in common
&lt;/h2&gt;

&lt;p&gt;Both are the same defect wearing different clothes. In the release it was links that resolve, so nothing is broken. In my gate it was a check that passes, so nothing is red. In both cases the text and the tooling were fine in isolation and the thing between them had quietly stopped being true.&lt;/p&gt;

&lt;p&gt;The practical version, for anyone maintaining a checker: a gate that only fails on breakage cannot tell you that it has started measuring the wrong thing. When the thing under test changes shape, read what your own check actually speaks to. Mine was one line of code and a protocol version, and I would not have found it by running it.&lt;/p&gt;

&lt;p&gt;The lesson I took from reading three specs this way is narrower than I expected. On a mature spec, the prose has been over hundreds of eyes and the review process works. The publishing machinery around it has been over far fewer. That is where a fresh reader still has something to contribute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/mcp-server-card" rel="noopener noreferrer"&gt;MCP server card&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/measurement-led-agent-readiness" rel="noopener noreferrer"&gt;Measurement-led agent readiness&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/blog/honesty-and-the-checker" rel="noopener noreferrer"&gt;Honesty and the checker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Response headers that help agents</title>
      <dc:creator>Erik Rekola</dc:creator>
      <pubDate>Tue, 28 Jul 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/turva-dev/response-headers-that-help-agents-5eh1</link>
      <guid>https://dev.to/turva-dev/response-headers-that-help-agents-5eh1</guid>
      <description>&lt;p&gt;Response headers are the metadata a server sends with every page, and the right ones let an AI agent work without parsing the full HTML. They are the cheapest place to make a site more legible to automated clients, because an agent reads them before it reads the body.&lt;/p&gt;

&lt;p&gt;A Link header can point an agent straight at a site's machine-readable resources, such as an API catalog or a markdown version of the page, so the agent finds them without crawling. A Vary header that includes Accept tells caches and agents that the site can return different formats for the same URL, which is what makes markdown content negotiation reliable. RateLimit and RateLimit-Policy headers let a well-behaved agent throttle itself instead of guessing, though as of July 2026 their IETF draft, revision 11 from May 2026, remains active without yet becoming a standard. Content-Language and a clean content type remove ambiguity about what the agent is reading.&lt;/p&gt;

&lt;p&gt;The reason headers matter is order. An agent fetches the response, reads the status and headers first, and decides what to do next from them. If the headers already say where the structured data is and what formats are available, the agent can skip the expensive step of parsing a page built for human display.&lt;/p&gt;

&lt;p&gt;Headers are easy to get wrong in ways that hurt agents. A missing Vary header breaks content negotiation. A Cache-Control immutable directive set on the wrong response can stop an agent from seeing an update. The fix is usually small and lives at the edge, which on turva.dev is a Cloudflare Worker that sets these headers on every response.&lt;/p&gt;

&lt;p&gt;For an audit of a site's response and discovery surface, contact &lt;a href="mailto:info@turva.dev"&gt;info@turva.dev&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/markdown-for-agents" rel="noopener noreferrer"&gt;Serving markdown to agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/prerendering-for-agents" rel="noopener noreferrer"&gt;Prerendering and why agents see empty pages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/well-known-for-agents" rel="noopener noreferrer"&gt;The /.well-known directory for agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://turva.dev/guides/response-headers-for-agents" rel="noopener noreferrer"&gt;https://turva.dev/guides/response-headers-for-agents&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>The /.well-known directory for agents</title>
      <dc:creator>Erik Rekola</dc:creator>
      <pubDate>Mon, 27 Jul 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/turva-dev/the-well-known-directory-for-agents-4cb7</link>
      <guid>https://dev.to/turva-dev/the-well-known-directory-for-agents-4cb7</guid>
      <description>&lt;p&gt;The /.well-known directory is a standard place at the root of a site where agents look for machine-readable descriptions of what the site offers. Instead of crawling pages and guessing, an agent fetches a predictable path and reads a manifest that points it to everything else.&lt;/p&gt;

&lt;p&gt;The idea comes from a long-standing web convention and now carries the files agents care about. An API catalog at a well-known path, defined by RFC 9727, lets an agent enumerate a site's public APIs from a single URL. A server card describes an MCP server and how to reach it. OAuth metadata describes how to authenticate. Payment and agent-payment manifests describe how to transact. security.txt says where to report a problem.&lt;/p&gt;

&lt;p&gt;The value is that discovery becomes a lookup rather than a search. An agent that knows the convention can ask one predictable question and get a map, which is faster and far more reliable than inferring structure from rendered HTML. A site that publishes a complete well-known surface is announcing its capabilities in the language agents already speak.&lt;/p&gt;

&lt;p&gt;A missing or thin well-known directory does not break a site for people, but it leaves an agent to guess, and most agents will simply move on. Publishing the manifests an agent expects is the difference between a capability that exists and a capability an agent can find.&lt;/p&gt;

&lt;p&gt;turva.dev publishes an API catalog, a server card, OAuth metadata, payment manifests, and a security contact under /.well-known. For an audit of a site's discovery surface, contact &lt;a href="mailto:info@turva.dev"&gt;info@turva.dev&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/mcp-server-card" rel="noopener noreferrer"&gt;MCP server cards explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/agent-authentication" rel="noopener noreferrer"&gt;How agents authenticate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/sitemaps-and-robots-for-agents" rel="noopener noreferrer"&gt;Sitemaps, robots.txt and agent access&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://turva.dev/guides/well-known-for-agents" rel="noopener noreferrer"&gt;https://turva.dev/guides/well-known-for-agents&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>Open Knowledge Format (OKF) explained</title>
      <dc:creator>Erik Rekola</dc:creator>
      <pubDate>Sun, 26 Jul 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/turva-dev/open-knowledge-format-okf-explained-4i8b</link>
      <guid>https://dev.to/turva-dev/open-knowledge-format-okf-explained-4i8b</guid>
      <description>&lt;p&gt;The Open Knowledge Format is an open specification from Google Cloud that represents a body of knowledge as a directory of plain markdown files. Each concept file carries a small block of YAML frontmatter and a free-form body. The goal is a portable way to hand an AI agent the context it needs, readable by a person and parseable by a machine, with no SDK and no catalog to lock into. Google Cloud published it in June 2026 as version 0.1.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an OKF bundle contains
&lt;/h2&gt;

&lt;p&gt;A bundle is a folder of markdown files, and the unit inside it is a concept. A concept is anything worth capturing for an agent: a table, a dataset, a metric, a runbook, an API. Every concept is one UTF-8 markdown document with two parts. A YAML frontmatter block at the top, fenced by a line of three dashes above and below, and a markdown body underneath.&lt;/p&gt;

&lt;p&gt;The format asks for exactly one field, type. Everything else is optional, including title, description, resource, tags and a timestamp. What types exist and what fields each carries is left to whoever produces the bundle. Concepts reference each other with ordinary markdown links, so the folder becomes a graph of related knowledge rather than a flat list of files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structural interoperability, not yet semantic
&lt;/h2&gt;

&lt;p&gt;Version 0.1 fixes a small set of things and leaves the rest open. It fixes the shape of a bundle as a folder of markdown files, the YAML frontmatter, two reserved filenames and the single required field. That is structural interoperability: any tool can open a bundle and know where the pieces are.&lt;/p&gt;

&lt;p&gt;What it does not fix is meaning. The format does not say what a metric concept must contain, or how two producers should agree on the same field names. That is semantic interoperability, and version 0.1 leaves it to producers and to conventions that have not been written yet. This is the line to keep in mind when reading the announcements around OKF. It standardizes the shape of the files, not yet what the files mean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where OKF fits with agent-readiness
&lt;/h2&gt;

&lt;p&gt;Agent-readiness, the kind measured by an independent scanner, is about whether an agent can reach and read your public site at all. OKF sits next to that, one layer in. It is a way to package the internal knowledge an agent works from once it is past the front door: the catalog, the metrics and the rules a decision depends on.&lt;/p&gt;

&lt;p&gt;So OKF is not a replacement for an llms.txt or a markdown surface on your site. It is the same instinct, plain text an agent can read without a special client, applied to the data and context behind the site rather than the pages in front of it. For a team thinking about what an agent acts on, not only what it can see, that is the part of the picture OKF addresses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do with it today
&lt;/h2&gt;

&lt;p&gt;OKF is new and small, version 0.1, and the semantic half is still open. That makes it worth understanding now and worth watching, but early to build an entire knowledge catalog on. If you already serve markdown to agents and keep an llms.txt, you have the instinct OKF formalizes, and adopting it later will be a short step rather than a rebuild.&lt;/p&gt;

&lt;p&gt;For an audit of how legibly AI agents can read your site and the data behind it, contact &lt;a href="mailto:info@turva.dev"&gt;info@turva.dev&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/llms-txt" rel="noopener noreferrer"&gt;llms.txt explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/markdown-for-agents" rel="noopener noreferrer"&gt;Serving markdown to agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/letting-agents-act-on-data" rel="noopener noreferrer"&gt;Letting agents act on data: the decision envelope&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://turva.dev/guides/open-knowledge-format" rel="noopener noreferrer"&gt;https://turva.dev/guides/open-knowledge-format&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>JSON-LD and structured data for agents</title>
      <dc:creator>Erik Rekola</dc:creator>
      <pubDate>Sat, 25 Jul 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/turva-dev/json-ld-and-structured-data-for-agents-e5p</link>
      <guid>https://dev.to/turva-dev/json-ld-and-structured-data-for-agents-e5p</guid>
      <description>&lt;p&gt;JSON-LD is a block of structured data in a page that states facts in a form a machine can read without interpreting prose. It tells an agent what the page is about, who runs it, what it sells, and at what price, as data rather than as sentences an agent has to parse and might misread.&lt;/p&gt;

&lt;p&gt;A human reads a price from a layout and a currency symbol. An agent reading raw HTML has to guess which number is the price and which is a shipping estimate. A JSON-LD Offer with a price and a currency removes the guess. The same applies to the organization behind a site, the services it offers, and the questions it answers, each expressed as a typed object an agent can rely on.&lt;/p&gt;

&lt;p&gt;Structured data also connects a page to the wider graph an agent builds. Declared types such as Organization, Service, FAQPage, and Article let an agent place a page in context and decide whether to trust and cite it. A page that states its facts as data is easier for a model to summarize correctly and to attribute.&lt;/p&gt;

&lt;p&gt;The cost of getting it wrong is silent. An agent does not report that it failed to parse a price, it just acts on a worse guess. Clean JSON-LD is one of the cheapest ways to make a page legible, and it sits in the same family as the response headers and well-known manifests an agent reads first.&lt;/p&gt;

&lt;p&gt;turva.dev declares JSON-LD for its organization, the person behind it, its services, and its guides, and the next scan reads the structured data as present. For an audit of a site's structured data, contact &lt;a href="mailto:info@turva.dev"&gt;info@turva.dev&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/get-cited-by-ai-assistants" rel="noopener noreferrer"&gt;How to get your site cited by AI assistants&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/llms-txt" rel="noopener noreferrer"&gt;llms.txt explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/well-known-for-agents" rel="noopener noreferrer"&gt;The /.well-known directory for agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://turva.dev/guides/json-ld-structured-data" rel="noopener noreferrer"&gt;https://turva.dev/guides/json-ld-structured-data&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What agents.json is</title>
      <dc:creator>Erik Rekola</dc:creator>
      <pubDate>Fri, 24 Jul 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/turva-dev/what-agentsjson-is-42ea</link>
      <guid>https://dev.to/turva-dev/what-agentsjson-is-42ea</guid>
      <description>&lt;p&gt;agents.json is a machine-readable file that declares what an AI agent can do on a site and how. Where llms.txt tells an agent what the site contains, agents.json describes the actions and endpoints an agent is allowed to use, so an automated client can move from reading to doing without a human wiring it up.&lt;/p&gt;

&lt;p&gt;The file lists the operations a site exposes to agents, often pointing at an OpenAPI description or specific endpoints, along with the authentication an agent needs. An agent reads it, learns which actions exist, and calls them within the rules the site sets.&lt;/p&gt;

&lt;p&gt;The reason it matters is that most sites expose actions only through a human interface, a form or a checkout flow that a person clicks through. An agent cannot reliably reverse-engineer that. A declared action surface removes the guesswork and turns a site from something an agent can read into something an agent can operate.&lt;/p&gt;

&lt;p&gt;agents.json sits beside the other declarations an agent looks for. An MCP server card describes a site's MCP server, an API catalog lists endpoints, and OAuth discovery describes how to authenticate. Each one removes a guess, and together they let an agent act on a user's behalf safely.&lt;/p&gt;

&lt;p&gt;A site does not need agents.json to be readable, but it needs something like it to be operable. The specification itself has stayed at version 0.1.0 since early 2025 and the ecosystem's momentum has moved to MCP and newer discovery surfaces, so treat agents.json as one declaration pattern rather than a settled standard. If the goal is for agents to complete tasks rather than just summarize the page, declaring the action surface is the step that makes that possible.&lt;/p&gt;

&lt;p&gt;For an audit of a site's capability and action surface, contact &lt;a href="mailto:info@turva.dev"&gt;info@turva.dev&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/mcp-server-card" rel="noopener noreferrer"&gt;MCP server cards explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/agent-authentication" rel="noopener noreferrer"&gt;How agents authenticate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/well-known-for-agents" rel="noopener noreferrer"&gt;The /.well-known directory for agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://turva.dev/guides/agents-json" rel="noopener noreferrer"&gt;https://turva.dev/guides/agents-json&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Agentic Resource Discovery and ai-catalog.json</title>
      <dc:creator>Erik Rekola</dc:creator>
      <pubDate>Thu, 23 Jul 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/turva-dev/agentic-resource-discovery-and-ai-catalogjson-3c9c</link>
      <guid>https://dev.to/turva-dev/agentic-resource-discovery-and-ai-catalogjson-3c9c</guid>
      <description>&lt;p&gt;Agentic Resource Discovery, or ARD, is an open specification for telling AI agents what a site offers, in one machine-readable file. Instead of inferring from pages whether a site has an MCP server, an agent interface, or an API, the site publishes a single index that names each resource and where to reach it. The specification appeared in 2026, is licensed under Apache 2.0, and builds on the AI Catalog data model maintained by a working group under the Linux Foundation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;A site advertises its agentic resources by serving a static JSON manifest at /.well-known/ai-catalog.json. The manifest is a small envelope with a specVersion (turva.dev publishes 1.0), a host block that names the operator, and an entries array. Each entry describes one resource with a stable identifier, a display name, a type, a url, and a short description. A resource can be an MCP server, an A2A agent, an API, or a skill set. A registry can crawl published catalogs and answer a capability query by pointing an agent at the right resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it sits
&lt;/h2&gt;

&lt;p&gt;ARD is a discovery layer, not a transport. It helps an agent find the right resource, which the agent then calls through that resource's own protocol, whether MCP, A2A, or a plain API. Discovery comes first and invocation second. The catalog does not replace the manifests it points to, it indexes them, so a site keeps its server card, its agent card, and its OpenAPI description, and adds one file that ties them together.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it relates to llms.txt
&lt;/h2&gt;

&lt;p&gt;An ai-catalog.json is not a ranking trick and it is not a content map. llms.txt tells an agent where a site's content lives. An ai-catalog tells an agent which agentic resources the site exposes and how to reach them. The two are complementary, and neither is about search ranking. Google has said publicly that llms.txt does not affect its search results, which is the same point agent-readiness has always made. These files are for agents that read and act.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it matters
&lt;/h2&gt;

&lt;p&gt;Adoption is early. In a June 2026 check I ran against their public well-known paths, none of the companies named as contributors to the specification yet served a discoverable ai-catalog.json, so publishing one now is a forward move rather than table stakes. The value is the same as every other discovery surface. A capability an agent cannot find is a capability that does not exist for that agent, and one predictable file turns a set of separate manifests into a single answer.&lt;/p&gt;

&lt;p&gt;turva.dev serves an ai-catalog.json at /.well-known/ai-catalog.json that indexes its MCP server, its A2A agent, its API, and its agent skills, each of which already resolves on its own. For an audit of a site's discovery surface, contact &lt;a href="mailto:info@turva.dev"&gt;info@turva.dev&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/well-known-for-agents" rel="noopener noreferrer"&gt;The /.well-known directory for agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/mcp-server-card" rel="noopener noreferrer"&gt;MCP server cards explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/agent-commerce-discovery" rel="noopener noreferrer"&gt;Agent commerce discovery: A2A, AP2, and ACP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://turva.dev/guides/agentic-resource-discovery" rel="noopener noreferrer"&gt;https://turva.dev/guides/agentic-resource-discovery&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Finishing the optional commerce checks</title>
      <dc:creator>Erik Rekola</dc:creator>
      <pubDate>Wed, 22 Jul 2026 13:07:11 +0000</pubDate>
      <link>https://dev.to/turva-dev/finishing-the-optional-commerce-checks-521n</link>
      <guid>https://dev.to/turva-dev/finishing-the-optional-commerce-checks-521n</guid>
      <description>&lt;p&gt;Agent-native payments are moving from proposal to plumbing. This summer the x402 protocol became a Linux Foundation project, with Visa, Mastercard, Google and Stripe among its founding members. The chain being built runs from discovery to transaction, which means a site's payment surface has to be something an agent can find and read before any money moves.&lt;/p&gt;

&lt;p&gt;isitagentready.com scores that surface in its Commerce category. The category is optional and does not change the overall score, so turva.dev already read 100/100 and Level 5 with three of the five commerce checks green. Two were still red, x402 and MPP. This is the log of taking them green and leaving settlement exactly where it was.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the scanner actually checks
&lt;/h2&gt;

&lt;p&gt;The two checks failed for a plain reason, and not the one an earlier note had assumed. The scanner exposes its own audit detail, so you can see what it fetches. For MPP it reads /openapi.json and looks for the payment fields directly on each payable operation. turva had declared them inside an offers array, a form the payment-discovery draft allows and this scanner does not read. Flattening the declaration to the single-offer form, same Stripe charge and same price, was all it needed.&lt;/p&gt;

&lt;p&gt;For x402 the scanner requests /api and expects an HTTP 402 with payment terms. turva served an open index at /api and a dedicated 402 endpoint elsewhere, so the probe saw a 200 and stopped. The fix was to make /api itself answer with the canonical x402 402, backed by a real wallet on Base, and to move the free endpoint index to /api/v1. The 402 is a genuine challenge, and the free discovery surfaces stay open at /openapi.json, under /.well-known, and at /api/v1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Settlement did not change
&lt;/h2&gt;

&lt;p&gt;What did not move is the part worth stating plainly. turva does not auto-settle. The 402 challenge and the OpenAPI discovery both declare a real payment surface, priced in USDC on Base and payable by card through Stripe, but the money is confirmed out of band rather than taken by the site. An agent can discover the offer and read the terms. A person or an agent then completes the purchase, and scope for bespoke work is agreed in writing first. A green check here says the surface is present and correct. It does not say the site quietly charged anyone.&lt;/p&gt;

&lt;p&gt;An earlier post left these two checks red, on the reasoning that satisfying them meant disturbing surfaces that were already correct. Reading the scanner's probe more closely showed a cleaner path, so the position changed. That is the use of measuring against a tool you do not control. It shows you where you were wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The services, payable now
&lt;/h2&gt;

&lt;p&gt;The three fixed offerings are live on Stripe, prices ex VAT:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://buy.stripe.com/aFadR995G5xV6hi43N5EY00" rel="noopener noreferrer"&gt;Agent-readiness audit, €6,500&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://buy.stripe.com/7sYcN5eq04tRfRSeIr5EY01" rel="noopener noreferrer"&gt;Monthly advisory, €3,000, minimum three months&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://buy.stripe.com/6oUaEX81C0dBfRSbwf5EY02" rel="noopener noreferrer"&gt;Implementation day, €1,500&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The audit is fixed scope and can be paid up front. Advisory and implementation start with a short written exchange to set scope, then run the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule, again
&lt;/h2&gt;

&lt;p&gt;A green check is worth something only when it reflects what an agent actually finds. Commerce is optional, so none of this moved the headline number. It moved whether the payment surface is real and discoverable for the moment agents start to pay, which is the part that will matter. The worker that produces these results is open source at github.com/erekola/turva-worker, readable line by line.&lt;/p&gt;

&lt;p&gt;For an agent-readiness audit that reports measured results, contact &lt;a href="mailto:info@turva.dev"&gt;info@turva.dev&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/x402-agent-payments" rel="noopener noreferrer"&gt;x402 and agent payments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/agentic-commerce-readiness" rel="noopener noreferrer"&gt;Agentic commerce readiness&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The twin is the page</title>
      <dc:creator>Erik Rekola</dc:creator>
      <pubDate>Wed, 22 Jul 2026 13:06:14 +0000</pubDate>
      <link>https://dev.to/turva-dev/the-twin-is-the-page-4ndo</link>
      <guid>https://dev.to/turva-dev/the-twin-is-the-page-4ndo</guid>
      <description>&lt;p&gt;Every page of this site lives as a markdown string inside the Worker's source file. For the guides and the blog posts that has always been literal. The Worker renders those pages from the markdown at request time, and a client that asks for text/markdown gets the same string untouched. Ten pages worked differently. The homepage, the services page, the validator and the other card-style pages were hand-written HTML, and the markdown lived beside them as a twin. Same content, two homes.&lt;/p&gt;

&lt;p&gt;Two homes means every edit happens twice, and sooner or later one home gets the edit and the other does not. I knew that when I chose the layout. For a while the honest description of the arrangement was a sentence I never liked writing: a checker keeps the pairs in sync, and I am not sure that is the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six drifts nobody saw
&lt;/h2&gt;

&lt;p&gt;The checker earned its keep before it retired. A parity gate went into the deploy checks the day before this conversion, comparing each hand-written page against its twin paragraph by paragraph. In its short life it found six real drifts, wording that had quietly diverged between the HTML and the markdown. None of them were visible by reading. I had read those pages many times.&lt;/p&gt;

&lt;h2&gt;
  
  
  One home for every sentence
&lt;/h2&gt;

&lt;p&gt;This week the two homes became one. The ten card pages now render their prose from the twin at request time. Seventeen small helpers, under two hundred lines between them, read a named section of the twin and render its paragraphs into the page. The structure around the prose stays hand-built. The hero, the terminal demo, the validator form and the price cards are HTML that the markdown does not try to describe. Every sentence now lives once, and editing the twin is editing the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate that replaced the comparison
&lt;/h2&gt;

&lt;p&gt;The parity comparison is gone, because there is nothing left to compare. What replaced it is a stricter check on the end state. Before every deploy a script walks each card function and fails the run if it finds a literal prose paragraph outside two named exceptions, if a twin section is neither rendered nor declared markdown-only, or if a function references a section its twin does not have. The gate is mutation-tested from both sides. A planted paragraph fails the run, and so does a misspelled section name. An exception that exists but goes unused fails it too, which keeps the exception list from rotting into a list of ghosts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving it with rendered output
&lt;/h2&gt;

&lt;p&gt;The gate was not enough on its own, because the risk in a rewrite like this is a rendering change nobody asked for. So every batch of the conversion shipped against a rendering harness. The same worker file runs in plain Node before and after the change, all ten pages and their markdown twins are snapshotted, and the outputs are diffed after normalization. The blog index came out byte-identical. The company page came out identical after normalization. Every other page changed only in ways the diff named, mostly apostrophes turning into HTML entities.&lt;/p&gt;

&lt;p&gt;The harness also caught one real bug before it went anywhere. A sentence on the validator page named the fetched path with a placeholder domain written in URL form. As hand-written HTML it was inert text. Rendered through the markdown pipeline it matched the bare URL rule and became a link to a domain that does not exist. The fix was rewording the sentence without the URL shape. The general lesson: prose that moves into a markdown renderer starts playing by markdown's rules, and rendered output is the only place you see that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did not change
&lt;/h2&gt;

&lt;p&gt;From the outside almost nothing moved. The scanner read the site before and after the conversion and reported the same result. The homepage still serves a markdown version that is deliberately shorter than its HTML, and the layout is still code. That boundary is the honest one to draw. The markdown is the content contract, and the structure around it is the site's own business.&lt;/p&gt;

&lt;p&gt;Two limits are worth stating plainly. The checks run on my machine before a deploy, and there is no CI behind them. And the blast radius of a bad edit is unchanged, because all content lives in template literals in module scope, so one stray interpolation marker would still take down every page rather than one. A build step would solve that differently, and at some size it wins. At this size, one file that renders itself is cheaper to keep honest.&lt;/p&gt;

&lt;p&gt;If you want to check any of this, request any guide or blog post with Accept: text/markdown and diff the response against its string in worker.js. They match byte for byte.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://turva.dev/blog/honesty-and-the-checker" rel="noopener noreferrer"&gt;When honesty and the checker disagree&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/blog/enforcing-the-rate-limit-i-advertised" rel="noopener noreferrer"&gt;Every response promised a rate limit. Nothing enforced it.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turva.dev/guides/markdown-for-agents" rel="noopener noreferrer"&gt;Serving markdown to agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>cloudflare</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
