Amazon Blocked Muse. The Request Had No Name on It.
If you build an agent that shops, books, or signs in for someone, the sites it visits will soon ask two questions of every request. Which agent sent this, and who said it could? Both have working answers today: a signed request under the IETF's Web Bot Auth draft, and a scoped grant in place of a stored password. This essay puts the code for each next to the mechanism it relies on. Meta's Muse and Amazon's block of it made the gap visible this week.
Starting Sunday, September 20, people who asked Meta's Muse agent to shop on Amazon got a refusal addressed to the machine. It said: "Continued access by an unauthorized AI agent violates Amazon's Conditions of Use, to which our customers have agreed". GeekWire broke the story. Meta's launch post says Muse "has no visibility into people's passwords or payment methods". Amazon says the agent "appears to capture and store customer credentials". Both statements can be true at once, and the distance between them is the most useful thing to understand about consumer agents this month.
What Meta built
Muse launched on September 8 in the US on iOS, Android, and muse.ai, and the announcement states its architecture plainly. Each person gets a "Muse Secure VM, a dedicated, virtual machine (VM) that houses both the agent and a person's data". A second process, the Sentinel, "runs on that same machine, kept apart from Muse at the system level". The announcement adds that "nothing Muse does reaches the internet unless the Sentinel approves it". Passwords and payment methods sit in secure storage so that "Muse can use them without seeing them".
Meta says a Confidential VM, encrypted with a key only the user holds, is coming. The integrations at launch are WhatsApp, email, browser automation, and checkout through Stripe Link. Meta lists Shop Pay and 1Password as next.
People wanted it. Sensor Tower's numbers, via CNBC, put Muse at 730,000 downloads in the first five days. On Friday, September 18, it was the top free iOS app in the US, ahead of ChatGPT. By Monday it was past 2.5 million downloads. The app is free, with $20 and $100 monthly tiers for heavier use.
Read as an engineer, the design is a serious containment model, and every part of it faces the user. The VM moves the agent's files, browser, and scratch work off your phone. The Sentinel stops the agent from doing things on the internet you did not ask for. Secure storage hides your password from the model, so an injected prompt on a web page cannot make the model recite it in a chat. All three protect the person who installed Muse from Muse, and none of them says anything to the website at the other end.
What the storefront sees
Now stand on the other side of the wire. A Muse shopping session reaches a store as a browser session from a machine in Meta's cloud. At some point it fills in the real customer's real password on the real login form. Browser automation is how Muse reaches a site that offers nothing else. After that, the session cookie that proves "this is the customer" lives in a VM the store has never heard of. Requests for order history and checkout come from that VM.
"Use them without seeing them" is a statement about the model. It means that the language model never has the password in its context. It says nothing about custody. The agent's browser can still fetch the stored credential. From the store's point of view, that is a customer password held in a third party's data center. Meta's sentence and Amazon's sentence describe the same stored password from two sides.
Amazon's second complaint, in GeekWire's summary, is that Muse does not identify itself when it browses. Browser automation drives an ordinary browser, and an ordinary browser sends ordinary headers. A site has three ways to sort a request into "customer" or "machine," and only one of them is cheap:
- Where it comes from. This is network reputation. A residential ISP address looks like a person, and a datacenter address looks like a server. Customers on VPNs, cloud desktops, and corporate proxies also look like servers.
- How it behaves. The site checks timing, pointer movement, and fingerprint consistency. This works until the automation gets better. After that, it is an arms race that the store pays for forever.
- What it says. The request declares an identity. Anyone can type a User-Agent string, so on its own it proves nothing. A signed declaration is different, and it is the part of this story with an engineering answer.
GeekWire notes that Amazon's own shopping agent, Buy for Me, "identifies itself and lets brands opt out". Amazon's spokesperson put the demand in one sentence: "third-party applications that offer to make purchases on behalf of customers from other businesses should operate openly and respect service provider decisions about whether or not to participate". Remove the corporate interest and the demand is for two protocol properties: declare yourself, and accept a refusal.
One checkout request from three senders. Pick a sender to read what arrives at the store, then set the store's policy. The board underneath scores all three senders against the same policy.
store policy
Illustrative request to shop.example. Header names and the signature parameters follow draft-ietf-webbotauth-httpsig-protocol-00 (September 1, 2026). The signature value is truncated. The delegated token on the signed agent shows what a declared path could carry. No large store offers this to consumers today.
Two outcomes on that board matter. Turn behavior detection off and the store accepts the undeclared agent, believing it served the customer. That is the case Amazon objects to. Turn the store's agent policy off and the store refuses the declared agent, while the undeclared one can still slip through. An agent that declares itself gets a clear refusal, and an agent that hides gets an arms race it can win for a while. A site can fix that backwards incentive only by making the declared path worth taking: faster, more reliable, and not blocked by default.
A store that recognizes an agent can allow it, rate-limit it, send it to an API, or refuse it with a reason. A store that cannot recognize agents has one lever. It blocks everything that looks automated, and that lever also hits the customer on a VPN. Declaration lets the store write a policy for agents in place of guessing which requests are automated.
How an agent says who it is
The declared path already has a draft standard. The IETF's webbotauth working group adopted HTTP Message Signatures for Automated Traffic, published as working group draft 00 on September 1, 2026. It is built on RFC 9421 HTTP message signatures and does three things.
First, the operator of an agent publishes its public keys as a JWKS-style key directory. The directory lives at a well-known path, /.well-known/http-message-signatures-directory, with the media type application/http-message-signatures-directory+json. Second, every request carries a Signature-Agent header that points at that directory, so the server can discover the key in-band.
Third, the agent signs the request. The Signature-Input header lists the covered components, at minimum the @authority the request is going to. It also carries four required parameters: created, expires, a keyid that is the base64url SHA-256 thumbprint of the key, and tag="web-bot-auth". The draft recommends an expiry of no more than 24 hours. Its own Ed25519 test vector looks like this:
Signature-Agent: agent2="https://signature-agent.test"
Signature-Input: sig2=("@authority" "signature-agent";key="agent2")
;created=1735689600
;keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U"
;alg="ed25519"
;expires=4889289600
;nonce="n9p433xm+NJ3ph3upfBIGmsuwHw387YV7Q/F+6BSpGCVjYCqQw6rznNA8PVVLySrAWsv0hQtFioQb6E1YsauiA=="
;tag="web-bot-auth"
Signature: sig2=:RdNFx5Bj6au3YgAMQL/RzmUlZE8QZLIaXGRpw985hWnwPfMxT228NMk6ehRS1PSl4e8PhbNZACSanGdhEwYCCg==:
Test vector E.2.1 from draft-ietf-webbotauth-httpsig-protocol-00, line breaks as in the draft. The draft notes these vectors cover the minimum and are "not a configuration to copy."
The server fetches the directory, checks the key thumbprint, verifies the signature over the covered components, and checks the timestamps. If all checks pass, the server knows one thing: whoever controls the keys at that directory signed this request. A captured signature replayed to a different host fails, because the host is in the signed components. Replayed against the same host, it works until it expires. In the draft's words, a signature covering only @authority "verifies against any method, path, or body sent to that authority until it expires". That rule shapes the signing code below.
On the agent's side, the work is three commands and one function. Generate an Ed25519 key and keep the private half in your secret store. Cloudflare's Web Bot Auth docs use OpenSSL:
openssl genpkey -algorithm ed25519 -out private-key.pem
openssl pkey -in private-key.pem -pubout -out public-key.pem
From Cloudflare's Web Bot Auth docs (updated July 1, 2026), which then convert the public key to a JWK with jwker or the WebCrypto API.
Publish the public key as a JWK Set at the well-known path. Set kid to the key's thumbprint, so a verifier can match the request's keyid against it. Serve the directory with a plain 200. The draft tells verifiers to treat any other status as a discovery failure and not to follow redirects. The draft gives this example response:
GET /.well-known/http-message-signatures-directory HTTP/1.1
Host: example.com
Accept: application/http-message-signatures-directory+json
HTTP/1.1 200 OK
Content-Type: application/http-message-signatures-directory+json
Cache-Control: max-age=86400
{
"keys": [{
"kty": "OKP",
"crv": "Ed25519",
"kid": "NFcWBst6DXG-N35nHdzMrioWntdzNZghQSkjHNMMSjw",
"x": "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs",
"use": "sig",
"nbf": 1712793600,
"exp": 1715385600
}]
}
From draft-ietf-webbotauth-httpsig-protocol-00, section 5.5.1. The Cache-Control lifetime also bounds how long a key you remove keeps verifying at sites that cached it (section 5.5.2).
Then sign every request. Cloudflare's web-bot-auth TypeScript package, one of the implementations the draft lists, builds the headers. The code below changes two things from its README example. First, it replaces the key, because the README signs with the published RFC 9421 test key. That key must never reach production. Second, it adds covered components, because the default covers only the authority and the Signature-Agent member.
import { generateNonce, sign } from "web-bot-auth";
import { signerFromJWK } from "web-bot-auth/crypto";
const signatureAgent = 'sig1="https://agent.example";type=directory';
const privateJwk = JSON.parse(process.env.AGENT_SIGNING_JWK!);
export async function signedFetch(url: string, init: RequestInit = {}) {
const request = new Request(url, init);
request.headers.set("Signature-Agent", signatureAgent);
const now = new Date();
const fields = await sign(request, {
signer: await signerFromJWK(privateJwk),
created: now,
expires: new Date(now.getTime() + 300_000),
nonce: generateNonce(),
additionalComponents: ["@method", "@path"],
});
request.headers.set("Signature", fields.signature);
request.headers.set("Signature-Input", fields.signatureInput);
return fetch(request);
}
Adapted from the web-bot-auth README. The additionalComponents field is a documented sign() option. We chose to cover @method and @path and to expire after 300 seconds, from the narrowing components the draft lists in section 5.2. The host agent.example is a placeholder.
Requests with a body need one more step. No signature component covers the body. For a checkout POST, the draft says an agent that needs body coverage must send a Content-Digest header and cover it too.
The signature proves less than it seems to. It names the operator, such as Meta, Amazon, or a startup. It does not name the person. It also says nothing about whether the customer who owns the account agreed to this purchase. Web Bot Auth answers "which agent". A store also needs to know "on whose behalf, allowed to do what", and that takes a second mechanism.
Checking the name at the door
Most of the draft's rules apply to the store's side, and two of them matter more than the cryptography. First, a Signature-Agent value is only a claim until the verifier fetches it. Before then, "verifiers MUST NOT attach policy to it". Second, key lookup "MUST be keyed on the (URL, key) pair, not on the key alone". A verifier that indexes by keyid alone will attribute a request to whatever URL the client asserted. It then uses a key that it learned somewhere else.
import { verify } from "web-bot-auth";
import { verifierFromJWK } from "web-bot-auth/crypto";
const WELL_KNOWN = "/.well-known/http-message-signatures-directory";
async function fetchDirectory(origin: string): Promise<JsonWebKey[]> {
const res = await fetch(new URL(WELL_KNOWN, origin), {
redirect: "manual",
headers: { Accept: "application/http-message-signatures-directory+json" },
});
if (res.status !== 200) throw new Error(`discovery failed: ${res.status}`);
return (await res.json()).keys;
}
export async function agentIdentity(request: Request): Promise<string | null> {
if (!request.headers.has("Signature")) return null;
const result = await verify(request, {
maxAge: 300,
resolver: async ({ keyid, signatureAgent }) => {
if (signatureAgent?.type !== "directory") throw new Error("unsupported discovery");
const keys = await directoryCache.getOrLoad(signatureAgent.uri, fetchDirectory);
const jwk = keys.find((k) => (k as { kid?: string }).kid === keyid);
if (!jwk) throw new Error("keyid is not published at this URL");
return verifierFromJWK(jwk);
},
validate: async ({ nonce }) => nonce !== undefined && nonceStore.claimOnce(nonce),
});
return new URL(WELL_KNOWN, result.signatureAgent!.uri).href;
}
This is our pattern, built on the web-bot-auth verify() API and the draft's rules. It is not code from either source. The rules come from sections 4.1, 5.4, 5.5 and appendices C.5 and C.6. The names directoryCache and nonceStore stand for your own storage. Cache directories by URL and respect their Cache-Control. Claim each nonce atomically, once.
When verify throws, the store does not mark the request as fake. The request goes back into the bot handling that the site already runs. The draft says that a fallback should not turn an unverifiable signature into a trusted identity. It also says that failed discovery is "operational throttling state, not proof that a signature is invalid". On success, agentIdentity returns a directory URL, and that URL is what you write policy against. A site can log, rate-limit, allow, or refuse it the way it treats an IP range today.
A store can also ask for a signature it did not get. RFC 9421 defines an Accept-Signature field for this purpose, and the draft says the status should be 403. For a checkout, the response names every component the store wants covered:
HTTP/1.1 403 Forbidden
Accept-Signature: sig1=("@method" "@authority" "@path" "content-digest"
"signature-agent";key="sig1");created;expires;nonce="<64 random bytes, base64>"
;tag="web-bot-auth"
Constructed from the RFC 9421 section 5.1 syntax and the draft's section 5.3, with line breaks added. The store chooses the nonce, which makes the resulting signature single-use.
Who the agent is acting for
An agent can act for a person at a website in two ways. It can become the person, by holding their password and logging in as them. It can also stay itself and carry a grant from the person. The grant is a token that names the agent, the person, and the allowed actions. Browser automation does the first on a site that offers nothing else. The enterprise side of the industry used the second to build agent access this summer.
WorkOS shipped Agent Auth on September 2. With it, you "define blueprints for what an agent is allowed to do, and it gets short-lived, tightly scoped tokens every time it runs". Each token identifies the agent, the user or organization it acts for, and its own permissions. You can revoke sessions instantly. On September 22, Browserbase joined Okta's Cross App Access ecosystem, which replaces static keys for browser agents. Browser agents get "dynamic, identity-based tokens that are scoped to exactly what the agent needs for each task". Okta issues them in real time through the user's active Okta identity.
Cross App Access is formally the Identity Assertion Authorization Grant, an OAuth extension that MCP has also adopted as an authorization extension. Mechanically, it is two OAuth requests, specified in the IETF draft of the same name (draft-04, May 21, 2026). First, the agent's client trades the user's ID token at the user's identity provider. It gets back a short, signed grant addressed to the store's authorization server:
POST /oauth2/token HTTP/1.1
Host: idp.example
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&requested_token_type=urn:ietf:params:oauth:token-type:id-jag
&audience=https://auth.shop.example/
&resource=https://api.shop.example/
&scope=catalog.read+cart.write+checkout
&subject_token=eyJraWQiOi...
&subject_token_type=urn:ietf:params:oauth:token-type:id_token
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=eyJhbGciOi...
Second, the client presents that grant at the store's own token endpoint. The access token that comes back can carry Rich Authorization Request details. With them, "checkout up to $50" becomes a field that the store enforces:
POST /oauth2/token HTTP/1.1
Host: auth.shop.example
Authorization: Basic <agent client credentials>
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=eyJhbGciOi...
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
{
"token_type": "Bearer",
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"expires_in": 900,
"authorization_details": [{
"type": "shop_checkout",
"actions": ["checkout"],
"max_amount": { "currency": "USD", "value": "50.00" }
}]
}
Request shapes from draft-ietf-oauth-identity-assertion-authz-grant-04, sections 4.3.1 and 4.4, with the draft's example hosts replaced. The shop_checkout type and its max_amount field are an example of a store-defined RAR type. They are not a published schema. The 900 second lifetime is our choice, and the draft's example uses 86400.
From the store's side, the two requests have one requirement. The store must run an authorization server that trusts the user's identity provider. Workforce apps have that relationship with Okta or Entra. A consumer storefront has it with nobody. That is why this path appears in enterprise changelogs this month and not at the Amazon checkout.
A consumer storefront's login form does not mint agent tokens. If a store has not built a delegated path, a consumer agent that wants to buy something there has one option: the password. So the credential dispute is about custody, and Meta's vault engineering does not settle it. Whoever holds the password holds the whole account. The difference shows up as soon as something goes wrong.
Same agent, two custody models. Pick an incident and watch how each model handles it. The two sliders set the exposure window: the time until someone notices, and the lifetime of a delegated token.
password in the agent's vault
delegated agent token
Illustrative model. The token row follows the properties that WorkOS and Okta describe for their agent tokens. Those tokens are short-lived, scoped, and revocable, and they name the agent and the principal. The $50 checkout cap is an example scope. All bars share one scale of 30 days.
Every incident in the drill ends the same way, for the same reason. A password is a bearer credential for the entire account, with no expiry and no name of its own. Anything that goes wrong with the agent goes wrong with the whole account, and the only fix is to change the password. A delegated token covers a slice of the account and carries a clock and a name. The agent's mistakes stay inside that slice. The store can also address the agent separately from the person.
Two gates facing opposite directions
Muse has one gate and the store has another, and the two gates guard different things. The Sentinel answers "does the user want this?" from inside Meta's VM, on Meta's side of the wire. The store's policy answers "does the store accept this?" on the store's side. Neither gate can see the other. A purchase that the Sentinel approved arrives at the store with no trace of that approval. A store's refusal reaches the Sentinel as a failed page load and nothing more.
An errand of six outbound actions. Each action passes the Sentinel first, then the store. The user sets the Sentinel rules, and the store sets its own. Change the rules on either side and the stamps recompute.
Illustrative errand. The Sentinel and its approve-before-egress role come from Meta's September 8 announcement. Its actual rule settings are not public, so the three rules here are examples. "Unaware" means the store accepted the request and believed the customer sent it.
Look at the store that opted out. If the agent declares itself there, the store refuses the first request, and the user learns at once that this store is off-limits. If the agent stays undeclared, the same store accepts everything and never knows. A Sentinel that checks every outbound step is the safest design for the agent's user. It works with either behavior toward the store. Meta chose which behavior Muse shows Amazon, and Amazon answered with a block.
Other stores chose the opposite. Shopify's CEO Tobias Lütke announced on September 21 that Shopify is working with Meta so Muse can do agentic checkout in Shopify stores. That is the "store opted in" button in real life: a declared, negotiated path in place of a scraped one.
The law is less settled than the engineering. Amazon sued Perplexity over its Comet browser agent. According to GeekWire, a judge blocked Comet in March 2026, and the Ninth Circuit reversed that decision in August. Courts are still deciding what a site can demand of an agent, while HTTP already lets an agent prove who it is.
A $38 cart, end to end
This example runs both mechanisms on one store. The agent's key directory lives at https://agent.example/.well-known/http-message-signatures-directory, and the server sends it with max-age=86400. The user granted the agent the checkout token above, capped at $50 and valid for 900 seconds.
- Browse. The agent sends
GET /products/4417signed over@authority,@method,@pathand itsSignature-Agentmember. The signature expires in 300 seconds and carries a fresh nonce. This is the agent's first request, so the store fetches the directory once, gets a 200, caches it for a day, and verifies. The store's logs now nameagent.examplein place of "a browser in a data center". - Check out. The agent sends
POST /checkoutwith aContent-Digestheader covered by a new signature. It also sendsAuthorization: Bearerwith the delegated token. The store checks the signature, claims the nonce, and then checks the token. The audience is the store, the subject is the customer, and the action ischeckout. The amount, $38.00, is under the $50.00 cap. The order goes through, and the store records it as "agent.example for customer U0194..." - Hit the cap. A week later the agent tries a $62.00 cart. The token check fails, and the store answers 403 with
error="insufficient_scope"inWWW-Authenticate(RFC 6750, section 3.1). The agent stops and asks the person. It does not retry without the signature.
The arithmetic shows why the setup is worth it. A captured request carries a signature that is good for one method on one path for 300 seconds. The store's nonce check also limits it to one use. An authority-only signature at the draft's recommended 24 hour ceiling would work on every endpoint of the store for 86,400 seconds. That is 288 times longer, before you count the nonce. On the credential side, the token dies after 900 seconds and cannot spend more than $50.
A stolen password is worse. With Fig. 2's default of six days before anyone notices, it gives 518,400 seconds of the whole account. That is 576 times the token's lifetime, with no cap at all. The store pays for all of this with one Ed25519 verification per request and one directory fetch per agent per day.
Where it breaks
| Symptom | Cause | Fix | Source |
|---|---|---|---|
| A valid signature shows up on a different path of the same store | Only @authority was covered | Cover @method and @path, add Content-Digest on bodies, expire in minutes, send a nonce | Draft, section 5.2 |
| Requests verify as coming from someone else's agent | The verifier indexes keys by keyid alone | Look keys up by (URL, key) pair, from the directory at that URL only | Draft, section 5.4 |
| Signed agents start failing after a CDN or proxy change | An intermediary rewrote the authority, path, or a signed header | Verify at the edge and pass the result to the origin over a trusted channel | Draft, appendix C.10 |
| A new agent fails for minutes, then works | A discovery failure was cached as if the signature were invalid, or the directory sits behind a redirect | Negative-cache for five minutes at most and retry with backoff. Serve the directory with a plain 200 | Draft, section 5.5 and appendix C.5 |
| Signatures fail intermittently | Clock skew against short lifetimes | Keep signer clocks synced. Allow a small skew at the verifier (the library's clockSkew option) | Draft, appendix C.6; web-bot-auth VerifyOptions |
| Anyone can sign as your agent | The RFC 9421 test key, or one key shared across unrelated agents, reached production | Generate a key per agent and purpose, publish only the public JWK, rotate by overlapping old and new | Draft, appendix C.12 and section 5.5.2 |
| Your agent reads as evasive | It retried without a signature after a 403 | Treat 403 and insufficient_scope as final and hand the step back to the person | Amazon's stated position, via GeekWire |
Build order, on both sides of the wire
If you build agents that act on the open web, do these steps in this order:
- Give each agent a name. Generate an Ed25519 key for each agent and purpose. Publish the directory at the well-known path. Never ship a test key.
- Sign every request narrowly. Cover the method and path. Expire signatures in minutes. Send a nonce. Add
Content-Digeston anything with a body. - Use grants in place of passwords. If a store runs OAuth, run the ID-JAG exchange. Ask for the narrowest authorization details. If a store offers only a login form, hand the step back to the person.
- Treat a refusal as an answer. Stop the attempt at a 403 or
insufficient_scope. An undeclared retry turns a policy disagreement into an evasion story. The retry is the part that ends up in the press. - Keep your gate's decisions attributable. Log each approval next to the signature label and the token it produced. Then you can defend a purchase when the store, the bank, or the customer asks.
If you run a site that agents will visit:
- Verify before you guess. Check signatures first, so a verified directory URL never lands in the bot-detection pile.
- Resolve keys correctly. Key on the (URL, key) pair. Respect the directory's cache lifetime. Negative-cache failures for minutes, not hours.
- Ask for what you want. Answer unsigned automation at sensitive endpoints with a 403. Include an
Accept-Signaturethat names the components and a nonce. - Offer a delegated path. Issue a token for search, cart, and capped checkout. It costs less to support than a permanent detection war. It also lets you revoke one agent without touching the customer. The signature answers "which agent", the token answers "for whom", and the store needs both answers.
Muse is careful work on the axis Meta optimized for, which is keeping one person's agent from hurting that person. The Amazon block shows what happens when a well-built inward gate meets a counterparty that cannot see it. Two things on the wire close that gap: a name on the request and a grant in place of the password. Both already exist, in an IETF draft and in two identity changelogs from this month.
Keep reading