Buscador de Oportunidades de Dominios Vencidos
Evalúa candidatos de dominios vencidos para un nicho específico. Puntúalos según adecuación temática, nivel de actividad histórica, limpieza del historial e idoneidad para redirección. Genera una lista corta conservadora y explicable para revisión humana.
Regla crítica: Cada recomendación debe incluir TANTO una justificación positiva
(why_selected) COMO una justificación de precaución (why_risky). Nunca generes una puntuación
simple sin explicación.
Regla de conservadurismo por defecto: Cuando las señales son incompletas o contradictorias, reduce el nivel de confianza. No muestres candidatos ambiguos como oportunidades fuertes. Los datos faltantes reducen la confianza, nunca la inflan.
Regla anti-abuso: Nunca fomentes redirecciones no relacionadas, construcción de PBNs o
reutilización de dominios donde el tema histórico no coincida con el nicho objetivo.
Lee references/guardrails.md para la política anti-abuso completa.
Paso 1: Verificación de Configuración
Verifica el entorno antes de hacer cualquier otra cosa.
Asegúrate de que curl y python3 (o python) estén disponibles:
curl --version > /dev/null 2>&1 && echo "curl: disponible" || echo "curl: FALTA"
python3 --version 2>/dev/null || python --version 2>/dev/null || echo "python: FALTA"
Verifica si hay una clave API de LLM opcional para puntuación mejorada de relevancia de nicho:
echo "LLM_API_KEY: ${LLM_API_KEY:+configurada}"
Si falta curl o python:
Detente. Indica al usuario: "Esta habilidad requiere curl y Python 3.10+. Por favor, instálalos e inténtalo de nuevo."
Si LLM_API_KEY no está configurada:
Continúa. La habilidad usará solo puntuación basada en reglas (coincidencia de cadenas de dominio,
análisis de títulos de Wayback, superposición de palabras clave). Notifica al usuario: "Ejecutando en modo
solo basado en reglas. Configura LLM_API_KEY para puntuación mejorada de relevancia de nicho."
Si LLM_API_KEY está configurada:
La habilidad usará puntuación mejorada con LLM para el análisis de relevancia temática.
Esto proporciona una evaluación contextual más profunda de la adecuación al nicho.
QA: Indica el modo de puntuación (mejorado con LLM o solo basado en reglas) y confirma que las herramientas están disponibles.
Paso 2: Recopilación de Entradas
Recopila las entradas requeridas y opcionales del usuario.
Requeridas:
target_niche(cadena): El nicho central a evaluar. Ejemplos: "herramientas para desarrolladores", "SaaS de IA", "ciberseguridad", "fintech".
Opcionales (preguntar solo si no se proporcionan):
seed_keywords(arreglo): Palabras clave para refinar la coincidencia temática. Si no se proporcionan, extrae automáticamente 3–5 palabras clave del nombre del nicho.candidate_domains(arreglo): Dominios específicos a evaluar. Si no se proporcionan, solicítalos al usuario.discovery_source(cadena): De dónde provienen los candidatos —manual,expireddomains-net,external-feed.min_snapshots(entero): Umbral mínimo de snapshots históricos. Valor por defecto: 10.max_risk_level(cadena):bajo,mediooalto. Controla cuán agresivamente se filtran los candidatos riesgosos. Valor por defecto:medio.intended_use(cadena):reconstruir,redirigirocualquiera. Valor por defecto:cualquiera.
Si no se proporcionan candidate_domains:
Pregunta: "Por favor, proporciona una lista de candidatos de dominios vencidos a evaluar. Puedes:
- Pegar nombres de dominio (uno por línea o separados por comas)
- Proporcionar la ruta de un archivo de texto con un dominio por línea
- Decir 'ejemplo' para ejecutar con un conjunto de demostración integrado para el nicho 'herramientas para desarrolladores'"
Si el usuario dice 'ejemplo': Usa este conjunto de demostración:
devtoolsweekly.com
codeshipnews.io
stackforgeapp.com
quickseorank.net
bestcheaphosting247.com
cloudbuildpro.dev
reactwidgetlib.com
megadealsshop.xyz
Después de recopilar todas las entradas, confirma: "Nicho objetivo: [nicho]. Evaluando [N] dominios candidatos. Modo de puntuación: [modo]. Uso previsto: [uso]."
Paso 3: Normalización de Candidatos
Limpia y valida la lista de candidatos antes de puntuar.
python3 -c "
import sys, re
domains = '''LISTA_DE_CANDIDATOS_AQUI'''.strip().split('\n')
seen = set()
valid = []
invalid = []
for d in domains:
d = d.strip().lower()
# Eliminar protocolos y rutas
d = re.sub(r'^https?://', '', d)
d = d.split('/')[0]
d = d.strip('.')
if not d:
continue
# Validación básica de TLD
if '.' not in d or len(d) < 4:
invalid.append(d)
continue
# Deduplicar
if d in seen:
continue
seen.add(d)
valid.append(d)
print(f'Candidatos válidos: {len(valid)}')
print(f'Eliminados (inválidos/duplicados): {len(invalid)}')
for v in valid:
print(f' ✓ {v}')
for i in invalid:
print(f' ✗ {i} (formato inválido)')
"
Reemplaza LISTA_DE_CANDIDATOS_AQUI con la lista real de dominios del Paso 2.
Indica: "[N] candidatos válidos después de la normalización. [M] eliminados (inválidos/duplicados)."
Si quedan 0 candidatos válidos, detente e indica al usuario: "No se encontraron candidatos de dominio válidos. Por favor, proporciona nombres de dominio en el formato 'ejemplo.com'."
Paso 4: Recopilación de Señales
Para cada candidato válido, recopila señales de fuentes públicas gratuitas. Ejecuta estas verificaciones secuencialmente por dominio.
4a: API Wayback CDX — Snapshots del Historial
Consulta la Wayback Machine para todos los snapshots históricos. Usamos limit=100000
y los parámetros from/to se omiten intencionalmente para que CDX
devuelva snapshots de toda la vida del dominio. Los resultados se ordenan
ascendentemente por marca de tiempo (más antiguos primero) por lo que first_capture y last_capture
son precisos:
curl -s "https://web.archive.org/cdx/search/cdx?url=DOMINIO_AQUI&output=json&fl=timestamp,statuscode&collapse=timestamp:6&limit=100000" \
| python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
if len(data) <= 1:
print(json.dumps({'domain': 'DOMINIO_AQUI', 'snapshots': 0, 'first_capture': None, 'last_capture': None, 'status_codes': {}, 'years_active': 0}))
else:
rows = data[1:] # saltar fila de encabezado
timestamps = [r[0] for r in rows] # ya ascendentes (más antiguos primero)
statuses = [r[1] for r in rows]
status_counts = {}
for s in statuses:
status_counts[s] = status_counts.get(s, 0) + 1
first_year = int(timestamps[0][:4])
last_year = int(timestamps[-1][:4])
print(json.dumps({
'domain': 'DOMINIO_AQUI',
'snapshots': len(rows),
'first_capture': timestamps[0],
'last_capture': timestamps[-1],
'status_codes': status_counts,
'years_active': last_year - first_year + 1
}))
except:
print(json.dumps({'domain': 'DOMINIO_AQUI', 'snapshots': 0, 'error': 'wayback_api_failed'}))
"
Límite de tasa: Espera 2 segundos entre llamadas a la API de Wayback para ser cortés con el servicio.
4b: Muestreo de Contenido de Wayback — Títulos de Páginas Históricas
Para candidatos con > 0 snapshots, obtén el snapshot más reciente para extraer el título de la página (usado para la puntuación de relevancia temática):
curl -s -L "https://web.archive.org/web/MARCA_DE_TIEMPO_MAS_RECIENTE/http://DOMINIO_AQUI" \
| python3 -c "
import sys, re
html = sys.stdin.read()[:50000]
title_match = re.search(r'<title[^>]*>(.*?)</title>', html, re.IGNORECASE | re.DOTALL)
title = title_match.group(1).strip() if title_match else 'no se encontró título'
# Extraer meta description también
meta_match = re.search(r'<meta[^>]*name=[\"']description[\"'][^>]*content=[\"'](.*?)[\"']', html, re.IGNORECASE)
desc = meta_match.group(1).strip() if meta_match else 'no se encontró descripción'
print(f'Título: {title}')
print(f'Descripción: {desc}')
"
Reemplaza MARCA_DE_TIEMPO_MAS_RECIENTE con la marca de tiempo más reciente del Paso 4a.
4c: Consulta RDAP — Estado de Registro
Usa el estándar RDAP basado en HTTP multiplataforma (reemplaza WHOIS dependiente del SO). Un HTTP 404 de RDAP significa que el dominio no está registrado (es decir, está genuinamente disponible o no rastreado) — eso es distinto de un fallo de red. Maneja ambos casos explícitamente:
python3 -c "
import urllib.request, urllib.error, json
domain = 'DOMINIO_AQUI'
try:
req = urllib.request.Request(
f'https://rdap.org/domain/{domain}',
headers={'User-Agent': 'Mozilla/5.0'}
)
with urllib.request.urlopen(req, timeout=10) as response:
data = json.loads(response.read().decode())
registrar = 'desconocido'
created = 'desconocido'
for entity in data.get('entities', []):
if 'registrar' in entity.get('roles', []):
try:
registrar = entity.get('vcardArray', [[]])[1][0][3]
except Exception:
pass
for event in data.get('events', []):
if event.get('eventAction') == 'registration':
created = event.get('eventDate', 'desconocido')
print(json.dumps({
'domain': domain,
'status': 'registered',
'registrar': registrar,
'created': created
}))
except urllib.error.HTTPError as e:
if e.code == 404:
# El dominio no tiene objeto RDAP — probablemente no registrado o no cubierto por RDAP
print(json.dumps({'domain': domain, 'status': 'unregistered_or_no_rdap_object'}))
else:
print(json.dumps({'domain': domain, 'error': f'rdap_http_error_{e.code}'}))
except Exception:
print(json.dumps({'domain': domain, 'error': 'rdap_lookup_failed'}))
"
4d: Análisis de Cadena de Dominio — Coincidencia de Palabras Clave
Puntúa la superposición de palabras clave entre el nombre del dominio y el nicho objetivo / palabras clave semilla:
python3 -c "
import re, json
domain = 'DOMINIO_AQUI'
niche = 'NICHO_AQUI'
seeds = SEMILLAS_JSON_AQUI # ej., ['devops', 'ci/cd', 'editor de código']
# Extraer palabras del dominio
domain_base = domain.rsplit('.', 1)[0] # eliminar TLD
domain_words = re.split(r'[-_.]', domain_base.lower())
# Verificar palabras del nicho
niche_words = niche.lower().split()
all_keywords = set(niche_words + [s.lower() for s in seeds])
matches = [w for w in domain_words if any(kw in w or w in kw for kw in all_keywords)]
match_ratio = len(matches) / max(len(domain_words), 1)
print(json.dumps({
'domain': domain,
'domain_words': domain_words,
'keyword_matches': matches,
'match_ratio': round(match_ratio, 2)
}))
"
4e: Evaluación de Relevancia de Nicho con LLM Gemini (si LLM_API_KEY está configurada)
Si la clave API de LLM está configurada, procesa por lotes todos los candidatos con sus señales recopiladas y solicita una evaluación contextual de relevancia de nicho.
Nota: El formato de solicitud/respuesta a continuación usa la API de Gemini (formato generateContent).
No es compatible con endpoints estilo OpenAI sin modificación.
Si usas un proveedor diferente, debes adaptar el cuerpo JSON y el análisis de la respuesta.
cat > /tmp/domain-relevance-request.json << 'ENDJSON'
{
"system_instruction": {
"parts": [{
"text": "Eres un analista de investigación SEO. Para cada candidato de dominio vencido proporcionado, evalúa su relevancia temática para el nicho objetivo especificado. Considera el nombre del dominio, el título de la página histórica y la meta descripción. Para cada dominio, genera un objeto JSON con: domain (cadena), relevance_score (entero 1-10), relevance_rationale (una oración explicando la puntuación), redirect_plausibility (entero 1-10), redirect_rationale (una oración). Genera solo un arreglo JSON. Sin comentarios antes o después."
}]
},
"contents": [{
"parts": [{
"text": "SEÑALES_DEL_DOMINIO_Y_CONTEXTO_DEL_NICHO_AQUI"
}]
}],
"generationConfig": {
"temperature": 0.2,
"maxOutputTokens": 2048
}
}
ENDJSON
Reemplaza SEÑALES_DEL_DOMINIO_Y_CONTEXTO_DEL_NICHO_AQUI con:
- El nicho objetivo y las palabras clave semilla
- Para cada candidato: nombre del dominio, título histórico, descripción, datos de coincidencia de palabras clave
Envía la solicitud a la API de Gemini:
curl -s -X POST \
"${LLM_API_ENDPOINT:-https://generativelanguage.googleapis.com/v1beta}/models/${LLM_MODEL:-gemini-2.0-flash}:generateContent?key=$LLM_API_KEY" \
-H "Content-Type: application/json" \
-d @/tmp/domain-relevance-request.json \
| python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
text = d['candidates'][0]['content']['parts'][0]['text']
print(text)
except (KeyError, IndexError, json.JSONDecodeError) as e:
print(json.dumps({'error': 'llm_response_parse_failed', 'detail': str(e)}))
"
Si la llamada al LLM o el análisis de la respuesta falla, registra el error y continúa con la puntuación solo basada en reglas. No detengas el flujo de trabajo.
Después de toda la recopilación de señales, indica: "Recopilación de señales completa para [N] candidatos. [M] aciertos de Wayback, [K] consultas RDAP exitosas."
Paso 5: Puntuación y Clasificación
Lee references/scoring-model.md para el marco de puntuación completo.
Para cada candidato, calcula puntuaciones en las 6 dimensiones:
-
Relevancia Temática (0–30): Combina la relación de coincidencia de palabras clave del dominio, el análisis de título/descripción histórica y la puntuación de relevancia del LLM (si está disponible). Sin LLM: usa relación de coincidencia de palabras clave × 15 + superposición de palabras clave del título × 15. Con LLM: usa relevance_score del LLM × 3.
-
Nivel de Actividad Histórica (0–25): Basado en la diversidad y frecuencia de snapshots de Wayback. Más snapshots capturados consistentemente a lo largo de múltiples años indica mayor actividad sostenida y legitimidad inferida.
-
Calidad del Contenido Histórico (0–15): Derivado del análisis del título de la página histórica y la meta descripción, verificando frases naturales versus relleno de palabras clave. Sin LLM: puntuación base de 8/15 ajustada por densidad de coincidencias exactas.
-
Limpieza del Historial (0–15): Basado en la cantidad de snapshots de Wayback, años activos, consistencia del código de estado y ausencia de indicadores de página de estacionamiento.
-
Idoneidad para Redirección (0–10): Basado en la continuidad temática entre el contenido histórico y el nicho objetivo. Usa la puntuación de redirect_plausibility del LLM si está disponible; de lo contrario, usa la relación de superposición de palabras clave.
-
Completitud de Señales (0–5): Cuenta cuántas fuentes de datos devolvieron datos utilizables (Wayback, RDAP, análisis de dominio, LLM si está configurado).
Calcula:
opportunity_score= suma de todas las puntuaciones de dimensión (0–100)confidence= basado en cuántas dimensiones tienen datos sólidos (ver scoring-model.md)recommended_action= basado en puntuación + confianza + banderas de riesgo (ver Paso 6)
Paso 6: Marcado de Riesgo y Filtrado
Lee references/risk-flags.md para las definiciones completas de banderas.
Aplica banderas de riesgo a cada candidato:
| Verificación | Banda Aplicada |
|---|---|
| Superposición de tema histórico < 30% con el nicho | topic_mismatch |
| Menos de 5 snapshots en Wayback | low_history_activity |
| Estado del dominio: registrado (no vencido/no disponible) | still_registered |
| Alta proporción de errores HTTP 4xx/5xx en el historial | http_error_history |
| Nombre del dominio sugiere exact match de baja calidad (ej., compra-barato-ahora.com) | low_quality_domain_pattern |
| Contenido histórico parece genérico o de estacionamiento | parked_or_generic_content |
| Fecha de creación del RDAP es muy reciente | recent_registration |
| Indicaciones de que el dominio se usó para spam o malware | abuse_history_suspected |
| Puntuación de relevancia del LLM baja | `low_llm_relevance |
| Sin datos históricos en absoluto | no_historical_data |
Paso 7: Salida de la Lista Corta
Genera una tabla de salida que contenga estos campos para cada candidato:
| Dominio | Puntuación de Oportunidad | Confianza | Acción Recomendada | Banderas de Riesgo | Por qué fue seleccionado | Por qué es riesgoso |
|---|---|---|---|---|---|---|
| ejemplo.com | 78 | media | considerar_para_reconstruir | topic_mismatch | Fuerte coincidencia de palabras clave en el nombre del dominio; título histórico relevante para el nicho de herramientas para desarrolladores. | El historial de contenido muestra solo un 40% de superposición con el nicho; posible redirección solo parcialmente relevante. |
Campos de Acción Recomendada:
fuerte_oportunidad— puntuación alta, baja riesgo, procederconsiderar_para_reconstruir— puntuación media-alta, riesgo medio, adecuado para reconstrucción del sitioconsiderar_para_redirigir— puntuación media, riesgo bajo-medio, adecuado para redirección 301riesgoso_descartar— puntuación baja o alto riesgo, no recomendadodatos_insuficientes— muy poca información para decidir
Explicación de Confianza:
alta: Señales fuertes de múltiples fuentes (Wayback, RDAP, LLM)media: Datos moderados de 1-2 fuentesbaja: Datos mínimos o contradictorios
Después de la tabla de salida, proporciona un breve resumen narrativo destacando los mejores candidatos y riesgos notables.
Siempre termina con: "Esta lista corta es para revisión humana. Verifica cualquier hallazgo confidente antes de tomar decisiones de inversión."
Referencias (solo lectura)
Todos los archivos de referencia se encuentran en ./references/ en el repositorio raíz:
scoring-model.md— marco de puntuación completorisk-flags.md— definiciones de todas las banderas de riesgoguardrails.md— política anti-abuso y pautas de uso éticoplaybook-scenarios.md— escenarios de ejemplo con salidas esperadas
Lee estos antes de ejecutar la puntuación final para una comprensión completa de las reglas.
Modo de Depuración
Si se establece DEBUG=true, imprime todas las señales crudas para cada candidato y los puntajes
intermedios de dimensión antes de la salida final de la lista corta. Usa este modo solo
para solucionar problemas o comprender puntuaciones inesperadas:
DEBUG=true ./run-opendirectory --niche "herramientas para desarrolladores" --example