mcpField
D
domain-expired-opportunity-finder

domain-expired-opportunity-finder

github.com/varnan-techマーケティング・グロース68745 回インストール2 週間前に更新
リポジトリ

domain-expired-opportunity-finder について

Expired Domain Opportunity Finderは、対象分野に照らして期限切れドメインを評価し、人が確認するための根拠付き候補リストを作るスキルです。履歴、分野との関連性、登録状況、リスクを組み合わせ、任意でLLMによる関連性評価も行います。

主な機能

  1. 01分野適合性、活動度、コンテンツ品質、履歴の評価
  2. 02Waybackのスナップショットと過去ページの確認
  3. 03RDAPによる登録状況の照会
  4. 04リスクフラグと候補フィルターの適用
  5. 05選定理由とリスクの提示

活用シーン

  1. 01特定分野に合う期限切れドメインの絞り込み
  2. 02再構築やリダイレクト用候補の比較
  3. 03人が判断する前のドメインリスク確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/varnan-tech/opendirectory --skill domain-expired-opportunity-finder

インストール後、Claude Code で/domain-expired-opportunity-finderを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

.env.example
README.md
evals/
evals.json
references/
guardrails.md
output-format.md
risk-flags.md
scoring-model.md

詳細

期限切れドメインの有望候補検索

特定のニッチにおける期限切れドメインの候補を評価します。トピックとの適合性、過去の活動レベル、履歴の健全性、リダイレクトへの適性を採点します。人間による確認のため、慎重で説明可能な候補リストを出力します。


重要なルール: すべての推奨には、肯定的な根拠(why_selected)と注意点(why_risky)の両方を必ず含めてください。説明なしにスコアだけを出力してはいけません。

慎重さを基本とするルール: シグナルが不完全または矛盾している場合は、信頼度を下げてください。曖昧な候補を有望な候補として提示してはいけません。データが欠けている場合は信頼度を下げ、決して上げてはいけません。

不正利用防止ルール: 無関係なリダイレクト、PBN の構築、または過去のトピックが対象ニッチと一致しないドメインの転用を決して推奨してはいけません。不正利用防止ポリシーの全文は references/guardrails.md を参照してください。


ステップ 1:セットアップの確認

何よりも先に、環境を確認してください。

curl と python3(または python)が利用可能か確認します。

curl --version > /dev/null 2>&1 && echo "curl: available" || echo "curl: MISSING"
python3 --version 2>/dev/null || python --version 2>/dev/null || echo "python: MISSING"

オプションの LLM API キーを確認し、ニッチとの関連性の採点を強化します。

echo "LLM_API_KEY: ${LLM_API_KEY:+set}"

curl または python が見つからない場合: 停止してください。ユーザーに次のように伝えます:"This skill requires curl and Python 3.10+. Please install them and try again."

LLM_API_KEY が設定されていない場合: 続行してください。このスキルではルールベースの採点のみを使用します(ドメイン文字列の照合、Wayback のタイトル分析、キーワードの重複)。ユーザーに次のように伝えます:"Running in rule-based-only mode. Set LLM_API_KEY for enhanced niche-relevance scoring."

LLM_API_KEY が設定されている場合: このスキルでは、トピックとの関連性の分析に LLM を活用した採点を使用します。 これにより、ニッチとの適合性をより深く評価できます。

QA:採点モード(llm-enhanced または rule-based-only)を明示し、ツールが利用可能であることを確認します。


ステップ 2:入力の収集

必須入力と任意入力をユーザーから収集します。

必須:

  • target_niche(文字列):評価対象となる中核的なニッチ。例:"開発者向けツール"、"AI SaaS"、"サイバーセキュリティ"、"フィンテック"。

