Tourto — Project Story Inspiration Downtown Toronto has something happening almost every night — a festival, a market, a pop-up, a show — but that information lives scattered across Instagram stories, word of mouth, and event pages that are impossible to search geographically. I wanted one thing: open a map, see what's actually happening right now, near me, without digging through five different apps. That's the whole premise Tourto starts from — a live, spatial answer to "what can I do tonight?"
What it does Tourto is a dashboard centered on downtown Toronto that pins real events onto an interactive map. Each pin is time-aware — it knows whether the event it represents is live, starting soon, upcoming, or ended, computed continuously from real start/end timestamps rather than a static label. Live events pulse. Ended ones fade. You can filter by category — festivals, live music, markets, arcades, art, nightlife, outdoor — and click any pin to fly in and see the details.
A floating "Ask AI" widget sits on top of the map, powered by Backboard.io. It's not a generic chatbot bolted on for show — every question gets answered with the actual currently-pinned events injected as context, so "what can I do for arcade this weekend?" gets a real answer grounded in real data. The integration is fully built and verified end-to-end; getting it to produce live responses right now is blocked purely on adding billing credit to the Backboard account (their free tier covers Memory/RAG, not chat completions) — more on that below.
How I built it The stack is Next.js (App Router, TypeScript), MapLibre GL JS for the map, Tailwind CSS v4, Framer Motion for the marker/popup animation, and Zustand for state.
I built it in three deliberate phases. First, the mapping environment on mock data — nine fake downtown Toronto events with times computed relative to page load, so I could see and tune the live/soon/upcoming/ended behavior immediately without waiting on any real API integration. Once the interaction model felt right, phase two wired up a server-side data pipeline (/api/pins) that fetches from the Ticketmaster Discovery API and the City of Toronto's own open-data festivals feed, normalizes both into a shared EventPin shape, geocodes addresses through OpenStreetMap's free Nominatim service, and deduplicates pins that likely describe the same real-world event. Phase three added the AI layer: a /api/chat route that pulls the same live pins data (via a small shared cache used by both routes), builds a grounded prompt out of it, and forwards it to Backboard.
The dedup step is the one place actual math shows up — two pins from different sources get merged if they're within roughly 150 meters of each other and their time windows overlap, using the haversine great-circle distance formula:
$$ d = 2r \cdot \arcsin\left(\sqrt{\sin^2\left(\frac{\Delta\varphi}{2}\right) + \cos(\varphi_1)\cos(\varphi_2)\sin^2\left(\frac{\Delta\lambda}{2}\right)}\right) $$
where $\varphi_1, \varphi_2$ are the two pins' latitudes, $\Delta\varphi$ and $\Delta\lambda$ are the latitude/longitude differences, and $r$ is Earth's radius — cheap enough to run pairwise over a few dozen pins without needing a spatial index.
Challenges I ran into Mapbox wanted a credit card just to sign up. I didn't want that, so I researched free/open alternatives and swapped to MapLibre GL JS (an open-source fork with the same API) on top of CARTO's free, key-less basemap tiles — no account anywhere in the stack. The map rendered completely blank street data after that swap — just a flat dark background with pins floating on nothing. Tracked it down to MapLibre trying to auto-locate its own tile-parsing Web Worker via import.meta.url, which doesn't resolve under Next.js's bundler — so new Worker("") was failing silently and no vector tiles were ever fetched. I only found this by instrumenting window.Worker itself in a headless browser to catch the exact broken call. The fix was serving MapLibre's worker files as static assets and pointing to them explicitly, kept in sync automatically via a postinstall script. Eventbrite's public search API turned out to be dead — shut off to third parties back in 2020. I'd assumed it as an obvious event source going in; had to drop it entirely and pivot to Ticketmaster + the city's own open data instead. Toronto's live open-data feed blocked my dev sandbox's requests (an Akamai WAF 403). Rather than block on that, I built the adapter to degrade gracefully — every source fails independently and silently falls back to nothing rather than breaking the whole pipeline, so the map is never one flaky government API away from looking broken. A subtler bug: leftover boilerplate CSS from the initial scaffold was silently overriding my intentional dark theme and font choice, because of how Tailwind v4's CSS cascade layers interact with plain unlayered CSS. It took a close screenshot comparison to even notice the header was rendering the wrong background color. The AI integration hit a billing wall, not a code wall. After wiring up Backboard end-to-end — auth, request, response, thread continuity, all correct — the response body came back as a billing notice: their free credit covers Memory/RAG but not chat completions. Rather than fake a working demo or quietly hide the limitation, I built the widget to surface a clear, honest message when the assistant isn't fully funded yet, the same graceful-degradation philosophy as the data pipeline. A real, working integration is worth more than a fake, always-on one. What I learned The map itself — markers, animation, filtering — was the easy part. The hard part was everything adjacent to it: verifying which "free" APIs are actually still free and actually still public before building around them, understanding a third-party platform's billing model before assuming a feature is done, and designing for graceful degradation from the start so that an outage or a billing gap never becomes a broken product. I also learned to distrust "it looks done" — several of these bugs were invisible until I actually screenshotted the running app and looked closely, rather than trusting that code compiling meant it worked.
What's next A small amount of billing credit to flip the AI widget from "built" to "live," a personal notes sidebar and a schedule/itinerary tab both wired to that same assistant, and eventually exploring whether event info trapped in Instagram post images — not just captions — can be pulled in responsibly.
Built With
- ai
- backboard
- carto
- chatbot
- css
- framer-motion
- gps
- javascript
- llm
- maplibre-gl-js
- maps
- nextjs
- node.js
- nominatim
- openstreetmap
- react
- react-map-gl
- replit
- rest-api
- tailwindcss
- ticketmaster-api
- typescript
- vector-tiles
- zustand
Log in or sign up for Devpost to join the conversation.