Inspiration

Every website ever built assumes a human is looking at it.

That assumption is baked in so deep we stopped seeing it. Pages exist because humans need somewhere to land. A CMS exists because someone has to author those pages in advance, for every visitor, hoping one of them fits.

Agents need none of it. An agent arrives with a specific intent already formed — "wide-fit running shoes under $140 that ship to Tel Aviv by Friday" — then does the absurd work of pulling apart a page built for nobody in particular to find the fraction that answers the question. A hero image it cannot use, a nav bar it will never click, a grid of 48 products to find the four that matter.

Everyone is asking how agents should read websites better. The right question is why the website is still a fixed artifact at all. If the visitor states its intent up front, the site does not need to have been written in advance — it can be composed on arrival, from the merchant's catalog and brand system. One agent, one intent, one purpose-built storefront, then it is gone.

WebMCP is what makes this possible, because for the first time the visitor tells the site what it came for before the site has to respond.

What it does

AMS is an agent that streams a storefront over WebMCP.

An AI shopping agent arrives at a merchant's store and finds a declared tool surface on navigator.modelContext. It states what the shopper wants. AMS composes a branded storefront for exactly that intent — the right products, the right comparisons, the right attributes surfaced — and streams it back as RFC 6902 patches against a live layout tree.

Not a page that was waiting for it. A store that did not exist until it asked.

The next turn, the shopper narrows: "actually, only the ones with a wide toe box." The storefront recomposes. Different products, different comparison axis, different layout, same brand. The experience is a conversation, not a destination.

The unit of the web stops being the page. It becomes the intent. There is no page to author, no page to maintain and no page to optimize — because there is no page until an agent asks for one.

The merchant keeps their brand inside the agentic conversation. Today an agent shopping on a shopper's behalf strips the merchant to a title and a price in someone else's chat window. AMS streams a branded surface into that conversation instead.

AMS declares 16 tools through navigator.modelContext.registerTool(). Every tool's description and enabled state is overridable per merchant via webmcpConfig, served at session creation — a merchant can rename, re-describe or switch off any tool without a deploy.

Tool What the agent gets
send_message States an intent in natural language. Hands it to the composing agent and returns its prose. The storefront recomposes as a side effect.
show_collection Asks for a named collection or category; the agent resolves it and composes a product layout.
get_products Two shapes. Called bare, a compact survey of the canvas — title, price, availability, image. Called with productIds, the full record: description, option axes, and every variant with its exact option values and buyability.
get_layout The raw layout tree — component types, props, children. Also the only way to read what was featured, since a look card carries its own product list.
get_messages Full conversation history, role-tagged.
get_status Session state: isRunning, hasLayout, error, live streaming prose, latest committed reply.
get_visitor_context Standing preferences the shopper has stated — size, fit, colour, budget — under the brand's own key names.
set_visitor_context States a standing preference. It survives every later recomposition.
open_product Navigates the canvas to a product detail view.
add_to_cart Adds by product plus {axis: value} selections. Same signal a human card click fires — stamped origin: 'webmcp'.
get_cart Every line with its chosen option values, quantity, price, the totals, and whether checkout is available.
update_cart_item Sets a line's quantity. Absolute, not a delta.
remove_from_cart Removes a line; without selections, every line of that product.
begin_checkout Hands off to Shopify's hosted checkout with the current cart.
send_signal Escape hatch: a low-level typed interaction signal for anything the typed tools miss.
clear_session Resets layout, conversation and session data.

Three decisions there are worth naming.

Agent-backed tools return the site's own voice, not a status. send_message and show_collection hand the request to the merchant's AI and resolve with its finished text. The operator contract, served to the calling agent at runtime through window.webmcp.instructions, tells it to relay that verbatim: "You operate the tools; the site AI is the voice." The merchant's brand voice survives the round trip instead of being paraphrased by somebody else's model. A turn that outruns the await window returns status: "running" — the caller polls get_status and reads latestResponse. The contract is that the site's voice reaches the agent, not that it arrives within a fixed budget.

The cart is addressed by selections, never by variant id. get_products deliberately returns no variant id: those ids are unstable across the optimistic-write window, so an id an external agent held across a turn could aim a write at a line that has already been replaced. Cart verbs take a product plus {axis: value} selections and bind late, against live state — a stale reference can only miss, and fail loudly. This replaced a first version where add_to_cart fell back to "first in-stock variant", so a shopper who asked for size 8 got whatever was in stock first.

add_to_cart books itself as a third actor — not the agent, not the visitor, but origin: 'webmcp'. Folding a WebMCP-driven add into either of the others would lie about which surface earned it, and that is the number a merchant asks about first.