任意(未指定の場合のみ質問):

  • seed_keywords(配列):トピックの照合を絞り込むキーワード。未指定の場合は、ニッチ名からキーワードを 3~5 個自動的に抽出します。
  • candidate_domains(配列):評価する特定のドメイン。未指定の場合は、ユーザーに入力を求めます。
  • discovery_source(文字列):候補の取得元 — manual、expireddomains-net、external-feed。
  • min_snapshots(整数):履歴スナップショットの最小しきい値。デフォルト:10。
  • max_risk_level(文字列):low、medium、high。リスクの高い候補をどの程度積極的に除外するかを指定します。デフォルト:medium。
  • intended_use(文字列):rebuild、redirect、either。デフォルト:either。

candidate_domains が指定されていない場合: 次のように質問します:"Please provide a list of expired domain candidates to evaluate. You can:

  1. Paste domain names (one per line or comma-separated)
  2. Provide a file path to a text file with one domain per line
  3. Say 'example' to run with a built-in demo set for the 'developer tools' niche"

ユーザーが 'example' と答えた場合: 次のデモセットを使用します。

devtoolsweekly.com
codeshipnews.io
stackforgeapp.com
quickseorank.net
bestcheaphosting247.com
cloudbuildpro.dev
reactwidgetlib.com
megadealsshop.xyz

すべての入力を収集したら、次のように確認します。 "Target niche: [niche]. Evaluating [N] candidate domains. Scoring mode: [mode]. Intended use: [use]."


ステップ 3:候補の正規化

採点の前に候補リストを整理し、検証します。

python3 -c "
import sys, re

domains = '''CANDIDATE_LIST_HERE'''.strip().split('\n')
seen = set()
valid = []
invalid = []

for d in domains:
    d = d.strip().lower()
    # Strip protocols and paths
    d = re.sub(r'^https?://', '', d)
    d = d.split('/')[0]
    d = d.strip('.')

    if not d:
        continue

    # Basic TLD validation
    if '.' not in d or len(d) < 4:
        invalid.append(d)
        continue

    # Deduplicate
    if d in seen:
        continue
    seen.add(d)
    valid.append(d)

print(f'Valid candidates: {len(valid)}')
print(f'Removed (invalid/duplicate): {len(invalid)}')
for v in valid:
    print(f'  ✓ {v}')
for i in invalid:
    print(f'  ✗ {i} (invalid format)')
"

CANDIDATE_LIST_HERE をステップ 2 の実際のドメインリストに置き換えます。

次のように伝えます:"[N] valid candidates after normalization. [M] removed (invalid/duplicate)."

有効な候補が 0 件の場合は停止し、ユーザーに次のように伝えます:"No valid domain candidates found. Please provide domain names in the format 'example.com'."


ステップ 4:シグナルの収集

有効な候補ごとに、無料の公開ソースからシグナルを収集します。 ドメインごとに、次のチェックを順番に実行します。

4a: Wayback CDX API — 履歴スナップショット

Wayback Machine に問い合わせて、過去のすべてのスナップショットを取得します。limit=100000 を使用し、CDX がドメインの全存続期間にわたるスナップショットを返すよう、from/to パラメーターは意図的に省略します。結果はタイムスタンプの昇順(古い順)で並べ替えられるため、first_capture と last_capture が正確になります。

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:
    data = json.load(sys.stdin)
    if len(data) <= 1:
        print(json.dumps({'domain': 'DOMAIN_HERE', 'snapshots': 0, 'first_capture': None, 'last_capture': None, 'status_codes': {}, 'years_active': 0}))
    else:
        rows = data[1:]  # skip header row
        timestamps = [r[0] for r in rows]  # already ascending (oldest first)
        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': 'DOMAIN_HERE',
            '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': 'DOMAIN_HERE', 'snapshots': 0, 'error': 'wayback_api_failed'}))
"

レート制限: サービスに配慮し、Wayback API の呼び出しの間は 2 秒待機してください。

4b: Wayback コンテンツのサンプリング — 過去のページタイトル

