Donde Vive Tu Cliente
Dada la utilidad de un producto y su ICP, rastrea publicaciones reales de dolor del ICP hasta sus comunidades de origen. Superpone señales de discusión sobre competidores. Descubre canales de Slack/Discord/newsletter/podcast/conferencia mediante DuckDuckGo. Puntúa cada canal por número de señales del ICP, tamaño, actividad y presencia de competidores. Genera un plan de acción clasificado: evidencia, táctica de entrada, ángulo de contenido, contra-pautas -- uno por canal. Sin adivinanzas. Solo canales rastreados por señales.
Regla crítica: Cada nombre de canal en la salida debe existir en la respuesta de la API de Reddit o en los resultados de búsqueda de DuckDuckGo de esta ejecución. Cada número de miembros debe provenir de la API about.json o de un fragmento de búsqueda -- nunca estimado. Cada recuento de señales del ICP debe coincidir con los datos sin procesar. Si un tipo de canal devuelve 0 resultados, reporta 0 -- no fabriques canales.
Errores Comunes
| El agente querrá... | Por qué está mal |
|---|---|
| Recomendar canales genéricos ("LinkedIn", "Twitter") | Cada canal debe ser específico con un nombre, número de miembros y URL. "Grupo de LinkedIn: DevOps para Equipos Empresariales (45K miembros)" -- no solo "LinkedIn". |
| Usar los mismos canales para cada ICP | El rastreo de señales es específico del ICP. Un ICP de DevOps y uno de Finanzas producen listas de canales completamente diferentes. Ejecuta el script fresco por ICP. |
| Inventar recuentos de miembros o nombres de comunidades | Cada nombre de canal debe provenir de resultados de DuckDuckGo o de la API de Reddit. Cada número de miembros debe provenir de la API o de un fragmento de búsqueda. Si no está disponible, escribe "número de miembros no encontrado". |
| Omitir la capa de competidores | Donde se discuten competidores = tu ICP está evaluando alternativas = el contexto de divulgación más candente. Siempre ejecuta la búsqueda de competidores incluso si el usuario no lo pidió. |
| Escribir tácticas de entrada que sean lanzamientos de producto | "Publica sobre tu producto en r/devops" no es una táctica de entrada. Las tácticas de entrada nombran el tipo específico de hilo, formato de contenido y norma comunitaria. |
| Tratar Reddit como el único tipo de canal | La salida debe incluir al menos 3 tipos de canal. Si solo se encuentra Reddit, busca explícitamente en DuckDuckGo Slack/Discord/newsletter/conferencia antes de detenerte. |
Paso 1: Verificación de Configuración
echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-no establecido -- la capa de competidores funciona a 60 req/h sin autenticación}"
echo ""
echo "Fuentes de datos que esta ejecución utilizará:"
echo " Reddit public JSON (sin autenticación, rastreo de señales)"
echo " Reddit about.json (sin autenticación, metadatos del subreddit)"
echo " HN Algolia API (sin autenticación, rastreo de señales)"
echo " DuckDuckGo HTML (sin autenticación, descubrimiento de canales)"
echo " GitHub API (${GITHUB_TOKEN:+autenticada, }opcional para enriquecimiento de competidores)"
Si GITHUB_TOKEN no está establecido: continúa. Todo el descubrimiento de canales principal funciona sin él.
Paso 2: Analizar el ICP
Recopila de la conversación:
product-- qué hace el producto (una oración)icp_role-- quién es el ICP (ej. "cofundadores técnicos", "ingenieros DevOps en empresas Serie A")icp_pain-- su problema principal (ej. "adquisición de clientes", "fatiga de alertas")category-- palabras clave de categoría de mercado (ej. "ventas GTM para startups", "monitoreo DevOps")competitors-- nombres opcionales de competidores (ej. "Clay, Apollo, HubSpot")
Cascada del ICP:
- Si la solicitud del usuario contiene producto + icp_role + icp_pain: extráelos directamente y continúa.
- Si la solicitud es escasa (solo categoría o solo nombre del producto): consulta
docs/icp.mdpara un perfil de ICP guardado. Combínalo con los detalles de la solicitud. - Si aún es insuficiente (falta icp_role o icp_pain): haz estas 3 preguntas, una a la vez:
- "¿Qué hace tu producto en una oración?"
- "¿Quién es tu cliente ideal? (rol, tipo de empresa, tamaño del equipo)"
- "¿Cuál es su problema principal antes de encontrar tu producto?"
- Guarda el ICP final en
docs/icp.mdpara que otras habilidades puedan reutilizarlo.
Guarda el archivo ICP si docs/icp.md aún no contiene este producto:
python3 << 'PYEOF'
import json, os
icp = {
"product": "PRODUCTO_AQUÍ",
"icp_role": "ROL_ICP_AQUÍ",
"icp_pain": "DOLOR_ICP_AQUÍ",
"competitors": ["COMP_1", "COMP_2"],
"category": "CATEGORÍA_AQUÍ"
}
os.makedirs("docs", exist_ok=True)
with open("/tmp/wcl-input.json", "w") as f:
json.dump(icp, f, indent=2)
# Actualizar docs/icp.md
icp_md_path = "docs/icp.md"
new_block = f"""## {icp['product']}
- **Rol del ICP:** {icp['icp_role']}
- **Dolor del ICP:** {icp['icp_pain']}
- **Competidores:** {', '.join(icp['competitors']) if icp['competitors'] else 'ninguno'}
- **Categoría:** {icp['category']}
"""
existing = open(icp_md_path).read() if os.path.exists(icp_md_path) else ""
if icp['product'] not in existing:
with open(icp_md_path, "a") as f:
f.write(new_block)
print(f"ICP guardado en {icp_md_path}")
else:
print(f"ICP ya existe en {icp_md_path}")
print(f"Producto: {icp['product']}")
print(f"Rol del ICP: {icp['icp_role']}")
print(f"Dolor del ICP: {icp['icp_pain']}")
print(f"Competidores: {', '.join(icp['competitors']) if icp['competitors'] else 'ninguno'}")
PYEOF
Paso 3: Ejecutar el Script de Recopilación de Datos Independiente
Verifica si el script existe:
ls scripts/fetch.py 2>/dev/null && echo "script disponible" || echo "no encontrado"
Ejecuta el descubrimiento de canales:
GITHUB_TOKEN="${GITHUB_TOKEN:-}" python3 scripts/fetch.py \
"$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(d['category'])")" \
--icp-role "$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(d['icp_role'])")" \
--icp-pain "$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(d['icp_pain'])")" \
--product "$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(d['product'])")" \
--competitors "$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(','.join(d['competitors']))")" \
--output /tmp/wcl-raw.json
Espera a que termine (permite hasta 5 minutos -- las búsquedas de Reddit + DuckDuckGo toman ~120 segundos en total).
Verifica la salida:
python3 -c "
import json
with open('/tmp/wcl-raw.json') as f:
d = json.load(f)
print(f'Publicaciones de Reddit encontradas: {d[\"reddit_posts_found\"]}')
print(f'Señales de HN encontradas: {d[\"hn_signals_found\"]}')
print(f'Canales descubiertos: {d[\"summary\"][\"total_channels\"]}')
print(f'Prioridad máxima: {len(d[\"summary\"][\"top_priority\"])}')
print(f'Por tipo: {d[\"summary\"][\"by_type\"]}')
print(f'Capa de competidores ejecutada: {d[\"summary\"][\"competitor_layer_ran\"]}')
"
Si total_channels < 3: informa al usuario: "Se encontraron menos de 3 canales. La descripción del ICP puede ser demasiado estrecha para la cobertura de Reddit/DDG. Intenta con palabras clave de categoría más amplias, o agrega nombres de competidores para activar la capa de competidores." Luego intenta un reintento con palabras clave de categoría más amplias antes de detenerte.
Paso 4: Imprimir Resumen de Canales
Carga los datos sin procesar e imprime una tabla de resumen clasificada:
python3 -c "
import json
with open('/tmp/wcl-raw.json') as f:
d = json.load(f)
channels = d['channels_discovered']
print(f'Canales encontrados: {len(channels)}')
print()
print(f'{'#':<4} {'Canal':<35} {'Tipo':<14} {'Miembros':<12} {'Señales':<13} {'Puntaje':<8} Nivel')
print('-' * 100)
for i, ch in enumerate(channels[:15], 1):
members = ch.get('members', 0)
m_str = f'{members//1000}K' if members >= 1000 else str(members) if members else '?'
print(f'{i:<4} {ch[\"nombre\"]:<35} {ch[\"tipo\"]:<14} {m_str:<12} {ch.get(\"icp_signal_count\",0):<13} {ch.get(\"channel_score\",0):<8} {ch.get(\"tier\",\"\")}')
"
Imprime las 3 publicaciones de evidencia principales del canal con mayor puntuación:
python3 -c "
import json
with open('/tmp/wcl-raw.json') as f:
d = json.load(f)
channels = d['channels_discovered']
if channels:
top = channels[0]
print(f'Canal principal: {top[\"name\"]}')
print(f'Publicaciones de evidencia:')
for ep in top.get('evidence_posts', [])[:3]:
print(f' [{ep.get(\"score\",0):.0f}] {ep.get(\"title\",\"\")}')
print(f' {ep.get(\"url\",\"\")}')
"
Paso 5: Enriquecimiento de Canales por IA
Ahora tienes los datos sin procesar del canal. Para cada canal en los niveles de prioridad máxima y alta, genera una entrada de plan de acción.
Carga todos los canales:
python3 -c "
import json
with open('/tmp/wcl-raw.json') as f:
d = json.load(f)
top_channels = [ch for ch in d['channels_discovered'] if ch.get('tier') in ('top-priority', 'high')]
print(json.dumps(top_channels, indent=2))
"
Para cada canal anterior, genera:
quién_está_aquí: 2 oraciones describiendo el tipo específico de ICP presente en este canal. Derívalo de las publicaciones de evidencia, la descripción del subreddit y el perfil del ICP. NO escribas "tu público objetivo" -- sé específico. Ejemplo: "Ingenieros DevOps en empresas de 50-500 personas que poseen la pila de infraestructura sin un equipo SRE dedicado. Publican sobre agotamiento por guardias, proliferación de Kubernetes y elegir entre observabilidad nativa de la nube y autoalojada."
táctica_de_entrada: Un movimiento de entrada específico y accionable. Nombra el tipo de hilo, formato de publicación y norma comunitaria. NO "interactúa con la comunidad". Ejemplo: "Encuentra el hilo semanal '¿En qué estás trabajando?' (publicado cada lunes por automoderator). Responde con un desafío técnico que resolviste en 3 oraciones -- qué se rompió, qué intentaste, qué funcionó. Sin mención del producto. Genera karma antes de publicar contenido independiente."
ángulo_de_contenido: El formato de contenido que obtiene mayor participación en este canal específico, derivado de los títulos y puntuaciones de las publicaciones de evidencia. Ejemplo: "Las autopsias técnicas superan los anuncios de producto 5:1 aquí. Formato: 'Migramos 200K usuarios de X a Y -- esto es lo que se rompió y por qué'. Números concretos + lo que falló = más votos positivos."
contra_pautas: 2-3 comportamientos específicos que hacen que las publicaciones sean eliminadas o la reputación destruida en esta comunidad. Derívalos de las reglas del subreddit (si están disponibles en la descripción) y los patrones de las publicaciones de evidencia. Ejemplo: ["Publicar enlaces de productos en hilos no promocionales -- los moderadores eliminan en cuestión de horas", "Preguntar '¿qué herramientas usas?' sin contexto específico -- marcado como recolección de investigación de mercado"]
Escribe el plan de acción enriquecido en /tmp/wcl-channels.json:
{
"playbook": [
{
"channel": "r/devops",
"evidence": "34 señales del ICP rastreadas aquí, puntaje de dolor promedio 180",
"quién_está_aquí": "...",
"táctica_de_entrada": "...",
"ángulo_de_contenido": "...",
"contra_pautas": ["...", "..."]
}
]
}
python3 -c "
import json
with open('/tmp/wcl-channels.json') as f:
d = json.load(f)
print(f'Entradas del plan de acción: {len(d[\"playbook\"])}')
for p in d['playbook']:
print(f' {p[\"channel\"]}')
"
Paso 6: Generar la Salida Clasificada Completa
Escribe el plan de acción completo clasificado en /tmp/wcl-output.json:
python3 << 'PYEOF'
import json
from datetime import datetime
with open('/tmp/wcl-input.json') as f:
inp = json.load(f)
with open('/tmp/wcl-raw.json') as f:
raw = json.load(f)
with open('/tmp/wcl-channels.json') as f:
enriched = json.load(f)
playbook_by_channel = {p['channel']: p for p in enriched['playbook']}
channels = raw['channels_discovered']
output = {
"date": raw['date'],
"product": inp['product'],
"icp_role": inp['icp_role'],
"icp_pain": inp['icp_pain'],
"competitors": inp.get('competitors', []),
"total_channels": raw['summary']['total_channels'],
"channels": []
}
for ch in channels:
name = ch['name']
playbook = playbook_by_channel.get(name, {})
output['channels'].append({
"rank": channels.index(ch) + 1,
"name": name,
"type": ch['type'],
"url": ch['url'],
"members": ch.get('members', 0),
"active_users": ch.get('active_users', 0),
"icp_signal_count": ch.get('icp_signal_count', 0),
"competitor_mentions": ch.get('competitor_mentions', 0),
"channel_score": ch.get('channel_score', 0),
"tier": ch.get('tier', ''),
"entry_type": ch.get('entry_type', 'open'),
"evidence_posts": ch.get('evidence_posts', []),
"quién_está_aquí": playbook.get('quién_está_aquí', ''),
"táctica_de_entrada": playbook.get('táctica_de_entrada', ''),
"ángulo_de_contenido": playbook.get('ángulo_de_contenido', ''),
"contra_pautas": playbook.get('contra_pautas', []),
})
with open('/tmp/wcl-output.json', 'w') as f:
json.dump(output, f, indent=2)
print(f"Salida escrita: /tmp/wcl-output.json")
print(f"Total de canales: {len(output['channels'])}")
PYEOF
Paso 7: Autoverificación de Calidad
python3 -c "
import json
with open('/tmp/wcl-raw.json') as f:
raw = json.load(f)
with open('/tmp/wcl-output.json') as f:
output = json.load(f)
full_text = json.dumps(output)
raw_channel_names = {ch['name'].lower() for ch in raw['channels_discovered']}
passes = 0
fails = 0
# Verificación 1: Sin guiones largos (em dash)
if chr(8212) in full_text:
print('FALLO: se encontró guión largo em en la salida -- reemplazar con guión normal')
fails += 1
else:
print('PASA: sin guiones largos em')
passes += 1
# Verificación 2: Sin palabras prohibidas
banned = ['powerful', 'robust', 'seamless', 'innovative', 'game-changing',
'streamline', 'leverage', 'transform', 'revolutionize']
found = [w for w in banned if w.lower() in full_text.lower()]
if found:
print(f'FALLO: palabras prohibidas encontradas: {found}')
fails += 1
else:
print('PASA: sin palabras prohibidas')
passes += 1
# Verificación 3: Al menos 3 tipos de canal
types = {ch['type'] for ch in output['channels']}
if len(types) < 3:
print(f'FALLO: solo {len(types)} tipo(s) de canal en la salida: {types}')
fails += 1
else:
print(f'PASA: {len(types)} tipos de canal: {types}')
passes += 1
# Verificación 4: Todos los nombres de canal existen en los datos sin procesar
for ch in output['channels']:
if ch['name'].lower() not in raw_channel_names:
print(f'FALLO: canal no encontrado en datos sin procesar: {ch[\"name\"]}')
fails += 1
if fails == 0:
print('PASA: todos los nombres de canal verificados en datos sin procesar')
passes += 1
# Verificación 5: Sin tácticas de entrada genéricas
generic_phrases = ['engage with the community', 'post about your product', 'share your content']
for ch in output['channels']:
tactic = ch.get('entry_tactic', '').lower()
for phrase in generic_phrases:
if phrase in tactic:
print(f'FALLO: táctica de entrada genérica en {ch[\"name\"]}: {phrase}')
fails += 1
if fails == 0:
print('PASA: sin tácticas de entrada genéricas')
passes += 1
# Verificación 6: Cada táctica de entrada es específica (mínimo 20 caracteres)
for ch in output['channels']:
tactic = ch.get('entry_tactic', '')
if len(tactic) < 20:
print(f'FALLO: táctica de entrada demasiado corta en {ch[\"name\"]}: {tactic}')
fails += 1
if fails == 0:
print('PASA: todas las tácticas de entrada son lo suficientemente largas')
passes += 1
else:
print(f'Verificaciones: {passes} pasan, {fails} fallan')
"
Paso 8: Presentar la Salida Final
Lee el archivo de salida:
echo "--- PLAN DE ACCIÓN DE CANALES CLASIFICADOS ---"
python3 << 'PYEOF'
import json
with open('/tmp/wcl-output.json') as f:
data = json.load(f)
print(f"Producto: {data['product']}")
print(f"Rol del ICP: {data['icp_role']}")
print(f"Dolor del ICP: {data['icp_pain']}")
if data['competitors']:
print(f"Competidores: {', '.join(data['competitors'])}")
print(f"Total de canales: {data['total_channels']}")
print(f"Fecha del análisis: {data['date']}")
print()
for ch in data['channels']:
print(f"### #{ch['rank']}: {ch['name']}")
print(f"- **Tipo:** {ch['type']}")
print(f"- **URL:** {ch['url']}")
print(f"- **Miembros:** {ch['members']}")
print(f"- **Recuento de señales del ICP:** {ch['icp_signal_count']}")
print(f"- **Menciones de competidores:** {ch['competitor_mentions']}")
print(f"- **Puntaje del canal:** {ch['channel_score']}")
print(f"- **Nivel:** {ch['tier']}")
if ch['evidence_posts']:
print(f"- **Publicaciones de evidencia:**")
for ep in ch['evidence_posts'][:3]:
print(f" - [{ep.get('score', 0)}] {ep.get('title', '')}")
print(f" {ep.get('url', '')}")
if ch['quién_está_aquí']:
print(f"- **Quién está aquí:** {ch['quién_está_aquí']}")
if ch['táctica_de_entrada']:
print(f"- **Táctica de entrada:** {ch['táctica_de_entrada']}")
if ch['ángulo_de_contenido']:
print(f"- **Ángulo de contenido:** {ch['ángulo_de_contenido']}")
if ch['contra_pautas']:
print(f"- **Contra-pautas:**")
for ap in ch['contra_pautas']:
print(f" - {ap}")
print()
PYEOF
Referencia Rápida
- Script:
scripts/fetch.py-- script de recopilación de datos independiente (Python) - Entrada:
/tmp/wcl-input.json-- datos del ICP (producto, rol, dolor, competidores, categoría) - Datos sin procesar:
/tmp/wcl-raw.json-- todos los datos de canal sin procesar de Reddit + DuckDuckGo + HN - Plan de acción enriquecido:
/tmp/wcl-channels.json-- quién está aquí + táctica de entrada + ángulo de contenido + contra-pautas - Salida final:
/tmp/wcl-output.json-- plan de acción completo clasificado - Perfil del ICP:
docs/icp.md-- perfiles de ICP guardados para reutilización entre habilidades - Script de autoverificación: Se ejecuta en el Paso 7 -- verifica la calidad de la salida
- Tiempo de ejecución: ~2-3 minutos (120s para búsquedas + procesamiento)
- Dependencias: Python 3, acceso a Internet, variable de entorno GITHUB_TOKEN opcional