{
  "name": "Find valuable expired domains with DomainKits and Moz",
  "nodes": [
    {
      "id": "d0000000-0000-0000-0000-000000000001",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -700,
        -260
      ],
      "parameters": {
        "width": 540,
        "height": 1180,
        "content": "## Find valuable expired domains with DomainKits and Moz\n\n### How it works\n\nOnce a day this workflow finds expired domains: names moving through the deletion lifecycle (expired, redemption, pending delete) that match your keyword. These names surface here before they reach drop lists and auctions, which is the point: you see them while there is still time to research and backorder.\n\nThe candidates then go to the Moz Links API in one batch call. Each domain comes back with Domain Authority, Page Authority, Spam Score and linking root domains. The report lists each candidate with its expiry stage and SEO metrics, sorted by Domain Authority. The workflow reports what the data says and leaves the judgement to you.\n\n### Setup steps\n\n- Set your keyword in the Set Target node. The downstream nodes derive from it.\n- Add your DomainKits API credentials to the Find Expired Domains node.\n- Create a free Moz account, generate an Access ID and Secret Key under API Access, and add them as a Basic Auth credential on the Get Moz Metrics node.\n- Configure the Send Report node with your SMTP credentials and recipients.\n\n### Customization\n\nSwap the Send Report node for Google Sheets to build a rolling candidate pool instead of a daily email. Tighten the filters in Find Expired Domains (age, length, character set) to raise the bar before Moz spends a row on a name. Swap the Get Moz Metrics node for your Ahrefs or Semrush endpoint if that is where your subscription lives; only the URL, the auth and the field names in Build Report change.\n\n### Data note\n\nResults contain domain names, registration dates, expiry stages and SEO metrics only. No registrant personal data is included (no names, emails, addresses or phone numbers), so the output is GDPR compliant and safe to forward or store."
      },
      "typeVersion": 1
    },
    {
      "id": "d0000000-0000-0000-0000-000000000002",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -40,
        -260
      ],
      "parameters": {
        "color": 7,
        "width": 760,
        "height": 560,
        "content": "## Collect\n\nRuns daily and searches expired domains across the deletion lifecycle that contain the keyword. Age and character filters keep the list to names with real history."
      },
      "typeVersion": 1
    },
    {
      "id": "d0000000-0000-0000-0000-000000000003",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        780,
        -260
      ],
      "parameters": {
        "color": 7,
        "width": 520,
        "height": 560,
        "content": "## Score with Moz\n\nSends the candidate list to the Moz Links API in one call and reads Domain Authority, Page Authority, Spam Score and linking root domains for each name."
      },
      "typeVersion": 1
    },
    {
      "id": "d0000000-0000-0000-0000-000000000004",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1360,
        -260
      ],
      "parameters": {
        "color": 7,
        "width": 1000,
        "height": 560,
        "content": "## Report and deliver\n\nMerges expiry data with SEO metrics, sorts by Domain Authority, converts the records to a CSV attachment and emails both."
      },
      "typeVersion": 1
    },
    {
      "id": "d1000000-0000-0000-0000-000000000001",
      "name": "Every Day at 2pm",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        0,
        80
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "triggerAtHour": 14
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "d1000000-0000-0000-0000-000000000002",
      "name": "Set Target",
      "type": "n8n-nodes-base.set",
      "position": [
        240,
        80
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "d2+1234567890-+1234567890",
              "name": "keyword",
              "type": "string",
              "value": "travel"
            },
            {
              "id": "d2+1234567890-+1234567890",
              "name": "domain_limit",
              "type": "number",
              "value": 50
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "d1000000-0000-0000-0000-000000000003",
      "name": "Find Expired Domains",
      "type": "n8n-nodes-domainkits.domainKits",
      "position": [
        480,
        80
      ],
      "parameters": {
        "limit": "={{ $json.domain_limit }}",
        "filters": {
          "age_range": "10-20,20+",
          "no_hyphen": true,
          "no_number": true
        },
        "keyword": "={{ $json.keyword }}",
        "resource": "expired",
        "operation": "search",
        "searchMode": "keyword"
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "d1000000-0000-0000-0000-000000000004",
      "name": "Prepare Batch",
      "type": "n8n-nodes-base.code",
      "position": [
        820,
        80
      ],
      "parameters": {
        "jsCode": "const candidates = $input.all()\n  .map(i => i.json)\n  .filter(d => d.domain);\n\n// One item carrying the whole batch: the Moz url_metrics endpoint accepts up\n// to 50 targets per call, so the entire list costs one API request.\nreturn [{\n  json: {\n    domains: candidates.map(d => d.domain).slice(0, 50),\n    candidates,\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "d1000000-0000-0000-0000-000000000005",
      "name": "Get Moz Metrics",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "maxTries": 3,
      "position": [
        1060,
        80
      ],
      "parameters": {
        "url": "https://lsapi.seomoz.com/v2/url_metrics",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ targets: $json.domains }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth"
      },
      "retryOnFail": true,
      "typeVersion": 4.2,
      "alwaysOutputData": true,
      "waitBetweenTries": 2000
    },
    {
      "id": "d1000000-0000-0000-0000-000000000006",
      "name": "Build Report",
      "type": "n8n-nodes-base.code",
      "position": [
        1400,
        80
      ],
      "parameters": {
        "jsCode": "const candidates = $('Prepare Batch').first().json.candidates;\n\n// Moz echoes each target back in the result's page field. Keying on it keeps\n// every metric attached to its own domain even when some targets fail.\nconst mozByDomain = new Map();\nconst mozResults = ($json.results || []).filter(r => r && r.page);\nfor (const r of mozResults) {\n  mozByDomain.set(r.page.replace(/\\/$/, '').toLowerCase(), r);\n}\n\nconst records = candidates.map(c => {\n  const m = mozByDomain.get(c.domain.toLowerCase()) || {};\n  return {\n    domain: c.domain,\n    registered_date: c.registered_date || '',\n    age: c.age ?? '',\n    expiry_stage: c.status || '',\n    domain_authority: m.domain_authority ?? '',\n    page_authority: m.page_authority ?? '',\n    spam_score: (m.spam_score === -1 ? '' : m.spam_score) ?? '',\n    linking_root_domains: m.root_domains_to_root_domain ?? '',\n  };\n});\n\nrecords.sort((a, b) => (Number(b.domain_authority) || 0) - (Number(a.domain_authority) || 0) || a.domain.localeCompare(b.domain));\n\nconst keyword = $('Set Target').first().json.keyword;\nconst scored = records.filter(r => r.domain_authority !== '').length;\n\nconst lines = records.map(r => {\n  const parts = [\n    r.domain,\n    `  registered ${r.registered_date || 'not reported'}, stage: ${r.expiry_stage || 'not reported'}`,\n    `  DA ${r.domain_authority === '' ? 'n/a' : r.domain_authority}, PA ${r.page_authority === '' ? 'n/a' : r.page_authority}, spam ${r.spam_score === '' ? 'n/a' : r.spam_score}, linking root domains ${r.linking_root_domains === '' ? 'n/a' : r.linking_root_domains}`,\n  ];\n  return parts.join('\\n');\n});\n\nconst body = [\n  `Expired domain candidates for \"${keyword}\"`,\n  'Expired domains across the deletion lifecycle (expired, redemption, pending delete), scored with Moz.',\n  '',\n  `${records.length} candidates, ${scored} scored, sorted by Domain Authority. Empty metrics mean Moz returned nothing for that name.`,\n  '',\n  records.length ? lines.join('\\n\\n') : 'No expired domains matched the keyword today.',\n  '',\n  'Records are attached as CSV.',\n].join('\\n');\n\nreturn [{\n  json: {\n    subject: `${keyword}: ${records.length} expired domain candidates`,\n    message: body,\n    records,\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "d1000000-0000-0000-0000-000000000007",
      "name": "Split Records",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        1640,
        80
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "records"
      },
      "typeVersion": 1
    },
    {
      "id": "d1000000-0000-0000-0000-000000000008",
      "name": "Records To CSV",
      "type": "n8n-nodes-base.convertToFile",
      "position": [
        1880,
        80
      ],
      "parameters": {
        "options": {},
        "operation": "csv"
      },
      "typeVersion": 1.1
    },
    {
      "id": "d1000000-0000-0000-0000-000000000009",
      "name": "Send Report",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        2120,
        80
      ],
      "parameters": {
        "text": "={{ $('Build Report').first().json.message }}",
        "options": {
          "attachments": "data"
        },
        "subject": "={{ $('Build Report').first().json.subject }}",
        "toEmail": "you@example.com",
        "fromEmail": "user@example.com",
        "emailFormat": "text"
      },
      "typeVersion": 2.1
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Set Target": {
      "main": [
        [
          {
            "node": "Find Expired Domains",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Report": {
      "main": [
        [
          {
            "node": "Split Records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Batch": {
      "main": [
        [
          {
            "node": "Get Moz Metrics",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Records": {
      "main": [
        [
          {
            "node": "Records To CSV",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Records To CSV": {
      "main": [
        [
          {
            "node": "Send Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Moz Metrics": {
      "main": [
        [
          {
            "node": "Build Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every Day at 2pm": {
      "main": [
        [
          {
            "node": "Set Target",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Expired Domains": {
      "main": [
        [
          {
            "node": "Prepare Batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}