$npx -y skills add Varnan-Tech/opendirectory --skill competitor-pr-finderGive it your product URL or description. It finds your top 5 competitors, runs three-track PR research across all of them (editorial, podcasts, communities), identifies which channels appear most frequently, looks up the journalist or host for each, and returns a tiered outreach
| 1 | # Competitor PR Finder |
| 2 | |
| 3 | Give it your product URL. It finds your competitors, researches every PR channel they used (news, podcasts, communities), surfaces the channels that appear across multiple competitors (your proven targets), finds the journalist or host for each, and drafts a personalized cold pitch for your product at every tier-1 channel. |
| 4 | |
| 5 | --- |
| 6 | |
| 7 | **Zero-hallucination policy:** Every channel, journalist name, story angle, and pitch detail in the output must trace to a specific Tavily search result or the fetched product page. This applies to: |
| 8 | - Competitor names: must appear in Tavily search results, not AI training knowledge |
| 9 | - Channel names: must have a URL in the search results |
| 10 | - Journalist/host names: must appear verbatim in a Tavily snippet |
| 11 | - Story angles: extracted from article/episode titles in search results only |
| 12 | - Pitch drafts: reference specific evidence from search data + product analysis |
| 13 | |
| 14 | --- |
| 15 | |
| 16 | ## Common Mistakes |
| 17 | |
| 18 | | The agent will want to... | Why that's wrong | |
| 19 | |---|---| |
| 20 | | Name a journalist from training knowledge | Every journalist name must trace to a search result snippet. Writing "Sarah Perez covers startups at TechCrunch" from memory is hallucination. | |
| 21 | | List channels without evidence URLs | Every channel in the output must have at least one URL from the PR search results proving a competitor was featured there. | |
| 22 | | Skip the competitor confirmation step | Always show discovered competitors and wait for the user to confirm. Wrong competitors = wasted searches and a useless output. | |
| 23 | | Generate generic pitches ("We'd love to be featured") | Every pitch must reference a specific angle from the evidence AND a specific differentiator from the product analysis. | |
| 24 | | Mark a channel as Tier 1 with only 1 competitor occurrence | Tier 1 = 3+ competitors. Tier 2 = exactly 2. Tier 3 = 1. Do not promote channels that haven't proven themselves. | |
| 25 | | Use em dashes in output | Replace all em dashes (--) with hyphens. | |
| 26 | |
| 27 | --- |
| 28 | |
| 29 | ## Read Reference Files Before Each Run |
| 30 | |
| 31 | ```bash |
| 32 | cat references/pr-channel-types.md |
| 33 | cat references/pitch-guide.md |
| 34 | cat references/tier-scoring.md |
| 35 | ``` |
| 36 | |
| 37 | --- |
| 38 | |
| 39 | ## Step 1: Setup Check |
| 40 | |
| 41 | ```bash |
| 42 | echo "TAVILY_API_KEY: ${TAVILY_API_KEY:+set}${TAVILY_API_KEY:-NOT SET -- required}" |
| 43 | echo "FIRECRAWL_API_KEY: ${FIRECRAWL_API_KEY:+set}${FIRECRAWL_API_KEY:-not set, Tavily extract will be used as fallback}" |
| 44 | ``` |
| 45 | |
| 46 | **If TAVILY_API_KEY is missing:** Stop immediately. Tell the user: "TAVILY_API_KEY is required to research competitors and find PR coverage. There is no fallback. Get it at app.tavily.com -- free tier: 1000 credits/month (about 43 full runs at ~23 searches/run). Add it to your .env file." |
| 47 | |
| 48 | **If only FIRECRAWL_API_KEY is missing:** Continue. Tavily extract will be used for the URL fetch. |
| 49 | |
| 50 | --- |
| 51 | |
| 52 | ## Step 2: Parse Input |
| 53 | |
| 54 | Collect from the conversation: |
| 55 | - `product_url`: the URL to fetch (required, unless user pastes a description directly) |
| 56 | - `product_name`: optional, derived from page if not provided |
| 57 | - `geography`: optional -- US / Europe / global. Default: US |
| 58 | |
| 59 | **If the user provides only a pasted description (no URL):** Skip Steps 3 and 4. Go directly to Step 4 (product analysis) using the pasted text as `product_content`. Set `page_source` to `user_description` and note in `data_quality_flags`. |
| 60 | |
| 61 | **If neither URL nor description:** Ask: "What is the URL of your product or startup? Or paste a short description: what it does, who it is for, and what makes it different from competitors." |
| 62 | |
| 63 | Derive product slug: |
| 64 | |
| 65 | ```bash |
| 66 | PRODUCT_SLUG=$(python3 -c " |
| 67 | from urllib.parse import urlparse |
| 68 | import sys |
| 69 | url = 'URL_HERE' |
| 70 | if url.startswith('http'): |
| 71 | host = urlparse(url).netloc.replace('www.', '') |
| 72 | print(host.split('.')[0]) |
| 73 | else: |
| 74 | import re |
| 75 | print(re.sub(r'[^a-z0-9]', '-', url[:30].lower()).strip('-')) |
| 76 | ") |
| 77 | echo "Product slug: $PRODUCT_SLUG" |
| 78 | ``` |
| 79 | |
| 80 | --- |
| 81 | |
| 82 | ## Step 3: Fetch Product Page |
| 83 | |
| 84 | **Primary: Firecrawl (if FIRECRAWL_API_KEY is set)** |
| 85 | |
| 86 | ```bash |
| 87 | curl -s -X POST https://api.firecrawl.dev/v1/scrape \ |
| 88 | -H "Authorization: Bearer $FIRECRAWL_API_KEY" \ |
| 89 | -H "Content-Type: application/json" \ |
| 90 | -d '{"url": "URL_HERE", "formats": ["markdown"], "onlyMainContent": true}' \ |
| 91 | | python3 -c " |
| 92 | import sys, json |
| 93 | d = json.load(sys.stdin) |
| 94 | content = d.get('data', {}).get('markdown', '') or d.get('markdown', '') |
| 95 | print(f'Fetched via Firecrawl: {len(content)} characters') |
| 96 | open |