{
  "openapi": "3.0.3",
  "info": {
    "title": "Signal Synth API",
    "version": "0.5.0",
    "description": "Quality-ranked scoreboard of learning-focused podcast episodes across 9 categories. Every new episode is transcribed, LLM-summarized, and scored across 5 content dimensions (originality, actionability, information density, technical depth, clarity) plus a calculated recency modifier. All public GET endpoints return JSON with CORS enabled. Rate limits apply per IP; 429 responses include Retry-After.\n\nFor agent integrations prefer the MCP endpoint at `/api/mcp` (JSON-RPC 2.0) — it exposes 8 tools and is better suited than the REST surface."
  },
  "servers": [
    {"url": "https://signalsynth.xyz"}
  ],
  "paths": {
    "/api/rankings": {
      "get": {
        "operationId": "listRankings",
        "summary": "Paginated ranked episodes with optional filters.",
        "description": "Returns the current ranked scoreboard. When `limit` is absent the legacy full-snapshot payload is returned (all eligible episodes). When `limit` is set, the response is paginated and includes `total` + `offset`.",
        "parameters": [
          {"name": "limit", "in": "query", "schema": {"type": "integer", "default": 50, "minimum": 1, "maximum": 200}, "description": "Number of episodes to return. Omit for full legacy payload."},
          {"name": "offset", "in": "query", "schema": {"type": "integer", "default": 0}},
          {"name": "category", "in": "query", "schema": {"type": "string", "enum": ["all", "tech", "business", "finance", "news", "culture", "science", "health", "education", "sports"]}},
          {"name": "sort", "in": "query", "schema": {"type": "string", "enum": ["signal", "aired", "ep"], "default": "signal"}, "description": "signal = overall_score, aired = publish_date, ep = title"},
          {"name": "dir", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"], "default": "desc"}},
          {"name": "q", "in": "query", "schema": {"type": "string"}, "description": "Substring search across title + podcast name."}
        ],
        "responses": {
          "200": {
            "description": "Paged or legacy rankings payload.",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/RankingsResponse"}}}
          }
        }
      }
    },
    "/api/category_counts": {
      "get": {
        "operationId": "categoryCounts",
        "summary": "Total episode counts per category (for navigation tabs).",
        "responses": {
          "200": {
            "description": "Map of category → count, plus `all` total.",
            "content": {"application/json": {"schema": {
              "type": "object",
              "properties": {
                "all": {"type": "integer"},
                "tech": {"type": "integer"},
                "business": {"type": "integer"},
                "finance": {"type": "integer"},
                "news": {"type": "integer"},
                "culture": {"type": "integer"},
                "science": {"type": "integer"},
                "health": {"type": "integer"},
                "education": {"type": "integer"},
                "sports": {"type": "integer"}
              }
            }}}
          }
        }
      }
    },
    "/api/shows": {
      "get": {
        "operationId": "listShows",
        "summary": "Ranked podcast shows (not episodes).",
        "description": "Ordered by a blended score: avg_signal × log(episode_count + 1) × recency_factor (60-day half-life). Minimum 2 episodes per show.",
        "parameters": [
          {"name": "category", "in": "query", "schema": {"type": "string"}},
          {"name": "limit", "in": "query", "schema": {"type": "integer", "default": 10, "maximum": 30}}
        ],
        "responses": {
          "200": {
            "description": "Ranked shows.",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ShowsResponse"}}}
          }
        }
      }
    },
    "/api/show_page": {
      "get": {
        "operationId": "getShow",
        "summary": "Full rollup for a single show.",
        "parameters": [
          {"name": "slug", "in": "query", "required": true, "schema": {"type": "string"}, "description": "Show slug, e.g. 'a16z'."}
        ],
        "responses": {
          "200": {"description": "Show detail with recent episodes.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ShowDetail"}}}},
          "404": {"description": "Unknown slug."}
        }
      }
    },
    "/api/insights": {
      "get": {
        "operationId": "getInsights",
        "summary": "Aggregated data for the Insights page — trending topics, category averages, score distribution, extremes, coverage, corpus totals.",
        "parameters": [
          {"name": "window_days", "in": "query", "schema": {"type": "integer", "default": 7}}
        ],
        "responses": {
          "200": {"description": "Insights blocks.", "content": {"application/json": {"schema": {"type": "object"}}}}
        }
      }
    },
    "/api/card": {
      "get": {
        "operationId": "renderShareCard",
        "summary": "1200×630 PNG social share card.",
        "parameters": [
          {"name": "variant", "in": "query", "required": true, "schema": {"type": "string", "enum": ["episode", "top5", "show"]}},
          {"name": "id", "in": "query", "schema": {"type": "string"}, "description": "Required when variant=episode or variant=show (show slug)."}
        ],
        "responses": {
          "200": {"description": "PNG image.", "content": {"image/png": {}}},
          "400": {"description": "Missing required parameter."},
          "404": {"description": "Episode/show not found."}
        }
      }
    },
    "/api/listen": {
      "get": {
        "operationId": "smartListen",
        "summary": "Smart Listen redirect — resolves to Spotify, then Apple, then publisher URL.",
        "parameters": [
          {"name": "id", "in": "query", "required": true, "schema": {"type": "string"}},
          {"name": "prefer", "in": "query", "schema": {"type": "string", "enum": ["spotify", "apple", "publisher"]}}
        ],
        "responses": {
          "302": {"description": "Redirect to the best Listen URL."},
          "404": {"description": "Unknown episode."}
        }
      }
    },
    "/api/episode/{id}": {
      "get": {
        "operationId": "getEpisodePage",
        "summary": "Server-rendered HTML episode detail page (for share-link unfurls and crawlers).",
        "parameters": [
          {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}
        ],
        "responses": {
          "200": {"description": "HTML page.", "content": {"text/html": {}}}
        }
      }
    },
    "/api/v1/episodes": {
      "get": {
        "operationId": "listEpisodesV1",
        "summary": "v1 compat surface — ranked list or single-episode detail when ?id= is present.",
        "parameters": [
          {"name": "id", "in": "query", "schema": {"type": "string"}},
          {"name": "category", "in": "query", "schema": {"type": "string"}},
          {"name": "limit", "in": "query", "schema": {"type": "integer", "default": 20, "maximum": 200}},
          {"name": "min_score", "in": "query", "schema": {"type": "number"}},
          {"name": "include_transcript", "in": "query", "schema": {"type": "boolean"}}
        ],
        "responses": {
          "200": {"description": "Ranked list or episode detail.", "content": {"application/json": {"schema": {"oneOf": [{"$ref": "#/components/schemas/RankingsResponse"}, {"$ref": "#/components/schemas/Episode"}]}}}}
        }
      }
    },
    "/api/mcp": {
      "post": {
        "operationId": "mcpJsonRpc",
        "summary": "MCP server — JSON-RPC 2.0 / Streamable HTTP.",
        "description": "8 tools exposed: get_top_episodes, get_episode, search_episodes, search_transcripts, list_shows, get_show, list_categories, explain_score. Full schema advertised via tools/list. See /.well-known/mcp.json for the MCP manifest.",
        "requestBody": {
          "required": true,
          "content": {"application/json": {"schema": {"type": "object", "properties": {
            "jsonrpc": {"type": "string", "enum": ["2.0"]},
            "id": {},
            "method": {"type": "string", "enum": ["initialize", "notifications/initialized", "ping", "tools/list", "tools/call"]},
            "params": {"type": "object"}
          }}}}
        },
        "responses": {
          "200": {"description": "JSON-RPC envelope.", "content": {"application/json": {"schema": {"type": "object"}}}}
        }
      }
    },
    "/weekly.rss": {
      "get": {
        "operationId": "weeklyRss",
        "summary": "Podcast RSS feed of the weekly Signal Synth show.",
        "responses": {
          "200": {"description": "RSS XML.", "content": {"application/rss+xml": {}}}
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Episode": {
        "type": "object",
        "properties": {
          "episode_id": {"type": "string"},
          "episode_title": {"type": "string"},
          "title": {"type": "string"},
          "podcast": {"type": "string"},
          "podcast_name": {"type": "string"},
          "podcast_slug": {"type": "string"},
          "category": {"type": "string"},
          "publish_date": {"type": "string", "format": "date-time"},
          "overall_score": {"type": "number"},
          "scoring_confidence": {"type": "number"},
          "score_breakdown": {
            "type": "object",
            "properties": {
              "actionability": {"type": "number"},
              "clarity": {"type": "number"},
              "information_density": {"type": "number"},
              "technical_depth": {"type": "number"},
              "originality": {"type": "number"}
            }
          },
          "verdict": {"type": "string", "enum": ["must_listen", "worth_your_time", "skip"]},
          "summary": {"type": "string"},
          "key_takeaways": {"type": "array", "items": {"type": "string"}},
          "best_for": {"type": "array", "items": {"type": "string"}},
          "why_listen": {"type": "string"},
          "audio_url": {"type": "string"},
          "listen_url": {"type": "string"},
          "source_link": {"type": "string"},
          "url": {"type": "string", "description": "Canonical page URL on signalsynth.xyz."},
          "card_image": {"type": "string", "description": "1200×630 OG card PNG URL."},
          "podcast_cover_url": {"type": "string"}
        }
      },
      "RankingsResponse": {
        "type": "object",
        "properties": {
          "api_version": {"type": "integer"},
          "ranking_snapshot_id": {"type": "string"},
          "ranking_version": {"type": "string"},
          "generated_at": {"type": "string", "format": "date-time"},
          "total": {"type": "integer", "description": "Total matching the filter (only set when paged)."},
          "limit": {"type": "integer"},
          "offset": {"type": "integer"},
          "category": {"type": "string"},
          "sort": {"type": "string"},
          "dir": {"type": "string"},
          "episodes": {"type": "array", "items": {"$ref": "#/components/schemas/Episode"}}
        }
      },
      "Show": {
        "type": "object",
        "properties": {
          "slug": {"type": "string"},
          "name": {"type": "string"},
          "episode_count": {"type": "integer"},
          "avg_signal": {"type": "number"},
          "median_signal": {"type": "number"},
          "top_signal": {"type": "number"},
          "latest_episode_at": {"type": "string", "format": "date-time"},
          "earliest_episode_at": {"type": "string", "format": "date-time"},
          "category_mode": {"type": "string"},
          "cover_image_url": {"type": "string"},
          "rank_score": {"type": "number"},
          "url": {"type": "string"},
          "card_image": {"type": "string"}
        }
      },
      "ShowsResponse": {
        "type": "object",
        "properties": {
          "generated_at": {"type": "string", "format": "date-time"},
          "category": {"type": "string"},
          "count": {"type": "integer"},
          "shows": {"type": "array", "items": {"$ref": "#/components/schemas/Show"}}
        }
      },
      "ShowDetail": {
        "allOf": [
          {"$ref": "#/components/schemas/Show"},
          {"type": "object", "properties": {
            "episodes": {"type": "array", "items": {"$ref": "#/components/schemas/Episode"}},
            "category_breakdown": {"type": "array", "items": {"type": "object", "properties": {"category": {"type": "string"}, "count": {"type": "integer"}}}}
          }}
        ]
      }
    }
  }
}