スナップショットが 0 件より多い候補について、最新のスナップショットを取得し、ページタイトルを抽出します(トピックとの関連性のスコアリングに使用します)。

curl -s -L "https://web.archive.org/web/LATEST_TIMESTAMP/http://DOMAIN_HERE" \
  | 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 title found'
# Extract meta description too
meta_match = re.search(r'<meta[^>]*name=[\"']description[\"'][^>]*content=[\"'](.*?)[\"']', html, re.IGNORECASE)
desc = meta_match.group(1).strip() if meta_match else 'no description found'
print(f'Title: {title}')
print(f'Description: {desc}')
"

LATEST_TIMESTAMP を、ステップ 4a で取得した最新のタイムスタンプに置き換えます。

4c: RDAP 検索 — 登録状況

プラットフォーム間で利用できる、HTTP ベースの RDAP 標準を使用します(OS に依存する WHOIS に代わるものです)。 RDAP の HTTP 404 は、ドメインが登録されていないことを意味します(つまり、実際に取得可能、または追跡されていません)。これはネットワーク障害とは異なるため、両方のケースを明示的に処理してください。

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:
        data = json.loads(response.read().decode())

    registrar = 'unknown'
    created = 'unknown'

    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', 'unknown')

    print(json.dumps({
        'domain': domain,
        'status': 'registered',
        'registrar': registrar,
        'created': created
    }))
except urllib.error.HTTPError as e:
    if e.code == 404:
        # Domain has no RDAP object — likely unregistered or not in RDAP coverage
        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: ドメイン文字列の分析 — キーワードの照合

ドメイン名と、対象ニッチ/シードキーワードの間で、キーワードの重複度をスコアリングします。

python3 -c "
import re, json

domain = 'DOMAIN_HERE'
niche = 'NICHE_HERE'
seeds = SEEDS_JSON_HERE  # e.g., ['devops', 'ci/cd', 'code editor']

# Extract words from domain
domain_base = domain.rsplit('.', 1)[0]  # remove TLD
domain_words = re.split(r'[-_.]', domain_base.lower())

# Check niche words
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: Gemini LLM によるニッチとの関連性評価(LLM_API_KEY が設定されている場合)

LLM API キーが設定されている場合は、収集したシグナルとともにすべての候補をまとめ、コンテキストに基づくニッチとの関連性評価を依頼します。

注: 以下のリクエスト/レスポンス形式では、Gemini API(generateContent 形式)を使用します。変更を加えずに OpenAI 形式のエンドポイントで使用することはできません。別のプロバイダーを使用する場合は、JSON ボディとレスポンスの解析方法を調整する必要があります。

cat > /tmp/domain-relevance-request.json << 'ENDJSON'
{
  "system_instruction": {
    "parts": [{
      "text": "You are an SEO research analyst. For each expired domain candidate provided, assess its topical relevance to the specified target niche. Consider the domain name, historical page title, and meta description. For each domain, output a JSON object with: domain (string), relevance_score (integer 1-10), relevance_rationale (one sentence explaining the score), redirect_plausibility (integer 1-10), redirect_rationale (one sentence). Output only a JSON array. No commentary before or after."
    }]
  },
  "contents": [{
    "parts": [{
      "text": "DOMAIN_SIGNALS_AND_NICHE_CONTEXT_HERE"
    }]
  }],
  "generationConfig": {
    "temperature": 0.2,
    "maxOutputTokens": 2048
  }
}
ENDJSON

DOMAIN_SIGNALS_AND_NICHE_CONTEXT_HERE を次の内容に置き換えます。

  • 対象ニッチとシードキーワード
  • 各候補のドメイン名、過去のタイトル、説明、キーワード照合データ

リクエストを 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)}))
"

LLM の呼び出しまたはレスポンスの解析に失敗した場合は、エラーを記録し、ルールベースのスコアリングのみで処理を続けてください。ワークフローを停止しないでください。

