Documentation

Everything you need to set up YTSkills with your OpenClaw agent.

OpenClaw Setup

YTSkills integrates with OpenClaw through a skill.md file. Your agent reads it once to learn the API, then can process YouTube videos on demand from any channel — WhatsApp, Telegram, Slack, Discord, and more.

Quick Start

  1. Point your OpenClaw agent to the skill file: https://ytskills.com/skill.md
  2. Create an account at ytskills.com and generate an API key.
  3. Give the API key to your agent — it will save it to ~/.config/ytskills/credentials.json
  4. Send a YouTube link to your agent in any channel and say "learn from this" — it handles the rest.
# Skill file URL for your OpenClaw agent:
https://ytskills.com/skill.md

How Skills are Stored

When your OpenClaw agent processes a YouTube video through YTSkills, the generated skill is saved as a markdown file in your agent's workspace. Skills are saved with descriptive names based on the video topic (e.g., react-hooks-tutorial.md) so your agent can reference them later.

~/.openclaw/workspace/skills/react-hooks-tutorial.md

Direct API Usage

curl / HTTP Client

You can also call the YTSkills API directly from scripts or custom tooling:

$ curl -H "Authorization: Bearer yt_your_key_here" \
  https://ytskills.com/api/audio/dQw4w9WgXcQ

Full API Reference (skill.md)

# YTSkills Skill Documentation

YTSkills is a service that allows AI agents to learn and improve their knowledge base and understanding of the world through YouTube videos. When a user shares a YouTube link and asks you to "learn from this," YTSkills extracts the content (audio or video) and generates a structured SKILL.md file that you can save and reference later.

## ⚠️ IMPORTANT: API Key Required

**You must obtain a valid API key from ytskills.com to use this service.** This prevents abuse and keeps request quotas fair. The process takes barely 2 minutes:

1. Visit ytskills.com and create an account.
2. Go to the API key page and click "Generate API Key".
3. Copy the API key and give it to the agent.

Save your API key in your `AGENT.md` for easy access:

```
## YTSkills Credentials
- API Key: yt_abc123...
- Base URL: https://ytskills.com/api
```

If the key is ever lost, you can regenerate a new one from the API key page at ytskills.com.

## Base URL

```
https://ytskills.com/api
```

## Authentication

All endpoints require a Bearer token in the Authorization header:

```
Authorization: Bearer YOUR_API_KEY
```

## Pricing & Credits

YTSkills uses a credit-based system. Each API call consumes credits based on the mode and the duration of the video.

### Plans

| Plan | Price | Credits | Audio | Video |
|------|-------|---------|-------|-------|
| Free | $0/mo | 50 credits | 1 credit/audio min | Audio-only (no video) |
| Pro | Coming Soon | 2,000 credits | 1 credit/audio min | 5 credits/video min |
| Max | Coming Soon | 4,000 credits | 1 credit/audio min | 5 credits/video min |

**Key details:**
- Current launch mode: Free tier is active. Pro/Max and video mode are coming soon.
- Credits reset monthly
- Video mode is currently coming soon during free-tier launch
- Credit cost is calculated based on the duration of the YouTube video, **rounded up to the nearest minute**
- Example: a 12.3-minute video costs 13 credits in audio mode and 65 credits in video mode

## Usage Flow

### How It Works

1. **User** sends you a YouTube link along with a message like "learn from this" or "study this video."
2. **You** extract the video ID from the YouTube URL.
3. **You** call the YTSkills API with the chosen mode (`audio` or `video`).
4. **The API** processes the video content through Gemini 3 Flash with a specialized system prompt.
5. **The API** returns a generated SKILL.md file along with metadata.
6. **You** save the generated skill file to an appropriate location with a descriptive name (e.g., `react-hooks-tutorial.md`) so you and the user can easily identify and reference it later.

### Extracting the Video ID

The video ID can be extracted from standard YouTube URL formats:

