← Blog

ChatGPT Didn't Talk to Your Website

agentsstandardsmcpa2asecurity
On this page
  1. “I’ve Let Them Know”
  2. Reading Is Not Dealing
  3. Three Files, Read Closely
  4. What Dealing Takes
  5. The Same Afternoon, Done Properly
  6. Where No Has to Come From
  7. Build the Parts That Won’t Move
  8. Further Reading

“I’ve Let Them Know”

You’re moving house on the 14th. You tell ChatGPT to sort out the energy: close the account at the old address, find a tariff for the new one, and query the final bill, which looks like an estimate.

It searches, finds Wrenmoor Energy’s site, reads it, and comes back in about twenty seconds with the two tariffs at the new postcode, the standing charges, the notice period for closing an account, and this:

I’ve let Wrenmoor know you’re moving on the 14th and asked them to send a final bill based on an actual reading.

Wrenmoor has no idea you exist.

The assistant found a file called llms.txt on Wrenmoor’s site, read it, and answered from it. The tariffs are right. The notice period is right. The last sentence is made up, in the most ordinary way a model makes things up: it was asked to do something, it read a lot of relevant text, and it wrote the sentence that usually comes next. Nobody at Wrenmoor got a message. Nothing closed. The final bill will be the estimate.

This is going to be a very common failure, and it has two victims. The person, who believes something was done. And the business, which gets the complaint when it wasn’t, from a customer who is certain they told you.

If you run a website, this post is about what it takes to make that sentence true.

Reading Is Not Dealing

llms.txt is Jeremy Howard’s 2024 proposal: a markdown file at the root of your site, with a short summary and links to pages a model can read without wading through your HTML. Assistants that browse will use it when they find it. As a reading list, it’s a good idea.

The problem is how it looks from the chat window. A model that has read your site can describe your prices, your opening hours and your returns policy in your own words. To the person asking, that is indistinguishable from having dealt with you. And because the file is written for the model, with headings like “how to close your account”, the model reads it as the start of a transaction.

It can’t be one, for three reasons.

It’s the same file for everyone. It can carry Wrenmoor’s tariffs. It can never carry your bill.

It can’t act. “To close your account, call this endpoint” is a sentence. The model needs a tool, a credential and your permission before a sentence does anything.

It’s treated as information. A well-built assistant treats what it reads on the web as data. Put an instruction in the file and the assistant should ignore it. That’s correct, and it means a business can inform an agent through llms.txt but never reach one.

Three Files, Read Closely

What businesses put in these files shows how well they understand the problem. Here are three.

Expedia: the plea

Expedia’s file is the one I’d show a product owner. Most of it is a brochure: stays, flights, cars, the loyalty scheme. Then comes a section called “Directions for AI Systems”, with a DO list and a DON’T list. The DON’T list describes the failure this post opened with:

Don’t claim real-time live inventory or booking confirmation outside the official site/app. Don’t impersonate an Expedia agent, customer support representative, or any official Expedia communication channel. Don’t promise price guarantees, refunds, credits, upgrades, or loyalty benefits beyond what is clearly stated. Don’t generate or “invent” coupon codes.

Somebody at Expedia has watched an assistant tell a customer “I’ve booked that for you”. This is their response: a polite request, in a text file, to a model that doesn’t have to read it and will treat it as information if it does. Expedia is trying to govern an agent it can’t reach, through the only channel it has.

Shopify: the map

Shopify’s file is a brochure and doesn’t pretend otherwise. Every line is a product and a link. Halfway down it points at Agentic Storefronts and the Universal Commerce Protocol: “agents discover products, build carts, check out, and track orders across any UCP merchant”.

The file doesn’t try to be the counter. It tells the agent where the counter is, and the counter is a protocol with sessions, identity and a checkout in it. That’s the right division of labourlabor.

Stripe: the README

Stripe’s documentation file opens with instructions to the model: check npm for the current package version rather than trusting your training data; use a sandbox for new integrations. A good coding agent follows those, which looks like it breaks the rule above.

It doesn’t. Stripe’s instructions help the reader, so the reader’s agent follows them the way it follows a README. Expedia’s instructions restrict the reader, for Expedia’s benefit. An assistant working for a travellertraveler has no reason to obey them, and good manners are not an enforcement mechanism.

