# Restaurant Menu Consistency Checker (`renaissant_vibraphone/restaurant-menu-consistency-checker`) Actor

Compare two menus for price differences and unmatched dishes, preserving source evidence for review.

- **URL**: https://apify.com/renaissant_vibraphone/restaurant-menu-consistency-checker.md
- **Developed by:** [AmirHossein Naseri](https://apify.com/renaissant_vibraphone) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$10.00 / 1,000 menu comparisons

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

## Restaurant Menu Consistency Checker

Compare two menus for the same restaurant location, meal period and service. Find price differences and dishes appearing in only one extracted menu. Every row preserves the original text and its location for review.

This is a small first version, not a universal menu reader. HTML pages need CSS selectors configured for their layout. PDFs must contain selectable text with simple single-column rows. Every finding requires human review; an unmatched dish can mean a naming difference or incomplete extraction.

### Try the fictional demo

In Apify Console use:

```json
{"demo": true}
```

The bundled demo needs no website access. Expect four comparison rows: one different price, one same price, and two dishes found in only one source. Demo runs have no custom comparison fee; platform usage may still apply depending on the pricing configuration.

### Inputs

For custom data, set `demo` to `false`, choose a common `currency` (EUR, USD or GBP), describe the common `context`, and provide exactly two entries in `menus`.

Text example:

```json
{
  "demo": false,
  "currency": "EUR",
  "context": "Central branch, dinner, dine-in",
  "menus": [
    {"label": "Website", "type": "text", "text": "Pizza (large) | 12.00\nSoup | 6.00"},
    {"label": "PDF", "type": "text", "text": "Pizza (large) | 10.00\nSoup | 6.00"}
  ]
}
```

For URL input, see `examples/html-and-pdf.json`. Its example domain is a placeholder: replace both URLs and the selectors before running it.

HTML source:

```json
{
  "label": "Website menu",
  "type": "html",
  "url": "https://YOUR-RESTAURANT.example/menu",
  "itemSelector": ".menu-item",
  "nameSelector": ".dish-name",
  "priceSelector": ".dish-price"
}
```

Each selected item must contain exactly one name and one price. Include portion size in the name, for example `Pizza (large)`. A selector that omits items gives incomplete coverage: inspect the source and extracted rows yourself. Text hidden through CSS alone is not detected.

PDF source:

```json
{"label": "PDF menu", "type": "pdf", "url": "https://YOUR-RESTAURANT.example/dinner.pdf"}
```

Supported PDF rows look like `Pizza (large) 12.00`. Pasted text can use the more explicit `Pizza (large) | 12.00`. Prices require two decimal places; comma or dot is accepted. Thousands separators, ranges, integer-only prices and multiple prices per dish are unsupported. Price symbols must match the common currency. `$` is interpreted as USD in this limited first version.

### Results

The default dataset contains flat comparison rows:

- `same_price`: the same normalized dish name has the same parsed price.
- `price_difference`: the same normalized name has different parsed prices.
- `only_in_source_a` / `only_in_source_b`: no exact normalized name match in the other extraction. This does not establish that a dish has been removed.

Names are normalized for Unicode, case and repeated whitespace. Punctuation, accents, sizes and weights remain significant. No fuzzy matching or automatic translation is performed. Duplicate normalized names cause an error; distinguish portions/sections explicitly.

The key-value store includes `REPORT` (HTML), `COMPARISON` (CSV) and `SUMMARY` (JSON with counts and warnings). Review the summary with the rows. Zero differences does not prove completeness or correctness.

### Limits

Exactly two sources per run, 300 parsed dishes per source, 3 MiB per download, 10 pages per PDF, and 200,000 extracted characters per text/page. PDF parsing runs in a separate process with a 15-second deadline and a Linux memory cap.

Scans, images, complicated PDF columns, JavaScript-rendered menus, login-protected pages, automatic page discovery, promotion dates and delivery platforms are unsupported. Public HTTPS on port 443 is required. Blocked sites are not bypassed. Requests check robots.txt; unverified or redirected robots files are refused conservatively. Only provide sources you are authorized to process.

The Actor does not change menus, contact restaurants or send customer data to an AI provider. Submitted inputs and results are stored in the run's Apify storage according to the account's retention and access settings. Do not put secrets in URLs.

### Local development

Use Python 3.14, matching the Docker runtime:

```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.lock.txt
python -m unittest discover -s tests -v
python run_local.py examples/demo.json
```

The runner creates a separate directory under `storage/` and prints the path to `report.html`. Use `python run_local.py path/to/input.json` for your own input. On Windows activate with `.venv\Scripts\Activate.ps1` instead.

The locked dependencies were resolved on Python 3.14/Linux. The direct dependencies are also listed in `requirements.txt` for deliberate dependency updates.

### Deploy

Install the Apify CLI, then run from this project directory:

```bash
apify login
apify push
```

The Dockerfile and Actor schemas are included. The default Actor name is `restaurant-menu-consistency-checker`. Change `.actor/actor.json` before pushing if you want another name. Test privately in the cloud before publishing or enabling paid billing.

### Optional monetization

Start with usage-only pricing while validating extraction on real menus. When ready, configure a pay-per-event custom primary event named exactly `comparison-completed` in Apify Console. Set synthetic start and dataset-item events to zero or remove them if your intended price is one fee per completed comparison. Set a test price yourself after measuring costs.

The code detects pay-per-event pricing. For non-demo input it checks that the budget can cover one event, processes both menus, stores reports and rows, then calls `Actor.charge(event_name="comparison-completed", count=1)`. A completed comparison with no differences is still a billable comparison. Warnings do not prevent charging. Validation, fetching or extraction failures before report completion do not trigger this custom event. Any separately configured synthetic events or usage charges follow your Console settings.

Test charging locally with `ACTOR_TEST_PAY_PER_EVENT=true python run_local.py examples/demo.json`. The local test does not charge real money; inspect the run directory's `datasets/charging-log` files. Then verify behavior in your own private cloud run and inspect event counts and costs before publication.

The local simulation also logs synthetic dataset-item events at SDK default prices. It does not load your future Console configuration. To sell one comparison for one fee, remove or zero those synthetic events in Console.

Use restart-on-error off for this initial version. A completed-run marker avoids repeating completed work, but charging and writing the marker are not an atomic transaction. Do not advertise exactly-once billing across interrupted/restarted runs; reconcile interrupted paid runs manually before retrying. Starting a new run creates a new comparison and may create a new charge.

See `BUILD-AND-DEPLOY.md` for the full beginner walkthrough and current official documentation links.

# Actor input Schema

## `demo` (type: `boolean`):

Uses bundled fictional menu text without website requests. Remove menus when demo is true. No custom comparison event is charged.

## `currency` (type: `string`):

Three-letter ISO currency code (e.g. EUR, USD, GBP).

## `context` (type: `string`):

Example: Central branch, dinner, dine-in. Both menus must describe the same context.

## `menus` (type: `array`):

See README examples. Each source needs label and type (text/html/pdf). Text requires text. URLs must be public HTTPS. HTML also requires itemSelector, nameSelector and priceSelector.

## Actor input object example

```json
{
  "demo": true,
  "currency": "EUR"
}
```

# Actor output Schema

## `findings` (type: `string`):

No description

## `report` (type: `string`):

No description

## `csv` (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 = {
    "demo": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("renaissant_vibraphone/restaurant-menu-consistency-checker").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 = { "demo": True }

# Run the Actor and wait for it to finish
run = client.actor("renaissant_vibraphone/restaurant-menu-consistency-checker").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 '{
  "demo": true
}' |
apify call renaissant_vibraphone/restaurant-menu-consistency-checker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,renaissant_vibraphone/restaurant-menu-consistency-checker"
        }
    }
}
```

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/wysZXTox7ky8Aregf/builds/Pat91g4Z5b5avYCLA/openapi.json