すべてのシグナルを収集した後、次のように報告します。 「[N] 件の候補のシグナル収集が完了しました。Wayback で [M] 件が見つかり、RDAP 検索が [K] 件成功しました。」


ステップ 5:スコアリングと分類

スコアリングの枠組みの全容については、references/scoring-model.md を参照してください。

各候補について、次の 6 つの評価軸でスコアを算出します。

  1. トピックとの関連性(0~30): ドメインのキーワード一致率、過去のタイトル/説明の分析、LLM の関連性スコア(利用可能な場合)を組み合わせます。LLM を使用しない場合:キーワード一致率 × 15 + タイトル内のキーワード重複度 × 15。LLM を使用する場合:LLM の relevance_score × 3。

  2. 過去の活動レベル(0~25): Wayback のスナップショットの多様性と頻度に基づきます。複数年にわたり一貫して取得されたスナップショットが多いほど、継続的な活動と正当性が高いと推定されます。

  3. 過去のコンテンツ品質(0~15): 過去のページタイトルとメタディスクリプションを分析し、自然な表現か、キーワードの詰め込みかを確認します。LLM を使用しない場合:基本スコアを 8/15 とし、完全一致の密度に応じて調整します。

  4. 履歴の健全性(0~15): Wayback のスナップショット数、活動年数、ステータスコードの一貫性、パーキングページを示す情報の有無に基づきます。

  5. リダイレクトの適合性(0~10): 過去のコンテンツと対象ニッチのトピックの連続性に基づきます。利用可能な場合は LLM の redirect_plausibility スコアを使用し、そうでない場合はキーワードの重複率を使用します。

  6. シグナルの完全性(0~5): 使用可能なデータを返したデータソース(Wayback、RDAP、ドメイン分析、設定されている場合は LLM)の数を数えます。

算出項目:

  • opportunity_score = すべての評価軸のスコアの合計(0~100)
  • confidence = 信頼性の高いデータがある評価軸の数に基づきます(scoring-model.md を参照)
  • recommended_action = スコア、信頼度、リスクフラグに基づきます(ステップ 6 を参照)

Step 6: リスクフラグ付けとフィルタリング

フラグの定義の全容については、references/risk-flags.md をお読みください。

各候補にリスクフラグを適用します。

チェック項目適用するフラグ
ターゲットニッチとの過去のトピックの重複率が 30% 未満topic_mismatch
有効期限切れ前のドメインの運用期間が 1 年未満short_history
Wayback のスナップショットが 3 件未満、またはすべてがパーキングページunclear_history
数年間の運用後に Wayback のスナップショットが突然途絶えているpossible_deindex
スナップショット数が min_snapshots を下回るweak_historical_activity
リダイレクト適性が 4/10 未満redirect_mismatch

推奨ロジックを適用します。

スコアとフラグ推奨
スコア ≥ 75、信頼度が high、かつ重大度が High のフラグがないhigh-priority-review
スコア ≥ 55、かつ信頼度が medium 以上review
スコア ≥ 55 だが、redirect_suitability < 4/10rebuild-only-review
スコア < 55、または重大度が High の重大なフラグがあるreject

max_risk_level フィルターを適用します。

  • max_risk_level = low の場合:重大度が Medium または High のフラグがある候補を除外します
  • max_risk_level = medium の場合:重大度が High のフラグがある候補のみを除外します
  • max_risk_level = high の場合:すべての候補を含めます(フィルターなし)

Step 7: 出力と保存

正確な JSON スキーマについては、references/output-format.md をお読みください。 必須の免責事項の文面については、references/guardrails.md をお読みください。

デフォルト:ショートリストモード。 recommended_action が high-priority-review、review、または rebuild-only-review の候補のみを表示します。

ユーザーが監査モードをリクエストした場合は、却下理由を含む詳細な評価項目の内訳とともに、すべての候補を表示します。

出力を提示します:
## Expired Domain Opportunity Finder — [YYYY-MM-DD]

