Building Podcurator: An AI-Powered Podcast Discovery Platform

November 10, 2025 (8mo ago)

I've always been frustrated by podcast discovery. Sure, there are charts and trending lists, but they never quite matched my specific interests. I wanted something smarter - a tool that could understand what topics I'm curious about and surface the perfect episodes. That's why I built podcurator.io.

The Problem with Podcast Discovery

Most podcast platforms rely on basic algorithms: popularity, categories, or keyword matching. But podcasts are nuanced. An episode titled "AI in Healthcare" could be about ethics, technology, business strategy, or patient care. Traditional search doesn't capture this depth.

I wanted to build a system that:

  • Understands the actual content of podcast episodes
  • Recommends based on interests, not just keywords
  • Surfaces hidden gems, not just popular shows
  • Learns from user preferences over time

The result? Podcurator.io - an AI-powered search that understands what you're looking for and explains why each episode matches your interests.

The Tech Stack

I chose a modern, AI-first stack built as a Turborepo monorepo with Bun:

Monorepo Architecture

  • Turborepo for monorepo management with shared packages
  • Bun 1.3+ as package manager and runtime (3x faster than npm)
  • apps/app - Next.js 15 frontend + API routes
  • packages/ - Shared services (AI, jobs, database, email, analytics)

Frontend & Framework

  • Next.js 15 with App Router and React 19
  • TypeScript for end-to-end type safety
  • TailwindCSS and Shadcn/UI for accessible components
  • TanStack Query for server state management

AI & Intelligence Layer

  • OpenRouter as the AI gateway with strategic model routing
  • Google Gemini 2.5 Flash for interactive tasks (0.32s TTFT - 100x faster)
  • Grok 4 Fast for background curation (190-344 tokens/sec throughput)
  • GPT-5 Mini for complex reasoning fallback
  • Vercel AI SDK for structured AI outputs

Backend & Database

  • Supabase for PostgreSQL with 15+ tables and 50+ indexes
  • Drizzle ORM for type-safe queries and migrations
  • NextAuth.js for authentication with multiple providers
  • Upstash Redis for rate limiting and request deduplication
  • Trigger.dev v4 for background jobs (Bun runtime, 3min max duration)

Email & Monetization

  • React Email for transactional email templates
  • Resend for email delivery
  • Polar.sh for subscriptions and credit-based pricing

Want to see this stack in action? Try podcurator.io for free - no signup required. Search for any topic and watch the AI work its magic.

Architecture Decisions

1. Strategic Multi-Model AI Routing

Instead of using one AI model for everything, I route different tasks to the optimal model based on latency, cost, and quality requirements:

// Real model selection from packages/services/src/openrouter-models.ts
const MODELS = {
  // Interactive tasks: Ultra-fast query parsing
  INTERACTIVE: "google/gemini-2.5-flash",        // 0.32s TTFT (100x faster)
 
  // Background tasks: High-throughput curation
  BACKGROUND: "x-ai/grok-4-fast",                // 190-344 tokens/sec
 
  // Complex reasoning: Fallback for edge cases
  REASONING: "openai/gpt-5-mini"
}

Why this matters:

  • Gemini 2.5 Flash parses user queries in real-time (under 350ms) for instant constraint extraction
  • Grok 4 Fast handles AI curation in background jobs where throughput > latency
  • Cost savings: Grok is 60% cheaper than GPT-5 with comparable quality for structured outputs
  • Resilience: If one provider has an outage, I can instantly switch to alternatives via OpenRouter

2. AI-Powered Curation with Structured Outputs

Instead of traditional vector search, I built a multi-stage AI curation pipeline that delivers highly relevant results:

Stage 1: Query Parsing (Gemini 2.5 Flash)

// User types: "short beginner-friendly interviews about AI"
// AI extracts structured constraints in 300ms:
{
  maxDurationMinutes: 30,           // "short"
  difficultyLevel: "beginner",      // "beginner-friendly"
  contentType: "interview",         // "interviews"
  topics: ["AI", "artificial intelligence"],
  preferredLanguage: "en",          // Auto-detected
  confidence: 0.95
}

Stage 2: Smart Database Search

  • Query local database first (already-indexed episodes)
  • Apply hard constraints (duration, language) via SQL
  • If insufficient results, hit Spotify API with intelligent pagination
  • Pre-filter API results before storing (save DB space)

Stage 3: AI Curation (Grok 4 Fast)

// Send candidate episodes to AI with Zod-validated structured output
{
  selectedEpisodes: [{
    episodeId: "xyz",
    relevanceScore: 0.87,      // 0.0-1.0
    reason: "Perfect match: discusses GPT-5 applications...",
    highlights: ["AI ethics discussion", "Practical examples"]
  }]
}

Why this beats vector search:

  • AI understands nuance (e.g., "beginner-friendly" ≠ just topic match)
  • Provides explainable recommendations (users see why each episode was selected)
  • Adaptable to complex queries ("no politics, recent episodes only")
  • No expensive embedding generation pipeline

3. Background Processing with Real-Time Progress