For agents that do not speak WebMCP natively, appending ?webmcp=1 installs a real host plus a window.webmcp client, so a JS-eval-capable browser agent can call the identical surface. It never overrides a real host — if Chrome or a bridge extension already provided navigator.modelContext, the sim is a no-op. That same flag also makes the URL a self-contained demo: it renders what the page is, and a prompt to paste into an agent. No repo to clone and nothing to send alongside the link.

How we built it

The storefront is composed, not served. A tool call arrives carrying an intent, and one fixed two-call flow runs:

  1. Reasoning — a multi-round tool loop bounded by step count and wall clock. The model fires tool calls as a parallel batch, sees the results and may fire another batch to refine. No prose, no layout.
  2. Compose — one streamText call with tools omitted entirely and the reasoning step's tool history flattened to plain text. It streams a jsonl-elements fence that parses line by line into RFC 6902 patch operations against the layout spec.

The tool-free compose call is structural, not stylistic. A single tool-bearing call that also composes cannot guarantee output: the model emits a phantom functionCall, finishes with finishReason='tool-calls' and the visitor gets a blank canvas. Strip the tool registry and a phantom call has nothing to execute — the model is forced to compose. It is the most important architectural line in the system.

The model split is not the one you would guess. Both calls run Gemini 3.6 Flash by default. The performance split is structural — two calls plus per-step reasoning budgets — not cheap-planner and expensive-composer. Reasoning runs at a minimal thinking floor because a fuller budget measured 13–24s per turn against ~4.6s warm: tried, measured, rejected. Claude Haiku 4.5 and any OpenRouter id are selectable per merchant.

The catalog is Shopify's, through Shopify's own MCP. Product search hits the Global Catalog MCP, per-store reads hit the Storefront MCP over JSON-RPC 2.0. Live products, live inventory, live pricing. AMS composes the experience; Shopify stays the system of record, so nothing is mirrored and nothing goes stale.

Serving is three planes. Vercel runs the Next.js app that mounts the storefront engine, Railway runs the Hono agent API and Supabase runs Postgres, with Redis holding session state. Streaming is a per-session WebSocket carrying typed events — tool_call, response_text_delta, layout_patch_op, layout_ready, done — so the agent acts on patches while the rest of the surface is still arriving.

Entity-id hydration cuts the token bill by roughly 80%. The model never writes product data. It writes entity ids, the catalog declares which props are resolved() and the client resolves them through a HydratorMap. Tokens go to judgment, not to transcribing a product feed.

Turns survive a dropped connection. Reconnect with ?lastEventId= and ?pendingMessageId= and the stream resumes — 8/8 deliberately faulted turns recovered, across both an abrupt terminate() and a clean close().

The wallet boundary is drawn by Shopify. Composition, browsing and cart assembly run freely through the tools. Checkout is a handoff to Shopify's hosted checkout, under Shopify's PCI scope, with every exit path a typed outcome the merchant can see. No order exists that a human did not complete.

Challenges we ran into

Three seconds. That was the whole engineering problem, and we are still arguing with it.

First: which three seconds. The budget that matters is first paint — the moment the visitor's surface starts changing — not turn completion, and those are far apart: first paint lands at p50 3498ms, while a complete multi-slot look settles in 15–25s. Our first version of that measurement had a broken paint signal that silently excluded real paints and biased toward faster turns, which is how we spent a week believing we passed.

Splitting the tool loop from the composition. The expensive part — deciding what the experience should be and fetching the facts — happens in a call that produces nothing user-visible. Composition is a separate tool-free streaming call that emits patches immediately. That is what makes the output guaranteed and the first patch early.

Pre-warming the KV cache, and learning what disqualifies it. Google refuses cachedContent on any request carrying a tools field, and the implicit cache silently skips tool-bearing requests too. We spent a long time uploading a cache at session creation that no live request ever attached — a dead path costing ~700ms of prefill every turn. The two-call split fixed it by construction: the compose call carries no tools, so it is cache-eligible.

Pre-resolving what can be, and deliberately not products. Policies, collections, articles and FAQs answer locally from per-merchant BM25 indexes. But we removed the local index over a product snapshot — it duplicated the MCP call and drifted from live inventory. Products resolve live every turn. A fast wrong price is worse than a slow right one, and this is the one place we pay the latency.

Knowing when to stop automating trust. Every fact on a surface resolves from Shopify through the hydrator, so the model literally cannot type a price. We also built a deterministic verify pass over the model's prose — and deleted it. It produced false gaps that contradicted correct output, and a guardrail that cries wolf is worse than none.

The one we did not take. Of that first paint, 595ms is conversational prose streamed before the layout fence opens. Reordering to emit patches first measured 420ms saved, and three of four merchants inside budget instead of one. We ran it, measured it and reverted it: the prompt requires the opener to name the operative criterion before the fence, because you cannot honor a requirement you have not yet stated. Fence-first converts a structural guarantee about constraint honesty into a behavioral one. That is a composition-quality trade no latency measurement is entitled to decide on its own.

