Every week I get invited to more AI events than I can attend. Bond AI, Founders Bay, AI Collective, half a dozen other newsletters, each one dropping a handful of events into my inbox with no way to tell, at a glance, which ones I’m actually free for. Public aggregators like Luma and Eventbrite don’t help either. The interesting events, the ones from niche community newsletters, never show up there in the first place. If you live in a city like New York or San Francisco, this is a familiar kind of overload, too many good options, scattered across too many places, with no single view of what actually fits your week.
So I built EventMatch. And I vibecoded it, using Antigravity, Gemini 3.5 Flash, and Google’s Agent Development Kit.
Here’s the walkthrough, what I wanted the app to do, how the agents are put together, and what building it actually taught me.
What I wanted the app to do
The core ask was simple to state and harder to deliver. Pull events from public platforms and from the private newsletters sitting in my inbox, check every one of them against my real calendar, and only show me what’s actually conflict free.
A few things went beyond that baseline. Preferences needed to be conversational, not a set of rigid tags, something like “a mix of tech networking, AI workshops, creative art showcases, and relaxed social gatherings,” not a checkbox list. And those preferences needed to stay stable. If I ask a one off question like “show me free events this weekend,” that shouldn’t silently overwrite my standing preferences. Only an explicit request to update them should.
The rest was polish that ended up counting for more than I expected. Price shown as a tier, free, , $ , $$$, instead of raw text pulled from the source. One register button per event, not two competing ones in the same modal.
The multi agent architecture
EventMatch runs on ADK 2.0, and the workflow alternates between plain Python nodes and LLM agents. That alternation is the whole design.
prepare_extractor (Python) sanitizes the incoming message and reads the current session state.
preference_extractor (LlmAgent) parses the user’s message to pull out location, and decides whether this is a persistent preference change or a one off search.
filter_events_node (Python) pulls public data from Luma and Eventbrite, triggers background fetches from Gmail and Google Calendar, and merges everything. This is where the actual grounding happens. It runs a millisecond level time overlap check against the live calendar and flags conflicts before anything reaches the model, and converts events to plain text descriptions to keep the token count down for the next step.
vibe_ranker (LlmAgent) takes the conflict free events and scores them against the user’s preferences, 0 to 100, with a short explanation for each one.
formatter_node (Python) closes the loop, and it’s the piece that holds the whole system together. The vibe_ranker only ever sees plain text descriptions and returns ranked IDs. formatter_node takes those IDs and re-joins them against the original database records. Registration URLs, host names, event locations, none of it passes through the model twice.
Python does the things Python is reliable at, filtering, matching, deduplicating. The LLM does the one thing it’s actually needed for, judging fit against a vague preference. An LLM asked to both evaluate and format a result will, often enough, paraphrase a URL wrong or invent a detail that sounds plausible. Keeping those two jobs separate removes that failure mode instead of trying to prompt it away.
Integrations
Underneath the agents is a fairly ordinary web app, FastAPI on the backend, Python 3.11, React on the frontend, built with Vite and Tailwind.
The integration that makes everything else possible is OAuth. EventMatch requests read only scopes for Gmail and Calendar through Google’s official client libraries. Once authorized, the backend caches the credentials and the UI shows the connected account in the navbar. Everything downstream, the newsletter parsing, the conflict checking, only works because that connection is live.
Public event data comes from Luma and Eventbrite, which meant working around scraping restrictions rather than clean APIs. Deployment is Docker on Cloud Run, provisioned with Terraform.
Building it, and what it taught me
I built this in Antigravity, following a spec driven process rather than jumping straight into code. Start with a product requirements document. Turn that into a high level design. Turn the design into a spec detailed enough for an agent to build from. Only then does actual development start.
Coding a working app over a weekend is genuinely exciting. It’s also where that process got tested, and where it broke down. The pattern showed up early and kept showing up. Fix one bug, move to the next, fix that one, and the first bug resurfaces. Back and forth, the same category of issue reappearing in a slightly different shape each time.
The spec was the actual problem. When the requirements document and the spec are vague about what the app needs to do and what it needs to be tested against, the agent has room to solve the same problem two different ways in two different places, and neither implementation knows about the other. Every fix becomes local. Nothing stays fixed.
The fix is spending more time upfront, at the PRD stage and the spec stage, reviewing what the agent actually produced there before writing a line of application code. Get the spec and the test plan tight enough that they cover what “done” actually means, and the back and forth during development drops off. Skip that step, and you pay for it later, one resurfacing bug at a time.


Top comments (0)