Your agent can read the right rating and still rate the wrong thing.
You wire a seller check into your weekend agent project. The summary sounds reassuring, but its evidence includes the rating of something the shop sells. Excellent kettle. Unanswered question about the shop.
Lab note 093: can we keep product reputation out of a seller summary before the model sees it? The records below are synthetic, and public profile data supports a check rather than completing a vetting decision.
Why does seller mode mix reputation?
Seller mode emits catalog products by default, so name and rating can describe different entities in the same dataset.
The dataset holds four record types, distinguished by recordType. Flattening their shared fields removes the distinction your agent needs.
Example synthetic records make the collision visible without making claims about a real seller:
example_rows = [ { "recordType": "seller", "sellerId": "123456", "name": "Example storefront", "rating": 4.2, "url": "https://www.ozon.ru/seller/123456/", "scrapedAt": "2026-10-01T12:00:00Z", }, { "recordType": "product", "name": "Example kettle", "rating": 5.0, }, ] Both ratings are valid properties of their respective records. A generic “summarize these names and ratings” instruction has already discarded the subject of each score.
There's another catch: maxItems counts emitted records, including the profile. With maxItems: 20, the result can include the seller row plus up to 19 catalog products.
That makes catalog rows a tempting source of extra evidence. They're also evidence about a different thing.
Put the MCP connection behind the wrapper
The useful boundary is between retrieval and interpretation:
agent supplies seller ID | v wrapper pins seller-only input | v Actor returns records | v type + identity checks -> explicit seller fields -> agent This MCP client configuration exposes the Actor to a private execution host. The server process inherits APIFY_TOKEN from its environment.
{ "mcpServers": { "seller-source": { "command": "npx", "args": [ "-y", "@apify/actors-mcp-server", "--actors", "vexaloom-labs/ozon-ru-product-pricing-ratings-and-seller-scraper" ] } } } That connection alone doesn't enforce a seller-only contract. It exposes the Actor's input surface.
Your summarizing agent should receive a narrower MCP tool, check_seller, whose handler applies the policy below. Keep the raw Actor tool available to the execution host rather than registering it alongside the wrapper for that agent.
Otherwise, the agent can simply choose the broader tool. We'd rather remove that decision.
Let the agent choose the target
For this wrapper, we'd accept a numeric seller ID. The Actor also accepts storefront URLs and slugs, but a numeric target gives this small implementation a direct identity comparison.
That restriction is deliberate. Supporting aliases requires an explicit resolution step, not a hopeful comparison between a storefront name and a URL.
| Setting | Wrapper policy | Reason |
|---|---|---|
sellerUrls | Build from the agent's single seller ID | Keeps the request tied to an explicit target |
mode | Pin to "seller" | Prevents a seller check from becoming a product lookup |
includeSellerProducts | Pin to false | Removes the catalog branch at retrieval |
maxItems | Pin to 1 | Bounds emitted records for this narrow task |
storefront | Pin to "ru" | Makes the selected market part of the tool contract |
Leave product filters outside this tool. Filtering inventory by price or product rating doesn't establish anything about the seller's profile rating.
If you later need assortment analysis, give it a separate tool with a separate response shape. The names should tell the agent which question each tool can answer.
Preserve the subject when you serialize
Disabling catalog emission is the input fix. Checking the returned records is the output fix.
The following projection rejects mixed types, ambiguous cardinality, and an identity mismatch before serialization. Its renamed keys belong to our wrapper, not to the Actor.
seller_fields = { "recordType": "recordType", "sellerId": "sellerId", "name": "seller_name", "rating": "seller_profile_rating", "reviewsCount": "seller_profile_reviews_count", "ordersCount": "seller_orders_count", "legalName": "seller_legal_name", "registrationNumber": "seller_registration_number", "url": "url", "scrapedAt": "scrapedAt", } def project_seller(rows, expected_seller_id): if any(row.get("recordType") != "seller" for row in rows): raise ValueError("Non-seller record in seller-check response") if len(rows) != 1: raise ValueError("Expected exactly one seller profile") seller_row, = rows if str(seller_row.get("sellerId", "")) != expected_seller_id: raise ValueError("Seller identity is missing or mismatched") if not seller_row.get("url") or not seller_row.get("scrapedAt"): raise ValueError("Seller evidence lacks source or observation time") return { alias: seller_row[source] for source, alias in seller_fields.items() if source in seller_row } We don't take the first row and declare it the requested entity. Position in a dataset isn't identity.
Missing optional fields stay missing. The Actor omits empty values, so an absent rating mustn't become a neutral score, and an absent registration number mustn't become evidence that registration doesn't exist.
Here's the handler logic to bind to your agent-facing MCP tool. It uses the Python client for execution; MCP registration remains specific to your host.
import os from apify_client import ApifyClient client = ApifyClient(os.environ["APIFY_TOKEN"]) def check_seller(seller_id: str): if ( not isinstance(seller_id, str) or not seller_id.isascii() or not seller_id.isdecimal() ): raise ValueError("Supply one numeric seller ID as a string") run_input = { "mode": "seller", "sellerUrls": [seller_id], "includeSellerProducts": False, "maxItems": 1, "storefront": "ru", } run = client.actor( "vexaloom-labs/ozon-ru-product-pricing-ratings-and-seller-scraper" ).call(run_input=run_input) if run["status"] != "SUCCEEDED": raise RuntimeError(f"Seller collection ended with {run['status']}") items = client.dataset(run["defaultDatasetId"]).list_items().items return project_seller(items, seller_id) The function accepts no list and offers no switch for adding inventory. The agent gets a target choice, not a collection-policy editor.
Budget the evidence after projection
Token trimming before entity selection can preserve the wrong evidence very efficiently.
Start with the seller projection, serialize it, then measure the exact text you intend to send. Don't truncate serialized JSON halfway through a source URL to meet a budget.
This cell checks rejection of the synthetic mixture and measures the accepted seller context:
import json import tiktoken try: project_seller(example_rows, "123456") except ValueError: pass else: raise AssertionError("Mixed records must be rejected") seller_context = project_seller( [row for row in example_rows if row.get("recordType") == "seller"], "123456", ) context_text = json.dumps( seller_context, ensure_ascii=False, separators=(",", ":") ) encoding = tiktoken.get_encoding("cl100k_base") context_tokens = len(encoding.encode(context_text)) context_tokens measures this encoding's token count, not a universal count for every model.
For a larger response, remove optional evidence deliberately and measure again. Preserve identity, source, and observation time together: a compact rating without its subject is how this annoyance started.
What still breaks the check
A successful run status isn't proof of complete collection. Partial failures can leave valid rows available, and mirror availability can affect coverage.
The wrapper therefore treats an empty response as an error, not as a seller with nothing concerning to report. It also refuses unrelated records instead of feeding them to the model as consolation data.
The worked example uses Ozon.ru Product Pricing, Ratings & Seller Scraper.
Legal identifiers help with entity matching, but their presence doesn't establish that a prospective business relationship is safe. Cross-check the legal entity through an appropriate independent source before making that decision.
Likewise, a product's favorable reviews can support a product assessment. They don't become seller-profile evidence merely because the product appears in that seller's catalog.
Give the agent a smaller promise
Our verdict: make the tool return seller evidence, then ask the agent to explain it.
A prompt that says “don't confuse the ratings” still leaves the confusion available. A typed projection removes the competing score and names the remaining score's subject.
Keep the agent's instruction equally narrow: describe the seller profile, identify missing evidence, and preserve the source and observation time. Don't turn the profile rating into a blanket approval.
That's a useful weekend-sized fix. The model can spend its attention on the evidence instead of deciding whose reputation it has been handed.
Reproduce the boundary check
- Run the synthetic cells to check that mixed records are rejected and inspect
context_tokens. - Bind
check_selleras your MCP handler, exposing only its string argument. - Set
APIFY_TOKENin the execution environment and substitute your intended seller ID. - Submit the following Actor input through the handler's pinned path.
Example input, using a placeholder seller ID that you should replace:
{ "mode": "seller", "sellerUrls": ["123456"], "includeSellerProducts": false, "maxItems": 1, "storefront": "ru" }