- `https://www.youtube.com/watch?v=VIDEO_ID`
- `https://youtu.be/VIDEO_ID`
- `https://www.youtube.com/embed/VIDEO_ID`
- `https://youtube.com/shorts/VIDEO_ID`

Extract only the video ID portion (the alphanumeric string, typically 11 characters).

## API Endpoint

### Generate Skill from YouTube Video

```http
GET /{mode}/{video-id}
Authorization: Bearer YOUR_API_KEY
```

**Path Parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `mode` | string | Yes | Processing mode: `audio` or `video` |
| `video-id` | string | Yes | The YouTube video ID extracted from the URL |

**Modes:**

- `audio` — Extracts and processes the audio track only. Available on all plans (including Free). Costs 1 credit per minute of video duration.
- `video` — Processes both the video and audio content for richer output. **Currently coming soon.**

**Response (success):**
```json
{
  "skill": "# Skill Title\n\nGenerated skill content in markdown format...",
  "metadata": {
    "video_id": "dQw4w9WgXcQ",
    "title": "Video Title",
    "duration_minutes": 12,
    "mode": "audio",
    "credits_used": 12,
    "credits_remaining": 38
  }
}
```

The `skill` field contains the full generated SKILL.md content in markdown format. The `metadata` field contains information about the processed video and credit usage.

**Response (insufficient credits):**
```json
{
  "error": "Insufficient credits. This video requires 50 credits but you only have 38 remaining.",
  "credits_required": 50,
  "credits_remaining": 38,
  "upgrade_url": "https://ytskills.com/pricing"
}
```

**Response (video mode on free plan):**
```json
{
  "error": "Video mode is coming soon. Free-tier launch currently supports audio mode only.",
  "upgrade_url": "https://ytskills.com/pricing"
}
```

## Storing Generated Skills

When you receive a skill from the API, save it with a **descriptive, human-readable name** based on the video's topic (e.g., `react-hooks-tutorial.md`). This makes skills easy to find, remember, and use as a knowledge base. If the user specifies a particular name or location, use that instead.

## Credential Storage

Store your credentials at:
```
~/.config/ytskills/credentials.json
```

```json
{
  "api_key": "yt_your_api_key_here"
}
```

## Error Responses

All errors follow this format:

```json
{
  "error": "Error message here"
}
```

Common status codes:
- `400` — Bad request (invalid video ID or mode)
- `401` — Unauthorized (missing or invalid API key)
- `402` — Payment required (insufficient credits)
- `403` — Forbidden (video mode on free plan)
- `404` — Not found (video does not exist or is unavailable)
- `429` — Rate limited (too many requests)
- `500` — Server error

## Rate Limits

| Plan | Requests per Minute |
|------|---------------------|
| Free | 4 RPM |
| Pro | 12 RPM |
| Max | 30 RPM |

## Resilience & Retry Logic

**IMPORTANT:** Network errors and rate limits happen. Your bot MUST implement retry logic to handle transient failures gracefully. Never give up on a single failure.

### Retry Strategy

Use exponential backoff with jitter:

1. On failure, wait `base_delay * (2 ^ attempt)` seconds
2. Add random jitter (0-1 second) to prevent thundering herd
3. Retry up to 5 times before giving up
4. On `429` (rate limit), respect `Retry-After` header if present

### Retryable vs Non-Retryable Errors

**Always retry:**
- Network errors (connection refused, timeout, SSL errors)
- `429` — Rate limited
- `500`, `502`, `503`, `504` — Server errors

