Developer guide
Agentic commerce — how an AI shopping agent reads BTAB
Every BTAB storefront is legible to an AI shopping/answer agent without executing JavaScript. All of it is read-only, public catalogue data — the same data the HTML already serves — so nothing here needs a credential.…
Written with the code it describes, in the same pull request. Plain Markdown: /developers/agentic-commerce.md
Every BTAB storefront is legible to an AI shopping/answer agent without executing JavaScript. All of it is read-only, public catalogue data — the same data the HTML already serves — so nothing here needs a credential. (The operator side — driving your own store from an agent — is the separate, authenticated Vendor MCP.)
Scope decision (2026-09-08): discovery aggregates at btab.app; the transaction stays on
the vendor's own storefront. The vendor remains seller of record, so attribution and the
platform-fee split are unchanged whether a buyer arrived by browser or by agent. Automated
checkout is not offered anywhere (agentic_checkout: false) until a transaction rail exists.
Per-store surfaces (any <slug>.btab.app or live custom domain)
Every surface is generated from the request host — there is no hand-written per-vendor file.
| Surface | What it is |
|---|---|
/agents.md | Generated instruction sheet: store identity, catalogue entry points, prices are AUD, published policies, enquiry contact, purchasing posture. |
/.well-known/ucp | Minimal, honest UCP merchant profile. capabilities: [] and transaction.agentic_checkout: false — we declare only what we serve. |
/sitemap.xml | Every product at its canonical slug URL + content pages. |
/robots.txt | Default-allow for search + AI crawlers; junk routes disallowed. |
/products/<name>-<id> | The PDP. Embeds schema.org Product + Offer (+ BreadcrumbList) JSON-LD. Numeric or stale-slug URLs 308 to the canonical slug. |
/products/<name>-<id>.json | The same Product + Offer JSON-LD as a first-class document (Shopify convention). See below. |
GET /products/{slug}.json
The PDP's structured data as a document, plus one non-schema block, purchase, that says
honestly what an agent may do next.
$ curl -s https://<store>/products/leather-sofa-42.json
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Leather Sofa",
"sku": "SOFA-1",
"productID": "42",
"url": "https://<store>/products/leather-sofa-42",
"offers": {
"@type": "Offer",
"url": "https://<store>/products/leather-sofa-42",
"price": "1299.00",
"priceCurrency": "AUD",
"availability": "https://schema.org/InStock",
"itemCondition": "https://schema.org/NewCondition"
},
"image": ["https://…/sofa-1.jpg"],
"description": "A comfy leather sofa",
"brand": { "@type": "Brand", "name": "ComfyCo" },
"category": "Furniture",
"purchase": {
"enquiry_only": false,
"agentic_checkout": false,
"url": "https://<store>/products/leather-sofa-42"
}
}
- The same data as the product page's JSON-LD, so the two can never disagree. Price is AUD, the binding currency. Description is plain text (HTML stripped).
- Same visibility as the product page. A product the store's own page does not show (not active, not approved, not selected by the store, or unpriced) is not here either.
purchase.enquiry_only: true— the vendor sells this product by enquiry, not cart. The Offer still carries the displayed price (the PDP shows it), but do not treat the product as add-to-cart-able; direct the buyer topurchase.url, which deep-links the enquiry form.purchase.agentic_checkoutisfalseeverywhere. Never attempt to buy programmatically; send the buyer topurchase.url.<slug>may be the canonicalname-<id>or a bare numeric id; a numeric id or stale slug 308s to the canonical<slug>.json. Unknown product or malformed ref →404 {"error":"product_not_found"}; unknown host →404 {"error":"store_not_found"}.Content-Type: application/json,Cache-Control: public, max-age=60,Access-Control-Allow-Origin: *.HEADworks.
The public address is the .json suffix; any other path the store answers it from is an
implementation detail and may change.
Platform front door (btab.app)
An agent shopping "Australia" should not have to find sixty storefronts one by one. btab.app
aggregates discovery only — it never re-serves a catalogue and never transacts; every
pointer leads to a store's own origin, where the transaction and the seller of record live.
Only published + discoverable stores are listed (the same predicate as
GET /api/public/stores).
| Surface | What it is |
|---|---|
https://btab.app/agents.md | Platform instruction sheet: what Btab is, how to discover stores (directory HTML + JSON, agentic sitemap), what every store serves, AUD pricing, purchasing posture, and the live store list. |
https://btab.app/.well-known/ucp | Platform profile — role: marketplace, capabilities: [], transaction.agentic_checkout: false, seller_of_record: store, discovery.* pointers, and stores[] each with its own agents + ucp URL. |
https://btab.app/sitemap-agentic-discovery.xml | Sitemap of agent surfaces: the platform's agents.md + ucp, then every store's agents.md, .well-known/ucp and sitemap.xml. Listed in robots.txt beside sitemap.xml. |
https://btab.app/llms.txt | Btab's own index for agents and developer tools (llmstxt.org form): every developer guide as Markdown, the API contract, the agent files above, the Help Centre and the blog. Per store, the storefront's /llms.txt is that store's own. |
https://api.btab.app/api/public/stores | The store directory as JSON ({ stores: [{ slug, name, tagline, category, url, logo_url, updated_at }] }). Public, 60/min/IP, cached 5 min. |
All three btab.app surfaces are generated per request from the store directory; the directory
fetch is cached for 10 minutes and never throws, so an
unreachable API yields an honest surface with an empty store list rather than an error.
Query the catalogue: the buyer-side MCP (Phase 3)
POST https://api.btab.app/api/public/catalog/mcp — Model Context Protocol
over streamable HTTP (JSON-RPC 2.0). No authentication (public catalogue data only), per-IP
rate limit 60/min, CORS-open. GET the same URL for a plain description. This is a separate server
from the authenticated, operator-side Vendor MCP; it shares the same JSON-RPC /
registry and fencing plumbing and serves btab-catalog.
UCP capability ids served — and the only ones declared on any /.well-known/ucp:
dev.ucp.shopping.catalog.search, dev.ucp.shopping.catalog.lookup.
| Tool | Arguments | Returns |
|---|---|---|
catalog_search | query (required), store? (slug, host or custom domain), limit? 1–48 (default 12) | Relevance-ranked products across every public store (or one store), each with its offers. A query that matches nothing returns an empty list — never the whole catalogue. |
catalog_lookup | product_id or url (a storefront product URL, .json accepted), store? | One product with every public store's offer for it — or that store's only when scoped. Unknown → tool error NOT_FOUND. |
Both are read tools (readOnlyHint: true). Every result is fenced as quoted data (content[0].text)
and also returned raw as structuredContent.
The result shape — a product with one OFFER per store (the "discovery aggregates, transaction stays on the store" decision made concrete):
{
"product_id": 42, "name": "Standing Desk", "sku": "DESK-42", "brand": "DeskCo", "category": "Desks",
"description": "A standing desk.", "image_url": "https://…/medium.jpg",
"offers": [
{ "store": "Example Store", "store_slug": "example-store", "store_url": "https://example-store.btab.app/",
"url": "https://example-store.btab.app/products/standing-desk-42",
"product_json": "https://example-store.btab.app/products/standing-desk-42.json",
"price_cents": 89900, "price": "899.00", "currency": "AUD", "in_stock": true,
"enquiry_only": false, "purchase_url": "https://example-store.btab.app/products/standing-desk-42" },
{ "store": "Another Store", "store_slug": "another-store", "…": "…", "price": "949.00", "enquiry_only": true,
"purchase_url": "https://another-store.btab.app/products/standing-desk-42#enquire",
"note": "Enquiry-only at this store: not add-to-cart-able — send the buyer to purchase_url to enquire." }
],
"note": "Prices are AUD (GST-inclusive) and belong to the store shown. Automated checkout is not available: send the buyer to purchase_url on the store, which is the seller of record."
}
- Visibility is the storefront's. A product appears when the store shows it (approved, active,
priced, with images), and only for stores that are published and discoverable (the same rule
as
GET /api/public/stores). enquiry_onlyis the effective value — the store's own setting first, else the product's or its supplier's default — the same rule checkout enforces.- Relevance is the storefront search's own ranking, so a search across stores orders products the way a store's own search box would.
- A live custom domain wins for store/product URLs (same rule as the directory).
$ curl -s -X POST https://api.btab.app/api/public/catalog/mcp -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"catalog_search","arguments":{"query":"standing desk","limit":3}}}'