# Peru Grocery Basket Aggregator (Metro, Wong, PlazaVea, Vivanda) (`automatic/peru-grocery-basket-aggregator`) Actor

Compare a grocery basket across Peru's four largest chains (Metro, Wong, PlazaVea, Vivanda): cheapest chain per item, basket totals per chain, and where switching stores saves money. Prices in PEN. Use specific item names with size hints (e.g. 'aceite vegetal 1L') for best results.

- **URL**: https://apify.com/automatic/peru-grocery-basket-aggregator.md
- **Developed by:** [Philip Kirkbride](https://apify.com/automatic) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.80 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Peru Grocery Basket Aggregator (Metro · Wong · PlazaVea · Vivanda)

Price a whole grocery basket across Peru's four largest chains in a single run and get the cheapest chain per item, the basket total at each chain, and where switching stores saves money. All prices in PEN (S/). Every item is searched live on all four chains, matched to the same product where possible, and returned as normalized records plus a basket summary.

### Quick Start

- Send a list of grocery items (up to 50); each one is priced at Metro, Wong, PlazaVea and Vivanda, matched across chains, and the cheapest chain per item is flagged.
- For shoppers and deal hunters comparing chains, and for price-research or personal-finance apps that need basket totals per chain.
- Run with this minimal input:

```json
{"basket": ["arroz extra 5kg", "leche evaporada chica"]}
```

### Conservative basket mode

Set `"matchingMode": "basket"` to reject full-text lookalikes. The default
remains `legacy`: existing free-text inputs (notably `leche evaporada chica`)
are broader than the canonical families supported here, so changing the default
would silently remove coverage.

```json
{"matchingMode":"basket","basket":["huevos 12-15 unidades","pechuga de pollo 1kg","agua mineral 1L"]}
```

Supported canonical identities: leche entera, huevos (12 or 15 pack, requested
quantity 12), arroz, pan molde, pechuga de pollo, manzanas, fresh tomate,
aceite vegetal, cafe molido and agua mineral. Include a target mass or volume
(e.g. `500g`, `1kg`, `250g`, `1L`). Unknown identities, missing categories,
unknown sizes, bundles, non-food categories, canned fish and wrong chicken
cuts are rejected, not guessed. Each returned product is checked against the
requested food family, its chain category and its normalized size before it
is accepted. Only accepted in-stock offers enter the dataset and totals.

Basket-mode `matched` means canonical match (not cross-chain SKU identity),
`matchBasis` is `canonical-category-unit`, and `matchConfidence` is a rule-based
evidence score: 1 for exact size, 0.9 for a different known size. This is not a
calibrated probability. `pricePerKg` or `pricePerL` gives a normalized rate.
`price` remains the checkout pack price; `basketPrice` is the proportional
cost for the requested quantity and is used for totals/cheapest comparisons
(`compareBasis: basket`). It is an estimate, not a promise of fractional-pack
checkout. Partial chain baskets are not like-for-like whole baskets; compare
coverage and shared items before interpreting a cheaper total as savings.

Coverage varies by chain and by day: an item with no confident match at a
chain is left out of that chain's total rather than replaced with a
lookalike, and no non-food offer is ever counted.

### Input

| Name | Type | Default | What it does |
|---|---|---|---|
| `basket` | array of strings | `["arroz extra 5kg", "leche evaporada chica"]` | 1–50 free-text grocery items to price, e.g. `pollo entero`, `aceite vegetal 1L`. Adding a size/format hint (`5kg`, `1L`, `30un`) materially improves cross-chain matching. |
| `matchingMode` | string | `legacy` | `basket` enables conservative canonical food/category/size matching. |
| `categoryHint` | string | — | Optional category applied to every item's search (e.g. `abarrotes`), resolved against each chain's own category list; chains where it doesn't resolve fall back to plain search. |
| `requestDelaySecs` | number | `1.0` | Pause between basket items, from 0.5 to 10 seconds. |

### Output

**Dataset** — one record per (item × chain) offer, plus an `unmatched` marker record when no chain has a plausible offer. Fields: `item`, `itemIndex`, `store`, `productId`/`productKey`, `name`, `brand`, `category`, `price`, `listPrice`, `currency` (always `PEN`), `unitMultiplier`, `measurementUnit`, `availableQuantity` (99999 means the chain reports stock as effectively unlimited, not a real count), `grams`, `pieceCount`, `pricePerKg`, `pricePerL`, `pricePerPiece`, `basketPrice`/`matchConfidence` (basket mode), `inStock`, `url`, `imageUrl`, `searchQuery`, `categoryResolved`, `matched`/`matchBasis` (cross-chain linkage), `isCheapest`/`compareBasis` (the cheapest decision), `collectedAt`.

**Key-value store `OUTPUT`** — the basket summary: `items` (cheapest chain per item with name/price/url), `totalsByChain` (total, item count and coverage per chain), `wholeBasket` (cheapest vs priciest chain and the savings on shared items), `mixAndMatch` (cheapest-per-item total), `summaryLine` (one human-readable verdict), and `warnings` (any chain that failed for an item — one flaky chain never kills a run).

Example record:

```json
{
  "recordType": "offer",
  "item": "arroz extra 5kg",
  "itemIndex": 0,
  "store": "metro",
  "productId": "34266",
  "productKey": "metro.pe:34266",
  "name": "Arroz Extra Metro 5kg",
  "brand": "Metro",
  "price": 17.4,
  "listPrice": null,
  "currency": "PEN",
  "unitMultiplier": 5,
  "measurementUnit": "kg",
  "grams": 5000,
  "pricePerKg": 3.48,
  "inStock": true,
  "url": "https://www.metro.pe/arroz-extra-metro-5kg-391214/p",
  "searchQuery": "arroz extra 5kg",
  "categoryResolved": true,
  "matched": true,
  "matchBasis": "name-unit",
  "isCheapest": true,
  "compareBasis": "perKg",
  "collectedAt": "2026-10-20T15:04:05Z"
}
```

### Getting accurate matches

Chain search is full-text, so a generic `aceite` can surface tuna-in-oil and `huevos` can surface quail eggs as cheaper lookalikes. Use specific item names with size hints — `aceite vegetal 1L`, `huevos de gallina`, `arroz extra 5kg` — or set `categoryHint`. Items that name a category in a chain's own list (`huevos`, `pollo entero`) are scoped to it automatically; lookalikes that still slip through are flagged `matched: false`, so you can tell a real low price from a substitution.

### FAQ

**How fresh are the prices?** Every item is searched live during the run; each record carries its UTC collection timestamp in `collectedAt`.

**How many items can I price?** 1–50 per run. Each item is searched on all four chains; a 10-item basket completes in roughly 15–20 seconds.

**How is "cheapest" decided?** Uniformly per item: per-kilogram when every offer has a per-kg price, per-piece when every offer has a pack count, otherwise the nominal price. Flagged lookalike offers still compete — the flags tell you when the winner is a substitution.

**What happens if one chain fails for an item?** The other chains still price it, the item appears in the run summary's `warnings`, and the run completes — one unavailable chain never fails the basket.

**Which currency are the prices in?** Always PEN (S/); every chain reports the same currency, so totals are directly comparable.

### Price

See the Actor's Pricing tab on Apify for current rates.

### What this Actor is not

A comparison tool, not a crawler: each item uses the chain's top first-page search results (top-sale ordering). No detail-page scraping, no other stores.

For help, contact Apify support and include the Actor run ID.

# Actor input Schema

## `basket` (type: `array`):

Grocery items to price, 1–50 free-text lines (e.g. 'arroz extra 5kg', 'leche evaporada chica', 'huevos', 'pollo entero'). Use SPECIFIC item names (+ size/format hints like '5kg', '1L', '30un'): chain full-text search bleeds across categories, so a generic 'aceite' can surface tuna-in-oil and 'huevos' can surface quail eggs as cheaper substitutions. 'aceite vegetal 1L', 'huevos de gallina', or the categoryHint input fix this. Optional since 0.2.3: an empty run input {} prices the two-item starter basket below (issue #203 auto-test gate).

## `matchingMode` (type: `string`):

legacy preserves free-text matching. basket requires a supported canonical food identity, compatible VTEX category and known size; only confident offers count, priced for the requested quantity.

## `categoryHint` (type: `string`):

Optional free-text category applied to every item's search (e.g. 'abarrotes'). Resolved per chain against that chain's own category tree. By default each item is already auto-scoped the same way when its own text names a category (e.g. 'huevos', 'pollo entero' — try adding such items to the basket); set this only to override that with one category for the whole basket. Chains where the hint does not uniquely resolve fall back to plain full-text search with categoryResolved=false on their records.

## `requestDelaySecs` (type: `number`):

Pause between items; within an item the four chains are queried concurrently but staggered — each chain starts 1/4 of this delay after the previous one. Minimum 0.5 seconds.

## Actor input object example

```json
{
  "basket": [
    "arroz extra 5kg",
    "leche evaporada chica"
  ],
  "matchingMode": "legacy",
  "requestDelaySecs": 1
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "basket": [
        "arroz extra 5kg",
        "leche evaporada chica"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("automatic/peru-grocery-basket-aggregator").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "basket": [
        "arroz extra 5kg",
        "leche evaporada chica",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("automatic/peru-grocery-basket-aggregator").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "basket": [
    "arroz extra 5kg",
    "leche evaporada chica"
  ]
}' |
apify call automatic/peru-grocery-basket-aggregator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automatic/peru-grocery-basket-aggregator"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/hk8Atf9cqiTfgc8E7/builds/RktYKtCKpsw4KEg7l/openapi.json