Search requests on podcurator.io run as background jobs with 7-step execution and live progress updates:

// packages/jobs/trigger/podcast-search.ts
export const podcastSearchTask = task({
  id: "podcast-search",
  retry: { maxAttempts: 3, factor: 2 },
  run: async (payload) => {
    // Step 1: AI Query Parsing (10% complete)
    await updateProgress(10, "Analyzing your query...");
    const constraints = await parseQuery(payload.query);
 
    // Step 2: Database Search (25% complete)
    await updateProgress(25, "Searching our database...");
    let episodes = await searchLocalDatabase(constraints);
 
    // Step 3: Spotify API Fallback (40% complete)
    if (episodes.length < minResults) {
      await updateProgress(40, "Fetching from Spotify...");
      episodes = await fetchFromSpotify(constraints);
    }
 
    // Step 4: AI Curation (60% complete)
    await updateProgress(60, "Curating personalized results...");
    const curated = await curateWithAI(episodes, payload.query);
 
    // Step 5: Save Results (80% complete)
    await updateProgress(80, "Saving results...");
    await saveToDatabase(curated);
 
    // Step 6: Background Enrichment (async, non-blocking)
    triggerEnrichmentJob(curated);
 
    // Step 7: Quality-Gated Credit Consumption (90% complete)
    if (avgRelevance >= 0.5) {
      await consumeCredit(payload.userId);
    }
 
    return { searchId, episodesFound: curated.length };
  }
});

Frontend sees live updates:

Analyzing your query... ████░░░░░░ 10%
Searching our database... ████████░░ 40%
Curating personalized results... ██████████ 60%

4. Request Deduplication & Rate Limiting

AI calls are expensive. I implemented multi-layer caching and deduplication:

Request Deduplication (Upstash Redis)

// Prevents double-charging if user clicks search twice
const cacheKey = `${userId}:${query}:${JSON.stringify(options)}`;
const existing = await redis.get(cacheKey);
 
if (existing) {
  return existing; // Return cached result for 30 seconds
}
 
// Cache new search for 30s to prevent concurrent duplicates
await redis.setex(cacheKey, 30, searchResult);

Rate Limiting:

  • Anonymous users: 1 search per 24 hours (demo tier)
  • Authenticated users: Unlimited searches with active subscription
  • Credit-based users: Pay-per-search model

AI Result Caching:

  • Curation results cached for 1 hour (SHA256 hash of query + episodes)
  • Query parsing cached in-memory (serverless container lifetime)
  • Episode metadata cached indefinitely (content rarely changes)

5. Quality-Gated Monetization (The Fairness Feature)

Here's what makes podcurator different: I only charge credits if the search delivers quality results.

// Only consume credit if results meet quality threshold
if (avgRelevanceScore >= 0.5 && episodesReturned > 0) {
  await consumeCredit(userId);
} else {
  // Free search - quality didn't meet bar
  await logLowQualitySearch(searchId);
}

Why this matters:

  • Users only pay when they get value
  • Builds trust (no charging for poor results)
  • Incentivizes me to continuously improve AI curation quality
  • Tracked in creditTransactions table for full audit trail

Quality Thresholds:

  • Minimum average relevance: 50% across all results
  • Per-episode minimum: 50% relevance score
  • Query confidence threshold: 40% (AI must understand the query)

6. Progressive Constraint Relaxation

The hardest problem in search: what to do when there are zero results?

My solution: Progressive constraint relaxation with transparency.

// Stage 1: All constraints applied
results = search({ duration: "20-30min", language: "en", difficulty: "beginner" });
 
if (results.length < minResults) {
  // Stage 2: Relax duration by ±30%, drop recency preference
  results = search({ duration: "14-39min", language: "en", difficulty: "beginner" });
}
 
if (results.length < minResults) {
  // Stage 3: Relax duration by ±60%, drop difficulty filter
  results = search({ duration: "8-48min", language: "en" });
}
 
if (results.length < minResults) {
  // Stage 4: Drop duration, keep language only (last resort)
  results = search({ language: "en" });
}

Why this works:

  • Users get results even with very specific constraints
  • AI explains which constraints were relaxed
  • Logged for quality improvement ("many users want X but we lack data")
  • Better than "0 results found" dead-end

7. Multi-Platform Support: Spotify + YouTube

Podcurator supports both audio podcasts (Spotify) AND video content (YouTube) with intelligent routing:

// packages/jobs/trigger/platform-router.ts
 
// Route based on content requirements:
if (query.includes("@youtube")) {
  return "youtube";
} else if (visualContentNeeded(query)) {
  // AI determines if visual is essential
  return "youtube";  // Cooking, coding tutorials, product reviews
} else {
  return "spotify";  // Interviews, business, storytelling
}

Unified Content Model:

  • Frontend sees UnifiedEpisode (not "video" vs "episode")
  • Backend adapts Spotify and YouTube data to common schema
  • Database has separate tables but unified API
  • Users search once, get best results regardless of platform

Key Challenges & Solutions

Challenge 1: Token Costs Spiraling Out of Control

