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.

SurfaceWhat it is
/agents.mdGenerated instruction sheet: store identity, catalogue entry points, prices are AUD, published policies, enquiry contact, purchasing posture.
/.well-known/ucpMinimal, honest UCP merchant profile. capabilities: [] and transaction.agentic_checkout: false — we declare only what we serve.
/sitemap.xmlEvery product at its canonical slug URL + content pages.
/robots.txtDefault-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>.jsonThe 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 to purchase.url, which deep-links the enquiry form.
  • purchase.agentic_checkout is false everywhere. Never attempt to buy programmatically; send the buyer to purchase.url.
  • <slug> may be the canonical name-<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: *. HEAD works.

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).

SurfaceWhat it is
https://btab.app/agents.mdPlatform 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/ucpPlatform 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.xmlSitemap 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.txtBtab'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/storesThe 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.

ToolArgumentsReturns
catalog_searchquery (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_lookupproduct_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_only is 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}}}'