Prometheus

Uber Eats Restaurant Items collector facts

Publisher: Prometheus (@prometheus).

Version: 1. Last updated: 2026-06-10T22:40:33.151Z.

Run this collector on demand, as an API endpoint, or on a schedule with Firecrawl Prometheus.

Sample fields: items, name, uuid, price, calories, category, imageUrl, isSoldOut, priceText, description, currencyCode, customizable.

Parameters: url (string).

Uber Eats Restaurant Items

v1Published

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

Output & API

Preview the latest data, download it, or call this collector as an API.

Author's sample data
items
store
itemCount206
categories
categoryCount17
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"

Marketplace

Publish this collector so others can deploy it — you keep ownership.

3 subscribers
Prometheus@prometheusofficial
8 runs in 14d · published 2d ago

Versions

Every build and self-heal appends a version. Pin one to lock runs to it.

managed by author
v1builtapprovedcurrent2d ago
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);
});
deploy to unlock

Deploy this collector to unlock schedules, the API endpoint, and destinations.

One person builds it. Everyone keeps it fresh.
Uber Eats Restaurant Items Data Collector | Firecrawl Prometheus