Prometheus

Uber Eats Restaurant Items

v1Published

The full menu catalog (categories and items with prices, calories, and details) for any Uber Eats store page.

Author's sample data
items
store
itemCount206
categories
categoryCount17
Publisher
2 subscribers
Prometheus@prometheusofficial
Every hour6 runs in 14d · published 20h ago
Parameters
--urlstringThe Uber Eats store page URL to scrape the menu from. default "https://www.ubereats.com/store/starbucks-1750-divisadero-street/frb7Q3ZbQOKg15qMgdBjkA" · e.g. "https://www.ubereats.com/store/starbucks-1750-divisadero-street/frb7Q3ZbQOKg15qMgdBjkA"
Versions
managed by author
v1builtapprovedcurrent21h ago
Schedulesdeploy to enable

Run this collector on a cadence — daily, hourly, your call.

API endpointdeploy to unlock

POST to run it on demand and get fresh data in the response.

Activitydeploy to track

6 subscriber runs in the last 14 days.

How this script collects data
import Firecrawl from "@mendable/firecrawl-js";
import * as cheerio from "cheerio";
import { parseArgs } from "node:util";

const apiKey = process.env.FIRECRAWL_API_KEY;
if (!apiKey) {
  console.error("FIRECRAWL_API_KEY is not set");
  process.exit(1);
}
const firecrawl = new Firecrawl({ apiKey });

// --url lets the script target any Uber Eats store page; defaults to the requested Starbucks store.
const { values: flags } = parseArgs({
  strict: true,
  options: {
    url: { type: "string" },
  },
});
const storeUrl =
  flags.url ??
  "https://www.ubereats.com/store/starbucks-1750-divisadero-street/frb7Q3ZbQOKg15qMgdBjkA";

/**
 * Uber Eats embeds the full store catalog as JSON inside a
 * <script id="__REACT_QUERY_STATE__"> tag. That JSON is HTML-safe-encoded:
 * every backslash in the original JSON is written as the literal text "%5C",
 * and the structural quotes/brackets are written as \uXXXX escapes. We reverse
 * both transforms to recover the real JSON, then read the getStoreV1 payload.
 */
function decodeReactQueryState(scriptText: string): any {
  const unescaped = JSON.parse('"' + scriptText.trim() + '"'); // " -> " etc.
  const restored = unescaped.replace(/%5C/g, "\\"); // %5C -> backslash
  return JSON.parse(restored);
}

function dollars(cents: unknown): number | null {
  return typeof cents === "number" ? Math.round(cents) / 100 : null;
}

// priceTagline reads like "$7.35 • 250 Cal." — pull the calorie figure out of it.
function caloriesFrom(tagline: unknown): number | null {
  if (typeof tagline !== "string") return null;
  const m = tagline.match(/([\d,]+)\s*Cal\./i);
  return m ? Number(m[1].replace(/,/g, "")) : null;
}

async function main() {
  console.error(`Scraping ${storeUrl}`);
  const res = await firecrawl.scrape(storeUrl, { formats: ["rawHtml"] });
  const html = (res as any).rawHtml ?? (res as any).html;
  if (!html) throw new Error("No HTML returned from Firecrawl scrape");

  const $ = cheerio.load(html);
  const scriptText = $("script#__REACT_QUERY_STATE__").first().text();
  if (!scriptText) throw new Error("Could not find __REACT_QUERY_STATE__ on the page");

  const state = decodeReactQueryState(scriptText);
  const queries: any[] = state.queries ?? [];
  const storeQuery = queries.find(
    (q) => Array.isArray(q.queryKey) && q.queryKey[0] === "getStoreV1",
  );
  if (!storeQuery) throw new Error("getStoreV1 query not found in embedded state");
  const store = storeQuery.state.data;

  const categories: Array<{ name: string; itemCount: number; items: any[] }> = [];
  const allItems: any[] = [];
  const seen = new Set<string>();

  const catalogSectionsMap = store.catalogSectionsMap ?? {};
  for (const sectionKey of Object.keys(catalogSectionsMap)) {
    for (const entry of catalogSectionsMap[sectionKey]) {
      // VERTICAL_GRID entries are the real menu categories. The HORIZONTAL_GRID
      // "Featured items" carousel only re-lists items from those categories, so
      // we skip it to keep the catalog free of duplicates.
      if (entry.type !== "VERTICAL_GRID") continue;
      const payload = entry.payload?.standardItemsPayload;
      if (!payload) continue;
      const categoryName: string = payload.title?.text ?? "Uncategorized";
      const catalogItems: any[] = payload.catalogItems ?? [];

      const items = catalogItems.map((it) => ({
        uuid: it.uuid as string,
        name: it.title as string,
        description: (it.itemDescription as string) ?? null,
        category: categoryName,
        price: dollars(it.price),
        priceText: it.priceTagline?.text ?? null,
        calories: caloriesFrom(it.priceTagline?.text),
        currencyCode: store.currencyCode ?? "USD",
        imageUrl: (it.imageUrl as string) ?? null,
        isSoldOut: Boolean(it.isSoldOut),
        customizable: Boolean(it.hasCustomizations),
      }));

      categories.push({ name: categoryName, itemCount: items.length, items });
      for (const item of items) {
        if (seen.has(item.uuid)) continue;
        seen.add(item.uuid);
        allItems.push(item);
      }
    }
  }

  console.error(
    `Found ${allItems.length} unique items across ${categories.length} categories`,
  );

  const out = {
    store: {
      name: store.title ?? null,
      uuid: store.uuid ?? null,
      slug: store.slug ?? null,
      city: store.citySlug ?? null,
      currencyCode: store.currencyCode ?? "USD",
      rating: store.rating?.ratingValue ?? null,
      url: storeUrl,
    },
    categoryCount: categories.length,
    itemCount: allItems.length,
    categories: categories.map((c) => ({ name: c.name, itemCount: c.itemCount })),
    items: allItems,
  };

  process.stdout.write(JSON.stringify(out));
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
Build prompt
Uber Eats Restaurant Items
One person builds it. Everyone keeps it fresh.
Uber Eats Restaurant Items — Prometheus