# Restaurant Menu Item-Price History (`coily_ackee/menu-price-history`) Actor

Track restaurant menu item prices over time. Records each dish's price from public schema.org menu data into an append-only price-history ledger, and computes a menu-price inflation index — the price history a one-shot scraper can't reconstruct.

- **URL**: https://apify.com/coily\_ackee/menu-price-history.md
- **Developed by:** [David Liu](https://apify.com/coily_ackee) (community)
- **Categories:** E-commerce, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

## Restaurant Menu Item-Price History (Keystone N1 probe)

Probe-grade v0 of Keystone candidate **N1 / GATE C1**: a niche time-series
scraper that extracts **per-item restaurant menu prices** and appends them to a
**never-delete, append-only history ledger**. The accumulating dated price
series is the product: a fresh, memoryless scraper can only see *today*, so the
months of dated history retained here are the practical edge — you had to have
been watching the source over time to have it.

**Integrity claim (what is provable today).** The ledger is **append-only and
tamper-evident to the operator**: it is never rewritten or deleted, and
`fingerprint()` is a self-computed SHA-256 over the file so *the operator* can
detect after-the-fact mutation of their own store. It is **not**, on its own,
cryptographically **un-backfillable** — a plain self-computed hash does not stop
the operator from recomputing it over back-dated rows. Provable un-backfillable
integrity (**Ed25519 hash-chain + Merkle roots + external anchoring**, carried
from the Deadman pattern) is a **deferred before-scale upgrade**; until it ships
the public copy claims only "append-only + tamper-evident," never "un-forgeable"
or "un-backfillable." (Claim only what is provable — the Deadman discipline.)

> Status: **feature branch only**, probe-grade. Not merged, not deployed, not
> published to Apify. The human publishes. See `CHANGE_NOTE.md`.

### What it does

1. **Scrape** a small, config-driven seed list of restaurants via a pluggable
   source adapter (default real source: schema.org `Menu` JSON-LD on each
   restaurant's own public page).
2. **Append** each observation `{source, restaurant_id, item_id, name, price,
   currency, captured_at}` to an **append-only JSONL ledger**, keyed by
   `(source, restaurant_id, item_id, date) -> price`, **idempotent** (re-running
   the same day is a no-op; a price *change* on a later day appends a new dated
   row; nothing is ever deleted).
3. **Query** the retained history: `priceHistory(item/restaurant over time)` and
   a menu-price **index** aggregate (mean/median % price change by cuisine or
   city over a window).

### Layout

| Path | Purpose |
|---|---|
| `.actor/actor.json` | Apify Actor definition |
| `.actor/input_schema.json` | Actor input schema (source, seeds, flags) |
| `.actor/dataset_schema.json` | Dataset output shape |
| `Dockerfile` | Apify Node image; installs `apify` SDK for the deployed Actor |
| `src/ledger.js` | **The product** — append-only, idempotent, never-delete ledger |
| `src/query.js` | Price-history + menu-price-index queries |
| `src/scrape.js` | Source-adapter dispatcher + polite runner |
| `src/adapters/jsonld.js` | Default real source: schema.org Menu JSON-LD |
| `src/adapters/fixture.js` | No-network adapter (selftest + safe default) |
| `src/config.js` | Gating flags (default-OFF where they gate behavior) |
| `src/main.js` | Actor entry (real run). Apify SDK loaded softly; runs standalone too |
| `test/selftest.js` | **State-neutral, no-network selftest** |
| `test/fixtures/` | Canned menu payloads + a menu-page HTML fixture |
| `seeds/restaurants.example.json` | Example seed list |

### Run the selftest (no network, mutates no committed state)

```bash
node test/selftest.js        # or: npm run selftest
````

It validates append / dedupe / change / never-delete / query / parser logic on
fixtures, and hashes the committed project tree before and after to prove it
mutates nothing. Ledger writes go to an OS temp dir that is cleaned up.

### Run a real scrape (manual — NOT part of the selftest)

```bash
## 1) Build a small seed list of restaurant menu-page URLs (see seeds/).
## 2) Provide it via input.json or Actor input:
cat > input.json <<'JSON'
{ "source": "jsonld",
  "seeds": [ { "restaurant_id": "some-place", "url": "https://…/menu", "cuisine": "Italian", "city": "NYC" } ],
  "ledgerPath": "./data/price-history.jsonl" }
JSON
node src/main.js
```

Default `source` is `fixture` (**no network**). A real network run must
explicitly opt into `jsonld`. Politeness (delay + normal UA) is enforced by
`src/scrape.js`. No login, no PII, only the exact seed URLs.

### Deferred / follow-ups

See `CHANGE_NOTE.md` for the source + ToS read and the deferred hardening list
(robots.txt preflight, anti-bot resilience, additional adapters, SQLite backend,
signed/Merkle ledger integrity carried from the Deadman pattern).

# Actor input Schema

## `source` (type: `string`):

Which extractor to run. 'jsonld' = read schema.org Menu JSON-LD from each restaurant's own public menu page (the real source). 'fixture' = NO NETWORK (canned payloads, for a safe offline test).

## `seeds` (type: `array`):

List of restaurants to track. Each entry: { restaurant\_id, url (public menu page with schema.org Menu JSON-LD), cuisine?, city? }. Replace these examples with your own. Use tolerant public menu pages only; no login, no PII.

## `ledgerPath` (type: `string`):

Path to the append-only JSONL history ledger. NEVER deleted; re-runs are idempotent. On Apify this is a per-run working file (the container FS is wiped between runs); the ledger is durably persisted across runs via the named key-value store below.

## `ledgerStoreName` (type: `string`):

Name of the NAMED Apify key-value store that durably holds the accumulated history ledger across runs. On Apify the container filesystem is wiped between runs, so the ledger is hydrated from this named store at run start and saved back after append (a named store persists across runs; the default store does not reliably). The accumulating history is the product's value. Ignored when run locally.

## `requestDelayMs` (type: `integer`):

Politeness delay between HTTP requests. Ignored for the offline fixture source.

## `userAgent` (type: `string`):

A normal, descriptive User-Agent. No spoofing of a real browser identity.

## `maxRestaurantsPerRun` (type: `integer`):

Hard cap on seed entries processed per run.

## `pushToDataset` (type: `boolean`):

Default ON. Extracted per-item price observations are written to the Actor dataset (this is what pay-per-result bills on). Turn OFF to run without producing dataset output.

## `emitQueries` (type: `boolean`):

Default ON. The run also computes a menu-price index and a sample price history and saves them to the key-value store.

## `allowZeroPrice` (type: `boolean`):

Default OFF. When OFF, a price of 0 (including a non-numeric value like 'free' that would strip to empty) is REJECTED so it never enters the never-delete ledger as a false $0.00 / -100% drop. Turn ON only when the source genuinely lists free items.

## Actor input object example

```json
{
  "source": "jsonld",
  "seeds": [
    {
      "restaurant_id": "los-tacos-no1-nyc",
      "url": "https://www.lostacos1.com/menus/",
      "cuisine": "Mexican",
      "city": "New York"
    },
    {
      "restaurant_id": "tableau-french-quarter-nola",
      "url": "https://www.tableaufrenchquarter.com/menu/dinner-menu/",
      "cuisine": "Creole-French",
      "city": "New Orleans"
    },
    {
      "restaurant_id": "residents-dc",
      "url": "https://www.residentsdc.com/menu/dinner-menu/",
      "cuisine": "New American",
      "city": "Washington, DC"
    },
    {
      "restaurant_id": "coral-tree-cafe-brentwood",
      "url": "https://www.coraltreecafe.com/menu/",
      "cuisine": "American",
      "city": "Brentwood, CA"
    }
  ],
  "ledgerPath": "./data/price-history.jsonl",
  "ledgerStoreName": "n1-price-history",
  "requestDelayMs": 2000,
  "maxRestaurantsPerRun": 25,
  "pushToDataset": true,
  "emitQueries": true,
  "allowZeroPrice": false
}
```

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("coily_ackee/menu-price-history").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("coily_ackee/menu-price-history").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{}' |
apify call coily_ackee/menu-price-history --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=coily_ackee/menu-price-history",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Restaurant Menu Item-Price History",
        "description": "Track restaurant menu item prices over time. Records each dish's price from public schema.org menu data into an append-only price-history ledger, and computes a menu-price inflation index — the price history a one-shot scraper can't reconstruct.",
        "version": "0.1",
        "x-build-id": "5ndYbHGTByLUq4bkb"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/coily_ackee~menu-price-history/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-coily_ackee-menu-price-history",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/coily_ackee~menu-price-history/runs": {
            "post": {
                "operationId": "runs-sync-coily_ackee-menu-price-history",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/coily_ackee~menu-price-history/run-sync": {
            "post": {
                "operationId": "run-sync-coily_ackee-menu-price-history",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "properties": {
                    "source": {
                        "title": "Source adapter",
                        "enum": [
                            "jsonld",
                            "fixture"
                        ],
                        "type": "string",
                        "description": "Which extractor to run. 'jsonld' = read schema.org Menu JSON-LD from each restaurant's own public menu page (the real source). 'fixture' = NO NETWORK (canned payloads, for a safe offline test).",
                        "default": "jsonld"
                    },
                    "seeds": {
                        "title": "Restaurant seed list",
                        "type": "array",
                        "description": "List of restaurants to track. Each entry: { restaurant_id, url (public menu page with schema.org Menu JSON-LD), cuisine?, city? }. Replace these examples with your own. Use tolerant public menu pages only; no login, no PII.",
                        "default": [
                            {
                                "restaurant_id": "los-tacos-no1-nyc",
                                "url": "https://www.lostacos1.com/menus/",
                                "cuisine": "Mexican",
                                "city": "New York"
                            },
                            {
                                "restaurant_id": "tableau-french-quarter-nola",
                                "url": "https://www.tableaufrenchquarter.com/menu/dinner-menu/",
                                "cuisine": "Creole-French",
                                "city": "New Orleans"
                            },
                            {
                                "restaurant_id": "residents-dc",
                                "url": "https://www.residentsdc.com/menu/dinner-menu/",
                                "cuisine": "New American",
                                "city": "Washington, DC"
                            },
                            {
                                "restaurant_id": "coral-tree-cafe-brentwood",
                                "url": "https://www.coraltreecafe.com/menu/",
                                "cuisine": "American",
                                "city": "Brentwood, CA"
                            }
                        ]
                    },
                    "ledgerPath": {
                        "title": "History ledger path",
                        "type": "string",
                        "description": "Path to the append-only JSONL history ledger. NEVER deleted; re-runs are idempotent. On Apify this is a per-run working file (the container FS is wiped between runs); the ledger is durably persisted across runs via the named key-value store below.",
                        "default": "./data/price-history.jsonl"
                    },
                    "ledgerStoreName": {
                        "title": "History store name (persists across runs)",
                        "type": "string",
                        "description": "Name of the NAMED Apify key-value store that durably holds the accumulated history ledger across runs. On Apify the container filesystem is wiped between runs, so the ledger is hydrated from this named store at run start and saved back after append (a named store persists across runs; the default store does not reliably). The accumulating history is the product's value. Ignored when run locally.",
                        "default": "n1-price-history"
                    },
                    "requestDelayMs": {
                        "title": "Delay between restaurants (ms)",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Politeness delay between HTTP requests. Ignored for the offline fixture source.",
                        "default": 2000
                    },
                    "userAgent": {
                        "title": "User-Agent",
                        "type": "string",
                        "description": "A normal, descriptive User-Agent. No spoofing of a real browser identity."
                    },
                    "maxRestaurantsPerRun": {
                        "title": "Max restaurants per run",
                        "minimum": 1,
                        "type": "integer",
                        "description": "Hard cap on seed entries processed per run.",
                        "default": 25
                    },
                    "pushToDataset": {
                        "title": "Push rows to Apify dataset",
                        "type": "boolean",
                        "description": "Default ON. Extracted per-item price observations are written to the Actor dataset (this is what pay-per-result bills on). Turn OFF to run without producing dataset output.",
                        "default": true
                    },
                    "emitQueries": {
                        "title": "Emit history/index queries",
                        "type": "boolean",
                        "description": "Default ON. The run also computes a menu-price index and a sample price history and saves them to the key-value store.",
                        "default": true
                    },
                    "allowZeroPrice": {
                        "title": "Allow $0 (free) prices",
                        "type": "boolean",
                        "description": "Default OFF. When OFF, a price of 0 (including a non-numeric value like 'free' that would strip to empty) is REJECTED so it never enters the never-delete ledger as a false $0.00 / -100% drop. Turn ON only when the source genuinely lists free items.",
                        "default": false
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
