Inspiration

A mid-sized independent artist usually plans a tour from last year's streaming numbers and gut feel. They play the same three big cities, miss the towns full of people who'd love their music, pick openers by who's free, and never pitch sponsors because they don't know which brands their fans actually care about.

Labels pay analysts to answer those questions. Independent artists can't. I wanted to build that analyst, and Qloo's taste graph turned out to be exactly the missing data: it knows where a taste is concentrated, which local artists share it and which brands those fans over-index on. That's a tour plan hiding in an API.

What it does

You tell TourScout, in plain words, something like "I'm When Chai Met Toast from Kochi, 6 shows across India in February, crowds of 500–1,500, we fly between cities", and an AI agent:

  1. Finds the artist on Qloo, and asks "is this you?" when the name is ambiguous. If the artist isn't on Qloo at all, it plans from 2–4 "sounds like" artists instead.
  2. Maps demand: it pulls Qloo's heatmap for the artist's taste, groups thousands of geohash cells into real cities, and compares that demand with where the artist already has listeners.
  3. Labels every city as Unserved, Proven, Potential or Low, and lets the artist pick.
  4. Routes the tour: dates in order, drive or flight distances, travel and rest days.
  5. Fills every stop, in parallel: venues that fit the audience, local openers smaller than the headliner who pull that crowd (with a MusicBrainz "based here" badge), sponsor brands the fans over-index on, and promo spots like record stores.
  6. Writes the brief: a sponsor pitch and an opener DM quoting real affinity numbers, plus CSV and calendar export.
  7. Takes changes like "skip Hyderabad" or "max 400 km a day", re-planning only what's needed.
  8. Proves its value: a Compare tab runs the same request through an LLM with no tools, side by side.

Artists can plan as a guest or sign in with Google or email to keep My tours on every device. The same Qloo tools are also a public MCP server, so anyone can use TourScout from Claude, ChatGPT or any MCP-compatible assistant.

How I built it

Architecture. A React 19 + Vite + Tailwind web app on Vercel talks to one FastAPI service on Render (Docker). That service contains:

  • a LangGraph agent with two human-in-the-loop checkpoints (interrupt), saved in Neon Postgres so a pause, a refine or a reload continues the same run;
  • a deterministic scoring engine in pure Python;
  • a TourScout MCP server with 7 Qloo tools (resolve_artist, demand_map, find_venues, find_openers, find_sponsors, find_promo_spots, search_tags), mounted at /mcp.

Progress streams to the browser over Server-Sent Events, so you watch the agent work step by step.

The core rule: Qloo is the source of truth, code computes the numbers, and the LLM only writes. Each heatmap cell has an affinity $a$ and a popularity $p$. Affinity is an over-index, not a fan count, so tiny cells can look huge: one cell in remote Ladakh topped India for Prateek Kuhad. So for each metro $m$ with $n_m$ cells I shrink its affinity toward the regional mean $\bar a$ and weight it by size:

$$ D_m = \operatorname{scale}{0\text{–}100}!\left(\frac{n_m\,\hat a_m + k\,\bar a}{n_m + k}\cdot \log{10}(\text{pop}_m)\right), \qquad k = 2 $$

Reach uses a square root, so a city with 300 listeners doesn't look "proven" next to one with 40,000:

$$ R_m = 100\sqrt{\frac{\text{listeners}_m}{\max_j \text{listeners}_j}}, \qquad \text{gap}_m = D_m - R_m $$

A city is Unserved when $D_m \ge 60$ and $\text{gap}_m \ge 25$. Heatmap cells join the city with the strongest pull $\text{pop}/(d + 5\,\text{km})^2$ instead of the nearest centre, which stopped Bengaluru's best cells from being handed to small towns 5 km away. The route is nearest-neighbour plus 2-opt over haversine distance with a 1.25 road factor, honouring max drive per day, travel days and a rest day every 5 shows.