Accomplishments that we're proud of

A web experience that is agentic-first, not agent-tolerant. Any WebMCP agent can call 16 declared tools, ask for anything and get a branded storefront built for its intent. There is no human version of this site that agents are borrowing.

The output is guaranteed. The tool-free compose call means a phantom tool call cannot produce a blank canvas. It sounds like a detail. It is the difference between a demo and a product.

Per-merchant fidelity is real, not a theme. 19 component types in the base catalog and four merchant component packages, each binding its own renderers behind a parity check that fails the build on a mismatch — Steve Madden 18 brand components, Babysense 28, Current 22, Shiree Odiz 25. Zero authored pages across all four.

It holds under load and queues instead of failing. 148/150 turns at 50 concurrent sessions, event-loop lag p99 ~12ms with the provider gate saturated, RSS steady in a 483–563MB band. No 5xx, no dropped stream. The ceiling is provider concurrency, not our process.

We measured honestly and published the failure. At 20 concurrent sessions across four real merchants: 60/60 turns completed, zero failures, first paint p50 3498ms, 35% of turns inside 3s, one merchant passing and three missing. A single warm merchant runs ~2.9s to first paint; a complete multi-slot look settles in 15–25s. We are close on the number we set out to hit and we are not there — and this is the number our own harness produced, not the one we would like to quote. The full run data ships in the repo.

The merchant's brand survives the agentic conversation. The operator contract makes the calling agent the mouthpiece and the merchant's AI the voice. This is the thing the industry has quietly conceded and we do not think it needs conceding.

What we learned

CMS is going to be replaced by AMS.

A content management system solves a problem that only exists when humans browse: someone has to author pages in advance, because a page has to exist before a visitor arrives. Templates, page trees, publishing workflows, preview environments — all of it is machinery for guessing what a visitor wants before they show up.

When the visitor declares its intent on arrival, that guess becomes unnecessary. You do not need pages authored ahead of time. You need a system that holds a brand's components, catalog and rules and composes the right surface on demand. That is a different category of software, and it is where the web is heading as agent traffic grows. Content management gives way to agentic management.

Latency is a design constraint, not an infrastructure one. The two-call split exists because a single call cannot be cache-eligible and tool-bearing at once. The reasoning floor exists because a fuller thinking budget cost 13–24s. None of that is tuning. It is shape.

Measure the number the visitor actually experiences, and distrust it until you have re-derived it. A wrong signal does not look wrong — it looks like a pass. The 2936ms "barely passes" we believed for weeks became 3498ms "fails" the moment the paint signal was fixed. And first paint is not the whole experience: the complete look lands far later, which is a different number and deserves to be quoted as one.

A median is a place to hide a dead tenant. A fleet-wide p50 passed while one merchant sat at 4432ms and another failed 0 of 30 turns, dying at init on a binding gap no single-merchant run could have surfaced. Multi-tenant load testing is not a scale exercise. It is the only way to find the tenant that is already broken.

Intent is a better primitive than a query. A query returns matches. An intent describes a decision the shopper is trying to make, and a decision can be designed for.

Agents will use websites more, not less. The prediction that they route around the web entirely gets it backwards. They will visit constantly. They will just never look at what we built for humans.

What's next for Agentic Stores (AMS) by 40rty

Close the last 500ms, honestly. We know where the time is: 595ms of pre-fence prose and the reasoning step's tool round-trip. The reordering is measured and waiting on a composition-quality eval rather than a latency argument, which is the correct order to resolve it in. And the await window on the agent-backed tools wants raising: a composing turn that outruns it is still composing, and the caller should not have to poll to find that out.

Deploy AMS for leading Shopify brands. Not a bigger demo — live agentic storefronts on stores with real traffic, where we can see what shopper agents actually ask for at volume.

Turn tool calls into merchandising intelligence. Every intent an agent brings is a signal no merchant has had: what shoppers ask their agents for, which intents the catalog cannot answer, where an agent gave up. That feeds the Prompt Library and Catalog Audit that AgentIQ by 40rty already runs, and origin: 'webmcp' attribution is where the analysis starts.

Category-native component libraries. Apparel decides on fit, electronics on compatibility, beauty on ingredients and skin match. The components that make a decision fast differ by category and should ship that way.

Push the pattern into the standard. Streaming a composed experience through a tool response is not a commerce-only idea. Two pieces are worth standardizing beyond us: agent-backed tools that await and return the site's own voice, and origin attribution on state-changing tool calls so a site can tell agent-driven actions from human ones. If those hold, that is how the agentic web works — and we would rather help define it than keep it to ourselves.

Built With

Share this project:

Updates

Submission history