AI agent commerce surfaces are coming. Google, Shopify and Walmart published UCP (Universal Commerce Protocol, Apache 2.0); Anthropic pushes MCP. For a PrestaShop store the question is no longer whether but how to become readable by these agents. Here's how we did it — decision by decision, with the real code from Fondouk.
The golden rule: implement the spec, not a blog post
UCP is a versioned spec (date YYYY-MM-DD) with JSON Schemas. We target the stable release 2026-04-08 and validate every response against the published schemas — not an internal copy. That's what guarantees a third-party agent understands us.
The discovery manifest lives at /.well-known/ucp. An agent reads it, finds the endpoint and the capabilities, then queries the store — nothing hard-coded:
{
"ucp": {
"version": "2026-04-08",
"services": {
"dev.ucp.shopping": [
{ "version": "2026-04-08", "transport": "rest",
"endpoint": "https://demo.fondouk.dev/fondouk/ucp",
"schema": "https://ucp.dev/2026-04-08/services/shopping/rest.openapi.json" },
{ "version": "2026-04-08", "transport": "mcp",
"endpoint": "https://demo.fondouk.dev/fondouk/ucp/mcp",
"schema": "https://ucp.dev/2026-04-08/services/shopping/mcp.json" }
]
},
"capabilities": {
"dev.ucp.shopping.catalog.search": [{ "version": "2026-04-08",
"schema": "https://ucp.dev/2026-04-08/capabilities/shopping/catalog/search.json" }],
"dev.ucp.shopping.catalog.lookup": [{ "version": "2026-04-08",
"schema": "https://ucp.dev/2026-04-08/capabilities/shopping/catalog/lookup.json" }]
},
"payment_handlers": {}
}
}
Decision #1 — ORM/Presenter, not the webservice
PrestaShop's /api webservice returns raw data: base price, no computed tax-inclusive price, no discounts applied, stock in a separate call (N+1). Reproducing the storefront price would mean re-implementing the price engine — a source of divergence.
So we use the internal classes (Product::getPriceStatic, StockAvailable, SpecificPrice). The single most important detail is that the price is computed in an anonymous-visitor context — this is the security model:
// Does the shop display tax-included prices? (group/shop setting)
$displayTax = (Product::getTaxCalculationMethod() === PS_TAX_INC);
$price = Product::getPriceStatic(
$idProduct,
$displayTax, // tax-incl or tax-excl, per the shop's display setting
$idProductAttribute,
$decimals,
null,
false,
/* usereduc */ true,
1,
false,
/* id_customer */ 0, // anonymous — no customer
null,
null,
$specificPriceOutput,
true,
true,
$context,
/* use_customer_price */ false // never a customer-specific price
);
// group = PS_UNIDENTIFIED_GROUP → public specific prices apply,
// B2B / group prices never do. What an anonymous visitor sees, nothing more.
That single call, with id_customer = 0 and use_customer_price = false, is why a B2B price can never leak through an agent request.
Decision #2 — /.well-known/ routing across PS8 and PS9
The front office is migrated to Symfony in neither PS8 nor PS9 (the FrontKernel is experimental). The portable mechanism is the legacy ModuleFrontController + moduleRoutes hook. Only the manifest needs the well-known path; the REST/MCP endpoints live under a module base:
public function hookModuleRoutes()
{
$mod = ['fc' => 'module', 'module' => 'fondouk'];
return [
'module-fondouk-ucp' => [
'rule' => '.well-known/ucp', 'keywords' => [],
'controller' => 'ucp', 'params' => $mod,
],
'module-fondouk-catalog-search' => [
'rule' => 'fondouk/ucp/catalog/search', 'keywords' => [],
'controller' => 'catalog', 'params' => $mod + ['action' => 'search'],
],
// … lookup, product, mcp, llms.txt
];
}
Prerequisite: URL rewriting. Known trap: a physical .well-known/ directory (created by Let's Encrypt) short-circuits Apache via the .htaccess -d rule. We detect and surface it; a static-file fallback is opt-in.
The pivot: a decoupled core
The most important piece isn't the module — it's the Capability Graph: a neutral pivot format (versioned JSON Schema) with zero PrestaShop dependency. The module feeds it by introspection; adapters project it into UCP (REST, MCP). Here's the shape a variant takes in the graph — note prices is an array (multi-currency) and quantity is marked internal, never serialized to an agent:
"variant": {
"required": ["id", "prices", "availability"],
"properties": {
"id": { "type": "string" },
"prices": { "type": "array", "items": { "required": ["price"],
"properties": { "price": { "$ref": "#/$defs/money" }, "list_price": { "$ref": "#/$defs/money" } } } },
"availability": { "required": ["available"], "properties": {
"available": { "type": "boolean" }, "status": { "type": "string" },
"quantity": { "type": "integer", "description": "Internal — NEVER exposed by adapters." } } }
}
}
A platform that can produce this graph inherits every adapter for free. That's the whole point of the pivot.
What an agent actually gets
Discover, then query — the endpoint comes from the manifest:
curl -s -X POST https://demo.fondouk.dev/fondouk/ucp/catalog/search \
-H 'Content-Type: application/json' \
-d '{"query":"t-shirt","pagination":{"limit":1}}'
{
"ucp": { "version": "2026-04-08",
"capabilities": { "dev.ucp.shopping.catalog.search": [{ "version": "2026-04-08" }] } },
"products": [{
"id": "gid://demo.fondouk.dev/1/product/1",
"title": "Hummingbird printed t-shirt",
"price_range": { "min": { "amount": 2294, "currency": "EUR" },
"max": { "amount": 2294, "currency": "EUR" } },
"variants": [{
"id": "gid://demo.fondouk.dev/1/variant/1-1", "title": "S / White",
"price": { "amount": 2294, "currency": "EUR" },
"availability": { "available": true, "status": "in_stock" }
}]
}],
"pagination": { "has_next_page": true, "cursor": "eyJvIjoxfQ", "total_count": 6 }
}
amount is in minor units (2294 = €22.94), currency explicit — as the published UCP schema mandates. The same graph feeds the MCP server, so tools/call search_catalog returns the same catalog data in its structuredContent.
A manifest is a promise
The detail that separates a usable server from a merely conformant one: only declare what actually responds. While an endpoint is a stub, its capability does not appear — services: {}, capabilities: {}. An empty manifest beats a lying one: on the day of a real agent test, a 501 on an advertised capability breaks trust.
Hardening, because "survive 50 agents" is tested, not declared
Token-bucket rate limiting (60/min, burst 120) with Retry-After, cache invalidated by product hooks, request-body and batch size caps (413/400). Proven under crawl on both PS8 and PS9: the store stays responsive.
In short
Read-only, zero configuration, public = public. Visibility is free forever; agent analytics and per-agent control are Fondouk Pro's paid layer; checkout and other platforms are backlog. The code is open (MIT): github.com/fondouk-dev/fondouk.
The module described in this guide is free and MIT-licensed: https://github.com/fondouk-dev/fondouk — you can query the live demo right now:
curl https://demo.fondouk.dev/.well-known/ucp
A Pro version with an AI-agent analytics dashboard (which agents visit your store, which products they view, structured-vs-storefront adoption ratio, per-agent control, GDPR tooling) is available on the official PrestaShop Addons marketplace: https://addons.prestashop.com/en/analytics-stats-prestashop-modules/98377-fondouk-pro-ai-agent-analytics-for-your-store.html
Top comments (0)