Abgelaufene-Domain-Chancen-Finder
Bewertet abgelaufene Domain-Kandidaten für eine bestimmte Nische. Bewertet sie nach thematischer Passung, historischem Aktivitätsniveau, Historiensauberkeit und Redirect-Eignung. Gibt eine konservative, erklärbare Shortlist zur menschlichen Prüfung aus.
Wichtige Regel: Jede Empfehlung MUSS sowohl eine positive Begründung (why_selected) ALS AUCH eine Vorsichtsbegründung (why_risky) enthalten. Gib niemals eine bloße Punktzahl ohne Erklärung aus.
Standardmäßig konservative Regel: Wenn Signale unvollständig oder widersprüchlich sind, senke das Konfidenzniveau. Präsentiere keine mehrdeutigen Kandidaten als starke Gelegenheiten. Fehlende Daten reduzieren die Konfidenz, niemals erhöhen sie sie.
Anti-Missbrauchs-Regel: Ermutige niemals zu themenfremden Weiterleitungen, PBN-Konstruktion oder Domain-Umnutzung, bei denen das historische Thema nicht zur Ziel-Nische passt. Lies references/guardrails.md für die vollständige Anti-Missbrauchs-Richtlinie.
Schritt 1: Setup-Prüfung
Überprüfe die Umgebung, bevor du irgendetwas anderes tust.
Stelle sicher, dass curl und python3 (oder python) verfügbar sind:
curl --version > /dev/null 2>&1 && echo "curl: verfügbar" || echo "curl: FEHLT"
python3 --version 2>/dev/null || python --version 2>/dev/null || echo "python: FEHLT"
Prüfe auf einen optionalen LLM-API-Schlüssel für verbesserte Nischen-Relevanzbewertung:
echo "LLM_API_KEY: ${LLM_API_KEY:+gesetzt}"
Wenn curl oder python fehlt:
Stoppe. Teile dem Benutzer mit: "Diese Fähigkeit erfordert curl und Python 3.10+. Bitte installiere sie und versuche es erneut."
Wenn LLM_API_KEY nicht gesetzt ist:
Fahre fort. Die Fähigkeit verwendet nur regelbasierte Bewertung (Domain-String-Matching, Wayback-Titelanalyse, Keyword-Überlappung). Hinweis an den Benutzer: "Läuft im rein regelbasierten Modus. Setze LLM_API_KEY für verbesserte Nischen-Relevanzbewertung."
Wenn LLM_API_KEY gesetzt ist:
Die Fähigkeit verwendet LLM-verbesserte Bewertung für thematische Relevanzanalyse. Dies bietet eine tiefere kontextuelle Beurteilung der Nischenpassung.
QA: Gib den Bewertungsmodus (llm-verbessert oder rein regelbasiert) an und bestätige, dass die Tools verfügbar sind.
Schritt 2: Eingabesammlung
Sammle die erforderlichen und optionalen Eingaben vom Benutzer.
Erforderlich:
target_niche(Zeichenkette): Die Kern-Nische, gegen die bewertet werden soll. Beispiele: "Entwickler-Tools", "AI SaaS", "Cybersicherheit", "Fintech".
Optional (nur fragen, wenn nicht bereitgestellt):
seed_keywords(Array): Schlüsselwörter zur Verfeinerung des thematischen Matchings. Wenn nicht bereitgestellt, extrahiere automatisch 3–5 Schlüsselwörter aus dem Nischennamen.candidate_domains(Array): Spezifische Domains, die bewertet werden sollen. Wenn nicht bereitgestellt, fordere den Benutzer auf.discovery_source(Zeichenkette): Woher die Kandidaten stammen —manual,expireddomains-net,external-feed.min_snapshots(Ganzzahl): Mindestschwellenwert für historische Snapshots. Standard: 10.max_risk_level(Zeichenkette):low,mediumoderhigh. Steuert, wie aggressiv riskante Kandidaten gefiltert werden. Standard:medium.intended_use(Zeichenkette):rebuild,redirectodereither. Standard:either.
Wenn keine candidate_domains bereitgestellt werden:
Frage: "Bitte gib eine Liste der zu bewertenden abgelaufenen Domain-Kandidaten an. Du kannst:
- Domainnamen einfügen (eine pro Zeile oder kommagetrennt)
- Einen Dateipfad zu einer Textdatei mit einer Domain pro Zeile angeben
- 'example' sagen, um mit einem integrierten Demo-Set für die Nische 'Entwickler-Tools' zu laufen"
Wenn der Benutzer 'example' sagt: Verwende dieses Demo-Set:
devtoolsweekly.com
codeshipnews.io
stackforgeapp.com
quickseorank.net
bestcheaphosting247.com
cloudbuildpro.dev
reactwidgetlib.com
megadealsshop.xyz
Nach dem Sammeln aller Eingaben bestätige: "Ziel-Nische: [Nische]. Bewertung von [N] Kandidaten-Domains. Bewertungsmodus: [Modus]. Verwendungszweck: [Zweck]."
Schritt 3: Kandidatennormalisierung
Bereinige und validiere die Kandidatenliste vor der Bewertung.
python3 -c "
import sys, re
domains = '''CANDIDATE_LIST_HERE'''.strip().split('\n')
gesehen = set()
gueltige = []
ungueltige = []
for d in domains:
d = d.strip().lower()
# Entferne Protokolle und Pfade
d = re.sub(r'^https?://', '', d)
d = d.split('/')[0]
d = d.strip('.')
if not d:
continue
# Grundlegende TLD-Validierung
if '.' not in d or len(d) < 4:
ungueltige.append(d)
continue
# Deduplizierung
if d in gesehen:
continue
gesehen.add(d)
gueltige.append(d)
print(f'Gültige Kandidaten: {len(gueltige)}')
print(f'Entfernt (ungültig/dupliziert): {len(ungueltige)}')
for v in gueltige:
print(f' ✓ {v}')
for i in ungueltige:
print(f' ✗ {i} (ungültiges Format)')
"
Ersetze CANDIDATE_LIST_HERE durch die tatsächliche Domain-Liste aus Schritt 2.
Gib an: "[N] gültige Kandidaten nach Normalisierung. [M] entfernt (ungültig/dupliziert)."
Wenn 0 gültige Kandidaten übrig sind, stoppe und teile dem Benutzer mit: "Keine gültigen Domain-Kandidaten gefunden. Bitte gib Domainnamen im Format 'example.com' an."
Schritt 4: Signalsammlung
Sammle für jeden gültigen Kandidaten Signale aus kostenlosen öffentlichen Quellen. Führe diese Prüfungen nacheinander pro Domain durch.
4a: Wayback CDX-API — Verlauf-Snapshots
Durchsuche die Wayback Machine nach allen historischen Snapshots. Wir verwenden limit=100000
und from/to-Parameter werden absichtlich weggelassen, damit CDX Snapshots
aus der gesamten Lebensdauer der Domain zurückgibt. Die Ergebnisse werden aufsteigend nach
Zeitstempel sortiert (älteste zuerst), sodass first_capture und last_capture
genau sind:
curl -s "https://web.archive.org/cdx/search/cdx?url=DOMAIN_HERE&output=json&fl=timestamp,statuscode&collapse=timestamp:6&limit=100000" \
| python3 -c "
import sys, json
try:
daten = json.load(sys.stdin)
if len(daten) <= 1:
print(json.dumps({'domain': 'DOMAIN_HERE', 'snapshots': 0, 'first_capture': None, 'last_capture': None, 'status_codes': {}, 'years_active': 0}))
else:
zeilen = daten[1:] # Kopfzeile überspringen
zeitstempel = [r[0] for r in zeilen] # bereits aufsteigend (älteste zuerst)
statuses = [r[1] for r in zeilen]
status_zaehler = {}
for s in statuses:
status_zaehler[s] = status_zaehler.get(s, 0) + 1
erstes_jahr = int(zeitstempel[0][:4])
letztes_jahr = int(zeitstempel[-1][:4])
print(json.dumps({
'domain': 'DOMAIN_HERE',
'snapshots': len(zeilen),
'first_capture': zeitstempel[0],
'last_capture': zeitstempel[-1],
'status_codes': status_zaehler,
'years_active': letztes_jahr - erstes_jahr + 1
}))
except:
print(json.dumps({'domain': 'DOMAIN_HERE', 'snapshots': 0, 'error': 'wayback_api_failed'}))
"
Ratenbegrenzung: Warte 2 Sekunden zwischen Wayback-API-Aufrufen, um den Dienst nicht zu überlasten.
4b: Wayback-Inhaltsabtastung — Historische Seitentitel
Für Kandidaten mit > 0 Snapshots, rufe den neuesten Snapshot ab, um den Seitentitel zu extrahieren (wird für die thematische Relevanzbewertung verwendet):
curl -s -L "https://web.archive.org/web/LATEST_TIMESTAMP/http://DOMAIN_HERE" \
| python3 -c "
import sys, re
html = sys.stdin.read()[:50000]
titel_match = re.search(r'<title[^>]*>(.*?)</title>', html, re.IGNORECASE | re.DOTALL)
titel = titel_match.group(1).strip() if titel_match else 'kein Titel gefunden'
# Extrahiere auch Meta-Beschreibung
meta_match = re.search(r'<meta[^>]*name=[\"']description[\"'][^>]*content=[\"'](.*?)[\"']', html, re.IGNORECASE)
beschreibung = meta_match.group(1).strip() if meta_match else 'keine Beschreibung gefunden'
print(f'Titel: {titel}')
print(f'Beschreibung: {beschreibung}')
"
Ersetze LATEST_TIMESTAMP durch den neuesten Zeitstempel aus Schritt 4a.
4c: RDAP-Lookup — Registrierungsstatus
Verwende den plattformunabhängigen HTTP-basierten RDAP-Standard (ersetzt betriebssystemabhängiges WHOIS). Ein HTTP-404 von RDAP bedeutet, dass die Domain nicht registriert ist (d. h. sie ist wirklich verfügbar oder nicht verfolgt) — das ist ein Unterschied zu einem Netzwerkfehler. Behandle beide Fälle explizit:
python3 -c "
import urllib.request, urllib.error, json
domain = 'DOMAIN_HERE'
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:
daten = json.loads(response.read().decode())
registrar = 'unbekannt'
erstellt = 'unbekannt'
for entity in daten.get('entities', []):
if 'registrar' in entity.get('roles', []):
try:
registrar = entity.get('vcardArray', [[]])[1][0][3]
except Exception:
pass
for event in daten.get('events', []):
if event.get('eventAction') == 'registration':
erstellt = event.get('eventDate', 'unbekannt')
print(json.dumps({
'domain': domain,
'status': 'registriert',
'registrar': registrar,
'erstellt': erstellt
}))
except urllib.error.HTTPError as e:
if e.code == 404:
# Domain hat kein RDAP-Objekt — wahrscheinlich nicht registriert oder nicht in der RDAP-Abdeckung
print(json.dumps({'domain': domain, 'status': 'nicht_registriert_oder_kein_rdap_objekt'}))
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: Domain-String-Analyse — Keyword-Matching
Bewerte die Keyword-Überlappung zwischen dem Domainnamen und der Ziel-Niche / den Seed-Keywords:
python3 -c "
import re, json
domain = 'DOMAIN_HERE'
niche = 'NICHE_HERE'
seeds = SEEDS_JSON_HERE # z. B. ['devops', 'ci/cd', 'code editor']
# Extrahiere Wörter aus der Domain
domain_basis = domain.rsplit('.', 1)[0] # entferne TLD
domain_woerter = re.split(r'[-_.]', domain_basis.lower())
# Prüfe Nischenwörter
niche_woerter = niche.lower().split()
alle_keywords = set(niche_woerter + [s.lower() for s in seeds])
treffer = [w for w in domain_woerter if any(kw in w or w in kw for kw in alle_keywords)]
treffer_quote = len(treffer) / max(len(domain_woerter), 1)
print(json.dumps({
'domain': domain,
'domain_woerter': domain_woerter,
'keyword_treffer': treffer,
'treffer_quote': round(treffer_quote, 2)
}))
"
4e: Gemini LLM Nischen-Relevanzbewertung (wenn LLM_API_KEY gesetzt ist)
Wenn der LLM-API-Schlüssel konfiguriert ist, fasse alle Kandidaten mit ihren gesammelten Signalen zusammen und frage nach einer kontextuellen Nischen-Relevanzbewertung.
Hinweis: Das Anfrage-/Antwortformat unten verwendet die Gemini-API (generateContent
Format). Es ist nicht ohne Modifikation mit OpenAI-ähnlichen Endpunkten kompatibel.
Wenn du einen anderen Anbieter verwendest, musst du den JSON-Textkörper und die Antwortanalyse anpassen.
cat > /tmp/domain-relevance-request.json << 'ENDJSON'
{
"system_instruction": {
"parts": [{
"text": "Du bist ein SEO-Forschungsanalyst. Für jeden bereitgestellten abgelaufenen Domain-Kandidaten bewertest du seine thematische Relevanz für die angegebene Ziel-Nische. Berücksichtige den Domainnamen, den historischen Seitentitel und die Meta-Beschreibung. Gib für jede Domain ein JSON-Objekt aus mit: domain (Zeichenkette), relevance_score (Ganzzahl 1-10), relevance_rationale (ein Satz, der die Punktzahl erklärt), redirect_plausibility (Ganzzahl 1-10), redirect_rationale (ein Satz). Gib nur ein JSON-Array aus. Kein Kommentar davor oder danach."
}]
},
"contents": [{
"parts": [{
"text": "DOMAIN_SIGNALS_AND_NICHE_CONTEXT_HERE"
}]
}],
"generationConfig": {
"temperature": 0.2,
"maxOutputTokens": 2048
}
}
ENDJSON
Ersetze DOMAIN_SIGNALS_AND_NICHE_CONTEXT_HERE durch:
- Die Ziel-Nische und die Seed-Keywords
- Für jeden Kandidaten: Domainname, historischer Titel, Beschreibung, Keyword-Matching-Daten
Sende die Anfrage an die Gemini-API:
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)}))
"
Wenn der LLM-Aufruf oder die Antwortanalyse fehlschlägt, protokolliere den Fehler und fahre fort mit rein regelbasierter Bewertung. Stoppe den Workflow nicht.
Gib nach Abschluss der Signalsammlung an: "Signalsammlung abgeschlossen für [N] Kandidaten. [M] Wayback-Treffer, [K] RDAP-Lookups erfolgreich."
Schritt 5: Bewertung & Klassifizierung
Lies references/scoring-model.md für das vollständige Bewertungsframework.
Berechne für jeden Kandidaten Punktzahlen in den 6 Dimensionen:
-
Thematische Relevanz (0–30): Kombiniere Domain-Keyword-Matching-Quote, historische Titel-/Beschreibungsanalyse und LLM-Relevanzbewertung (falls verfügbar). Ohne LLM: Keyword-Matching-Quote × 15 + Titel-Keyword-Überlappung × 15. Mit LLM: LLM relevance_score × 3.
-
Historisches Aktivitätsniveau (0–25): Basierend auf der Vielfalt und Häufigkeit der Wayback-Snapshots. Mehr Snapshots, die konsistent über mehrere Jahre erfasst wurden, deuten auf höhere anhaltende Aktivität und abgeleitete Legitimität hin.
-
Historische Inhaltsqualität (0–15): Abgeleitet aus der Analyse des historischen Seitentitels und der Meta-Beschreibung, wobei auf natürliche Formulierung im Vergleich zu Keyword-Stuffing geprüft wird. Ohne LLM: Basispunktzahl von 8/15, angepasst an die Dichte exakter Übereinstimmungen.
-
Historiensauberkeit (0–15): Basierend auf der Anzahl der Wayback-Snapshots, aktiven Jahren, Konsistenz der Statuscodes und dem Fehlen von Parkseiten-Indikatoren.
-
Redirect-Eignung (0–10): Basierend auf der thematischen Kontinuität zwischen historischen Inhalten und der Ziel-Nische. Verwende den LLM redirect_plausibility-Wert, wenn verfügbar; ansonsten die Keyword-Überlappungsquote.
-
Signalvollständigkeit (0–5): Zähle, wie viele Datenquellen nutzbare Daten zurückgegeben haben (Wayback, RDAP, Domain-Analyse, LLM falls konfiguriert).
Berechne:
opportunity_score= Summe aller Dimensionspunktzahlen (0–100)confidence= basierend darauf, wie viele Dimensionen starke Daten haben (siehe scoring-model.md)recommended_action= basierend auf Punktzahl + Konfidenz + Risikohinweise (siehe Schritt 6)
Schritt 6: Risikomarkierung & Filterung
Lies references/risk-flags.md für die vollständigen Flag-Definitionen.
Wende Risikohinweise auf jeden Kandidaten an:
| Prüfung | Gesetzter Hinweis |
|---|---|
| Historische Themenüberschneidung < 30% mit |
… (truncated for translation)