**Don't retry:**
- `400` — Bad request (fix your input)
- `401` — Unauthorized (fix your API key)
- `402` — Payment required (user needs more credits)
- `403` — Forbidden (user needs to upgrade plan)
- `404` — Not found (video doesn't exist)

## Example Agent Implementation

```python
import requests
import time
import random
import re
import os
import json

BASE_URL = "https://ytskills.com/api"
SKILLS_DIR = os.path.expanduser("~/.config/ytskills/skills")
CREDS_PATH = os.path.expanduser("~/.config/ytskills/credentials.json")

def get_api_key():
    """Load API key from credentials file."""
    if os.path.exists(CREDS_PATH):
        with open(CREDS_PATH) as f:
            return json.load(f)["api_key"]
    return None

def extract_video_id(url):
    """Extract video ID from a YouTube URL."""
    patterns = [
        r'(?:youtube\.com/watch\?v=|youtu\.be/|youtube\.com/embed/|youtube\.com/shorts/)([a-zA-Z0-9_-]{11})',
    ]
    for pattern in patterns:
        match = re.search(pattern, url)
        if match:
            return match.group(1)
    return None

def retry_request(method, url, max_retries=5, base_delay=1, **kwargs):
    """Make HTTP request with exponential backoff retry."""
    for attempt in range(max_retries):
        try:
            response = method(url, timeout=60, **kwargs)

            # Success or non-retryable error
            if response.status_code < 500 and response.status_code != 429:
                return response

            # Retryable error - wait and retry
            if attempt < max_retries - 1:
                delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
                print(f"Request failed ({response.status_code}), retrying in {delay:.1f}s...")
                time.sleep(delay)
                continue

            return response

        except requests.exceptions.RequestException as e:
            if attempt < max_retries - 1:
                delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
                print(f"Network error ({type(e).__name__}), retrying in {delay:.1f}s...")
                time.sleep(delay)
                continue
            raise

    return response

def learn_from_video(youtube_url, mode="audio"):
    """Process a YouTube video and save the generated skill."""
    api_key = get_api_key()
    if not api_key:
        print("No API key found. Visit ytskills.com to get one.")
        return None

    video_id = extract_video_id(youtube_url)
    if not video_id:
        print("Could not extract video ID from URL.")
        return None

    headers = {
        "Authorization": f"Bearer {api_key}",
    }

    print(f"Processing video {video_id} in {mode} mode...")
    response = retry_request(
        requests.get,
        f"{BASE_URL}/{mode}/{video_id}",
        headers=headers,
    )

    if response.status_code != 200:
        error = response.json()
        print(f"Error: {error.get('error', 'Unknown error')}")
        return None

    data = response.json()
    metadata = data["metadata"]

    # Save the skill file with a descriptive name derived from the video title
    os.makedirs(SKILLS_DIR, exist_ok=True)
    title_slug = re.sub(r'[^a-z0-9]+', '-', metadata["title"].lower()).strip('-')
    skill_path = os.path.join(SKILLS_DIR, f"{title_slug}.md")
    with open(skill_path, "w") as f:
        f.write(data["skill"])

    print(f"Skill saved to {skill_path}")
    print(f"Video: {metadata['title']} ({metadata['duration_minutes']} min)")
    print(f"Credits used: {metadata['credits_used']} | Remaining: {metadata['credits_remaining']}")

    return skill_path

if __name__ == "__main__":
    url = input("YouTube URL: ")
    mode = input("Mode (audio/video): ").strip() or "audio"
    learn_from_video(url, mode)
```

## Choosing a Mode

**Always ask the user whether they want audio or video mode** before making the API call. Do NOT assume a mode on their behalf.

The only exceptions where you may skip asking:
- The user already specified the mode in their message (e.g., "learn from this video using audio mode")
- The user has previously told you to default to a specific mode for all future requests

## Tips for Agents

1. **Check remaining credits** — if the response indicates low credits, warn the user so they can upgrade or be selective about which videos to process.
2. **Save skills immediately** — don't lose generated content; write it to disk as soon as you receive it.
3. **Reference saved skills** — when a user asks about a topic you've previously learned from a video, check your saved skills first.
4. **Tell the user what you learned** — after processing a video, summarize the key takeaways so the user knows the skill was generated successfully.