The agent did exactly what my tool told it. That was the problem.
In May, an AI agent working inside a customer's IDE asked my API a simple question: what is the public URL for this form?
My tool answered confidently. It answered wrong.
It handed back a booking URL for something that was not a booking. The agent had no reason to doubt it. So it did what a good assistant does. It built eight pages around that URL and shipped them to a live site. Eight cards, all reading "we couldn't load this booking." It took about a day to unpick.
The agent wasn't wrong. My tool was.
Confidence is the failure mode
I want to be precise about what broke, because "the API had a bug" is the wrong lesson.
The tool returned a well-formed answer. Status 200. Valid JSON. A URL that resolved. Nothing in the response said "I am guessing," because nothing in the response could say that. And the tool's own description text repeated the same wrong address, so an agent reading the docs got the error twice and called it corroboration.
A human would have clicked the link and seen a broken page. An agent takes the answer and builds.
That asymmetry is the whole problem. When your reader is a person, a confident wrong answer costs them a minute. When your reader is an agent with write access to a production site, a confident wrong answer costs eight pages.
So I stopped treating "returns 200" as the bar.
Four things I changed
One address, aware of what it is
Forms and bookings were two kinds of the same thing living at two addresses. Now every flow lives at /f/<flow_id> on your own domain. That route reads the kind of flow behind it and serves the right page. It no longer assumes.
Old links still work. /book/<flow_id> permanently redirects to the canonical route, so nothing you have already shared has to be reissued.
The preview tool returns that canonical URL. Its description now matches what the code does.

Errors that say what you sent
Every error carries a structured envelope instead of a sentence:
{
"error": {
"code": "WRONG_FLOW_KIND",
"message": "This URL is for kind='booking' flows. Flow 3f0b1b83 is kind='form'.",
"what_you_sent": { "flow_id": "3f0b1b83", "kind": "form" },
"what_was_expected": { "kind": "booking" },
"suggested_action": "Use the form endpoint instead.",
"suggested_url": "/api/v1/forms/3f0b1b83"
}
}An agent can recover from that without guessing. It is told what it sent, what was expected, and where to go next.
Successes that admit their limits
This is the piece I am most glad I built. A success response can now carry a guidance block. It says what this resource is for, and what it is not for. What to call next. The one caveat that matters. The pitfalls people actually hit, and the hard limits.
Six keys, frozen. use, not, next, warn, pitfalls, limits.

The not and pitfalls fields are the ones that matter, and they are the ones no ordinary API response has. They are the negative space. They are where I get to say "you are about to call the booking endpoint on a form, and that will look like it worked."
It started as opt-in. It is not anymore. Call the API with a token and you get guidance by default. An agent should not have to know to ask for the truth. Add ?format=json and it goes away. Browser sessions never see it.
A way for your agent to check its own work
The last piece is the one that would have caught the original incident: content_visual_check. Your agent gives it a URL and gets back a real browser's opinion, a screenshot, and the page's actual DOM.
One rule comes with it, and it is not obvious. Assert on the custom element, not the page text:
dom.shadow_hosts.includes("spideriq-form") ← correct
body_text_preview.includes("Submit") ← always fails
An embedded form renders in a cross-origin frame, so its field labels are invisible to the parent page even when the form is working perfectly. Check the wrong thing and a healthy page reports as broken. That rule now ships inside the tool's own description, which is the only place it will actually be read.
The number, and what it is worth
Across both of my verification runs, the preview tool returned zero wrong URLs.
I want to label that honestly, because a number without a source is decoration. That was measured by me, on my own code, twice. The method was a clean install of the published package with a grep for the old URL pattern, plus live requests against a real tenant. As of 20 May, re-confirmed 9 August. No outside party checked it.
It is a real result. It is not an audit. Treat it as the former.
The part I got wrong
Here is the bit I would rather not write.
When I declared this finished, one of the four pillars did not work in production. The guidance block fired on one of our two server processes and not the other, and public traffic went to the one without it. The code was correct in both. The registration was missing in one.
It sat that way for eighteen days after I called the work complete.
The failure that started all of this was a tool being confidently wrong. The failure at the end was me being confidently done. Same shape, different layer. I verified the code and did not verify the surface a customer actually reaches.
It is fixed, and I re-checked it against the public URL rather than the source before writing this sentence.
What this means for you
If you point an agent at SpiderPublish™ today, three things are true that were not true in May. The URLs it gets back are the ones that work. The errors it gets back tell it what to do next. And it can look at the page it just built and tell you honestly whether it rendered.
None of that makes an agent smarter. It just stops me lying to it.