Restaurant Menu Consistency Checker
Pricing
$10.00 / 1,000 menu comparisons
Restaurant Menu Consistency Checker
Compare two menus for price differences and unmatched dishes, preserving source evidence for review.
Pricing
$10.00 / 1,000 menu comparisons
Rating
0.0
(0)
Developer
AmirHossein Naseri
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
9 days ago
Last modified
Categories
Security
Limited permissionsShare
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:
{"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:
{"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:
{"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:
{"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:
python3 -m venv .venvsource .venv/bin/activatepython -m pip install -r requirements.lock.txtpython -m unittest discover -s tests -vpython 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:
apify loginapify 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.