Problem: Initial prototype spent $0.15 per search (unsustainable at scale).

Solution:

  • Strategic model routing: Gemini for parsing (10x cheaper), Grok for curation (60% cheaper than GPT-4)
  • Token counting: Use js-tiktoken to calculate exact token usage before AI calls
  • Aggressive caching: SHA256-hashed results cached for 1 hour
  • Pre-filtering: Apply hard constraints via SQL before sending to AI (reduces token count by 70%)

Result: Reduced to $0.02 per search (7.5x cost reduction)

Challenge 2: Zero Results Problem

Problem: Users with specific constraints (e.g., "5-minute Spanish comedy podcasts about physics") got zero results.

Solution: Progressive constraint relaxation

  • 4-stage fallback system automatically relaxes constraints
  • AI explains which constraints were relaxed
  • Users still get results instead of frustrating dead-end
  • Logged for content gap analysis

Result: Zero results rate dropped from 23% to <2%

Challenge 3: Quality Consistency

Problem: AI curation quality varied wildly (60-95% relevance).

Solution: Quality gates + fallback mechanisms

  • Structured output validation with Zod schemas
  • Fallback to keyword-based scoring if AI fails
  • Only charge credits when relevance ≥ 50%
  • Retry logic with exponential backoff

Result: Consistent 70-85% relevance across all searches

Challenge 4: Spotify API Rate Limits

Problem: Hitting Spotify rate limits during peak usage.

Solution: Database-first approach

  • Check local database before hitting Spotify API
  • Smart pagination (max 3 batches, early termination)
  • Pre-filter results before storing
  • Cache Spotify responses for 24 hours

Result: 80% of searches served from database, 95% reduction in API calls

What I Learned

1. Choose the right model for the job, not the best model

  • Gemini 2.5 Flash (0.32s latency) beats GPT-5 for query parsing despite being "less powerful"
  • Speed matters more than marginal quality improvements for interactive tasks
  • Grok 4 Fast handles structured outputs as well as GPT-5 at 60% of the cost

2. Quality-gated monetization builds trust

  • Users appreciate only being charged when results meet quality bars
  • This incentivizes continuous improvement (my revenue depends on quality)
  • Transparent pricing > extractive pricing

3. Progressive degradation > hard failures

  • "We relaxed your duration constraint" beats "0 results found" every time
  • Users care about getting something useful more than perfect matches
  • Constraint relaxation data reveals content gaps to fill

4. Structured outputs are game-changing

  • Zod-validated AI responses prevent 90% of bugs
  • Vercel AI SDK's structured output mode is production-ready
  • Fallback to keyword scoring when AI fails ensures reliability

5. Monorepo + Bun = developer velocity

  • Shared packages eliminate code duplication
  • Bun's speed makes installs/builds 3x faster than npm
  • Type safety across packages catches bugs at compile time

6. Database schema design matters more than you think

  • 50+ strategic indexes reduced query times from 2s to 50ms
  • Composite indexes for sorting (e.g., popularity DESC, publishedAt DESC)
  • CUID2 IDs provide security + sortability without UUIDv7 complexity

Technical Achievements

After 2 months of development, here's what I shipped:

Performance:

  • Sub-350ms query parsing (Gemini 2.5 Flash)
  • 50ms average database query time (optimized indexes)
  • 7-step background job pipeline with real-time progress
  • 80% cache hit rate (reducing API costs by 95%)

Quality:

  • 70-85% relevance scores across all searches
  • <2% zero-results rate (down from 23%)
  • Quality-gated monetization (only charge for good results)
  • Structured AI outputs with Zod validation

Scale:

  • 15+ database tables with 50+ strategic indexes
  • Turborepo monorepo with 8 shared packages
  • Multi-platform support (Spotify + YouTube)
  • Request deduplication preventing double-charges

Developer Experience:

  • Full TypeScript coverage across frontend/backend
  • Drizzle ORM with type-safe migrations
  • Trigger.dev v4 with Bun runtime
  • Comprehensive error handling and retry logic

Try It Yourself

Ready to discover your next favorite podcast episode? Check out podcurator.io - it's free to try with no signup required. Just describe what you're interested in, and let the AI find the perfect episodes for you.

I'd love to hear your feedback on X or via the feedback form on podcurator.io.

If you're building something similar or want to chat about AI-powered content curation, feel free to reach out on X or LinkedIn.

What's Next

I'm actively working on:

  • Enhanced AI models: Testing Claude 4.5 Sonnet for even better curation quality
  • Personalization engine: Learn from user listening history over time
  • Creator dashboard: Analytics and insights for podcast hosts
  • Advanced filters: Guest appearances, network preferences, episode series
  • Community features: Share curated playlists, follow other curators

Building in public has been incredibly rewarding. The technical challenges around AI cost optimization, quality consistency, and user trust have pushed me to think differently about product design.

If you're building AI-powered products and want to chat about architecture decisions, cost optimization, or quality gates, I'd love to connect.


Want to build your own AI-powered SaaS? I'm available for consulting and freelance projects. Get in touch.