# Youtube Title Tag Optimizer

> Optimize YouTube video titles, tags, and descriptions before publishing using YouTube Data API v3. Analyze top-ranking videos for a keyword to reverse-engineer winning title patterns, extract effective tags, and generate optimized title variations. Use when users want to (1) Optimize a video title before publishing, (2) Find the best tags for a video, (3) Analyze what title patterns work for a ke…

- **Type:** Skill
- **Install:** `agentstack add skill-nikhilbhansali-youtube-data-skills-youtube-title-tag-optimizer`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [nikhilbhansali](https://agentstack.voostack.com/s/nikhilbhansali)
- **Installs:** 0
- **Category:** [Content & Media](https://agentstack.voostack.com/c/content-and-media)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [nikhilbhansali](https://github.com/nikhilbhansali)
- **Source:** https://github.com/nikhilbhansali/youtube-data-skills/tree/master/skills/youtube-title-tag-optimizer

## Install

```sh
agentstack add skill-nikhilbhansali-youtube-data-skills-youtube-title-tag-optimizer
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# YouTube Title & Tag Optimizer

Optimize your video title, tags, and description before publishing by analyzing what works for top-ranking videos.

## Usage

```
/youtube-title-tag-optimizer "air fryer recipes"
/youtube-title-tag-optimizer "Python tutorial" --my-title "Learn Python in 10 Minutes"
/youtube-title-tag-optimizer --keyword "home workout" --my-title "Best Home Workout for Beginners 2024"
```

## Instructions

When the user invokes this skill:

### Step 1: Parse Arguments

Extract from the user's input:
- **Keyword** (required): The topic/keyword to optimize for
- **--my-title "..."** (optional): User's working title to score and improve

### Step 2: Get API Key

Check the user's Claude memory for a YouTube Data API v3 key. If not found, ask:
> "I need a YouTube Data API v3 key to analyze ranking videos. You can get one from the Google Cloud Console. Please paste your key."

### Step 3: Write the Script

Write the following Python script to `/tmp/_yt_title_optimizer_XXXX.py` (where XXXX is a random suffix, e.g. `$(openssl rand -hex 4)`):

```python
#!/usr/bin/env python3
"""
YouTube Title & Tag Optimizer

Analyzes top-ranking videos for a keyword to extract winning title patterns,
effective tags, and generate optimization recommendations.

Usage:
    YT_API_KEY=KEY python3 /tmp/_yt_title_optimizer_XXXX.py "keyword"
    YT_API_KEY=KEY python3 /tmp/_yt_title_optimizer_XXXX.py "keyword" --my-title "My Title"
"""

import argparse
import json
import os
import re
import sys
from datetime import datetime, timezone
from collections import Counter

try:
    from googleapiclient.discovery import build
    from googleapiclient.errors import HttpError
except ImportError:
    print("ERROR: google-api-python-client not installed. Run: pip3 install google-api-python-client")
    sys.exit(1)

def parse_duration(iso_duration: str) -> int:
    m = re.match(r"PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?", iso_duration or "")
    if not m:
        return 0
    return int(m.group(1) or 0) * 3600 + int(m.group(2) or 0) * 60 + int(m.group(3) or 0)

def format_number(n: int) -> str:
    if n >= 1_000_000: return f"{n/1_000_000:.1f}M"
    if n >= 1_000: return f"{n/1_000:.1f}K"
    return str(n)

def analyze_title(title: str) -> dict:
    """Analyze a single title for patterns."""
    analysis = {
        "length_chars": len(title),
        "length_words": len(title.split()),
        "has_number": bool(re.search(r'\d', title)),
        "has_question": title.rstrip().endswith("?"),
        "has_how_to": bool(re.search(r'how\s+to', title, re.I)),
        "has_brackets": bool(re.search(r'[\[\(]', title)),
        "has_parentheses": bool(re.search(r'\(', title)),
        "has_caps_word": bool(re.search(r'\b[A-Z]{2,}\b', title)),
        "has_emoji": bool(re.search(r'[^\w\s,.\-!?\'\"()\[\]:;/\\@#$%^&*+=~`|<>]', title)),
        "has_year": bool(re.search(r'20[12]\d', title)),
        "has_list_format": bool(re.search(r'^\d+\s', title)),
        "has_vs": bool(re.search(r'\bvs\.?\b', title, re.I)),
        "has_colon": ":" in title,
        "has_pipe": "|" in title,
        "has_dash_separator": " - " in title or " — " in title,
        "has_exclamation": "!" in title,
        "starts_with_how": title.lower().startswith("how"),
        "starts_with_why": title.lower().startswith("why"),
        "starts_with_what": title.lower().startswith("what"),
        "starts_with_number": bool(re.match(r'^\d', title)),
    }

    # Detect power words
    power_words = {
        "urgency": ["now", "today", "immediately", "urgent", "hurry", "fast", "quick", "instantly", "stop"],
        "curiosity": ["secret", "hidden", "surprising", "shocking", "unexpected", "weird", "strange", "mystery", "truth", "reveal"],
        "value": ["free", "best", "top", "ultimate", "complete", "guide", "hack", "tips", "tricks", "easy", "simple"],
        "emotional": ["amazing", "incredible", "insane", "mind-blowing", "life-changing", "game-changer", "beautiful", "perfect", "love", "hate"],
        "fear": ["mistake", "wrong", "avoid", "never", "worst", "dangerous", "warning", "risk", "fail", "scam", "don't"],
    }

    title_lower = title.lower()
    found_power_words = {}
    for category, words in power_words.items():
        matches = [w for w in words if w in title_lower]
        if matches:
            found_power_words[category] = matches
    analysis["power_words"] = found_power_words

    # Detect title structure/formula
    structures = []
    if re.match(r'^\d+\s', title):
        structures.append("listicle")
    if re.search(r'how\s+to', title, re.I):
        structures.append("how-to")
    if title.rstrip().endswith("?"):
        structures.append("question")
    if re.search(r'\bvs\.?\b', title, re.I):
        structures.append("comparison")
    if re.search(r'\breview\b', title, re.I):
        structures.append("review")
    if re.search(r'in\s+\d+\s+(minute|min|second|sec|hour|day|step)', title, re.I):
        structures.append("time-bound")
    if re.search(r'for\s+(beginners|newbies|noobs|starters)', title, re.I):
        structures.append("beginner-targeted")
    if re.search(r'(complete|ultimate|full|definitive)\s+guide', title, re.I):
        structures.append("comprehensive-guide")
    if re.search(r'\b(do|don\'t|never|stop|avoid)\b', title, re.I):
        structures.append("imperative")
    analysis["structures"] = structures

    return analysis

def main():
    parser = argparse.ArgumentParser(description="Optimize YouTube titles and tags")
    parser.add_argument("keyword", help="Keyword to optimize for")
    parser.add_argument("--my-title", default=None, help="Your working title to score")
    parser.add_argument("--output-dir", default=None, help="Output directory")
    args = parser.parse_args()

    api_key = os.environ.get("YT_API_KEY")
    if not api_key:
        print("ERROR: YT_API_KEY environment variable not set.")
        sys.exit(1)

    youtube = build("youtube", "v3", developerKey=api_key)
    quota_used = 0

    print(f"Analyzing keyword: {args.keyword}")
    if args.my_title:
        print(f"Your title: {args.my_title}\n")

    # --- Search by relevance ---
    print("Fetching top-ranking videos (by relevance)...")
    try:
        relevance_resp = youtube.search().list(
            part="snippet", q=args.keyword, type="video",
            order="relevance", maxResults=50,
        ).execute()
        relevance_ids = [item["id"]["videoId"] for item in relevance_resp.get("items", [])]
        quota_used += 100
    except HttpError as e:
        print(f"Search error: {e}")
        sys.exit(1)

    # --- Search by view count ---
    print("Fetching most-viewed videos...")
    try:
        viewcount_resp = youtube.search().list(
            part="snippet", q=args.keyword, type="video",
            order="viewCount", maxResults=50,
        ).execute()
        viewcount_ids = [item["id"]["videoId"] for item in viewcount_resp.get("items", [])]
        quota_used += 100
    except HttpError as e:
        viewcount_ids = []
        print(f"View count search error: {e}")

    # Combine and deduplicate
    all_ids = list(dict.fromkeys(relevance_ids + viewcount_ids))
    print(f"Total unique videos: {len(all_ids)}")

    # --- Fetch video details ---
    print("Fetching video details...")
    videos = []
    for i in range(0, len(all_ids), 50):
        batch = all_ids[i:i+50]
        try:
            resp = youtube.videos().list(
                part="snippet,statistics,contentDetails",
                id=",".join(batch),
            ).execute()
            videos.extend(resp.get("items", []))
            quota_used += 1
        except HttpError as e:
            print(f"Error: {e}")
    print(f"  Got details for {len(videos)} videos")

    # --- Process videos ---
    processed = []
    all_tags = Counter()
    title_analyses = []

    for v in videos:
        views = int(v.get("statistics", {}).get("viewCount", 0))
        likes = int(v.get("statistics", {}).get("likeCount", 0))
        comments = int(v.get("statistics", {}).get("commentCount", 0))
        duration = parse_duration(v.get("contentDetails", {}).get("duration", ""))
        title = v["snippet"]["title"]
        tags = v.get("snippet", {}).get("tags", [])

        for tag in tags:
            all_tags[tag.lower()] += 1

        t_analysis = analyze_title(title)
        title_analyses.append(t_analysis)

        processed.append({
            "video_id": v["id"],
            "title": title,
            "title_analysis": t_analysis,
            "channel": v["snippet"]["channelTitle"],
            "views": views,
            "likes": likes,
            "comments": comments,
            "engagement_rate": round((likes + comments) / max(views, 1) * 100, 2),
            "duration_sec": duration,
            "tags": tags,
            "tag_count": len(tags),
            "description_first_line": v["snippet"].get("description", "").split("\n")[0][:200],
            "published_at": v["snippet"]["publishedAt"],
        })

    processed.sort(key=lambda x: x["views"], reverse=True)

    # --- Aggregate title analysis ---
    total = len(title_analyses) or 1
    aggregate_title = {
        "avg_length_chars": round(sum(t["length_chars"] for t in title_analyses) / total, 1),
        "avg_length_words": round(sum(t["length_words"] for t in title_analyses) / total, 1),
        "pct_has_number": round(sum(1 for t in title_analyses if t["has_number"]) / total * 100, 1),
        "pct_question": round(sum(1 for t in title_analyses if t["has_question"]) / total * 100, 1),
        "pct_how_to": round(sum(1 for t in title_analyses if t["has_how_to"]) / total * 100, 1),
        "pct_brackets": round(sum(1 for t in title_analyses if t["has_brackets"]) / total * 100, 1),
        "pct_caps_word": round(sum(1 for t in title_analyses if t["has_caps_word"]) / total * 100, 1),
        "pct_emoji": round(sum(1 for t in title_analyses if t["has_emoji"]) / total * 100, 1),
        "pct_year": round(sum(1 for t in title_analyses if t["has_year"]) / total * 100, 1),
        "pct_list_format": round(sum(1 for t in title_analyses if t["has_list_format"]) / total * 100, 1),
        "pct_colon": round(sum(1 for t in title_analyses if t["has_colon"]) / total * 100, 1),
        "pct_pipe": round(sum(1 for t in title_analyses if t["has_pipe"]) / total * 100, 1),
        "pct_exclamation": round(sum(1 for t in title_analyses if t["has_exclamation"]) / total * 100, 1),
    }

    structure_counter = Counter()
    for t in title_analyses:
        for s in t["structures"]:
            structure_counter[s] += 1
    aggregate_title["structures"] = {k: round(v/total*100, 1) for k, v in structure_counter.most_common()}

    power_category_counter = Counter()
    for t in title_analyses:
        for cat in t["power_words"]:
            power_category_counter[cat] += 1
    aggregate_title["power_word_categories"] = {k: round(v/total*100, 1) for k, v in power_category_counter.most_common()}

    word_counter = Counter()
    stop_words = {"the", "a", "an", "is", "it", "in", "on", "at", "to", "for", "of", "and", "or", "but", "with", "you", "your", "my", "this", "that", "i", "me", "we", "how", "what", "why", "do", "does", "can", "will", "be", "are", "was", "not", "no", "so", "if"}
    for v in processed:
        words = re.findall(r'[a-zA-Z]{3,}', v["title"].lower())
        for w in words:
            if w not in stop_words:
                word_counter[w] += 1
    aggregate_title["common_title_words"] = word_counter.most_common(30)

    # --- Score user's title if provided ---
    my_title_score = None
    if args.my_title:
        my_analysis = analyze_title(args.my_title)
        score = 0
        feedback = []

        if 40  80:
            feedback.append("Title may be too long -- could get truncated in search results")
        else:
            score += 10
            feedback.append("Title length is acceptable but could be optimized")

        keyword_lower = args.keyword.lower()
        title_lower = args.my_title.lower()
        if keyword_lower in title_lower:
            score += 20
            if title_lower.startswith(keyword_lower) or title_lower[:20].find(keyword_lower) >= 0:
                score += 5
                feedback.append("Keyword appears early in title -- great for SEO")
            else:
                feedback.append("Keyword is present in title")
        else:
            kw_words = keyword_lower.split()
            matches = sum(1 for w in kw_words if w in title_lower)
            if matches > 0:
                score += 10
                feedback.append(f"Partial keyword match ({matches}/{len(kw_words)} words)")
            else:
                feedback.append("WARNING: Keyword not found in title -- critical for SEO")

        if my_analysis["has_number"]:
            score += 10
            feedback.append("Contains a number -- increases click-through rate")

        if my_analysis["power_words"]:
            score += 10
            cats = list(my_analysis["power_words"].keys())
            feedback.append(f"Uses power words ({', '.join(cats)})")
        else:
            feedback.append("Consider adding power words for emotional impact")

        if my_analysis["structures"]:
            score += 10
            feedback.append(f"Uses proven structure: {', '.join(my_analysis['structures'])}")
        else:
            feedback.append("Consider using a proven structure (how-to, listicle, question)")

        if my_analysis["has_brackets"]:
            score += 5
            feedback.append("Uses brackets -- can boost CTR by 33%")

        if my_analysis["has_caps_word"]:
            score += 5
            feedback.append("Strategic use of CAPS for emphasis")

        top_10_titles = [v["title"] for v in processed[:10]]
        common_patterns_in_top = set()
        for t in top_10_titles:
            ta = analyze_title(t)
            for s in ta["structures"]:
                common_patterns_in_top.add(s)

        matching_patterns = set(my_analysis["structures"]) & common_patterns_in_top
        if matching_patterns:
            score += 10
            feedback.append(f"Matches top-performer patterns: {', '.join(matching_patterns)}")

        my_title_score = {
            "title": args.my_title,
            "score": min(score, 100),
            "analysis": my_analysis,
            "feedback": feedback,
        }

    # --- Build output ---
    output = {
        "keyword": args.keyword,
        "analyzed_at": datetime.now(timezone.utc).isoformat(),
        "total_videos_analyzed": len(processed),
        "aggregate_title_analysis": aggregate_title,
        "top_tags": all_tags.most_common(50),
        "tag_stats": {
            "unique_tags": len(all_tags),
            "avg_tags_per_video": round(sum(v["tag_count"] for v in processed) / len(processed), 1) if processed else 0,
        },
        "top_videos": [
            {
                "title": v["title"],
                "video_id": v["video_id"],
                "views": v["views"],
                "engagement_rate": v["engagement_rate"],
                "channel": v["channel"],
                "tag_count": v["tag_count"],
                "title_analysis": v["title_analysis"],
                "description_first_line": v["description_first_line"],
            }
            for v in processed[:20]
        ],
        "all_videos": processed,
        "my_title_score": my_title_score,
        "quota_used": {
            "total_estimated": quota_used,
        },
    }

    # --- Save ---
    safe_kw = re.sub(r'[^a-zA-Z0-9_-]', '_', args.keyword)[:100]
    date_str = datetime.now().strftime("%Y%m%d")
    output_dir = args.output_dir or f"yt_optimize_{safe_kw}_{date_str}"
    os.makedirs(output_dir, exist_ok=True)

    output_file = os.path.join(output_dir, "title_tag_data.json")
    try:
        with open(output_file, "w", encoding="utf-8") as f:
            json.dump(output, f, indent=2, ensure_ascii=False)
    except (IOErr

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [nikhilbhansali](https://github.com/nikhilbhansali)
- **Source:** [nikhilbhansali/youtube-data-skills](https://github.com/nikhilbhansali/youtube-data-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** yes
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-nikhilbhansali-youtube-data-skills-youtube-title-tag-optimizer
- Seller: https://agentstack.voostack.com/s/nikhilbhansali
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