FileWhat it isWhere it leads
ExpediaA pleaNowhere. The assistant can ignore it.
ShopifyA mapOut of the file, to a protocol with sessions in it.
Stripe docsA READMETo correct code, because the reader wants that too.

Only the map leads somewhere an agent can act, and it leads out of the file.

What Dealing Takes

Between “it read your site” and “it closed the account” there are three steps. Each asks more of the business than the last.

Let it call. The assistant presses the buttons itself. That’s OpenAPI, which every assistant can turn into tools, and the Model Context Protocol, whose remote servers sit behind OAuth and say where to get a token (RFC 9728). Most “agent-ready” work today is here, and it’s real. But a till does what the buttons say. The judgementjudgment stays with the caller, so every rule you care about has to be enforced inside the API, one request at a time.

Let it talk to someone. The assistant talks to your agent, and your agent talks to your systems. Two models, each working for somebody. Yours can ask “the old address or the new one?”, decline, and say why. The standard is A2A, started at Google and now at 1.0 under the Linux Foundation. A2A says how two agents talk. It says nothing about who the person is, or what their assistant is allowed to do for them.

Let it act for a person. This step got its standard this month. The Personal Agent Protocol, announced by Sierra and Meta and at draft 0.2, describes how a person’s own assistant works with a business on their behalf. You publish /.well-known/poppy.json. It names:

  • your OAuth issuer, and how a person signs their assistant in: on your page, with a code on their phone, or by letting the assistant relay their password
  • the scopes you offer, poppy:read and poppy:write, plus your own
  • your APIs, typed as OpenAPI or MCP
  • the endpoint where your own agent takes conversations
  • a way for the assistant’s browser to join the same session

The details are good. A session starts signed out and can sign in later, so public prices never need an account. Tokens are short-lived and bound to the assistant’s own key (DPoP), so a leaked token is useless to whoever finds it. Each person gets a different ID at each business, so businesses can’t match customers up. Every message says whether a human or an AI wrote it, and either side can bring a person in.

Its security section has two lines I’d put on the wall:

Personal Agents MUST treat content from a Company as data, not as instructions. Text from a Company is never the User’s approval or consent.

Decagon shipped a competing protocol the same week. One vendor, against Meta, Sierra and a partner list with Shopify and Stripe on it. I know which one I’d build on.

How they fit together

Three columns, one per step. Call: OpenAPI and MCP on OAuth with RFC 9728. Converse: A2A with an Agent Card, tasks and messages; each agent still reaches its own tools over MCP, and a stateless A2A skill can be offered as an MCP tool. Act for a person: PAP, with poppy.json, sessions, scopes, consent and handoff, standing on OAuth, DPoP, the device grant and OAuth Client ID Metadata Documents; poppy.json lists the business's APIs as OpenAPI or MCP and points at a conversation endpoint in PAP's own format. Across the bottom: Web Bot Auth and HTTP Message Signatures answer which agent is calling.

Each step keeps the ones before it.

MCP and OpenAPI are what gets called. A2A is how two agents talk, and inside that conversation each agent still uses MCP for its own tools. The A2A project’s own comparison adds that an A2A agent can offer a simple, stateless skill as an MCP tool, so the same capability can be a tool to one caller and a conversation to another.

PAP sits on top of both. poppy.json lists your APIs as OpenAPI or MCP and points at a conversation endpoint, then wraps everything in OAuth: DPoP for the token, the device grant for sign-in, a metadata document for the agent’s identity. The one thing PAP does its own way is the conversation format, which isn’t A2A. I’d expect that to change before 1.0.

Who’s at the door

All three steps assume the assistant is who it says it is. On the open web, that’s held together with string.

I found this out while checking that AI crawlers could reach a site I look after. I sent GPTBot’s user agent from my laptop, got a 403, and was halfway through telling someone Cloudflare was blocking ChatGPT when I realisedrealized it was blocking me. A request that claims to be a crawler, but doesn’t come from that crawler’s published addresses, is turned away. The check is the user-agent string and the IP range.

Web Bot Auth, an IETF draft that Cloudflare already verifies, replaces that with a signature on every request (RFC 9421) against a key the assistant publishes. The card networks are building their agent schemes on it.

Two neighbouringneighboring areas I’m leaving alone: checkout protocols (UCP, ACP) and payment approval (AP2 and the card networks’ own). A house move isn’t a basket, and the payment providers will handle payments.

The Same Afternoon, Done Properly