Keeping the LLM honest. The writer (Gemini, falling back to Groq's gpt-oss-120b) sees short aliases like c1v1 instead of Qloo UUIDs, plus only the numbers I computed. A validator checks every venue, opener and brand ID and every quoted number against what Qloo returned. If anything was invented, the brief is rejected and rewritten.

Production. The stack:

  • Neon Postgres for saved runs, the tours table and a private Qloo cache;
  • Neon Auth (Google + email), with the API verifying 15-minute EdDSA JWTs against Neon's public keys (JWKS);
  • Langfuse tracing: one trace per run, a span per step, a generation per LLM call;
  • per-IP and per-account rate limits, a request queue, and GitHub Actions CI with a real Postgres;
  • about 140 tests in total.

Challenges I ran into

  • Affinity isn't fan count. Raw Qloo affinity ranked empty mountain cells above Chicago. Shrinkage plus $\log_{10}(\text{population})$ fixed it, and a backtest confirmed it (see below).
  • Places with the same name. "Kochi" resolved to Kōchi, Japan, so an India tour started with an 8,755 km flight. Small Indian cities sent to Qloo as text came back with venues in Germany and a farm in Brazil. I now resolve the home city inside the tour's own country and send Qloo coordinates and a radius instead of names. That also turned up genuinely local openers and Kerala brands that the text search had missed entirely.
  • LLMs make things up. In one live run the writer invented a Chennai venue. The validator caught it and the second attempt passed. Without the validator, that would have reached the user.
  • Free-tier limits. Gemini's daily quota ran out mid-evaluation, and Groq's free tier also caps 200k tokens a day (about 25–30 plans). I built a failover chain with per-model circuit breakers and retry-after handling, plus one clear message for users when every model is at its limit. Full Qloo UUIDs once made a prompt too big (HTTP 413), which is why the writer sees short aliases.
  • Real-world deployment bugs. Neon's free tier suspends idle databases, which silently killed my single long-lived connection, so every plan failed until a restart. I moved to health-checked connection pools. The MCP library only accepted localhost by default, so the public /mcp returned 421. A load drill showed 7 of 10 simultaneous visitors being turned away, so I added a queue (10 of 10 now get a plan) and an SSE heartbeat so long silent steps don't drop connections.

Accomplishments that I'm proud of

I tested TourScout against an LLM with no tools on 30 artists across the US, Europe and India, including 4 not on Qloo and 4 edge cases:

TourScout LLM only
Openers actually based in the show's city (MusicBrainz) 53% 3%
Openers smaller than the headliner and sharing a genre 77% 19%
Venues that exist in that city 100% 69% (India: 47%)
Back-to-back long drives without a day off 0 9
Sponsor lists preferred by a blind judge 19 10
Behaviour checks (prompt injection, empty regions, impossible dates, name traps) 13 / 13 n/a

Backtest against real tours (setlist.fm, 12 artists): 42% of the cities from each artist's latest real tour land in TourScout's top 10, against 34% for a population ranking and 1% for random. TourScout wins for 9 of 12 artists.

I'm also proud that, as a solo builder, I shipped a real product rather than a demo:

  • live, with accounts and saved tours;
  • 11/11 failure drills passing on the live site;
  • a public MCP server anyone's AI agent can use.

What I learned

  • Give the LLM less to decide. My best quality jump came from moving numbers, rankings and routing into tested code and letting the model only write. Grounding plus a validator beat any prompt trick.
  • Taste data needs geography done carefully. The hardest bugs weren't in the AI but in place names, distances and what a heatmap cell really means.
  • Measure against a baseline. The Compare tab and the eval suite told me where TourScout truly wins (local openers, real venues) and where it doesn't (speed, US sponsor picks), and that shaped what I fixed.
  • Production is its own project. Database suspends, proxy timeouts, host checks, cookies on different domains and free-tier quotas each broke something real only after deployment.

What's next for TourScout

  • Real reach data: import from Spotify for Artists and Apple Music, instead of screenshots and CSVs.
  • Booking-ready venues: capacity and contact details from booking platforms; Qloo has no capacity data, so today I honestly say "capacity: verify".
  • Speed: a plan takes about 50 s on free tiers. Paid model tiers and more caching should bring that down a lot.
  • More markets and multi-region tours, such as a European run after an Indian one.
  • Tour-wide merch and poster ideas, built from the same taste signals.
  • A commercial Qloo key and my own Google sign-in, so TourScout can open to every independent artist.

Built With

Share this project:

Updates

Submission history