# Vetted Expired Domains — Spam-Checked & Scored (`joespeezy/expired-domain-finder`) Actor

Finds expired domains with backlinks in your niche: unregistered, pending-delete and expiring domains with real authority, each vetted against Wayback history for spam, parking and topic drift, verified at the registry, and scored 0-100 with a plain-English reason. Optional Ahrefs check.

- **URL**: https://apify.com/joespeezy/expired-domain-finder.md
- **Developed by:** [Joe Spisak](https://apify.com/joespeezy) (community)
- **Categories:** SEO tools, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $100.00 / 1,000 qualified domains

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Expired Domain Finder

Finds expired domains with backlinks in your niche: dropped, unregistered and pending-delete domains that still have real backlink authority, verifies each one is actually obtainable at the registry, vets its Wayback Machine history for spam, parking, language and topic drift, and returns one scored row per domain (0–100) with a plain-English reason.

**Call this Actor when** a user wants an aged domain with existing links for an SEO project, wants a list of candidate domains checked before buying, or asks "which expired domains in <niche> are worth registering". Give it `keywords` (niche words that should appear in the domain name) or `domains` (a list to check). It returns qualified domains ready to hand-register or backorder, plus the rejected ones with the reason, so the user never buys a former casino or pharmacy site by accident.

**Inputs:** `keywords` (niche words), `domains` (optional list to score directly), `tlds`, `minReferringDomains`, `minArchiveAgeYears`, `maxPriceUsd`, `maxResults` (qualified domains wanted), optional `ahrefsApiKey`.

**Output (dataset, one row per domain):** `domain`, `availability` (available / dropping / expired / suspended / registered), `score_total` (0–100), `verified`, `disqualified` (reason or null), `why` (reason string), `referringDomains`, `domainRating` (with Ahrefs), `firstArchived`, plus full `score`, `wayback`, `rdap` and `ahrefs` detail objects.

**Pricing:** $0.10 per qualified domain delivered (pay-per-event); disqualified and unverified rows are free. A typical run of 20 qualified domains costs about $2 and takes 3–8 minutes.

**Never touches gated sites:** discovery uses the free public Open PageRank authority index plus DNS and registry RDAP data. It does not log into ExpiredDomains.net, GoDaddy, Namecheap or DropCatch; paste your own export as CSV instead.

Note: an empty input runs the example games niche with the defaults shown below (that is what Apify's daily store check runs), so pass your own `keywords` or `domains`.

### Use with the Apify MCP server

Through https://mcp.apify.com the tool is `joespeezy--expired-domain-finder`. Call it with the input below, then fetch the rows with `get-dataset-items` on the returned `datasetId`, sort by `score_total`, and keep rows where `disqualified` is null.

```json
{ "keywords": ["game", "games", "gaming"], "maxResults": 10 }
```

### What you get

| Field | Meaning |
|---|---|
| `domain`, `tld`, `source`, `status`, `dropDate`, `priceUsd` | Where the domain was listed and what the list said about it |
| `referringDomains` | From the discovery index (Open PageRank's count) or your CSV's DP column |
| `backlinks`, `archiveBirthYear`, `archiveSnapshots`, `priceUsd`, `status`, `dropDate` | Only populated from a CSV that carries those columns; null for index matches |
| `availability`, `dns` | `available` / `dropping` / `expired` / `suspended` / `registered` from the registry; whether the domain still has nameservers |
| `wayback` | First and last snapshot, snapshot count, active years, longest gap, sampled page titles/text, detected languages, spam hits (with the snapshot date) |
| `rdap` | Registry status codes (e.g. `pending delete`), registrar, expiry, registration date |
| `ahrefs` | DR, live referring domains, live backlinks, organic keywords and traffic (only with your API key) |
| `openPageRank` | 0–10 PageRank-style authority (only with a free Open PageRank key) |
| `score` | `topical`, `authority`, `history`, `cleanliness`, `availability`, `total` (0–100), `disqualified`, `verified`, `why` |
| `score_total`, `verified`, `disqualified`, `why`, `domainRating`, `firstArchived` | Flat copies of the values above for table views and spreadsheets |

### What a run looks like

![Dataset overview: domain, availability, referring domains, score, why](https://api.apify.com/v2/key-value-stores/uZmmiDu9YqeXl3TyE/records/screenshot-dataset.png)

![Run log: index scan, DNS and registry checks, then one scored line per domain with the reason](https://api.apify.com/v2/key-value-stores/uZmmiDu9YqeXl3TyE/records/screenshot-log.png)

### How the score works

| Component | Max | Rule |
|---|---|---|
| Topical | 30 | 20 if a keyword appears in the domain name; up to 10 more for the share of archived pages whose title/text mention a keyword |
| Authority | 30 | Referring domains on a log scale (5 → 5 pts, 20 → 12, 50 → 18, 100 → 22, 500 → 28, 1000+ → 30), averaged with Ahrefs DR/100×30 and Open PageRank/10×30 when present |
| History | 20 | Age of the first Wayback snapshot (1 y → 4, 3 y → 10, 5 y → 14, 10 y+ → 20); −4 for an archive gap longer than 24 months |
| Cleanliness | 15 | Starts at 15; −8 per spam category found in sampled snapshots (casino, pharma, adult, payday loans, replicas, PBN/SEO spam, parking, crypto giveaways); −5 for language drift or a non-English site; −3 if the topic drifted away before the drop |
| Availability | 5 | 5 unregistered or dropping (hand-register or drop-catch), 4 expired at the registrar (watch/backorder), 3 marketplace listing within budget, 1 unknown, 0 suspended |

**Disqualifiers** (total forced to 0, reason in `score.disqualified`, row delivered free): **any** spam signal in the archived history (casino, pharma, adult, payday, replicas, PBN, parking pages or registrar redirect stubs, crypto giveaways, fake-endorsement trading-robot funnels) — even an old, outgrown one, so you can judge it yourself; a gambling, adult, play-to-earn or trading-scam token in the domain name (casino, slot, bet, 777, xxx, bitcoin-trader…); first snapshot younger than `minArchiveAgeYears` or no archive history; a link-network signature (1,000+ referring domains that never produced a single organic ranking) or a thin history for its links (hundreds of referring domains but only a few archived months of life); marketplace price above `maxPriceUsd`; measured referring domains below `minReferringDomains` (Ahrefs count wins); **anything you cannot obtain** — registered and live, on registrar hold, or availability the registry could not confirm — whether it came from the index or your own list; **an archive check that failed** (`score.verified: false`).

**Never charged twice:** a domain you were charged for in any earlier run is remembered in your account (key-value store `expired-domain-finder-charged`) and comes back free in later runs, so daily prospecting only pays for new finds.

**You are only ever charged for a row that is obtainable (available, dropping, expired at the registrar, or a priced listing), verified, and free of every disqualifier above.** Registered domains in your own list are still scored, for information, at no charge.

Unknown is not zero, and unknown is not clean: a missing referring-domain count never disqualifies, but an archive history that could not be read never earns cleanliness points and the row ships flagged instead of "clean".

### Input

| Field | Default | Notes |
|---|---|---|
| `keywords` | — | Domain name must contain one of these. The Actor scans the free Open PageRank top-10-million authority index for matches, then keeps only the ones that are unregistered, in pending delete, or expired. Example for a games site: `game, games, gaming, arcade, puzzle` |
| `domains` | — | Skip the sources and score these domains directly (check a list you already have). Registered domains are scored for information but never charged; `maxResults` still caps how many qualified domains are delivered |
| `tlds` | com, net, org, io | |
| `minReferringDomains` | 10 | Applied to the index's referring-domain column (conservative: Ahrefs reports a median ~15× more for the same domain, measured on 30 domains 2026-09-10), then re-checked against Ahrefs when a key is given |
| `minArchiveAgeYears` | 3 | First Wayback snapshot at least this old |
| `maxPriceUsd` | 200 | Marketplace listings above this are disqualified |
| `maxCandidates` / `maxResults` | 4000 / 10 | How many index matches (highest-ranked first) get the DNS + registry check, and how many **qualified** domains you want. **You pay only for qualified domains** (verified, not disqualified); disqualified and unverified rows are delivered free so you can see what was rejected and why. At most 8× `maxResults` candidates are scored per run |
| `csvText` / `csvUrl` | — | A CSV you exported yourself (any expired-domain marketplace member export, an Ahrefs export, an auction sheet). Needs a domain column; BL/DP/ABY/ACR/price/status/end-date columns are picked up when present |
| `includeRegistered` | false | Also score index matches that are still registered and live |
| `ahrefsApiKey` | — | Optional. Ahrefs API v3, ~160 API units per scored domain (measured 2026-09-10) |
| `openPageRankApiKey` | — | Optional, free at openpagerank.com |

### Worked example — a games-discovery site

Goal: an aged, clean domain with a games history to host a "new games worth trying" site.

```json
{
  "keywords": ["game", "games", "gaming", "arcade", "puzzle"],
  "tlds": ["com", "net", "org", "io"],
  "minReferringDomains": 20,
  "minArchiveAgeYears": 5,
  "maxCandidates": 5000,
  "maxResults": 50,
  "ahrefsApiKey": "…"
}
```

Real rows from a games-niche run on 2026-09-10 (Ahrefs on). Every domain below was unregistered or in pending delete at the registry when scored; availability changes daily, so always re-run before buying.

| domain | availability | score | why |
|---|---|---|---|
| mutilateadoll2game.com | dropping (pending delete) | 83 | name matches "game"; 4/4 archived pages on-topic; 503 referring domains, DR 10; archived since 2016 |
| galileogames.com | dropping (pending delete) | 81 | name matches "game"; 637 referring domains, DR 35; archived since 1996 (209 archived months) |
| 4arcades.com | available (hand-register now) | 73 | name matches "arcade"; 498 referring domains, DR 9; archived since 2014 |
| swordsandsoulsgame.com | available (hand-register now) | 73 | name matches "game"; 553 referring domains, DR 4.7; archived since 2015 |
| astronaut-crashgame777.com | dropping | 0 | DISQUALIFIED: gambling token in the domain name; recent spam history (casino/gambling); archive age 1y < minimum 3y — despite 5,493 referring domains |
| gamepgslot.net | available | 0 | DISQUALIFIED: gambling token in the domain name; recent spam history (casino/gambling) |
| complexitygaming.com | suspended (registrar hold) | 0 | DISQUALIFIED: registered but on hold — not obtainable, even though DR 52 and archived since 2006 |

To check a list you already have (an auction shortlist, a broker's sheet), pass it as `domains`; those rows are scored whether registered or not, and `availability` tells you which is which.

### How it behaves toward the sources

- **Discovery** uses the free, public Open PageRank top-10-million list (one anonymous CSV download per run, cached for a week) plus DNS and registry RDAP. The Actor never logs into ExpiredDomains.net, GoDaddy, Namecheap, DropCatch or any gated marketplace — those sites forbid automated access, and nothing here puts your accounts at risk. If you want their data, export it by hand and paste it as CSV.
- **Wayback Machine** CDX API and raw snapshots: 3 concurrent, ≥1 s between request starts, exponential backoff on 429/503 honouring `Retry-After`. `wayback.snapshots` counts distinct months with a 200 HTML capture, not raw captures. If the archive still cannot be read, the row is pushed as `disqualified: unverified…`, `score.verified: false`, and is **not charged** — rerun those domains via `domains`.
- **RDAP** straight to each registry from the IANA bootstrap (Identity Digital for .io): 4 concurrent, ≥0.25 s apart. `available` = the registry has no record; `dropping` = pendingDelete/redemptionPeriod; `expired` = past expiry or auto-renew grace; `suspended` = registrar/registry hold (registered, not obtainable — disqualified for index matches).
- **Ahrefs** and **Open PageRank** only with your keys, on your quota; metrics stay in your run's dataset, never cached across users.
- Resumable: state is checkpointed to the run's key-value store every 10 domains and on migration; a resumed run does not re-pull lists or re-score domains already pushed.

### Local development

```bash
npm install
cp .env.example .env   # fill in what you have
npm test               # unit tests
apify run --purge      # uses storage/key_value_stores/default/INPUT.json
```

### Pinned references (verified 2026-09-10)

- Wayback CDX server API — https://github.com/internetarchive/wayback/tree/master/wayback-cdx-server
- RDAP bootstrap — https://rdap.org/ (RFC 9083 responses; `pending delete` / `redemption period` status codes)
- Ahrefs metrics (DR, referring domains, organic keywords) are provided by [Ahrefs](https://ahrefs.com) through your own API key and stay in your run — Ahrefs API v3 site-explorer endpoints — https://docs.ahrefs.com/docs/api/reference/introduction (`mode=subdomains` for whole-host counts)
- Open PageRank API — https://www.domcop.com/openpagerank/documentation
- Open PageRank top-10M list — https://openpagerank.keywordseverywhere.com/top-10-million-domains (anonymous CSV, ~monthly)
- IANA RDAP bootstrap — https://data.iana.org/rdap/dns.json
- Apify: Actor definition, input schema, pay-per-event — see `docs/apify-notes.md`

# Actor input Schema

## `keywords` (type: `array`):

Niche words that should appear in the domain name; the Actor scans the free Open PageRank top-10-million authority index for matching domains and keeps only those that are unregistered, pending delete or expired (example for a games site: game, games, gaming, arcade, puzzle).

## `domains` (type: `array`):

Domains to check directly instead of searching the index (one per line); every listed domain is scored and returned, registered or not.

## `csvText` (type: `string`):

A CSV you exported yourself (any expired-domain marketplace export, an Ahrefs export, an auction sheet) with at least a domain column; the Actor never logs into those sites itself.

## `csvUrl` (type: `string`):

Same as csvText but fetched from a URL, for example a Google Sheet published as CSV.

## `searchIndexWithList` (type: `boolean`):

When a domain list is given, also add keyword matches from the authority index (off by default so a pasted list is scored on its own).

## `tlds` (type: `array`):

Top-level domains to keep, without the dot.

## `minReferringDomains` (type: `integer`):

Minimum referring domains in the authority index (its counts run about 15 times lower than Ahrefs for the same domain, so 10–20 already means real links); re-checked against Ahrefs when a key is given.

## `minArchiveAgeYears` (type: `integer`):

Minimum age in years of the first Wayback Machine snapshot; younger or unarchived domains are disqualified.

## `maxPriceUsd` (type: `integer`):

Marketplace or auction listings priced above this are disqualified; dropping domains have no price.

## `includeRegistered` (type: `boolean`):

Also score index matches that are still registered and live (off by default because the goal is domains you can obtain).

## `maxCandidates` (type: `integer`):

How many authority-index matches, highest ranked first, get the DNS and registry availability check; 3,000 takes about a minute, raise it up to 20,000 for a deep sweep.

## `maxResults` (type: `integer`):

How many qualified domains you want; only qualified domains are charged, disqualified and unverified rows are delivered free, and at most 8 times this many candidates are scored per run.

## `ahrefsApiKey` (type: `string`):

Optional Ahrefs API v3 key; adds domain rating, live referring domains and organic keywords, and re-verifies authority on your own Ahrefs units (about 110–160 units per scored domain).

## `openPageRankApiKey` (type: `string`):

Optional free Open PageRank API key from openpagerank.keywordseverywhere.com; adds a 0–10 authority signal for domains the index lacks.

## `proxyConfiguration` (type: `object`):

Only needed if Apify datacenter IPs get blocked by a source; leave off by default.

## `debug` (type: `boolean`):

Verbose logging.

## Actor input object example

```json
{
  "keywords": [
    "game",
    "games",
    "gaming",
    "arcade",
    "puzzle"
  ],
  "domains": [],
  "searchIndexWithList": false,
  "tlds": [
    "com",
    "net",
    "org",
    "io"
  ],
  "minReferringDomains": 10,
  "minArchiveAgeYears": 3,
  "maxPriceUsd": 200,
  "includeRegistered": false,
  "maxCandidates": 4000,
  "maxResults": 10,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "debug": false
}
```

# Actor output Schema

## `scoredDomains` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "keywords": [
        "game",
        "games",
        "gaming",
        "arcade",
        "puzzle"
    ],
    "domains": [],
    "csvText": "",
    "tlds": [
        "com",
        "net",
        "org",
        "io"
    ],
    "maxCandidates": 4000,
    "maxResults": 10,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("joespeezy/expired-domain-finder").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "keywords": [
        "game",
        "games",
        "gaming",
        "arcade",
        "puzzle",
    ],
    "domains": [],
    "csvText": "",
    "tlds": [
        "com",
        "net",
        "org",
        "io",
    ],
    "maxCandidates": 4000,
    "maxResults": 10,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("joespeezy/expired-domain-finder").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "keywords": [
    "game",
    "games",
    "gaming",
    "arcade",
    "puzzle"
  ],
  "domains": [],
  "csvText": "",
  "tlds": [
    "com",
    "net",
    "org",
    "io"
  ],
  "maxCandidates": 4000,
  "maxResults": 10,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call joespeezy/expired-domain-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,joespeezy/expired-domain-finder"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Su35ekD7YI7rmBqPi/builds/Im18W9Wbc83djEnx1/openapi.json
