WEM Price Compare
What this MCP does
Searches and compares retailer product offers, prices, ratings, shipping, price history, and verification evidence.
Tools
Input schema
{'type': 'object', 'anyOf': [{'required': ['gtin']}, {'required': ['slug']}, {'required': ['wem_id']}, {'required': ['title']}], 'properties': {'gtin': {'type': 'string', 'description': 'Product barcode: EAN-13, UPC-A, EAN-8 or GTIN-14. Preferred key.'}, 'slug': {'type': 'string', 'description': 'WEM catalogue slug, or a wem3.ai/pl/{slug} URL / host path. Hosts may pass either form.'}, 'title': {'type': 'string', 'description': 'Product or model name, or a bare MPN / merchant SKU (e.g. AF400UK). Weakest identifier — used only when no barcode, slug, or WEM ID is available. Same relevance gate as verify_offer; a miss means fall back to search_products, not a guess.'}, 'wem_id': {'type': 'string', 'description': 'WEM ID (W + 10 Crockford characters + check). Active catalogue products only.'}, 'currency': {'type': 'string', 'description': "ISO 4217 code for the shopper's market. Default GBP. Decides which retailers are searched, not just how the answer reads — pass it explicitly, because WEM does not infer the market from IP. When WEM holds the product only in another currency it says so rather than presenting a foreign listing as the answer."}}}
Output schema
{'type': 'object', 'required': ['found'], 'properties': {'next': {'type': 'object', 'properties': {'url': {'type': 'string'}, 'reason': {'type': 'string'}, 'surface': {'enum': ['chrome_extension'], 'type': 'string'}}, 'description': "Where to send the shopper when WEM's set is thin. chrome_extension means compare the same product on any retailer page — WEM shows that price even without an affiliate programme."}, 'found': {'type': 'boolean', 'description': 'False when the product is not in WEM’s catalogue. The rest of the fields below are then absent — do not read that as the product being unavailable or unpriced. Check `unavailable` before describing the result, then follow `nextTool`.'}, 'links': {'type': 'array', 'items': {'type': 'object', 'properties': {'url': {'type': 'string', 'description': 'WEM tracked redirect. Relay it exactly; never rewrite or shorten it.'}, 'price': {'type': 'number'}, 'currency': {'type': 'string'}, 'markdown': {'type': 'string', 'description': 'The same link as `[Retailer — £0.00](url)`, for hosts rendering markdown.'}, 'retailer': {'type': 'string'}}}, 'description': 'The tracked links from `offers`, cheapest first, pre-formatted to quote. Give these to the user when you name an offer: they carry the attribution WEM is funded by, and a retailer URL you compose yourself does not. Present even when the host renders a WEM card — never assume the card reached the user.'}, 'offers': {'type': 'array', 'items': {'type': 'object', 'properties': {'id': {'type': 'string'}, 'url': {'type': 'string', 'description': 'WEM tracked link to the retailer. Send the user here — WEM never takes payment.'}, 'brand': {'type': ['string', 'null']}, 'image': {'type': ['string', 'null']}, 'price': {'type': 'number', 'description': 'Indicative price. The retailer sets the final price at checkout.'}, 'title': {'type': 'string'}, 'rating': {'type': ['number', 'null']}, 'seller': {'type': ['string', 'null']}, 'channel': {'enum': ['retailer', 'marketplace'], 'type': 'string', 'description': 'retailer is a named shop (Boots, Currys). marketplace is eBay/AliExpress/Temu-style parallel listings. Do not present a marketplace cluster as competing authorised retailers.'}, 'inStock': {'type': ['boolean', 'null']}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}, 'provider': {'type': 'string', 'description': 'Retailer slug, e.g. "ebay", "currys".'}, 'shipping': {'type': ['object', 'null'], 'properties': {'cost': {'type': ['number', 'null']}, 'free': {'type': 'boolean'}, 'estimate': {'type': ['string', 'null'], 'description': 'Delivery window when the feed stated one.'}}}, 'verified': {'type': 'boolean'}, 'affiliate': {'type': 'boolean', 'description': 'False when this is a retailer page WEM observed without a programme. Still a real listing; the click is not commission-bearing. Absent means the usual partner path.'}, 'unitPrice': {'type': 'object', 'properties': {'per': {'enum': ['100ml', 'litre', '100g', 'kg', 'item'], 'type': 'string'}, 'amount': {'type': 'number'}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}}, 'description': 'Price per 100ml, 100g, litre, kg or item, from the size this row\'s title states. Present only when the rows differ in size, in one unit across all of them, and only for things bought by the amount (beauty, food, supplements), never an appliance\'s capacity. When comparing value across sizes, compare these, never the prices: a smaller size is not cheaper because its price is lower. Quote it beside the price ("£29.95, £99.83 per 100ml"). Absent does not mean the price is per unit.'}, 'lastSeenAt': {'type': ['string', 'null'], 'description': 'When WEM last VISITED this offer row — not when it read the price. The refresh sweep touches this even when the retailer lookup fails, so it is not evidence the price is current. Use `priceAgeDays` to date a price; never this.'}, 'reviewCount': {'type': ['number', 'null']}, 'priceAgeDays': {'type': ['number', 'null'], 'description': 'Whole days since WEM last actually READ this price from the retailer or a datafeed. Null means WEM cannot say — common and correct for Amazon, whose licence caps price retention at 24 hours. Report null as undated; never present it as current.'}, 'priceRefresh': {'enum': ['live-api', 'feed', 'crawl', 'none'], 'type': 'string', 'description': 'What re-reads this retailer’s prices: `live-api` a product-lookup API, `feed` a partner datafeed, `crawl` WEM reading the retailer’s own public product page, `none` nothing at all. `none` means the figure is frozen at whatever seeded it and will not move however long it sits — say so rather than quoting it flat. A `crawl` price was observed on the retailer’s page rather than supplied by them, so date it with `priceAgeDays` and expect `affiliate: false` unless a programme also exists.'}, 'priceQualifier': {'enum': ['from'], 'type': 'string', 'description': 'Present when `price` is the OPENING price of a range, not the price of this item: a multi-variation listing where the seller advertises its cheapest variant and the shopper picks a size on the page. Quote it as "from £X", never as the price or the cheapest. Absent means the price is firm for the row as described.'}}}, 'description': 'Ascending by price. Barcode, slug and WEM ID rows are the product. A title match is inferred — check identity.strength before stating it as exact.'}, 'reason': {'type': 'string', 'description': 'Present only when `found` is false: which identifier missed, and why. When `unavailable` is true this describes the outage rather than a missing product — quote it as the reason the lookup failed, not as a fact about the product.'}, 'search': {'type': 'object', 'properties': {'url': {'type': 'string'}, 'reason': {'type': 'string'}, 'markdown': {'type': 'string', 'description': 'The same link, pre-formatted.'}}, 'description': 'Present when this result names no offer. GIVE THE USER THIS LINK — it is the answer when WEM has nothing else to say, and `markdown` is ready to paste. WEM searches retailers live on that page, including shops it holds no affiliate programme with, so an empty or withheld result here is not evidence the product is unavailable or unpriced.'}, 'source': {'type': 'string'}, 'product': {'type': 'object', 'required': ['slug', 'title'], 'properties': {'gtin': {'type': ['string', 'null'], 'description': 'The barcode identity was resolved on.'}, 'slug': {'type': 'string'}, 'brand': {'type': ['string', 'null']}, 'image': {'type': ['string', 'null'], 'description': 'Product photo, served from wem3.ai. Null when none is available — do not substitute one.'}, 'title': {'type': 'string'}, 'wem_id': {'type': ['string', 'null'], 'description': 'WEM ID of this active product. Omit when unknown; never invent one.'}}}, 'ranking': {'type': 'object', 'properties': {'basis': {'type': 'string'}, 'reason': {'type': 'string'}, 'parameters': {'type': 'string', 'description': 'The published ranking parameters.'}}, 'description': 'Why the offers are in this order. Offers are ordered by price, lowest first; affiliate commission is not an input.'}, 'coverage': {'type': 'object', 'properties': {'low': {'type': 'number'}, 'high': {'type': 'number'}, 'kind': {'enum': ['marketplace_only'], 'type': 'string'}, 'currency': {'type': 'string'}, 'listings': {'type': 'number'}, 'provider': {'type': 'string', 'description': 'ebay, aliexpress, or marketplace when mixed.'}}, 'description': 'Present when surviving rows include a marketplace cluster at similar prices. That is not a retail floor — do not name the cheapest marketplace listing as the deal. The listings are still in the payload; give the user those links. If `next` points at the Chrome extension, send the shopper there for shops WEM does not yet hold as partners.'}, 'currency': {'type': ['string', 'null']}, 'degraded': {'type': 'object', 'properties': {'note': {'type': 'string'}, 'skipped': {'type': 'array', 'items': {'type': 'string'}}}, 'description': 'Present when this call hit its time budget and skipped enrichment. The offers returned are COMPLETE — only the extras were dropped. Do not report a missing catalogue block or missing observed retailers as WEM holding nothing, and do not retry automatically.'}, 'identity': {'type': ['object', 'null'], 'properties': {'method': {'enum': ['gtin', 'slug', 'listing', 'title'], 'type': 'string'}, 'strength': {'enum': ['exact', 'catalogued', 'inferred'], 'type': 'string', 'description': 'exact = barcode; catalogued = a WEM key or listing id; inferred = matched on the product name, MPN or SKU and still a guess.'}}, 'description': 'How the shopper’s words reached this product. Decides how you describe the match: on `inferred`, say WEM matched it by name (not by barcode) and do not call the identity confirmed, even when `identityBasis` is `barcode`; on `exact`, the barcode itself resolved it.'}, 'lowPrice': {'type': ['number', 'null']}, 'nextTool': {'type': 'object', 'properties': {'tool': {'enum': ['search_products'], 'type': 'string'}, 'query': {'type': 'string'}}, 'description': 'Present only when `found` is false: the call to make next, with the text to pass. This is the recovery path, not a suggestion — a named model that misses the catalogue is the ordinary case, and the live search is where its offers are.'}, 'highPrice': {'type': ['number', 'null']}, 'disclosure': {'type': 'string', 'description': 'Disclosure to relay once per answer, verbatim. Its wording changes with the rows: it states whether every outbound link is affiliate-tracked, only some are, or none are. Never substitute the version you saw last time — a row marked `affiliate: false` earns WEM nothing, and saying otherwise misdescribes it to the shopper.'}, 'unavailable': {'type': 'boolean', 'description': 'Present and true only when WEM could not reach its catalogue at all. `found` is false for the same reason it is on an ordinary miss, so the two are indistinguishable without this flag. When it is set, WEM does not know whether it holds the product: say the lookup could not be completed, never that WEM has no offers, no price, or does not stock it. Still call `nextTool` — the live retailer search does not depend on the catalogue.'}, 'priceHistory': {'type': 'object', 'properties': {'low': {'type': ['number', 'null']}, 'days': {'type': 'number'}}, 'description': 'The 90-day low, for telling a real discount from a repackaged one.'}, 'identityBasis': {'enum': ['barcode', 'curated-grouping'], 'type': 'string', 'description': 'How this product’s offers are grouped — NOT how the shopper’s words found the product; that is `identity`. `barcode`: the product carries a GTIN, so every offer here is that same item. `curated-grouping`: it carries none, and the grouping is an inference. Never call a result a barcode match on the strength of this field: when `identity.strength` is `inferred`, say WEM matched the product by name.'}, 'lastConfirmedAt': {'type': ['string', 'null'], 'description': 'When WEM last actually read any price in this answer. Null means none of them can be dated — say so rather than implying the answer is current.'}}, 'description': 'Every verified retailer offer for one catalogue product, cheapest first. When `found` is false WEM simply does not hold this product yet — that is a normal answer, not a failure, and it says nothing about whether the product exists or what it costs. Call `search_products` with `nextTool.query` before telling the user anything; reporting "WEM returns nothing" without doing so is wrong, because the live retailer search routinely finds supply this catalogue has not ingested.'}
Input schema
{'type': 'object', 'required': ['products'], 'properties': {'products': {'type': 'array', 'items': {'type': 'object', 'required': ['provider', 'product_id'], 'properties': {'provider': {'type': 'string'}, 'product_id': {'type': 'string'}}}, 'maxItems': 5, 'minItems': 2, 'description': 'List of products to compare (2-5 items)'}}}
Output schema
{'type': 'object', 'required': ['comparison', 'count', 'disclosure'], 'properties': {'count': {'type': 'number'}, 'comparison': {'type': 'array', 'items': {'type': 'object', 'properties': {'id': {'type': 'string'}, 'url': {'type': 'string', 'description': 'WEM tracked link to the retailer. Send the user here — WEM never takes payment.'}, 'brand': {'type': ['string', 'null']}, 'image': {'type': ['string', 'null']}, 'price': {'type': 'number', 'description': 'Indicative price. The retailer sets the final price at checkout.'}, 'title': {'type': 'string'}, 'badges': {'type': ['array', 'null'], 'items': {'type': 'string'}}, 'rating': {'type': ['number', 'null']}, 'seller': {'type': ['string', 'null']}, 'channel': {'enum': ['retailer', 'marketplace'], 'type': 'string', 'description': 'retailer is a named shop (Boots, Currys). marketplace is eBay/AliExpress/Temu-style parallel listings. Do not present a marketplace cluster as competing authorised retailers.'}, 'inStock': {'type': ['boolean', 'null']}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}, 'features': {'type': ['array', 'null'], 'items': {'type': 'string'}}, 'provider': {'type': 'string', 'description': 'Retailer slug, e.g. "ebay", "currys".'}, 'shipping': {'type': ['object', 'null'], 'properties': {'cost': {'type': ['number', 'null']}, 'free': {'type': 'boolean'}, 'estimate': {'type': ['string', 'null'], 'description': 'Delivery window when the feed stated one.'}}}, 'affiliate': {'type': 'boolean', 'description': 'False when this is a retailer page WEM observed without a programme. Still a real listing; the click is not commission-bearing. Absent means the usual partner path.'}, 'unitPrice': {'type': 'object', 'properties': {'per': {'enum': ['100ml', 'litre', '100g', 'kg', 'item'], 'type': 'string'}, 'amount': {'type': 'number'}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}}, 'description': 'Price per 100ml, 100g, litre, kg or item, from the size this row\'s title states. Present only when the rows differ in size, in one unit across all of them, and only for things bought by the amount (beauty, food, supplements), never an appliance\'s capacity. When comparing value across sizes, compare these, never the prices: a smaller size is not cheaper because its price is lower. Quote it beside the price ("£29.95, £99.83 per 100ml"). Absent does not mean the price is per unit.'}, 'reviewCount': {'type': ['number', 'null']}, 'priceQualifier': {'enum': ['from'], 'type': 'string', 'description': 'Present when `price` is the OPENING price of a range, not the price of this item: a multi-variation listing where the seller advertises its cheapest variant and the shopper picks a size on the page. Quote it as "from £X", never as the price or the cheapest. Absent means the price is firm for the row as described.'}}}}, 'disclosure': {'type': 'string', 'description': 'Disclosure to relay once per answer, verbatim. Its wording changes with the rows: it states whether every outbound link is affiliate-tracked, only some are, or none are. Never substitute the version you saw last time — a row marked `affiliate: false` earns WEM nothing, and saying otherwise misdescribes it to the shopper.'}}}
Input schema
{'type': 'object', 'required': ['query'], 'properties': {'query': {'type': 'string', 'description': 'Product name, barcode, ASIN, WEM ID, or wem3.ai/pl URL. A specific model should prefer compare_offers; this still resolves one if the host sends it here.'}, 'currency': {'type': 'string', 'description': "ISO 4217 code to quote in. Default GBP. Offers in other currencies are withheld, never converted. Pass the shopper's market explicitly — WEM does not infer currency or retailer market from IP."}, 'max_price': {'type': 'number', 'description': 'Budget cap (GBP)'}, 'priorities': {'type': 'array', 'items': {'enum': ['cheapest', 'best_rated', 'free_shipping', 'fastest'], 'type': 'string'}, 'description': 'What matters most (in order of importance)'}, 'include_used': {'type': 'boolean', 'description': 'If true, include used and refurbished listings. Default false — a used item is a different good, not a cheaper one.'}}}
Output schema
{'type': 'object', 'required': ['recommendation', 'reason'], 'properties': {'next': {'type': 'object', 'properties': {'url': {'type': 'string'}, 'reason': {'type': 'string'}, 'surface': {'enum': ['chrome_extension'], 'type': 'string'}}, 'description': "Where to send the shopper when WEM's set is thin. chrome_extension means compare the same product on any retailer page — WEM shows that price even without an affiliate programme."}, 'score': {'type': 'number', 'description': 'Internal ranking score. Not a price and not a rating — do not quote it.'}, 'reason': {'type': 'string', 'description': 'Why this was picked, or why nothing was.'}, 'search': {'type': 'object', 'properties': {'url': {'type': 'string'}, 'reason': {'type': 'string'}, 'markdown': {'type': 'string', 'description': 'The same link, pre-formatted.'}}, 'description': 'Present when this result names no offer. GIVE THE USER THIS LINK — it is the answer when WEM has nothing else to say, and `markdown` is ready to paste. WEM searches retailers live on that page, including shops it holds no affiliate programme with, so an empty or withheld result here is not evidence the product is unavailable or unpriced.'}, 'partial': {'type': 'boolean', 'description': 'True when the live retailer fan-out hit its 3-second budget and some providers had not answered. The recommendation is the lowest among those that did. Do not call it the market floor, and do not retry automatically.'}, 'coverage': {'type': 'object', 'properties': {'low': {'type': 'number'}, 'high': {'type': 'number'}, 'kind': {'enum': ['marketplace_only'], 'type': 'string'}, 'currency': {'type': 'string'}, 'listings': {'type': 'number'}, 'provider': {'type': 'string', 'description': 'ebay, aliexpress, or marketplace when mixed.'}}, 'description': 'Present when surviving rows include a marketplace cluster at similar prices. That is not a retail floor — do not name the cheapest marketplace listing as the deal. The listings are still in the payload; give the user those links. If `next` points at the Chrome extension, send the shopper there for shops WEM does not yet hold as partners.'}, 'degraded': {'type': 'object', 'properties': {'note': {'type': 'string'}, 'skipped': {'type': 'array', 'items': {'type': 'string'}}}, 'description': 'Present when this call hit its time budget and skipped enrichment. The offers returned are COMPLETE — only the extras were dropped. Do not report a missing catalogue block or missing observed retailers as WEM holding nothing, and do not retry automatically.'}, 'filtered': {'type': ['object', 'null'], 'description': 'Withheld candidates tallied by reason (e.g. accessories, wrong model). Report this count rather than implying the search was exhaustive.', 'additionalProperties': {'type': 'number'}}, 'disclosure': {'type': 'string', 'description': 'Disclosure to relay once per answer, verbatim. Its wording changes with the rows: it states whether every outbound link is affiliate-tracked, only some are, or none are. Never substitute the version you saw last time — a row marked `affiliate: false` earns WEM nothing, and saying otherwise misdescribes it to the shopper.'}, 'alternatives': {'type': 'array', 'items': {'type': 'object', 'properties': {'id': {'type': 'string'}, 'url': {'type': 'string', 'description': 'WEM tracked link to the retailer. Send the user here — WEM never takes payment.'}, 'brand': {'type': ['string', 'null']}, 'image': {'type': ['string', 'null']}, 'price': {'type': 'number', 'description': 'Indicative price. The retailer sets the final price at checkout.'}, 'title': {'type': 'string'}, 'rating': {'type': ['number', 'null']}, 'seller': {'type': ['string', 'null']}, 'channel': {'enum': ['retailer', 'marketplace'], 'type': 'string', 'description': 'retailer is a named shop (Boots, Currys). marketplace is eBay/AliExpress/Temu-style parallel listings. Do not present a marketplace cluster as competing authorised retailers.'}, 'inStock': {'type': ['boolean', 'null']}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}, 'provider': {'type': 'string', 'description': 'Retailer slug, e.g. "ebay", "currys".'}, 'shipping': {'type': ['object', 'null'], 'properties': {'cost': {'type': ['number', 'null']}, 'free': {'type': 'boolean'}, 'estimate': {'type': ['string', 'null'], 'description': 'Delivery window when the feed stated one.'}}}, 'affiliate': {'type': 'boolean', 'description': 'False when this is a retailer page WEM observed without a programme. Still a real listing; the click is not commission-bearing. Absent means the usual partner path.'}, 'unitPrice': {'type': 'object', 'properties': {'per': {'enum': ['100ml', 'litre', '100g', 'kg', 'item'], 'type': 'string'}, 'amount': {'type': 'number'}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}}, 'description': 'Price per 100ml, 100g, litre, kg or item, from the size this row\'s title states. Present only when the rows differ in size, in one unit across all of them, and only for things bought by the amount (beauty, food, supplements), never an appliance\'s capacity. When comparing value across sizes, compare these, never the prices: a smaller size is not cheaper because its price is lower. Quote it beside the price ("£29.95, £99.83 per 100ml"). Absent does not mean the price is per unit.'}, 'reviewCount': {'type': ['number', 'null']}, 'priceQualifier': {'enum': ['from'], 'type': 'string', 'description': 'Present when `price` is the OPENING price of a range, not the price of this item: a multi-variation listing where the seller advertises its cheapest variant and the shopper picks a size on the page. Quote it as "from £X", never as the price or the cheapest. Absent means the price is firm for the row as described.'}}}, 'description': 'Listings still worth showing, including when recommendation is null.'}, 'catalogMatch': {'type': ['object', 'null'], 'properties': {'slug': {'type': 'string'}, 'title': {'type': 'string'}, 'offers': {'type': 'array', 'items': {'type': 'object', 'properties': {'id': {'type': 'string'}, 'url': {'type': 'string', 'description': 'WEM tracked link to the retailer. Send the user here — WEM never takes payment.'}, 'brand': {'type': ['string', 'null']}, 'image': {'type': ['string', 'null']}, 'price': {'type': 'number', 'description': 'Indicative price. The retailer sets the final price at checkout.'}, 'title': {'type': 'string'}, 'rating': {'type': ['number', 'null']}, 'seller': {'type': ['string', 'null']}, 'channel': {'enum': ['retailer', 'marketplace'], 'type': 'string', 'description': 'retailer is a named shop (Boots, Currys). marketplace is eBay/AliExpress/Temu-style parallel listings. Do not present a marketplace cluster as competing authorised retailers.'}, 'inStock': {'type': ['boolean', 'null']}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}, 'provider': {'type': 'string', 'description': 'Retailer slug, e.g. "ebay", "currys".'}, 'shipping': {'type': ['object', 'null'], 'properties': {'cost': {'type': ['number', 'null']}, 'free': {'type': 'boolean'}, 'estimate': {'type': ['string', 'null'], 'description': 'Delivery window when the feed stated one.'}}}, 'verified': {'type': 'boolean', 'description': 'True when WEM has recently observed this price on the retailer’s own surface. False means the identity is still catalogue-resolved but the price is indicative (partner feed or stale) — do not present it as verified.'}, 'affiliate': {'type': 'boolean', 'description': 'False when this is a retailer page WEM observed without a programme. Still a real listing; the click is not commission-bearing. Absent means the usual partner path.'}, 'unitPrice': {'type': 'object', 'properties': {'per': {'enum': ['100ml', 'litre', '100g', 'kg', 'item'], 'type': 'string'}, 'amount': {'type': 'number'}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}}, 'description': 'Price per 100ml, 100g, litre, kg or item, from the size this row\'s title states. Present only when the rows differ in size, in one unit across all of them, and only for things bought by the amount (beauty, food, supplements), never an appliance\'s capacity. When comparing value across sizes, compare these, never the prices: a smaller size is not cheaper because its price is lower. Quote it beside the price ("£29.95, £99.83 per 100ml"). Absent does not mean the price is per unit.'}, 'lastSeenAt': {'type': ['string', 'null'], 'description': 'When WEM last VISITED this offer row — not when it read the price. The refresh sweep touches this even when the retailer lookup fails, so it is not evidence the price is current. Use `priceAgeDays` to date a price; never this.'}, 'reviewCount': {'type': ['number', 'null']}, 'priceAgeDays': {'type': ['number', 'null'], 'description': 'Whole days since WEM last actually READ this price from the retailer or a datafeed. Null means WEM cannot say — common and correct for Amazon, whose licence caps price retention at 24 hours. Report null as undated; never present it as current.'}, 'priceRefresh': {'enum': ['live-api', 'feed', 'crawl', 'none'], 'type': 'string', 'description': 'What re-reads this retailer’s prices: `live-api` a product-lookup API, `feed` a partner datafeed, `crawl` WEM reading the retailer’s own public product page, `none` nothing at all. `none` means the figure is frozen at whatever seeded it and will not move however long it sits — say so rather than quoting it flat. A `crawl` price was observed on the retailer’s page rather than supplied by them, so date it with `priceAgeDays` and expect `affiliate: false` unless a programme also exists.'}, 'priceQualifier': {'enum': ['from'], 'type': 'string', 'description': 'Present when `price` is the OPENING price of a range, not the price of this item: a multi-variation listing where the seller advertises its cheapest variant and the shopper picks a size on the page. Quote it as "from £X", never as the price or the cheapest. Absent means the price is firm for the row as described.'}}}}, 'source': {'type': 'string'}, 'currency': {'type': ['string', 'null']}, 'identity': {'type': ['object', 'null'], 'properties': {'method': {'enum': ['gtin', 'slug', 'listing', 'title'], 'type': 'string'}, 'strength': {'enum': ['exact', 'catalogued', 'inferred'], 'type': 'string', 'description': 'exact = barcode; catalogued = a WEM key or listing id; inferred = matched on the product name, MPN or SKU and still a guess.'}}, 'description': 'How the shopper’s words reached this product. Decides how you describe the match: on `inferred`, say WEM matched it by name (not by barcode) and do not call the identity confirmed, even when `identityBasis` is `barcode`; on `exact`, the barcode itself resolved it.'}, 'lowPrice': {'type': ['number', 'null']}, 'productPage': {'type': 'string'}, 'identityBasis': {'enum': ['barcode', 'curated-grouping'], 'type': 'string', 'description': 'What this block’s identity rests on. `barcode` — the canonical product carries a GTIN and these offers are the same physical item. `curated-grouping` — it carries none, so the grouping is an inference of the same kind a title match is; do not describe it to the user as barcode-confirmed, and do not let it outrank a cheaper row in `products` on price alone. This is how the offers are grouped, not how the query matched the product — see `identity`.'}, 'lastConfirmedAt': {'type': ['string', 'null'], 'description': 'When WEM last actually read any price in this block. Null means none of them can be dated — say so rather than implying the block is current.'}, 'cheaperElsewhere': {'type': ['object', 'null'], 'properties': {'note': {'type': 'string', 'description': 'Plain-language restatement, safe to relay.'}, 'identity': {'enum': ['inferred'], 'type': 'string', 'description': 'Always `inferred` — matched on title, not barcode. Say so when quoting it.'}, 'verified': {'type': 'boolean', 'description': 'Always false.'}, 'savingVsCatalogue': {'type': 'number', 'description': 'How much cheaper this row is than the block’s lowest offer.'}}, 'description': 'Present only on a `curated-grouping` block that a live row in `products` undercuts. WEM is telling you its own catalogue block is not the best price it found. Quote this row as the cheaper option with its identity stated as unconfirmed; never present the catalogMatch price as the lowest when this is set.'}}, 'description': 'Offers WEM holds under one catalogue product. Check `identityBasis`: on `barcode` the product carries a GTIN and every offer here is that same item, so prefer them and cite their prices over anything in `products`. On `curated-grouping` the product carries no barcode, the grouping is an inference like any title match, and a cheaper row in `products` may well be the same item — see `cheaperElsewhere`. Whether the shopper’s words reached this product by barcode or by name is `identity`, a separate question: on `inferred`, say WEM matched it by name.'}, 'recommendation': {'type': ['object', 'null'], 'properties': {'id': {'type': 'string'}, 'url': {'type': 'string', 'description': 'WEM tracked link to the retailer. Send the user here — WEM never takes payment.'}, 'brand': {'type': ['string', 'null']}, 'image': {'type': ['string', 'null']}, 'price': {'type': 'number', 'description': 'Indicative price. The retailer sets the final price at checkout.'}, 'title': {'type': 'string'}, 'rating': {'type': ['number', 'null']}, 'seller': {'type': ['string', 'null']}, 'channel': {'enum': ['retailer', 'marketplace'], 'type': 'string', 'description': 'retailer is a named shop (Boots, Currys). marketplace is eBay/AliExpress/Temu-style parallel listings. Do not present a marketplace cluster as competing authorised retailers.'}, 'inStock': {'type': ['boolean', 'null']}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}, 'provider': {'type': 'string', 'description': 'Retailer slug, e.g. "ebay", "currys".'}, 'shipping': {'type': ['object', 'null'], 'properties': {'cost': {'type': ['number', 'null']}, 'free': {'type': 'boolean'}, 'estimate': {'type': ['string', 'null'], 'description': 'Delivery window when the feed stated one.'}}}, 'verified': {'type': 'boolean', 'description': 'True when WEM resolved this offer’s identity AND recently observed its price on the retailer’s surface. A recommendation can be unverified and still the best candidate — report it as such.'}, 'affiliate': {'type': 'boolean', 'description': 'False when this is a retailer page WEM observed without a programme. Still a real listing; the click is not commission-bearing. Absent means the usual partner path.'}, 'unitPrice': {'type': 'object', 'properties': {'per': {'enum': ['100ml', 'litre', '100g', 'kg', 'item'], 'type': 'string'}, 'amount': {'type': 'number'}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}}, 'description': 'Price per 100ml, 100g, litre, kg or item, from the size this row\'s title states. Present only when the rows differ in size, in one unit across all of them, and only for things bought by the amount (beauty, food, supplements), never an appliance\'s capacity. When comparing value across sizes, compare these, never the prices: a smaller size is not cheaper because its price is lower. Quote it beside the price ("£29.95, £99.83 per 100ml"). Absent does not mean the price is per unit.'}, 'reviewCount': {'type': ['number', 'null']}, 'priceQualifier': {'enum': ['from'], 'type': 'string', 'description': 'Present when `price` is the OPENING price of a range, not the price of this item: a multi-variation listing where the seller advertises its cheapest variant and the shopper picks a size on the page. Quote it as "from £X", never as the price or the cheapest. Absent means the price is firm for the row as described.'}}, 'description': 'The single lowest-priced plausible match, or null when nothing matched confidently.'}, 'liveSearchSkipped': {'type': 'boolean', 'description': 'True when a catalogue hit answered the question without spending retailer API quota.'}, 'pending_providers': {'type': 'array', 'items': {'type': 'string'}, 'description': 'Providers still running when the budget expired. Present only with partial. Their prices are not in this answer.'}}, 'description': 'A null `recommendation` with `coverage.kind` marketplace_only still has listings in `alternatives` — give those links. A null with no alternatives is a genuine miss. Never soften a genuine miss into a suggestion.'}
Input schema
{'type': 'object', 'properties': {}}
Output schema
{'type': 'object', 'required': ['categories', 'providers', 'currency'], 'properties': {'currency': {'type': 'string'}, 'providers': {'type': 'array', 'items': {'type': 'object', 'properties': {'name': {'type': 'string'}, 'displayName': {'type': 'string'}}}, 'description': 'Retailers currently enabled. WEM compares only feeds it is licensed to use.'}, 'categories': {'type': 'array', 'items': {'type': 'object', 'properties': {'name': {'type': 'string'}, 'examples': {'type': 'string'}}}}}}
Input schema
{'type': 'object', 'required': ['id'], 'properties': {'id': {'type': 'string', 'description': 'Receipt id, as returned on verify_offer as receipt.id (wem-evr-...).'}}}
Output schema
{'type': 'object', 'required': ['id', 'standardVersion', 'observed_at', 'source', 'verifier', 'signed'], 'properties': {'id': {'type': 'string'}, 'amount': {'type': ['number', 'null']}, 'signed': {'type': 'boolean'}, 'source': {'type': 'string'}, 'verdict': {'type': 'string'}, 'currency': {'type': ['string', 'null']}, 'issuedAt': {'type': 'string'}, 'verifier': {'type': 'string'}, 'signature': {'type': ['string', 'null']}, 'observed_at': {'type': 'string'}, 'amount_minor': {'type': ['number', 'null']}, 'identity_method': {'type': ['string', 'null']}, 'standardVersion': {'type': 'string'}}, 'description': 'A previously issued evidence receipt, looked up by id.'}
Input schema
{'type': 'object', 'anyOf': [{'required': ['wem_id']}, {'required': ['provider', 'product_id']}], 'properties': {'wem_id': {'type': 'string', 'description': 'WEM ID (W + 10 Crockford characters + check). Active catalogue products only.'}, 'provider': {'type': 'string', 'description': 'Provider name (e.g. "ebay", "awin")'}, 'product_id': {'type': 'string', 'description': 'Product ID from search results'}}}
Output schema
{'type': 'object', 'required': ['url', 'disclosure'], 'properties': {'id': {'type': 'string'}, 'url': {'type': 'string', 'description': 'WEM tracked link to the retailer. Send the user here — WEM never takes payment.'}, 'brand': {'type': ['string', 'null']}, 'image': {'type': ['string', 'null']}, 'price': {'type': 'number', 'description': 'Indicative price. The retailer sets the final price at checkout.'}, 'title': {'type': 'string'}, 'badges': {'type': ['array', 'null'], 'items': {'type': 'string'}}, 'rating': {'type': ['number', 'null']}, 'seller': {'type': ['string', 'null']}, 'channel': {'enum': ['retailer', 'marketplace'], 'type': 'string', 'description': 'retailer is a named shop (Boots, Currys). marketplace is eBay/AliExpress/Temu-style parallel listings. Do not present a marketplace cluster as competing authorised retailers.'}, 'inStock': {'type': ['boolean', 'null']}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}, 'features': {'type': ['array', 'null'], 'items': {'type': 'string'}}, 'provider': {'type': 'string', 'description': 'Retailer slug, e.g. "ebay", "currys".'}, 'shipping': {'type': ['object', 'null'], 'properties': {'cost': {'type': ['number', 'null']}, 'free': {'type': 'boolean'}, 'estimate': {'type': ['string', 'null'], 'description': 'Delivery window when the feed stated one.'}}}, 'affiliate': {'type': 'boolean', 'description': 'False when this is a retailer page WEM observed without a programme. Still a real listing; the click is not commission-bearing. Absent means the usual partner path.'}, 'unitPrice': {'type': 'object', 'properties': {'per': {'enum': ['100ml', 'litre', '100g', 'kg', 'item'], 'type': 'string'}, 'amount': {'type': 'number'}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}}, 'description': 'Price per 100ml, 100g, litre, kg or item, from the size this row\'s title states. Present only when the rows differ in size, in one unit across all of them, and only for things bought by the amount (beauty, food, supplements), never an appliance\'s capacity. When comparing value across sizes, compare these, never the prices: a smaller size is not cheaper because its price is lower. Quote it beside the price ("£29.95, £99.83 per 100ml"). Absent does not mean the price is per unit.'}, 'disclosure': {'type': 'string', 'description': 'Disclosure to relay once per answer, verbatim. Its wording changes with the rows: it states whether every outbound link is affiliate-tracked, only some are, or none are. Never substitute the version you saw last time — a row marked `affiliate: false` earns WEM nothing, and saying otherwise misdescribes it to the shopper.'}, 'description': {'type': ['string', 'null']}, 'reviewCount': {'type': ['number', 'null']}, 'priceQualifier': {'enum': ['from'], 'type': 'string', 'description': 'Present when `price` is the OPENING price of a range, not the price of this item: a multi-variation listing where the seller advertises its cheapest variant and the shopper picks a size on the page. Quote it as "from £X", never as the price or the cheapest. Absent means the price is firm for the row as described.'}}, 'description': 'Full provider record for one product, plus a WEM tracked link.'}
Input schema
{'type': 'object', 'required': ['identifiers'], 'properties': {'currency': {'type': 'string', 'description': "ISO 4217 code for the shopper's market. Default GBP. Lowest prices are quoted in this currency only, never converted; a product WEM holds only in another currency comes back found with lowPrice null and heldInCurrencies set."}, 'identifiers': {'type': 'array', 'items': {'type': 'string'}, 'maxItems': 20, 'minItems': 1, 'description': '1-20 barcodes (EAN-13, UPC-A, EAN-8, GTIN-14), Amazon ASINs, WEM IDs, catalogue slugs or wem3.ai/pl URLs. Mixed kinds are fine; a comma-separated string is also accepted. Duplicates are looked up once. Each entry must be the identifier alone: not a product name, and not a name with a barcode in it.'}}}
Output schema
{'type': 'object', 'required': ['results', 'counts', 'currency', 'links', 'disclosure'], 'properties': {'next': {'type': 'object', 'properties': {'tool': {'type': 'string'}, 'reason': {'type': 'string'}}, 'description': 'The WEM tool that answers the follow-up question, and what to pass it.'}, 'links': {'type': 'array', 'items': {'type': 'object', 'properties': {'url': {'type': 'string'}, 'input': {'type': 'string'}, 'title': {'type': 'string'}, 'markdown': {'type': 'string'}}}, 'description': 'The WEM product-page links for found rows, pre-formatted to quote. Give these to the user when you name a found product.'}, 'counts': {'type': 'object', 'properties': {'found': {'type': 'number'}, 'notFound': {'type': 'number'}, 'overLimit': {'type': 'number', 'description': 'Identifiers beyond the per-call limit, not looked up.'}, 'requested': {'type': 'number'}, 'duplicates': {'type': 'number'}, 'notChecked': {'type': 'number'}, 'unrecognised': {'type': 'number'}}}, 'results': {'type': 'array', 'items': {'type': 'object', 'required': ['input', 'kind', 'status'], 'properties': {'kind': {'enum': ['gtin', 'asin', 'wem_id', 'slug', 'unrecognised'], 'type': 'string'}, 'note': {'type': 'string', 'description': 'Why a row is not found, unrecognised or not checked. Quote it rather than paraphrase.'}, 'input': {'type': 'string', 'description': 'The identifier as sent, for joining rows back.'}, 'status': {'enum': ['found', 'not_found', 'unrecognised', 'not_checked'], 'type': 'string', 'description': 'not_found: WEM does not hold it yet — never say the product does not exist or has no price; look it up with compare_offers or search_products first. not_checked: not reached in this call, and not a miss. unrecognised: not an identifier this tool reads (product names go to compare_offers).'}, 'product': {'type': 'object', 'required': ['slug', 'title', 'url'], 'properties': {'url': {'type': 'string', 'description': 'WEM product page listing every retailer WEM holds. Relay it exactly.'}, 'gtin': {'type': ['string', 'null']}, 'slug': {'type': 'string'}, 'title': {'type': 'string'}, 'wem_id': {'type': 'string'}}}, 'currency': {'type': 'string'}, 'identity': {'type': ['object', 'null'], 'properties': {'method': {'enum': ['gtin', 'slug', 'listing', 'title'], 'type': 'string'}, 'strength': {'enum': ['exact', 'catalogued', 'inferred'], 'type': 'string'}}}, 'lowPrice': {'type': ['number', 'null'], 'description': 'Lowest indicative price at a named retailer in `currency`. Null when WEM holds the product only in another currency (see heldInCurrencies) — never convert one yourself — or only as marketplace listings.'}, 'retailers': {'type': 'number', 'description': 'Distinct retailers holding it in `currency`. One retailer is a price, not a comparison — do not call it the cheapest.'}, 'lastConfirmedAt': {'type': ['string', 'null'], 'description': 'When WEM last actually read a price for this row. Null means none can be dated — say so.'}, 'heldInCurrencies': {'type': 'array', 'items': {'type': 'string'}, 'description': 'Present when lowPrice is null: the currencies WEM does hold it in.'}, 'lowPriceRetailer': {'type': ['string', 'null']}, 'marketplaceListings': {'type': 'number'}, 'marketplaceLowPrice': {'type': 'number', 'description': 'Cheapest marketplace listing (eBay, AliExpress…), present only when it undercuts lowPrice or no named retailer holds the product. A seller’s price, not a retail floor: never quote it as the product’s price or the cheapest.'}}}}, 'currency': {'type': 'string'}, 'disclosure': {'type': 'string', 'description': 'Disclosure to relay once per answer, verbatim. Its wording changes with the rows: it states whether every outbound link is affiliate-tracked, only some are, or none are. Never substitute the version you saw last time — a row marked `affiliate: false` earns WEM nothing, and saying otherwise misdescribes it to the shopper.'}, 'unavailable': {'type': 'boolean', 'description': 'Present and true when WEM could not reach its catalogue. Rows it could not check are not_checked, not not_found — do not report them as products WEM lacks.'}, 'overLimitNote': {'type': 'string'}}, 'description': 'One row per distinct identifier, in the order sent. A row is a catalogue fact, not an offer: it says whether WEM holds the product and what the lowest price is, and links to WEM’s product page. Call compare_offers with product.slug for the retailer offers and their links.'}
Input schema
{'type': 'object', 'required': ['query'], 'properties': {'gtin': {'type': 'string', 'description': 'Product barcode (EAN/UPC/GTIN) when the host already has one. Feed rows are matched on this before title.'}, 'limit': {'type': 'number', 'description': 'Max results to return (default 10, max 30)'}, 'query': {'type': 'string', 'description': 'Product name, barcode (EAN/UPC/GTIN), ASIN, MPN, merchant SKU, WEM ID, wem3.ai/pl URL, or comma-separated IDs of those kinds.'}, 'sort_by': {'enum': ['relevance', 'price_asc', 'price_desc', 'rating'], 'type': 'string', 'description': 'Sort order for results'}, 'category': {'type': 'string', 'description': 'Filter by category (electronics, fashion, beauty, home, sports, collectibles)'}, 'max_price': {'type': 'number', 'description': 'Maximum price filter (GBP)'}, 'min_price': {'type': 'number', 'description': 'Minimum price filter (GBP)'}, 'providers': {'type': 'array', 'items': {'type': 'string'}, 'description': 'Limit to specific providers (e.g. ["ebay", "awin"]). Omit for all.'}}}
Output schema
{'type': 'object', 'required': ['products', 'totalResults', 'query', 'disclosure'], 'properties': {'next': {'type': 'object', 'properties': {'url': {'type': 'string'}, 'reason': {'type': 'string'}, 'surface': {'enum': ['chrome_extension'], 'type': 'string'}}, 'description': "Where to send the shopper when WEM's set is thin. chrome_extension means compare the same product on any retailer page — WEM shows that price even without an affiliate programme."}, 'query': {'type': 'string'}, 'search': {'type': 'object', 'properties': {'url': {'type': 'string'}, 'reason': {'type': 'string'}, 'markdown': {'type': 'string', 'description': 'The same link, pre-formatted.'}}, 'description': 'Present when this result names no offer. GIVE THE USER THIS LINK — it is the answer when WEM has nothing else to say, and `markdown` is ready to paste. WEM searches retailers live on that page, including shops it holds no affiliate programme with, so an empty or withheld result here is not evidence the product is unavailable or unpriced.'}, 'partial': {'type': 'boolean', 'description': 'True when the live retailer fan-out hit its 3-second budget and some providers had not answered. The rows returned are real; the market may be wider. Ask again for the rest.'}, 'coverage': {'type': 'object', 'properties': {'low': {'type': 'number'}, 'high': {'type': 'number'}, 'kind': {'enum': ['marketplace_only'], 'type': 'string'}, 'currency': {'type': 'string'}, 'listings': {'type': 'number'}, 'provider': {'type': 'string', 'description': 'ebay, aliexpress, or marketplace when mixed.'}}, 'description': 'Present when surviving rows include a marketplace cluster at similar prices. That is not a retail floor — do not name the cheapest marketplace listing as the deal. The listings are still in the payload; give the user those links. If `next` points at the Chrome extension, send the shopper there for shops WEM does not yet hold as partners.'}, 'degraded': {'type': 'object', 'properties': {'note': {'type': 'string'}, 'skipped': {'type': 'array', 'items': {'type': 'string'}}}, 'description': 'Present when this call hit its time budget and skipped enrichment. The offers returned are COMPLETE — only the extras were dropped. Do not report a missing catalogue block or missing observed retailers as WEM holding nothing, and do not retry automatically.'}, 'filtered': {'type': ['object', 'null'], 'description': 'Withheld candidates tallied by reason (e.g. accessories, wrong model). Report this count rather than implying the search was exhaustive.', 'additionalProperties': {'type': 'number'}}, 'products': {'type': 'array', 'items': {'type': 'object', 'properties': {'id': {'type': 'string'}, 'url': {'type': 'string', 'description': 'WEM tracked link to the retailer. Send the user here — WEM never takes payment.'}, 'brand': {'type': ['string', 'null']}, 'image': {'type': ['string', 'null']}, 'price': {'type': 'number', 'description': 'Indicative price. The retailer sets the final price at checkout.'}, 'title': {'type': 'string'}, 'rating': {'type': ['number', 'null']}, 'seller': {'type': ['string', 'null']}, 'channel': {'enum': ['retailer', 'marketplace'], 'type': 'string', 'description': 'retailer is a named shop (Boots, Currys). marketplace is eBay/AliExpress/Temu-style parallel listings. Do not present a marketplace cluster as competing authorised retailers.'}, 'inStock': {'type': ['boolean', 'null']}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}, 'provider': {'type': 'string', 'description': 'Retailer slug, e.g. "ebay", "currys".'}, 'shipping': {'type': ['object', 'null'], 'properties': {'cost': {'type': ['number', 'null']}, 'free': {'type': 'boolean'}, 'estimate': {'type': ['string', 'null'], 'description': 'Delivery window when the feed stated one.'}}}, 'affiliate': {'type': 'boolean', 'description': 'False when this is a retailer page WEM observed without a programme. Still a real listing; the click is not commission-bearing. Absent means the usual partner path.'}, 'unitPrice': {'type': 'object', 'properties': {'per': {'enum': ['100ml', 'litre', '100g', 'kg', 'item'], 'type': 'string'}, 'amount': {'type': 'number'}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}}, 'description': 'Price per 100ml, 100g, litre, kg or item, from the size this row\'s title states. Present only when the rows differ in size, in one unit across all of them, and only for things bought by the amount (beauty, food, supplements), never an appliance\'s capacity. When comparing value across sizes, compare these, never the prices: a smaller size is not cheaper because its price is lower. Quote it beside the price ("£29.95, £99.83 per 100ml"). Absent does not mean the price is per unit.'}, 'reviewCount': {'type': ['number', 'null']}, 'priceQualifier': {'enum': ['from'], 'type': 'string', 'description': 'Present when `price` is the OPENING price of a range, not the price of this item: a multi-variation listing where the seller advertises its cheapest variant and the shopper picks a size on the page. Quote it as "from £X", never as the price or the cheapest. Absent means the price is firm for the row as described.'}}}}, 'disclosure': {'type': 'string', 'description': 'Disclosure to relay once per answer, verbatim. Its wording changes with the rows: it states whether every outbound link is affiliate-tracked, only some are, or none are. Never substitute the version you saw last time — a row marked `affiliate: false` earns WEM nothing, and saying otherwise misdescribes it to the shopper.'}, 'sizeSpread': {'type': 'object', 'properties': {'note': {'type': 'string'}, 'sizes': {'type': 'array', 'items': {'type': 'string'}, 'description': 'The distinct sizes found, as the listings wrote them.'}, 'comparable': {'enum': [False], 'type': 'boolean'}}, 'description': 'Present when the returned rows are different sizes and the query named none, so price ordering is meaningless: the cheapest row is cheapest because it is less of the product. Do NOT name a cheapest, a lowest price, or a best deal across these rows. State the size beside every price and ask the shopper which size they want. Where rows carry `unitPrice`, that is the like-for-like figure: quote it, and rank on it if the shopper asks which is better value. The rows are real — give the user those links.'}, 'catalogMatch': {'type': ['object', 'null'], 'properties': {'slug': {'type': 'string'}, 'title': {'type': 'string'}, 'offers': {'type': 'array', 'items': {'type': 'object', 'properties': {'id': {'type': 'string'}, 'url': {'type': 'string', 'description': 'WEM tracked link to the retailer. Send the user here — WEM never takes payment.'}, 'brand': {'type': ['string', 'null']}, 'image': {'type': ['string', 'null']}, 'price': {'type': 'number', 'description': 'Indicative price. The retailer sets the final price at checkout.'}, 'title': {'type': 'string'}, 'rating': {'type': ['number', 'null']}, 'seller': {'type': ['string', 'null']}, 'channel': {'enum': ['retailer', 'marketplace'], 'type': 'string', 'description': 'retailer is a named shop (Boots, Currys). marketplace is eBay/AliExpress/Temu-style parallel listings. Do not present a marketplace cluster as competing authorised retailers.'}, 'inStock': {'type': ['boolean', 'null']}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}, 'provider': {'type': 'string', 'description': 'Retailer slug, e.g. "ebay", "currys".'}, 'shipping': {'type': ['object', 'null'], 'properties': {'cost': {'type': ['number', 'null']}, 'free': {'type': 'boolean'}, 'estimate': {'type': ['string', 'null'], 'description': 'Delivery window when the feed stated one.'}}}, 'verified': {'type': 'boolean', 'description': 'True when WEM has recently observed this price on the retailer’s own surface. False means the identity is still catalogue-resolved but the price is indicative (partner feed or stale) — do not present it as verified.'}, 'affiliate': {'type': 'boolean', 'description': 'False when this is a retailer page WEM observed without a programme. Still a real listing; the click is not commission-bearing. Absent means the usual partner path.'}, 'unitPrice': {'type': 'object', 'properties': {'per': {'enum': ['100ml', 'litre', '100g', 'kg', 'item'], 'type': 'string'}, 'amount': {'type': 'number'}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}}, 'description': 'Price per 100ml, 100g, litre, kg or item, from the size this row\'s title states. Present only when the rows differ in size, in one unit across all of them, and only for things bought by the amount (beauty, food, supplements), never an appliance\'s capacity. When comparing value across sizes, compare these, never the prices: a smaller size is not cheaper because its price is lower. Quote it beside the price ("£29.95, £99.83 per 100ml"). Absent does not mean the price is per unit.'}, 'lastSeenAt': {'type': ['string', 'null'], 'description': 'When WEM last VISITED this offer row — not when it read the price. The refresh sweep touches this even when the retailer lookup fails, so it is not evidence the price is current. Use `priceAgeDays` to date a price; never this.'}, 'reviewCount': {'type': ['number', 'null']}, 'priceAgeDays': {'type': ['number', 'null'], 'description': 'Whole days since WEM last actually READ this price from the retailer or a datafeed. Null means WEM cannot say — common and correct for Amazon, whose licence caps price retention at 24 hours. Report null as undated; never present it as current.'}, 'priceRefresh': {'enum': ['live-api', 'feed', 'crawl', 'none'], 'type': 'string', 'description': 'What re-reads this retailer’s prices: `live-api` a product-lookup API, `feed` a partner datafeed, `crawl` WEM reading the retailer’s own public product page, `none` nothing at all. `none` means the figure is frozen at whatever seeded it and will not move however long it sits — say so rather than quoting it flat. A `crawl` price was observed on the retailer’s page rather than supplied by them, so date it with `priceAgeDays` and expect `affiliate: false` unless a programme also exists.'}, 'priceQualifier': {'enum': ['from'], 'type': 'string', 'description': 'Present when `price` is the OPENING price of a range, not the price of this item: a multi-variation listing where the seller advertises its cheapest variant and the shopper picks a size on the page. Quote it as "from £X", never as the price or the cheapest. Absent means the price is firm for the row as described.'}}}}, 'source': {'type': 'string'}, 'currency': {'type': ['string', 'null']}, 'identity': {'type': ['object', 'null'], 'properties': {'method': {'enum': ['gtin', 'slug', 'listing', 'title'], 'type': 'string'}, 'strength': {'enum': ['exact', 'catalogued', 'inferred'], 'type': 'string', 'description': 'exact = barcode; catalogued = a WEM key or listing id; inferred = matched on the product name, MPN or SKU and still a guess.'}}, 'description': 'How the shopper’s words reached this product. Decides how you describe the match: on `inferred`, say WEM matched it by name (not by barcode) and do not call the identity confirmed, even when `identityBasis` is `barcode`; on `exact`, the barcode itself resolved it.'}, 'lowPrice': {'type': ['number', 'null']}, 'productPage': {'type': 'string'}, 'identityBasis': {'enum': ['barcode', 'curated-grouping'], 'type': 'string', 'description': 'What this block’s identity rests on. `barcode` — the canonical product carries a GTIN and these offers are the same physical item. `curated-grouping` — it carries none, so the grouping is an inference of the same kind a title match is; do not describe it to the user as barcode-confirmed, and do not let it outrank a cheaper row in `products` on price alone. This is how the offers are grouped, not how the query matched the product — see `identity`.'}, 'lastConfirmedAt': {'type': ['string', 'null'], 'description': 'When WEM last actually read any price in this block. Null means none of them can be dated — say so rather than implying the block is current.'}, 'cheaperElsewhere': {'type': ['object', 'null'], 'properties': {'note': {'type': 'string', 'description': 'Plain-language restatement, safe to relay.'}, 'identity': {'enum': ['inferred'], 'type': 'string', 'description': 'Always `inferred` — matched on title, not barcode. Say so when quoting it.'}, 'verified': {'type': 'boolean', 'description': 'Always false.'}, 'savingVsCatalogue': {'type': 'number', 'description': 'How much cheaper this row is than the block’s lowest offer.'}}, 'description': 'Present only on a `curated-grouping` block that a live row in `products` undercuts. WEM is telling you its own catalogue block is not the best price it found. Quote this row as the cheaper option with its identity stated as unconfirmed; never present the catalogMatch price as the lowest when this is set.'}}, 'description': 'Offers WEM holds under one catalogue product. Check `identityBasis`: on `barcode` the product carries a GTIN and every offer here is that same item, so prefer them and cite their prices over anything in `products`. On `curated-grouping` the product carries no barcode, the grouping is an inference like any title match, and a cheaper row in `products` may well be the same item — see `cheaperElsewhere`. Whether the shopper’s words reached this product by barcode or by name is `identity`, a separate question: on `inferred`, say WEM matched it by name.'}, 'totalResults': {'type': 'number', 'description': 'Count of products returned, after weak matches were withheld.'}, 'catalogMatches': {'type': 'array', 'items': {'type': ['object', 'null'], 'properties': {'slug': {'type': 'string'}, 'title': {'type': 'string'}, 'offers': {'type': 'array', 'items': {'type': 'object', 'properties': {'id': {'type': 'string'}, 'url': {'type': 'string', 'description': 'WEM tracked link to the retailer. Send the user here — WEM never takes payment.'}, 'brand': {'type': ['string', 'null']}, 'image': {'type': ['string', 'null']}, 'price': {'type': 'number', 'description': 'Indicative price. The retailer sets the final price at checkout.'}, 'title': {'type': 'string'}, 'rating': {'type': ['number', 'null']}, 'seller': {'type': ['string', 'null']}, 'channel': {'enum': ['retailer', 'marketplace'], 'type': 'string', 'description': 'retailer is a named shop (Boots, Currys). marketplace is eBay/AliExpress/Temu-style parallel listings. Do not present a marketplace cluster as competing authorised retailers.'}, 'inStock': {'type': ['boolean', 'null']}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}, 'provider': {'type': 'string', 'description': 'Retailer slug, e.g. "ebay", "currys".'}, 'shipping': {'type': ['object', 'null'], 'properties': {'cost': {'type': ['number', 'null']}, 'free': {'type': 'boolean'}, 'estimate': {'type': ['string', 'null'], 'description': 'Delivery window when the feed stated one.'}}}, 'verified': {'type': 'boolean', 'description': 'True when WEM has recently observed this price on the retailer’s own surface. False means the identity is still catalogue-resolved but the price is indicative (partner feed or stale) — do not present it as verified.'}, 'affiliate': {'type': 'boolean', 'description': 'False when this is a retailer page WEM observed without a programme. Still a real listing; the click is not commission-bearing. Absent means the usual partner path.'}, 'unitPrice': {'type': 'object', 'properties': {'per': {'enum': ['100ml', 'litre', '100g', 'kg', 'item'], 'type': 'string'}, 'amount': {'type': 'number'}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}}, 'description': 'Price per 100ml, 100g, litre, kg or item, from the size this row\'s title states. Present only when the rows differ in size, in one unit across all of them, and only for things bought by the amount (beauty, food, supplements), never an appliance\'s capacity. When comparing value across sizes, compare these, never the prices: a smaller size is not cheaper because its price is lower. Quote it beside the price ("£29.95, £99.83 per 100ml"). Absent does not mean the price is per unit.'}, 'lastSeenAt': {'type': ['string', 'null'], 'description': 'When WEM last VISITED this offer row — not when it read the price. The refresh sweep touches this even when the retailer lookup fails, so it is not evidence the price is current. Use `priceAgeDays` to date a price; never this.'}, 'reviewCount': {'type': ['number', 'null']}, 'priceAgeDays': {'type': ['number', 'null'], 'description': 'Whole days since WEM last actually READ this price from the retailer or a datafeed. Null means WEM cannot say — common and correct for Amazon, whose licence caps price retention at 24 hours. Report null as undated; never present it as current.'}, 'priceRefresh': {'enum': ['live-api', 'feed', 'crawl', 'none'], 'type': 'string', 'description': 'What re-reads this retailer’s prices: `live-api` a product-lookup API, `feed` a partner datafeed, `crawl` WEM reading the retailer’s own public product page, `none` nothing at all. `none` means the figure is frozen at whatever seeded it and will not move however long it sits — say so rather than quoting it flat. A `crawl` price was observed on the retailer’s page rather than supplied by them, so date it with `priceAgeDays` and expect `affiliate: false` unless a programme also exists.'}, 'priceQualifier': {'enum': ['from'], 'type': 'string', 'description': 'Present when `price` is the OPENING price of a range, not the price of this item: a multi-variation listing where the seller advertises its cheapest variant and the shopper picks a size on the page. Quote it as "from £X", never as the price or the cheapest. Absent means the price is firm for the row as described.'}}}}, 'source': {'type': 'string'}, 'currency': {'type': ['string', 'null']}, 'identity': {'type': ['object', 'null'], 'properties': {'method': {'enum': ['gtin', 'slug', 'listing', 'title'], 'type': 'string'}, 'strength': {'enum': ['exact', 'catalogued', 'inferred'], 'type': 'string', 'description': 'exact = barcode; catalogued = a WEM key or listing id; inferred = matched on the product name, MPN or SKU and still a guess.'}}, 'description': 'How the shopper’s words reached this product. Decides how you describe the match: on `inferred`, say WEM matched it by name (not by barcode) and do not call the identity confirmed, even when `identityBasis` is `barcode`; on `exact`, the barcode itself resolved it.'}, 'lowPrice': {'type': ['number', 'null']}, 'productPage': {'type': 'string'}, 'identityBasis': {'enum': ['barcode', 'curated-grouping'], 'type': 'string', 'description': 'What this block’s identity rests on. `barcode` — the canonical product carries a GTIN and these offers are the same physical item. `curated-grouping` — it carries none, so the grouping is an inference of the same kind a title match is; do not describe it to the user as barcode-confirmed, and do not let it outrank a cheaper row in `products` on price alone. This is how the offers are grouped, not how the query matched the product — see `identity`.'}, 'lastConfirmedAt': {'type': ['string', 'null'], 'description': 'When WEM last actually read any price in this block. Null means none of them can be dated — say so rather than implying the block is current.'}, 'cheaperElsewhere': {'type': ['object', 'null'], 'properties': {'note': {'type': 'string', 'description': 'Plain-language restatement, safe to relay.'}, 'identity': {'enum': ['inferred'], 'type': 'string', 'description': 'Always `inferred` — matched on title, not barcode. Say so when quoting it.'}, 'verified': {'type': 'boolean', 'description': 'Always false.'}, 'savingVsCatalogue': {'type': 'number', 'description': 'How much cheaper this row is than the block’s lowest offer.'}}, 'description': 'Present only on a `curated-grouping` block that a live row in `products` undercuts. WEM is telling you its own catalogue block is not the best price it found. Quote this row as the cheaper option with its identity stated as unconfirmed; never present the catalogMatch price as the lowest when this is set.'}}, 'description': 'Offers WEM holds under one catalogue product. Check `identityBasis`: on `barcode` the product carries a GTIN and every offer here is that same item, so prefer them and cite their prices over anything in `products`. On `curated-grouping` the product carries no barcode, the grouping is an inference like any title match, and a cheaper row in `products` may well be the same item — see `cheaperElsewhere`. Whether the shopper’s words reached this product by barcode or by name is `identity`, a separate question: on `inferred`, say WEM matched it by name.'}, 'description': 'Present when the query was a comma-separated batch of identifiers and more than one catalogue product resolved. Each entry has the same shape as `catalogMatch`. Prefer barcode-basis blocks; treat MPN/SKU/title blocks as inferred.'}, 'pending_providers': {'type': 'array', 'items': {'type': 'string'}, 'description': 'Providers still running when the budget expired. Present only with partial.'}}}
Input schema
{'type': 'object', 'properties': {'limit': {'type': 'number', 'description': 'Max promotions to return (default 10, max 25).'}, 'query': {'type': 'string', 'description': 'Words that must appear in the offer, e.g. "hoverboard" or "free delivery".'}, 'currency': {'type': 'string', 'description': "ISO 4217 code for the shopper's market: GBP (default) or USD. Promotions are regional, so this decides which retailers' offers are read."}, 'merchant': {'type': 'string', 'description': 'Retailer name, e.g. "iHoverboard". Omit for every retailer.'}}}
Output schema
{'type': 'object', 'required': ['promotions', 'count', 'currency', 'links', 'disclosure'], 'properties': {'more': {'type': 'number', 'description': 'Further matching promotions beyond `limit`.'}, 'note': {'type': 'string'}, 'count': {'type': 'number'}, 'links': {'type': 'array', 'items': {'type': 'object', 'properties': {'url': {'type': 'string'}, 'title': {'type': 'string'}, 'markdown': {'type': 'string'}, 'merchant': {'type': 'string'}}}, 'description': 'The tracked links from `promotions`, pre-formatted. Give these to the user when you name a promotion.'}, 'market': {'type': 'string', 'description': 'ISO country code the promotions were read for.'}, 'reason': {'enum': ['none_matched', 'not_configured', 'upstream_error', 'unsupported_market'], 'type': 'string', 'description': 'Present only when no promotion is shown. None of these means the retailer has no offer on — not_configured and upstream_error mean WEM could not look.'}, 'search': {'type': 'object', 'properties': {'url': {'type': 'string'}, 'reason': {'type': 'string'}, 'markdown': {'type': 'string', 'description': 'The same link, pre-formatted.'}}, 'description': 'Present when this result names no offer. GIVE THE USER THIS LINK — it is the answer when WEM has nothing else to say, and `markdown` is ready to paste. WEM searches retailers live on that page, including shops it holds no affiliate programme with, so an empty or withheld result here is not evidence the product is unavailable or unpriced.'}, 'currency': {'type': 'string'}, 'withheld': {'type': 'object', 'description': 'Matching promotions WEM did not show, by reason. codeNotPermitted and mentionsCode mean a checkout code WEM may not publish: the code may well exist — never say there is none.', 'additionalProperties': {'type': 'number'}}, 'disclosure': {'type': 'string', 'description': 'Disclosure to relay once per answer, verbatim. Its wording changes with the rows: it states whether every outbound link is affiliate-tracked, only some are, or none are. Never substitute the version you saw last time — a row marked `affiliate: false` earns WEM nothing, and saying otherwise misdescribes it to the shopper.'}, 'promotions': {'type': 'array', 'items': {'type': 'object', 'required': ['merchant', 'title', 'kind', 'url'], 'properties': {'url': {'type': 'string', 'description': 'WEM tracked redirect to the retailer’s promotion page. Relay it exactly; never rewrite it.'}, 'code': {'type': 'string'}, 'kind': {'enum': ['sale', 'code'], 'type': 'string', 'description': 'code rows carry `code`, and appear only where the retailer’s programme lets WEM publish it.'}, 'terms': {'type': 'string', 'description': 'The retailer’s own conditions. Relay them with the offer.'}, 'title': {'type': 'string'}, 'endsAt': {'type': ['string', 'null'], 'description': 'When the retailer said it ends. Null means no end was stated — do not invent one.'}, 'network': {'type': 'string'}, 'merchant': {'type': 'string'}, 'startsAt': {'type': ['string', 'null']}, 'description': {'type': 'string'}}}, 'description': 'Ending soonest first. Each row is one live promotion WEM is allowed to show.'}, 'withheldNote': {'type': 'string', 'description': 'Quote this rather than paraphrase it when codes were withheld.'}}, 'description': 'Promotions the retailer published to its affiliate network, filtered by WEM’s programme terms. A promotion is the retailer’s claim about some of its range, not a price — never compute a discounted price from it unless its terms name the product.'}
Input schema
{'type': 'object', 'required': ['description'], 'properties': {'limit': {'type': 'number', 'description': 'Max results (default 10, max 20)'}, 'category': {'type': 'string', 'description': 'Optional category filter'}, 'max_price': {'type': 'number', 'description': 'Maximum price (GBP)'}, 'min_price': {'type': 'number', 'description': 'Minimum price (GBP)'}, 'description': {'type': 'string', 'description': 'Natural language description of what the user is looking for'}}}
Output schema
{'type': 'object', 'required': ['products', 'totalResults', 'disclosure'], 'properties': {'next': {'type': 'object', 'properties': {'url': {'type': 'string'}, 'reason': {'type': 'string'}, 'surface': {'enum': ['chrome_extension'], 'type': 'string'}}, 'description': "Where to send the shopper when WEM's set is thin. chrome_extension means compare the same product on any retailer page — WEM shows that price even without an affiliate programme."}, 'query': {'type': 'string'}, 'reason': {'type': 'string', 'description': 'Present when semantic is false, and never a judgement on the shopper query. `index_empty` — the vector query ran and matched nothing. `no_catalogue_client` / `no_embedding_key` — WEM is misconfigured, not the catalogue empty. `embedding_failed` / `query_failed` — an upstream call failed; retrying later may succeed. `timed_out` — an upstream call did not answer in time; this says nothing about the catalogue.'}, 'search': {'type': 'object', 'properties': {'url': {'type': 'string'}, 'reason': {'type': 'string'}, 'markdown': {'type': 'string', 'description': 'The same link, pre-formatted.'}}, 'description': 'Present when this result names no offer. GIVE THE USER THIS LINK — it is the answer when WEM has nothing else to say, and `markdown` is ready to paste. WEM searches retailers live on that page, including shops it holds no affiliate programme with, so an empty or withheld result here is not evidence the product is unavailable or unpriced.'}, 'filtered': {'type': ['object', 'null'], 'description': 'Withheld candidates tallied by reason (e.g. accessories, wrong model). Report this count rather than implying the search was exhaustive.', 'additionalProperties': {'type': 'number'}}, 'products': {'type': 'array', 'items': {'type': 'object', 'properties': {'id': {'type': 'string'}, 'url': {'type': 'string', 'description': 'WEM tracked link to the retailer. Send the user here — WEM never takes payment.'}, 'brand': {'type': ['string', 'null']}, 'image': {'type': ['string', 'null']}, 'price': {'type': 'number', 'description': 'Indicative price. The retailer sets the final price at checkout.'}, 'title': {'type': 'string'}, 'rating': {'type': ['number', 'null']}, 'seller': {'type': ['string', 'null']}, 'channel': {'enum': ['retailer', 'marketplace'], 'type': 'string', 'description': 'retailer is a named shop (Boots, Currys). marketplace is eBay/AliExpress/Temu-style parallel listings. Do not present a marketplace cluster as competing authorised retailers.'}, 'inStock': {'type': ['boolean', 'null']}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}, 'provider': {'type': 'string', 'description': 'Retailer slug, e.g. "ebay", "currys".'}, 'shipping': {'type': ['object', 'null'], 'properties': {'cost': {'type': ['number', 'null']}, 'free': {'type': 'boolean'}, 'estimate': {'type': ['string', 'null'], 'description': 'Delivery window when the feed stated one.'}}}, 'affiliate': {'type': 'boolean', 'description': 'False when this is a retailer page WEM observed without a programme. Still a real listing; the click is not commission-bearing. Absent means the usual partner path.'}, 'unitPrice': {'type': 'object', 'properties': {'per': {'enum': ['100ml', 'litre', '100g', 'kg', 'item'], 'type': 'string'}, 'amount': {'type': 'number'}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}}, 'description': 'Price per 100ml, 100g, litre, kg or item, from the size this row\'s title states. Present only when the rows differ in size, in one unit across all of them, and only for things bought by the amount (beauty, food, supplements), never an appliance\'s capacity. When comparing value across sizes, compare these, never the prices: a smaller size is not cheaper because its price is lower. Quote it beside the price ("£29.95, £99.83 per 100ml"). Absent does not mean the price is per unit.'}, 'reviewCount': {'type': ['number', 'null']}, 'priceQualifier': {'enum': ['from'], 'type': 'string', 'description': 'Present when `price` is the OPENING price of a range, not the price of this item: a multi-variation listing where the seller advertises its cheapest variant and the shopper picks a size on the page. Quote it as "from £X", never as the price or the cheapest. Absent means the price is firm for the row as described.'}}}}, 'semantic': {'type': 'boolean', 'description': 'True only when the vector path actually ran.'}, 'disclosure': {'type': 'string', 'description': 'Disclosure to relay once per answer, verbatim. Its wording changes with the rows: it states whether every outbound link is affiliate-tracked, only some are, or none are. Never substitute the version you saw last time — a row marked `affiliate: false` earns WEM nothing, and saying otherwise misdescribes it to the shopper.'}, 'catalogMatch': {'type': ['object', 'null'], 'properties': {'slug': {'type': 'string'}, 'title': {'type': 'string'}, 'offers': {'type': 'array', 'items': {'type': 'object', 'properties': {'id': {'type': 'string'}, 'url': {'type': 'string', 'description': 'WEM tracked link to the retailer. Send the user here — WEM never takes payment.'}, 'brand': {'type': ['string', 'null']}, 'image': {'type': ['string', 'null']}, 'price': {'type': 'number', 'description': 'Indicative price. The retailer sets the final price at checkout.'}, 'title': {'type': 'string'}, 'rating': {'type': ['number', 'null']}, 'seller': {'type': ['string', 'null']}, 'channel': {'enum': ['retailer', 'marketplace'], 'type': 'string', 'description': 'retailer is a named shop (Boots, Currys). marketplace is eBay/AliExpress/Temu-style parallel listings. Do not present a marketplace cluster as competing authorised retailers.'}, 'inStock': {'type': ['boolean', 'null']}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}, 'provider': {'type': 'string', 'description': 'Retailer slug, e.g. "ebay", "currys".'}, 'shipping': {'type': ['object', 'null'], 'properties': {'cost': {'type': ['number', 'null']}, 'free': {'type': 'boolean'}, 'estimate': {'type': ['string', 'null'], 'description': 'Delivery window when the feed stated one.'}}}, 'verified': {'type': 'boolean', 'description': 'True when WEM has recently observed this price on the retailer’s own surface. False means the identity is still catalogue-resolved but the price is indicative (partner feed or stale) — do not present it as verified.'}, 'affiliate': {'type': 'boolean', 'description': 'False when this is a retailer page WEM observed without a programme. Still a real listing; the click is not commission-bearing. Absent means the usual partner path.'}, 'unitPrice': {'type': 'object', 'properties': {'per': {'enum': ['100ml', 'litre', '100g', 'kg', 'item'], 'type': 'string'}, 'amount': {'type': 'number'}, 'currency': {'type': 'string', 'description': 'ISO 4217 code.'}}, 'description': 'Price per 100ml, 100g, litre, kg or item, from the size this row\'s title states. Present only when the rows differ in size, in one unit across all of them, and only for things bought by the amount (beauty, food, supplements), never an appliance\'s capacity. When comparing value across sizes, compare these, never the prices: a smaller size is not cheaper because its price is lower. Quote it beside the price ("£29.95, £99.83 per 100ml"). Absent does not mean the price is per unit.'}, 'lastSeenAt': {'type': ['string', 'null'], 'description': 'When WEM last VISITED this offer row — not when it read the price. The refresh sweep touches this even when the retailer lookup fails, so it is not evidence the price is current. Use `priceAgeDays` to date a price; never this.'}, 'reviewCount': {'type': ['number', 'null']}, 'priceAgeDays': {'type': ['number', 'null'], 'description': 'Whole days since WEM last actually READ this price from the retailer or a datafeed. Null means WEM cannot say — common and correct for Amazon, whose licence caps price retention at 24 hours. Report null as undated; never present it as current.'}, 'priceRefresh': {'enum': ['live-api', 'feed', 'crawl', 'none'], 'type': 'string', 'description': 'What re-reads this retailer’s prices: `live-api` a product-lookup API, `feed` a partner datafeed, `crawl` WEM reading the retailer’s own public product page, `none` nothing at all. `none` means the figure is frozen at whatever seeded it and will not move however long it sits — say so rather than quoting it flat. A `crawl` price was observed on the retailer’s page rather than supplied by them, so date it with `priceAgeDays` and expect `affiliate: false` unless a programme also exists.'}, 'priceQualifier': {'enum': ['from'], 'type': 'string', 'description': 'Present when `price` is the OPENING price of a range, not the price of this item: a multi-variation listing where the seller advertises its cheapest variant and the shopper picks a size on the page. Quote it as "from £X", never as the price or the cheapest. Absent means the price is firm for the row as described.'}}}}, 'source': {'type': 'string'}, 'currency': {'type': ['string', 'null']}, 'identity': {'type': ['object', 'null'], 'properties': {'method': {'enum': ['gtin', 'slug', 'listing', 'title'], 'type': 'string'}, 'strength': {'enum': ['exact', 'catalogued', 'inferred'], 'type': 'string', 'description': 'exact = barcode; catalogued = a WEM key or listing id; inferred = matched on the product name, MPN or SKU and still a guess.'}}, 'description': 'How the shopper’s words reached this product. Decides how you describe the match: on `inferred`, say WEM matched it by name (not by barcode) and do not call the identity confirmed, even when `identityBasis` is `barcode`; on `exact`, the barcode itself resolved it.'}, 'lowPrice': {'type': ['number', 'null']}, 'productPage': {'type': 'string'}, 'identityBasis': {'enum': ['barcode', 'curated-grouping'], 'type': 'string', 'description': 'What this block’s identity rests on. `barcode` — the canonical product carries a GTIN and these offers are the same physical item. `curated-grouping` — it carries none, so the grouping is an inference of the same kind a title match is; do not describe it to the user as barcode-confirmed, and do not let it outrank a cheaper row in `products` on price alone. This is how the offers are grouped, not how the query matched the product — see `identity`.'}, 'lastConfirmedAt': {'type': ['string', 'null'], 'description': 'When WEM last actually read any price in this block. Null means none of them can be dated — say so rather than implying the block is current.'}, 'cheaperElsewhere': {'type': ['object', 'null'], 'properties': {'note': {'type': 'string', 'description': 'Plain-language restatement, safe to relay.'}, 'identity': {'enum': ['inferred'], 'type': 'string', 'description': 'Always `inferred` — matched on title, not barcode. Say so when quoting it.'}, 'verified': {'type': 'boolean', 'description': 'Always false.'}, 'savingVsCatalogue': {'type': 'number', 'description': 'How much cheaper this row is than the block’s lowest offer.'}}, 'description': 'Present only on a `curated-grouping` block that a live row in `products` undercuts. WEM is telling you its own catalogue block is not the best price it found. Quote this row as the cheaper option with its identity stated as unconfirmed; never present the catalogMatch price as the lowest when this is set.'}}, 'description': 'Offers WEM holds under one catalogue product. Check `identityBasis`: on `barcode` the product carries a GTIN and every offer here is that same item, so prefer them and cite their prices over anything in `products`. On `curated-grouping` the product carries no barcode, the grouping is an inference like any title match, and a cheaper row in `products` may well be the same item — see `cheaperElsewhere`. Whether the shopper’s words reached this product by barcode or by name is `identity`, a separate question: on `inferred`, say WEM matched it by name.'}, 'totalResults': {'type': 'number'}}, 'description': 'Empty when the vector path cannot rank (`semantic: false`), with `reason` naming which cause applies — they have different fixes, so do not read them all as an empty index. `semantic` is true only when pgvector actually ranked products.'}
Input schema
{'type': 'object', 'anyOf': [{'required': ['gtin']}, {'required': ['slug']}, {'required': ['wem_id']}, {'required': ['provider', 'externalId']}, {'required': ['title']}], 'properties': {'gtin': {'type': 'string', 'description': 'Product barcode: EAN-13, UPC-A, EAN-8 or GTIN-14. Strongest identifier.'}, 'slug': {'type': 'string', 'description': 'WEM canonical slug, as in wem3.ai/pl/{slug}.'}, 'price': {'type': 'number', 'description': 'The price being claimed. Omit to ask only what the verified offers are.'}, 'title': {'type': 'string', 'description': 'Product title. Weakest identifier — used only when no id is available.'}, 'wem_id': {'type': 'string', 'description': 'WEM ID (W + 10 Crockford characters + check). Active catalogue products only.'}, 'currency': {'type': 'string', 'description': 'ISO 4217 code for the claimed price. Default GBP.'}, 'provider': {'type': 'string', 'description': "Retailer slug for the listing being checked, e.g. 'currys'."}, 'retailer': {'type': 'string', 'description': 'Retailer the price was claimed at — slug or display name.'}, 'externalId': {'type': 'string', 'description': "The retailer's own product id (ASIN, eBay item number). Use with provider."}}}
Output schema
{'type': 'object', 'required': ['verdict', 'offers', 'summary', 'source', 'disclosure', 'receipt'], 'properties': {'offers': {'type': 'array', 'items': {'type': ['object', 'null'], 'properties': {'url': {'type': 'string'}, 'price': {'type': 'number'}, 'inStock': {'type': ['boolean', 'null'], 'description': 'null means the retailer did not report availability. Say "stock not confirmed" — never present null as in stock.'}, 'currency': {'type': 'string'}, 'provider': {'type': 'string'}, 'retailer': {'type': 'string', 'description': 'Shopper-facing retailer name.'}, 'totalCost': {'type': 'object', 'properties': {'tax': {'type': ['number', 'null']}, 'fees': {'type': ['number', 'null']}, 'item': {'type': 'number'}, 'unknown': {'type': 'array', 'items': {'type': 'string'}}, 'currency': {'type': 'string'}, 'shipping': {'type': ['number', 'null']}, 'delivered': {'type': 'number'}, 'confidence': {'enum': ['stated', 'unknown'], 'type': 'string'}}, 'description': 'Item plus any stated extras. When unknown is non-empty, delivered is a floor — never relay it as what the shopper will pay.'}}}}, 'source': {'type': 'string'}, 'product': {'type': ['object', 'null'], 'properties': {'slug': {'type': 'string'}, 'title': {'type': 'string'}}}, 'receipt': {'type': 'object', 'properties': {'id': {'type': 'string'}, 'amount': {'type': ['number', 'null'], 'description': 'Major-unit decimal, same units as verify_offer prices.'}, 'signed': {'type': 'boolean', 'description': 'True when WEM signed this receipt with a dedicated HMAC key. False when no key is configured (fail closed). Never derived from CRON_SECRET.'}, 'source': {'type': 'string', 'description': 'Citable URI of the standard this receipt is issued under.'}, 'verdict': {'type': 'string'}, 'currency': {'type': ['string', 'null']}, 'issuedAt': {'type': 'string', 'description': 'Alias of observed_at, kept for the unsigned edition.'}, 'verifier': {'type': 'string', 'description': 'Always "wem".'}, 'signature': {'type': ['string', 'null'], 'description': 'hmac-sha256=<hex> when signed; otherwise null.'}, 'observed_at': {'type': 'string', 'description': 'When WEM read the price in amount, or "unobserved" when nothing dates it — then do not present amount as current. ACP #197.'}, 'amount_minor': {'type': ['number', 'null'], 'description': 'Integer minor units of amount, ACP form.'}, 'identity_method': {'type': ['string', 'null']}, 'standardVersion': {'type': 'string'}}, 'description': 'Citation handle for this answer. Field names follow ACP suggested_price (observed_at, source, amount). signed is true only when WEM issued an HMAC with a dedicated key; otherwise false (fail closed).'}, 'summary': {'type': 'string', 'description': 'Written so quoting it verbatim is accurate. Prefer quoting it to paraphrasing the verdict code.'}, 'verdict': {'enum': ['confirmed', 'price_moved', 'not_at_retailer', 'no_claim', 'unknown_product'], 'type': 'string'}, 'betterBy': {'type': ['number', 'null'], 'description': 'Saving from taking `cheapest` over the claimed price. Never negative.'}, 'cheapest': {'type': ['object', 'null'], 'properties': {'url': {'type': 'string'}, 'price': {'type': 'number'}, 'inStock': {'type': ['boolean', 'null'], 'description': 'null means the retailer did not report availability. Say "stock not confirmed" — never present null as in stock.'}, 'currency': {'type': 'string'}, 'provider': {'type': 'string'}, 'retailer': {'type': 'string', 'description': 'Shopper-facing retailer name.'}, 'totalCost': {'type': 'object', 'properties': {'tax': {'type': ['number', 'null']}, 'fees': {'type': ['number', 'null']}, 'item': {'type': 'number'}, 'unknown': {'type': 'array', 'items': {'type': 'string'}}, 'currency': {'type': 'string'}, 'shipping': {'type': ['number', 'null']}, 'delivered': {'type': 'number'}, 'confidence': {'enum': ['stated', 'unknown'], 'type': 'string'}}, 'description': 'Item plus any stated extras. When unknown is non-empty, delivered is a floor — never relay it as what the shopper will pay.'}}}, 'identity': {'type': ['object', 'null'], 'properties': {'method': {'enum': ['gtin', 'slug', 'listing', 'title'], 'type': 'string'}, 'strength': {'enum': ['exact', 'catalogued', 'inferred'], 'type': 'string', 'description': 'exact = barcode; catalogued = a WEM key or the retailer’s own listing id; inferred = matched on the product name and still a guess.'}}, 'description': 'How the product was identified — a separate question from whether the price checks out. Never present a `strength` of "inferred" as a verified identity.'}, 'disclosure': {'type': 'string', 'description': 'Disclosure to relay once per answer, verbatim. Its wording changes with the rows: it states whether every outbound link is affiliate-tracked, only some are, or none are. Never substitute the version you saw last time — a row marked `affiliate: false` earns WEM nothing, and saying otherwise misdescribes it to the shopper.'}, 'resolvedBy': {'type': ['string', 'null'], 'description': 'Which identifier resolved the product.'}, 'claimMatched': {'type': ['object', 'null'], 'properties': {'url': {'type': 'string'}, 'price': {'type': 'number'}, 'inStock': {'type': ['boolean', 'null'], 'description': 'null means the retailer did not report availability. Say "stock not confirmed" — never present null as in stock.'}, 'currency': {'type': 'string'}, 'provider': {'type': 'string'}, 'retailer': {'type': 'string', 'description': 'Shopper-facing retailer name.'}, 'totalCost': {'type': 'object', 'properties': {'tax': {'type': ['number', 'null']}, 'fees': {'type': ['number', 'null']}, 'item': {'type': 'number'}, 'unknown': {'type': 'array', 'items': {'type': 'string'}}, 'currency': {'type': 'string'}, 'shipping': {'type': ['number', 'null']}, 'delivered': {'type': 'number'}, 'confidence': {'enum': ['stated', 'unknown'], 'type': 'string'}}, 'description': 'Item plus any stated extras. When unknown is non-empty, delivered is a floor — never relay it as what the shopper will pay.'}}}, 'comparisonSet': {'type': ['object', 'null'], 'properties': {'exhaustive': {'type': 'boolean', 'description': 'Always false. Never present this answer as the lowest price available anywhere.'}, 'observedAt': {'type': ['string', 'null']}, 'offersCompared': {'type': 'number'}, 'retailersCompared': {'type': 'number'}}, 'description': 'How wide the comparison behind `cheapest` was. `exhaustive` is always false: WEM does not see every retailer, so `cheapest` is the lowest offer WEM holds, never the lowest that exists. Relay it as such.'}, 'lastConfirmedAt': {'type': ['string', 'null'], 'description': 'When WEM last read a price among the offers compared — the freshness of this answer. Null means none of them carries a dated reading: say the prices are undated.'}, 'toleranceApplied': {'type': ['number', 'null']}}, 'description': '`unknown_product` means the claim could NOT be checked. It never means the claim is false and must not be reported as one.'}
Recent tool changes
Similar MCP servers
AgentLot Marketplace
Enables agents and users to discover, bid on, purchase, deliver, and settle paid digital services and compute work.
apex-x1
Provides X1 and Arc token screening, liquidity and exit-risk checks, wallet and bridge data, non-custodial token operations, comm…
SpellBook Finance
Provides Magic: The Gathering and other trading-card market prices, buylist offers, arbitrage analysis, sealed-product EV, seller…
API Direct
Searches and retrieves data from social networks, news and Google services, while also providing Amazon product, seller, ranking,…
Digital Manager Guru
Provides Digital Manager Guru checkout, customer, transaction, subscription, affiliate, coupon, blocklist, and digital-product sa…
Pagar.me
Connects to Pagar.me to manage customers, cards, orders, charges, invoices, payment captures, confirmations, retries, refunds, an…
amazon-product-research-mcp
Researches Amazon products, brands, sellers, buy-box activity, sourcing profitability, category competition, and supplier opportu…
AgentLux
Operates an agent avatar marketplace with item generation, purchases, creator earnings, public profiles, social activity, and ERC…