Prometheus

Upcoming Meetup Developer Events collector facts

Publisher: sdc (@sdc).

Version: 1. Last updated: 2026-06-13T09:38:49.618Z.

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

Sample fields: count, events, id, url, group, title, status, attendees, startTime, startTimeDisplay, source, keywords.

Parameters: keywords (string), location (string), max-items (number).

Upcoming Meetup Developer Events

v1Published

The top ~10 upcoming developer-related Meetup.com events for a given location, sorted soonest-first.

Output & API

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

Author's sample data
count10
events
sourcemeetup.com
keywordsdeveloper
locationus--ca--San Francisco
scrapedAt2026-06-13T09:35:07.447Z
Parameters
--keywordsstringSearch term for events, e.g. 'developer', 'AI', 'cybersecurity'. default "developer" · e.g. "developer"
--locationstringMeetup location slug, e.g. 'us--ca--San Francisco' or 'gb--eng--London'. Format is country--region--city. default "us--ca--San Francisco" · e.g. "us--ca--San Francisco"
--max-itemsnumberMaximum number of events to return. default 10 · e.g. 10

Marketplace

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

0 subscribers
sdc@sdc
0 runs in 14d · published 14h ago

Versions

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

managed by author
v1builtapprovedcurrent14h 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 });

// --- Parameters -------------------------------------------------------------
const { values: flags } = parseArgs({
  strict: true,
  options: {
    keywords: { type: "string" }, // --keywords=developer (default "developer")
    location: { type: "string" }, // --location="us--ca--San Francisco" (Meetup location slug)
    "max-items": { type: "string" }, // --max-items=10 (default 10)
  },
});

const keywords = (flags.keywords ?? "developer").trim();
// Meetup uses an internal location slug, e.g. "us--ca--San Francisco" or "gb--eng--London".
const location = (flags.location ?? "us--ca--San Francisco").trim();
const maxItems = Math.max(1, Number(flags["max-items"] ?? "10"));
if (!Number.isFinite(maxItems)) {
  console.error("--max-items must be a number");
  process.exit(1);
}

type Event = {
  id: string;
  title: string;
  url: string;
  group: string | null;
  startTime: string | null; // ISO 8601, e.g. "2026-06-16T22:00:00-07:00"
  startTimeDisplay: string | null; // human label, e.g. "Tue, Jun 16 · 3:00 PM PDT"
  attendees: number | null;
  status: string | null; // e.g. "Waitlist", or null when open
};

function buildSearchUrl(): string {
  const params = new URLSearchParams({
    keywords,
    source: "EVENTS",
    location,
  });
  return `https://www.meetup.com/find/?${params.toString()}`;
}

async function main() {
  const searchUrl = buildSearchUrl();
  console.error(`Scraping Meetup events: ${searchUrl}`);

  const doc = await firecrawl.scrape(searchUrl, {
    formats: ["html"],
    integration: "prometheus",
  });

  const html = doc.html;
  if (!html) {
    throw new Error("no HTML returned for the Meetup find page");
  }

  const $ = cheerio.load(html);
  const cards = $("div[data-eventref]");
  if (cards.length === 0) {
    throw new Error(
      "no event cards (div[data-eventref]) found on the Meetup find page — layout may have changed or no events for this location/keywords",
    );
  }

  const now = Date.now();
  const seen = new Set<string>();
  const events: Event[] = [];

  cards.each((_, el) => {
    const card = $(el);
    const id = (card.attr("data-eventref") || "").trim();
    if (!id || seen.has(id)) return;

    const anchor = card.find('a[data-event-label="Event Card"]').first();
    const rawHref = anchor.attr("href") || card.find("a[href]").first().attr("href") || "";
    if (!rawHref) return;
    // Drop tracking query params to keep a clean, stable event URL.
    const url = rawHref.split("?")[0];

    const title = (card.find("h3").first().attr("title") || card.find("h3").first().text() || "").trim();
    if (!title) return;

    const timeEl = card.find("time").first();
    const rawDatetime = (timeEl.attr("datetime") || "").trim();
    // Meetup appends a "[America/Los_Angeles]" zone id that breaks Date parsing — strip it.
    const startTime = rawDatetime ? rawDatetime.replace(/\[[^\]]*\]\s*$/, "") : null;
    const startTimeDisplay = timeEl.text().trim() || null;

    // Group line is rendered as "by <Group Name>".
    let group: string | null = null;
    card.find("div").each((_, d) => {
      if (group) return;
      const t = $(d).text().trim();
      if (/^by\s+\S/.test(t) && $(d).children().length === 0) {
        group = t.replace(/^by\s+/, "").trim();
      }
    });

    // Attendee count is its own span like "10 attendees" — match the span text
    // exactly so a nearby rating (e.g. "4.8") can't bleed into the number.
    let attendees: number | null = null;
    card.find("span").each((_, s) => {
      if (attendees !== null) return;
      const m = $(s).text().trim().match(/^([\d,]+)\s+attendees?$/i);
      if (m) attendees = Number(m[1].replace(/,/g, ""));
    });

    // Status badge (e.g. "Waitlist", "Sold Out"); null means open for registration.
    let status: string | null = null;
    const badge = card.find("span").filter((_, s) => {
      const t = $(s).text().trim();
      return /^(Waitlist|Sold Out|Cancelled|Almost full|Full)$/i.test(t);
    }).first();
    if (badge.length) status = badge.text().trim();

    // Scope: upcoming/open only — drop events whose start time is in the past.
    if (startTime) {
      const ts = Date.parse(startTime);
      if (Number.isFinite(ts) && ts < now) return;
    }

    seen.add(id);
    events.push({ id, title, url, group, startTime, startTimeDisplay, attendees, status });
  });

  // Sort soonest-first, then keep the top N.
  events.sort((a, b) => {
    const ta = a.startTime ? Date.parse(a.startTime) : Number.POSITIVE_INFINITY;
    const tb = b.startTime ? Date.parse(b.startTime) : Number.POSITIVE_INFINITY;
    return ta - tb;
  });

  const out = {
    source: "meetup.com",
    keywords,
    location,
    scrapedAt: new Date().toISOString(),
    count: Math.min(events.length, maxItems),
    events: events.slice(0, maxItems),
  };

  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.