UCP for AI shopping agents
FLUF speaks the Universal Commerce Protocol: agents can discover, search and look up secondhand inventory.
FLUF speaks the Universal Commerce Protocol (UCP), the open standard Google, Shopify, Etsy, Walmart and others use to let AI shopping agents discover and transact with merchants. Any UCP-aware agent can find FLUF's secondhand catalogue, search it, and look items up — no FLUF account or token needed.
What is exposed is the public catalogue: in-stock secondhand items from FLUF sellers and partners. Every item is unique and quantity one.
Phase I is read-only. Catalog search and lookup are live. Checkout is not yet exposed; each product carries a url where the item can be bought.
Discovery
GET https://fluf.io/.well-known/ucpThe profile advertises two transports for the dev.ucp.shopping service and two capabilities:
| Capability | Transport | Endpoint |
|---|---|---|
dev.ucp.shopping.catalog.search | REST | POST https://fluf.io/wp-json/fluf/ucp/v1/catalog/search |
dev.ucp.shopping.catalog.lookup | REST | POST …/catalog/lookup (batch) and POST …/catalog/product (single) |
| both | MCP | POST https://fluf.io/wp-json/fluf/ucp/v1/mcp |
Send a UCP-Agent header identifying your agent profile, as the spec requires:
UCP-Agent: profile="https://your-agent.example/profile"Requests without it are still served, but the header is how we know who is calling.
Search
curl -X POST https://fluf.io/wp-json/fluf/ucp/v1/catalog/search \
-H 'Content-Type: application/json' \
-H 'UCP-Agent: profile="https://your-agent.example/profile"' \
-d '{
"query": "vintage Ralph Lauren corduroy jacket",
"filters": { "price": { "max": { "amount": 10000, "currency": "GBP" } }, "categories": ["Jackets"] },
"pagination": { "limit": 20 }
}'const res = await fetch("https://fluf.io/wp-json/fluf/ucp/v1/catalog/search", {
method: "POST",
headers: {
"Content-Type": "application/json",
"UCP-Agent": "profile=\"https://your-agent.example/profile\"",
},
body: JSON.stringify({
"query": "vintage Ralph Lauren corduroy jacket",
"filters": {
"price": {
"max": {
"amount": 10000,
"currency": "GBP"
}
},
"categories": [
"Jackets"
]
},
"pagination": {
"limit": 20
}
}),
});
const data = await res.json();import requests
res = requests.post(
"https://fluf.io/wp-json/fluf/ucp/v1/catalog/search",
headers={
"UCP-Agent": "profile=\"https://your-agent.example/profile\"",
},
json={
"query": "vintage Ralph Lauren corduroy jacket",
"filters": {
"price": {"max": {"amount": 10000, "currency": "GBP"}},
"categories": ["Jackets"],
},
"pagination": {"limit": 20},
},
)
data = res.json()<?php
// composer require guzzlehttp/guzzle
$client = new GuzzleHttp\Client();
$res = $client->request('POST', 'https://fluf.io/wp-json/fluf/ucp/v1/catalog/search', [
'headers' => [
'UCP-Agent' => 'profile="https://your-agent.example/profile"',
],
'json' => [
'query' => 'vintage Ralph Lauren corduroy jacket',
'filters' => [
'price' => ['max' => ['amount' => 10000, 'currency' => 'GBP']],
'categories' => ['Jackets'],
],
'pagination' => ['limit' => 20],
],
]);
$data = json_decode((string) $res->getBody(), true);Returns the standard UCP envelope with a products array and cursor pagination:
{
"ucp": { "version": "2026-08-25", "capabilities": { "dev.ucp.shopping.catalog.search": [{ "version": "2026-08-25" }] } },
"products": [ { "id": "fluf_12345", "title": "…", "price_range": { "min": { "amount": 7200, "currency": "GBP" }, "max": { … } }, "variants": [ { "id": "fluf_12345_0", "price": { … }, "availability": { "available": true, "status": "in_stock" }, "url": "https://fluf.io/catalogue/…" } ] } ],
"pagination": { "has_next_page": true, "cursor": "Mg==", "total_count": 418 }
}queryis free text and ranks by relevance.filters.priceamounts are ISO 4217 minor units (10000= £100.00). A bound in another currency is converted to GBP for filtering and an info message says so.filters.categoriestakes a category name or path; the first value is applied.pagination.limitis capped at 48. Pass the returnedcursorfor the next page.
Lookup
curl -X POST https://fluf.io/wp-json/fluf/ucp/v1/catalog/lookup \
-H 'Content-Type: application/json' \
-d '{ "ids": ["fluf_12345", "fluf_67890_0"] }'const res = await fetch("https://fluf.io/wp-json/fluf/ucp/v1/catalog/lookup", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
"ids": [
"fluf_12345",
"fluf_67890_0"
]
}),
});
const data = await res.json();import requests
res = requests.post(
"https://fluf.io/wp-json/fluf/ucp/v1/catalog/lookup",
json={"ids": ["fluf_12345", "fluf_67890_0"]},
)
data = res.json()<?php
// composer require guzzlehttp/guzzle
$client = new GuzzleHttp\Client();
$res = $client->request('POST', 'https://fluf.io/wp-json/fluf/ucp/v1/catalog/lookup', [
'json' => ['ids' => ['fluf_12345', 'fluf_67890_0']],
]);
$data = json_decode((string) $res->getBody(), true);Product ids are fluf_, variant ids fluf_; a bare numeric id also works. Items that have sold since you found them are simply absent from the response, with an info message counting them. Up to 50 ids per call.
POST /catalog/product with { "id": "fluf_12345" } returns one product in full. Secondhand items have no option axes, so selected and preferences are accepted and ignored.
What a product carries
Each UCP Product has exactly one Variant. Beyond the standard fields (title, description, price, media, categories, availability, seller):
| Field | |
|---|---|
options | Size, Colour and Condition as selected options on the variant |
categories | The FLUF category path (taxonomy: merchant) and, where mapped, the Google product category |
metadata.condition | Machine value: new, like_new, used_excellent, used_good, used_fair, used |
metadata.brand, metadata.department | As listed by the seller |
metadata.source_channel | The marketplace the item was imported from |
metadata.source_url | Where present, the item's original listing |
metadata.quantity, metadata.unique_item | Always 1 and true |
list_price | Present when the item is discounted from its regular price |
Prices are in the seller's own currency. Availability is as of the last sync with the seller's channels, so re-check an item with lookup or get_product immediately before presenting it as available.
MCP
The same capabilities are available as a stateless Streamable HTTP MCP server at https://fluf.io/wp-json/fluf/ucp/v1/mcp, with the tool names the UCP MCP binding defines: search_catalog, lookup_catalog, get_product. Arguments follow the binding: { "meta": { "ucp-agent": { "profile": "…" } }, "catalog": { … } }, where catalog is the same body the REST endpoint takes. Results come back in structuredContent.
This is a public catalogue server and is separate from the fluf-mcp package, which works a seller's own account with a token.
Limits
- 120 requests per minute per IP, then
429. - Search is capped at 10,000 results deep; narrow the query instead of paging further.
- Transport errors are HTTP
400/429/500; business outcomes are HTTP200with amessagesarray, per the spec.
Sellers
Your in-stock items are included automatically if they appear in the FLUF public catalogue. To opt a store out, email [email protected].
Something missing? Email [email protected] — we prioritise by what people actually ask for.
