Buscador de PR de Competidores
Proporciona la URL de tu producto. Encuentra tus competidores, investiga cada canal de PR que utilizaron (noticias, podcasts, comunidades), identifica los canales que aparecen en múltiples competidores (tus objetivos probados), localiza al periodista o anfitrión de cada uno y redacta una propuesta fría personalizada para tu producto en cada canal de nivel 1.
Política de cero alucinaciones: Cada canal, nombre de periodista, ángulo de historia y detalle de la propuesta en el resultado debe rastrearse hasta un resultado de búsqueda específico de Tavily o la página del producto obtenida. Esto aplica para:
- Nombres de competidores: deben aparecer en los resultados de búsqueda de Tavily, no en el conocimiento de entrenamiento de la IA
- Nombres de canales: deben tener una URL en los resultados de búsqueda
- Nombres de periodistas/anfitriones: deben aparecer textualmente en un fragmento de Tavily
- Ángulos de historia: extraídos únicamente de títulos de artículos/episodios en los resultados de búsqueda
- Borradores de propuestas: deben hacer referencia a evidencia específica de los datos de búsqueda + análisis del producto
Errores Comunes
| El agente tenderá a... | Por qué está mal |
|---|---|
| Mencionar un periodista a partir del conocimiento de entrenamiento | Cada nombre de periodista debe rastrearse hasta un fragmento de resultado de búsqueda. Escribir "Sarah Perez cubre startups en TechCrunch" de memoria es una alucinación. |
| Listar canales sin URLs de evidencia | Cada canal en el resultado debe tener al menos una URL de los resultados de búsqueda de PR que demuestre que un competidor apareció allí. |
| Saltarse el paso de confirmación de competidores | Siempre muestra los competidores descubiertos y espera a que el usuario los confirme. Competidores incorrectos = búsquedas desperdiciadas y un resultado inútil. |
| Generar propuestas genéricas ("Nos encantaría aparecer") | Cada propuesta debe hacer referencia a un ángulo específico de la evidencia Y a un diferenciador específico del análisis del producto. |
| Marcar un canal como Nivel 1 con solo 1 ocurrencia de competidor | Nivel 1 = 3+ competidores. Nivel 2 = exactamente 2. Nivel 3 = 1. No promociones canales que no hayan demostrado su valor. |
| Usar guiones largos en el resultado | Reemplaza todos los guiones largos (—) por guiones comunes (-). |
Leer Archivos de Referencia Antes de Cada Ejecución
cat references/pr-channel-types.md
cat references/pitch-guide.md
cat references/tier-scoring.md
Paso 1: Verificación de Configuración
echo "TAVILY_API_KEY: ${TAVILY_API_KEY:+set}${TAVILY_API_KEY:-NO ESTABLECIDA -- requerida}"
echo "FIRECRAWL_API_KEY: ${FIRECRAWL_API_KEY:+set}${FIRECRAWL_API_KEY:-no establecida, se usará Tavily extract como respaldo}"
Si falta TAVILY_API_KEY: Detente inmediatamente. Dile al usuario: "TAVILY_API_KEY es necesaria para investigar competidores y encontrar cobertura de PR. No hay respaldo. Consíguela en app.tavily.com -- nivel gratuito: 1000 créditos/mes (aproximadamente 43 ejecuciones completas a ~23 búsquedas/ejecución). Agrégala a tu archivo .env."
Si solo falta FIRECRAWL_API_KEY: Continúa. Se usará Tavily extract para la obtención de la URL.
Paso 2: Analizar la Entrada
Recopila de la conversación:
product_url: la URL a obtener (requerida, a menos que el usuario pegue una descripción directamente)product_name: opcional, derivado de la página si no se proporcionageography: opcional -- EE. UU. / Europa / global. Valor predeterminado: EE. UU.
Si el usuario proporciona solo una descripción pegada (sin URL): Omite los Pasos 3 y 4. Ve directamente al Paso 4 (análisis del producto) usando el texto pegado como product_content. Establece page_source como user_description y anótalo en data_quality_flags.
Si no hay ni URL ni descripción: Pregunta: "¿Cuál es la URL de tu producto o startup? O pega una breve descripción: qué hace, para quién es y qué lo diferencia de los competidores."
Deriva el slug del producto:
PRODUCT_SLUG=$(python3 -c "
from urllib.parse import urlparse
import sys
url = 'URL_HERE'
if url.startswith('http'):
host = urlparse(url).netloc.replace('www.', '')
print(host.split('.')[0])
else:
import re
print(re.sub(r'[^a-z0-9]', '-', url[:30].lower()).strip('-'))
")
echo "Slug del producto: $PRODUCT_SLUG"
Paso 3: Obtener la Página del Producto
Principal: Firecrawl (si FIRECRAWL_API_KEY está establecida)
curl -s -X POST https://api.firecrawl.dev/v1/scrape \
-H "Authorization: Bearer $FIRECRAWL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "URL_HERE", "formats": ["markdown"], "onlyMainContent": true}' \
| python3 -c "
import sys, json
d = json.load(sys.stdin)
content = d.get('data', {}).get('markdown', '') or d.get('markdown', '')
print(f'Obtenido via Firecrawl: {len(content)} caracteres')
open('/tmp/cprf-product-raw.md', 'w').write(content)
"
Respaldo: Tavily extract (si FIRECRAWL_API_KEY no está establecida)
curl -s -X POST https://api.tavily.com/extract \
-H "Content-Type: application/json" \
-d "{\"api_key\": \"$TAVILY_API_KEY\", \"urls\": [\"URL_HERE\"]}" \
| python3 -c "
import sys, json
d = json.load(sys.stdin)
content = d.get('results', [{}])[0].get('raw_content', '')
print(f'Obtenido via Tavily extract: {len(content)} caracteres')
open('/tmp/cprf-product-raw.md', 'w').write(content)
"
Punto de control:
python3 -c "
content = open('/tmp/cprf-product-raw.md').read()
if len(content) < 200:
print('ERROR: se obtuvieron menos de 200 caracteres')
else:
print(f'Contenido OK: {len(content)} caracteres')
"
Si el contenido tiene menos de 200 caracteres: Deja de obtener. Dile al usuario: "La página del producto no devolvió contenido legible; es probable que el sitio use renderizado JavaScript y haya bloqueado la obtención. Por favor, pega una breve descripción directamente: qué hace, para quién es y qué lo diferencia."
Paso 4: Análisis del Producto (IA)
Imprime el contenido de la página:
python3 -c "
content = open('/tmp/cprf-product-raw.md').read()[:5000]
print('=== PÁGINA DEL PRODUCTO (primeros 5000 caracteres) ===')
print(content)
"
Instrucciones para la IA: Analiza la página del producto anterior y extrae:
product_name: el nombre del producto o empresaone_line_description: qué hace, para quién, propuesta de valor central. Menos de 20 palabras. Sin lenguaje de marketing. Ejemplo: "Automatización de CI/CD para equipos de desarrolladores que autogestionan sus pipelines."industry_taxonomy:l1(nivel superior: ej. herramientas de desarrollo / fintech / healthtech / consumo),l2(sector: ej. devops / pagos / telemedicina),l3(nicho específico: ej. automatización de CI/CD / pagos integrados / consulta de video asíncrona). Etiquetas vagas como "tecnología" por sí solas no son aceptables.differentiators: exactamente 2-3 cosas específicas que distinguen este producto de los competidores genéricos. Estos se alimentan directamente a los borradores de propuestas; sé específico. Ejemplo: ["Ejecutor de pipeline autogestionado -- ningún dato sale de tu infraestructura", "Soporte nativo para monorepos con generación dinámica de pasos"]icp:buyer_persona(título del puesto),company_type(tipo de empresa),company_size(tamaño de la empresa)geography_bias: EE. UU. / Europa / global / no claropage_source: "live_page" o "user_description"
Escribe en /tmp/cprf-product-analysis.json:
python3 << 'PYEOF'
import json
analysis = {
# COMPLETA a partir de tu análisis anterior
"product_name": "",
"one_line_description": "",
"industry_taxonomy": {"l1": "", "l2": "", "l3": ""},
"differentiators": [],
"icp": {"buyer_persona": "", "company_type": "", "company_size": ""},
"geography_bias": "EE. UU.",
"page_source": "live_page"
}
json.dump(analysis, open('/tmp/cprf-product-analysis.json', 'w'), indent=2)
print('Análisis del producto escrito.')
PYEOF
Verifica:
python3 -c "
import json
a = json.load(open('/tmp/cprf-product-analysis.json'))
print('Producto:', a['product_name'])
print('Industria:', a['industry_taxonomy']['l1'], '>', a['industry_taxonomy']['l2'], '>', a['industry_taxonomy']['l3'])
print('Diferenciadores:')
for d in a['differentiators']:
print(f' - {d}')
"
Paso 4b: Fase 1 -- Descubrimiento de Competidores
ls scripts/research.py 2>/dev/null && echo "script encontrado" || echo "ERROR: no se encontró scripts/research.py -- no se puede continuar"
python3 scripts/research.py \
--phase discover \
--product-analysis /tmp/cprf-product-analysis.json \
--tavily-key "$TAVILY_API_KEY" \
--output /tmp/cprf-competitors-raw.json
Imprime los resultados para revisión de la IA:
python3 -c "
import json
data = json.load(open('/tmp/cprf-competitors-raw.json'))
print(f'Búsquedas realizadas: {len(data[\"competitor_searches\"])}')
for s in data['competitor_searches']:
print(f'\nConsulta: {s[\"query\"]}')
print(f'Respuesta: {s.get(\"answer\",\"\")[:400]}')
for r in s.get('results', [])[:5]:
print(f' - {r[\"title\"]} | {r[\"url\"]}')
print(f' {r.get(\"content\",\"\")[:200]}')
"
Instrucciones para la IA: Lee los resultados de búsqueda anteriores. Elige exactamente 5 empresas competidoras que:
- Aparezcan en los títulos, respuestas o fragmentos de los resultados de búsqueda
- Estén en el mismo nicho L3 que el producto que se analiza
- Sean productos competidores reales (no agencias, consultorías o artículos de listas)
- Sean distintas entre sí (no la misma empresa con diferentes nombres)
Para cada competidor, escribe: name, url (del resultado de búsqueda donde apareció), description (una oración del fragmento), source_url (la URL del resultado de búsqueda donde se encontraron).
Paso 5: Confirmación de Competidores
Muestra los competidores descubiertos al usuario:
python3 << 'PYEOF'
import json
analysis = json.load(open('/tmp/cprf-product-analysis.json'))
# COMPLETA: 5 competidores de los resultados de búsqueda anteriores
candidates = [
# {"name": str, "url": str, "description": str, "source_url": str}
]
print(f"\nSe encontraron 5 competidores para {analysis['product_name']} en {analysis['industry_taxonomy']['l3']}:\n")
for i, c in enumerate(candidates, 1):
print(f" {i}. {c['name']} -- {c['description']}")
print(f" {c['url']}")
data = json.load(open('/tmp/cprf-competitors-raw.json'))
data['competitor_candidates'] = candidates
json.dump(data, open('/tmp/cprf-competitors-raw.json', 'w'), indent=2)
PYEOF
Dile al usuario: "Estos son los 5 competidores que investigaré para cobertura de PR. Agrega, elimina o intercambia cualquiera, o di 'se ve bien' para continuar."
Espera la confirmación. Si el usuario edita la lista (agrega/elimina/intercambia), actualiza los candidatos en consecuencia. Luego escribe la lista confirmada:
python3 << 'PYEOF'
import json
# COMPLETA: lista de competidores confirmada (después de la revisión del usuario)
confirmed = [
# {"name": str, "url": str}
]
json.dump({"confirmed_competitors": confirmed}, open('/tmp/cprf-competitors-confirmed.json', 'w'), indent=2)
print(f"Se confirmaron {len(confirmed)} competidores para la investigación de PR.")
for c in confirmed:
print(f" - {c['name']} ({c['url']})")
PYEOF
Paso 6: Investigación de PR en Tres Vertientes (Fase 2)
python3 scripts/research.py \
--phase pr-research \
--competitors /tmp/cprf-competitors-confirmed.json \
--product-analysis /tmp/cprf-product-analysis.json \
--tavily-key "$TAVILY_API_KEY" \
--output /tmp/cprf-pr-raw.json
Esto ejecuta 3 búsquedas por competidor (15 en total):
- Vertiente A (Editorial):
"[competidor]" featured press coverage TechCrunch Forbes Wired article interview - Vertiente B (Podcasts):
"[competidor]" founder CEO podcast interview appeared on episode - Vertiente C (Comunidades):
"[competidor]" site:reddit.com OR site:news.ycombinator.com OR site:producthunt.com
Imprime el resumen de cobertura:
python3 -c "
import json
data = json.load(open('/tmp/cprf-pr-raw.json'))
print(f'Competidores investigados: {data[\"competitors_researched\"]}')
print()
for r in data['results']:
print(f'{r[\"competitor\"]}:')
for track, tdata in r['tracks'].items():
n = len(tdata.get('results', []))
print(f' {track:12}: {n} resultados')
"
Si las 3 vertientes para un competidor devuelven 0 resultados: Este competidor tiene muy poca cobertura de prensa. Anótalo en data_quality_flags y continúa -- el patrón entre competidores aún funcionará con los 4 restantes.
Paso 7: Análisis de Patrones (IA)
Imprime todos los resultados de PR sin procesar:
python3 -c "
import json
data = json.load(open('/tmp/cprf-pr-raw.json'))
for r in data['results']:
print(f'\n=== {r[\"competitor\"]} ===')
for track, tdata in r['tracks'].items():
print(f'\n--- Vertiente {track.upper()} ---')
print(f'Consulta: {tdata[\"query\"]}')
print(f'Respuesta: {tdata.get(\"answer\",\"\")[:400]}')
for item in tdata.get('results', [])[:5]:
print(f' Título: {item[\"title\"]}')
print(f' URL: {item[\"url\"]}')
print(f' Fragmento: {item.get(\"content\",\"\")[:200]}')
"
Instrucciones para la IA: Lee TODOS los resultados de búsqueda anteriores. Construye un mapa de frecuencia de canales.
Paso 1 -- Normaliza las URLs al dominio raíz: https://techcrunch.com/2023/06/article-title → techcrunch.com. https://open.spotify.com/episode/... → identifícalo como podcast (episodio de spotify). https://www.reddit.com/r/devops/ → reddit.com/r/devops.
Paso 2 -- Cuenta las ocurrencias: ¿Cuántos competidores diferentes aparecieron en los resultados de cada dominio raíz de canal? Un canal que aparece en la Vertiente A del Competidor A Y en la Vertiente A del Competidor B cuenta como frecuencia 2.
Paso 3 -- Clasifica los canales (sigue references/tier-scoring.md):
- Nivel 1: apareció en 3+ competidores
- Nivel 2: apareció en exactamente 2 competidores
- Nivel 3: apareció en 1 competidor
Paso 4 -- Extrae ángulos de historia de los títulos de artículos/episodios en los resultados. Clasifica cada uno como: anuncio-de-financiación / lanzamiento-de-producto / historia-del-fundador / artículo-de-tendencia / creación-de-categoría / guía / comparación / premio. No infieras; solo clasifica ángulos visibles en los títulos.
Paso 5 -- Clasifica el tipo de canal para cada uno: editorial / podcast / comunidad / boletín.
Escribe en /tmp/cprf-pr-patterns.json:
python3 << 'PYEOF'
import json
patterns = {
"tier_1_channels": [
# COMPLETA: canales que aparecen en 3+ competidores
# Cada uno: {"channel_name": str, "channel_url": str, "channel_type": str,
# "frequency": int, "found_in_competitors": [str],
# "evidence_urls": [str], "story_angles_used": [str],
# "journalist_name": "", "journalist_beat": ""}
],
"tier_2_channels": [
# COMPLETA: canales que aparecen en exactamente 2 competidores
# Cada uno: {"channel_name": str, "channel_url": str, "channel_type": str,
# "frequency": int, "found_in_competitors": [str],
# "evidence_urls": [str], "story_angles_used": [str],
# "journalist_name": "", "journalist_beat": ""}
],
"tier_3_channels": [
# COMPLETA: canales que aparecen en 1 competidor
# Cada uno: {"channel_name": str, "channel_url": str, "channel_type": str,
# "frequency": int, "found_in_competitors": [str],
# "evidence_urls": [str], "story_angles_used": [str],
# "journalist_name": "", "journalist_beat": ""}
],
"data_quality_flags": []
}
json.dump(patterns, open('/tmp/cprf-pr-patterns.json', 'w'), indent=2)
print('Análisis de patrones de PR escrito.')
PYEOF
Paso 8: Investigación de Periodistas y Anfitriones
Para canales de Nivel 1 y Nivel 2 solo. Omite el Nivel 3: no vale la pena el esfuerzo de divulgación.
Busca al periodista o anfitrión específico que cubre tu nicho para cada canal de nivel 1 y 2:
python3 scripts/research.py \
--phase journalist \
--patterns /tmp/cprf-pr-patterns.json \
--product-analysis /tmp/cprf-product-analysis.json \
--tavily-key "$TAVILY_API_KEY" \
--output /tmp/cprf-journalists-raw.json
Imprime los resultados de la investigación de periodistas:
python3 -c "
import json
data = json.load(open('/tmp/cprf-journalists-raw.json'))
print(f'Canales investigados: {len(data[\"channels_researched\"])}')
for channel, result in data['results'].items():
print(f'\n=== {channel} ===')
print(f'Búsqueda: {result[\"search_query\"]}')
print(f'Respuesta: {result.get(\"answer\",\"\")[:300]}')
"
Instrucciones para la IA: Lee los resultados de la investigación de periodistas. Para cada canal de nivel 1 y 2, extrae el periodista o anfitrión más relevante que escriba sobre tu nicho de producto (L3). Proporciona:
journalist_name: nombre completo del periodista/anfitriónjournalist_email(opcional, si se encuentra en uno de los primeros 3 resultados)journalist_beat: qué cubren (ej., "Startups SaaS", "Herramientas DevOps", "Fundadores de Fintech")source_url: la URL de los resultados de búsqueda donde se encontró esta informaciónevidence_snippet: una cita textual del fragmento de búsqueda que mencione al periodista
Si no se encuentra un periodista/anfitrión para un canal: Establece journalist_name como "No encontrado" y escribe un finding_note que explique por qué (ej., "Este canal de YouTube no muestra nombres de anfitriones en sus páginas de episodios"). No inventes un nombre.
Actualiza el archivo de patrones:
python3 << 'PYEOF'
import json
patterns = json.load(open('/tmp/cprf-pr-patterns.json'))
journalist_data = json.load(open('/tmp/cprf-journalists-raw.json'))
for channel_entry in patterns['tier_1_channels'] + patterns['tier_2_channels']:
channel_key = channel_entry['channel_url']
if channel_key in journalist_data['results']:
j_result = journalist_data['results'][channel_key]
channel_entry['journalist_name'] = j_result.get('journalist_name', '')
channel_entry['journalist_email'] = j_result.get('journalist_email', '')
channel_entry['journalist_beat'] = j_result.get('journalist_beat', '')
channel_entry['journalist_source_url'] = j_result.get('source_url', '')
json.dump(patterns, open('/tmp/cprf-pr-patterns.json', 'w'), indent=2)
print('Archivo de patrones actualizado con datos de periodistas.')
PYEOF
Paso 9: Crear la Lista de Contactos de Divulgación
Lee el archivo de clasificación de niveles para verificar las reglas:
cat references/tier-scoring.md
Construye una tabla de canales agrupada por nivel:
python3 << 'PYEOF'
import json
patterns = json.load(open('/tmp/cprf-pr-patterns.json'))
print('\n===== LISTA DE CONTACTOS DE PR =====')
print()
for tier_key, tier_label in [('tier_1_channels', 'NIVEL 1: CANALES PROBADOS (3+ competidores)'),
('tier_2_channels', 'NIVEL 2: CANALES EMERGENTES (2 competidores)'),
('tier_3_channels', 'NIVEL 3: CANALES DE OPORTUNIDAD (1 competidor)')]:
channels = patterns[tier_key]
if not channels:
continue
print(f'\n## {tier_label}')
print()
print(f'| Canal | URL | Tipo | Frecuencia | Periodista | Ángulo de Historia |')
print(f'|---|---|---|---|---|---|')
for ch in channels:
j_name = ch.get('journalist_name', 'No encontrado') or 'No encontrado'
angle = ch.get('story_angles_used', ['General'])[0]
print(f'| {ch["channel_name"]} | {ch["channel_url"]} | {ch["channel_type"]} | {ch["frequency"]} | {j_name} | {angle} |')
json.dump(patterns, open('/tmp/cprf-pr-patterns.json', 'w'), indent=2)
PYEOF
Paso 10: Redactar Propuestas Frías (IA)
Lee la guía de propuestas:
cat references/pitch-guide.md
Instrucciones para la IA: Redacta exactamente 1 propuesta fría para cada canal de Nivel 1. (Nivel 2 y 3 se pueden omitir o crear bajo demanda.)
Cada propuesta debe incluir:
journalist_name: el nombre del periodista/anfitriónoutlet: el nombre del canal/publicaciónstory_angle: el ángulo de historia extraído de la evidencia de PR del competidorproduct_angle: cómo se relaciona el diferenciador del producto con ese ángulo (basado en el análisis del producto)pitch_type: newswire / story-pitch / podcast-pitch / community-post / comparison-articlepitch_body: el borrador completo de la propuesta, escrito en el tono adecuado para el canal. No uses comillas de ningún tipo en el pitch_body; usa comillas angulares (« »).
Ejemplo de salida de propuesta:
{
"pitch": {
"target": "Sarah Perez",
"outlet": "TechCrunch",
"subject": "Automatización de CI/CD autogestionada: cómo [Producto] está resolviendo el problema de seguridad de datos que las soluciones en la nube pasan por alto",
"story_angle": "Artículo de tendencia sobre por qué los equipos de DevOps están volviendo a soluciones autogestionadas",
"product_angle": "Diferenciador de autogestionado + seguridad de datos",
"pitch_type": "story-pitch",
"pitch_body": "Hola Sarah, vi tu reciente artículo sobre DevOps autogestionado en TechCrunch. En [Producto], creemos que la seguridad de datos no debería ser un compromiso al escalar pipelines de CI/CD. Nuestro ejecutor autogestionado garantiza que ningún dato salga de la infraestructura del cliente, mientras ofrece funciones como soporte nativo para monorepos que faltan en soluciones en la nube. Creo que a tus lectores les encantaría una inmersión en cómo estamos abordando esta brecha. ¿Te interesaría una demo o una entrevista con nuestro CTO?"
}
}
Validación: Verifica cada propuesta contra estas reglas:
- ¿El ángulo de historia se traza a un título de artículo/episodio específico en los resultados de búsqueda?
- ¿El ángulo del producto se traza a una línea específica del análisis del producto?
- ¿Se menciona al periodista/anfitrión correcto?
- ¿La propuesta evita el lenguaje genérico de relaciones públicas?
- ¿El
pitch_bodytiene menos de 150 palabras? - ¿La línea de asunto tiene menos de 15 palabras?
Después de la redacción, consolida en el archivo de salida final:
python3 << 'PYEOF'
import json
patterns = json.load(open('/tmp/cprf-pr-patterns.json'))
product = json.load(open('/tmp/cprf-product-analysis.json'))
output = {
"product": product['product_name'],
"industry": product['industry_taxonomy'],
"generated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"outreach_list": patterns,
"pitches": [
# COMPLETA: 1 propuesta por canal de nivel 1
],
"next_steps": [
"Revisa cada propuesta en busca de personalización antes de enviar.",
"Verifica la información de contacto del periodista en LinkedIn o Twitter.",
"Da seguimiento después de 7 días si no hay respuesta.",
"Los canales de Nivel 2 y Nivel 3 se pueden abordar más adelante."
]
}
json.dump(output, open('/tmp/cprf-final-output.json', 'w'), indent=2)
print(f'Resultado final escrito: {len(output["pitches"])} propuestas para {len(output["outreach_list"]["tier_1_channels"])} canales de nivel 1.')
PYEOF
Entrega al Usuario
Lee de /tmp/cprf-final-output.json y muéstrale al usuario:
- Resumen de tu producto (nombre, industria, diferenciadores)
- Lista de contactos -- tabla con canales agrupados por nivel (Nivel 1, 2, 3)
- Propuestas listas para enviar -- propuesta completa para cada canal de Nivel 1
- Próximos pasos
Formato de entrega: Usa formato de tabla para la lista de contactos y bloques de cita o código para las propuestas. Mantén el JSON crudo como archivo adjunto disponible si se solicita.