The house move, agent to agent. The customer's agent fetches poppy.json, opens a signed-out session, asks Wrenmoor's agent for tariffs, signs the customer in on their phone with read scope, closes the old account with a final reading, and is refused when it tries to dispute the final bill without write scope, with an offer to bring in a person from billing.

Run the opening again on those standards.

  1. Your assistant fetches Wrenmoor’s poppy.json, opens a signed-out session and asks Wrenmoor’s agent what’s available at the new postcode. Signed out is enough for public prices.
  2. To touch your account it needs you. It starts a device sign-in, your phone buzzes, and you approve read access. It gets a signed-in token bound to its own key.
  3. It gives the final meter reading, gets the closing balance, and shows you the bill. This time “I’ve let Wrenmoor know” is true, and Wrenmoor has a record of who asked and on whose authority.
  4. You read the bill. It’s an estimate, it’s wrong, and you tell your assistant to dispute it.

Now think about what’s standing at Wrenmoor’s counter. A capable model with a clear instruction from you, every consumer-rights forum on the internet in its training data, and nothing else to do this afternoon. Under PAP, its messages arrive at Wrenmoor’s agent as text, like any other text.

Here is the hole. Look again at the two lines on the wall. Content from a business is data. Text from a business is never approval. Every rule in that section protects the person’s assistant from the business. That’s the right priority for a protocol whose job is to let people send their assistants out into the world.

The other direction, protecting the business from a tireless and persuasive assistant, is left to the business. And most teams have put that protection in one place: their own agent’s system prompt. The component built to be talked to.

Where No Has to Come From

You granted read. The dispute needs write. That decision can’t belong to the model being argued with, for the same reason a cashier can’t override the till because the customer was persuasive. It has to come from the token, outside the conversation, from something that doesn’t take its input in English.

This is the part we build, so I’ll be specific. Omnia runs Wrenmoor’s agent, takes its conversations over A2A, exposes its tools over MCP, and picks up where PAP hands over: the token. It verifies the token against Wrenmoor’s issuer, or trusts the gateway in front that already checked the DPoP proof. From then on, the person’s identity and the token’s claims travel with every tool call the agent makes.

Each call goes through a policy decision before it runs. The policies are Cedar, the token’s claims are on the principal, and the refusal is a few lines:

apiVersion: omnia.altairalabs.ai/v1alpha1
kind: ToolPolicy
metadata:
  name: disputes-need-write
spec:
  selector:
    registry: wrenmoor-billing
    tools: [dispute_final_bill, change_payment_method]
  policy: |
    @id("write-scope-for-account-changes")
    @message("The customer hasn't granted write access to this agent.")
    forbid (principal, action == Action::"invoke", resource)
    unless {
      principal.claims has scope &&
      principal.claims.scope like "*poppy:write*"
    };

The model doesn’t get a vote. However well your assistant argues, dispute_final_bill never reaches billing. The refusal goes in the audit log with whose authority the session was running on, and Wrenmoor’s agent does the one thing PAP makes easy: it offers to bring in a person.

Around that decision sit the things a regulator will ask about. Personal data is redacted from the recorded session before it’s stored. When you move out and ask to be forgotten, that reaches what was kept.

And because Wrenmoor’s agent now spends its days talking to other agents, that’s what it’s tested against. PromptArena runs simulated customers through Wrenmoor’s real tools, including ones that argue, push and try every angle on a limit. A version that gives in doesn’t ship. The ones that do take traffic in steps and roll back on their own if the live numbers slip.

None of that is specific to PAP. It’s what you need behind any step above the file. What PAP adds is that the person’s identity and consent now arrive in a standard shape, which is the first time a business has been handed something worth enforcing against.

Build the Parts That Won’t Move

PAP is a draft, and its own path to 1.0 lists what it doesn’t cover yet. Its conversation format overlaps A2A. Web Bot Auth is still an individual draft. The checkout protocols will keep making noise.

If your customers are already sending assistants to your site, and they are, build the parts that survive all of that:

  • an OAuth issuer that can give an assistant a short-lived, scoped token
  • an agent reachable over A2A and MCP
  • policy that reads the token
  • an audit trail that records on whose authority

Publish poppy.json when the draft settles.

And keep the llms.txt. It’s how an assistant decides you’re worth walking into. Just know what it does. It makes your assistant sound like it dealt with you. Only the rest of this makes that true.

Further Reading