**Target niche:** [niche]
**Seed keywords:** [keywords]
**Intended use:** [rebuild/redirect/either]
**Candidates evaluated:** [N]
**Shortlisted:** [M]
**Rejected:** [K]
**Scoring mode:** [llm-enhanced / rule-based-only]

---

### 1. [domain.com] — Score: [N]/100 | Confidence: [level] | Action: [recommendation]

**Topical fit:** [summary]
**Activity level:** [summary]
**Content quality:** [summary]
**History:** [summary]
**Redirect suitability:** [level]
**Risk flags:** [flags or "none"]

**Why selected:** [rationale]
**Why risky:** [rationale]

---

[repeat for each shortlisted domain, ranked by opportunity_score descending]

---

**Disclaimer:** These results are research recommendations, not guarantees
of SEO value. Redirect analysis should only be considered when strong
topic continuity exists between the expired domain and your target site.
Search engine algorithms change frequently. Always perform manual due
diligence — including checking current index status, reviewing the full
backlink profile with a commercial tool, and verifying domain history —
before making any acquisition decision. This skill does not endorse or
facilitate manipulative SEO practices.

構造化された JSON 出力を保存します:

mkdir -p docs/expired-domain-intel
OUTFILE="docs/expired-domain-intel/$(date +%Y-%m-%d).json"
cat > "$OUTFILE" << 'EOF'
JSON_OUTPUT_HERE
EOF
echo "Saved to $OUTFILE"

ショートリストの条件を満たす候補が 0 件の場合: 「現在のリスク許容度では、ニッチ「[niche]」のショートリスト条件を満たす候補はありませんでした。これは正常な結果です。評価したドメインが十分に適合していなかったことを意味します。次の方法をお試しください:

  1. 別の候補ドメインを指定する
  2. シードキーワードの範囲を広げる
  3. max_risk_level を 'high' に設定して、境界線上の候補を確認する
  4. 監査モードで実行し、候補が却下された理由を確認する」

自己 QA チェックリスト

出力を提示する前に、すべての項目を確認してください。

  • ショートリスト内の各ドメインに why_selected と why_risky の両方がある
  • ショートリスト内に、重大度が High のリスクフラグを持ち、アクションが high-priority-review のドメインがない
  • redirect_mismatch のあるドメインには、review ではなく rebuild-only-review のラベルが付いている
  • 出力の末尾にガードレールの免責事項が記載されている
  • 誇張表現がない:「guaranteed」、「easy win」、「safe to redirect」、「SEO hack」など
  • scoring_mode が LLM を使用したかどうかを正しく反映している
  • 候補が opportunity_score の降順に並んでいる
  • JSON 出力が docs/expired-domain-intel/YYYY-MM-DD.json に保存されている
  • Wayback API の呼び出しがすべてレート制限されている(呼び出し間隔は 2 秒)

違反があれば、提示する前に修正してください。


良い出力の例

  • 各ドメインにスコア、信頼度、アクション、リスク評価がある
  • 各候補に固有の具体的な要約が 1~2 文で記載されている(一般的な内容ではない)
  • リスクフラグが記載され、why_risky で説明されている
  • ショートリストは少数に絞られている(量より質)。通常、10~20 件の一括評価から 2~5 件
  • 慎重な判断:迷った場合は却下するか、信頼度を下げる
  • 各ドメインが選ばれた理由、または却下された理由をユーザーが正確に理解できる

悪い出力の例

  • 説明のないスコアのみ
  • 「このドメインの指標は良好です」のような一般的な要約(具体的な内容が必須)
  • 深刻なリスクフラグがあるドメインへの高優先度の推奨
  • トピックが一致しないドメインへのリダイレクト推奨
  • 末尾に免責事項がない
  • SEO の成果を約束する誇張表現
  • ショートリストの候補が多すぎる(このスキルは寛容ではなく、選別的であるべき)

コメント