# No-Website Leads (Verified) (`adam261987/no-website-leads-verified`) Actor

Local business leads with no live website, a social-only presence, a dead/parked domain, or a DIY-builder site -- every candidate is actually fetched and checked, not just filtered on an empty Google Maps field.

- **URL**: https://apify.com/adam261987/no-website-leads-verified.md
- **Developed by:** [Adam Aly](https://apify.com/adam261987) (community)
- **Categories:** Lead generation, Automation, Agents
- **Stats:** 5 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 result delivereds

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

## No-Website Leads (Verified)

Finds local businesses with **no live website, a social-only presence, a dead/parked
domain, or a DIY-builder site** -- ready-to-pitch leads for web design agencies, SEO
consultants, and local lead-gen teams.

### What makes this different from other "no website" scrapers

Every competing Actor we researched on Apify Store determines "no website" purely by
checking whether the Google Maps listing's website field is empty. That produces a lot
of false positives (a business can have a perfectly good site that just isn't linked on
Maps) and misses the very common case of a business whose only real web presence is a
Facebook page.

This Actor instead:

1. **Actually fetches and checks every candidate website** (`src/classify.js`) --
   classifying it as healthy, outdated/DIY-builder, a dead link, a parked/for-sale
   domain, a directory profile, or genuinely absent -- instead of trusting an empty
   Maps field.
2. **Defaults to "healthy" on any ambiguous case.** A false "you have no website" pitch
   to a business that has a working site is the fastest way to burn a buyer's trust.
3. **Cross-sources Facebook** (`src/facebook.js`) for businesses whose Maps listing has
   no website field but who do have a linked Facebook Page -- a lead type every
   competitor we found misses entirely.
4. **Attempts email recovery for site-less leads** (`src/emailRecovery.js`), the #1
   complaint about this whole category of tool ("there are no websites to scrape for
   emails").
5. **Deduplicates across runs**, not just within one run (`src/dedupe.js`), so a
   customer running the same search next month doesn't get billed for leads they
   already have.
6. **Bills pay-per-event**, not pay-per-result -- a small charge for every business
   checked, a larger charge only for confirmed qualified leads. See "Pricing" below.

### Project structure

```
.actor/
  actor.json          Actor manifest
  input_schema.json    Defines the input form shown in Apify Console
src/
  main.js             Entry point / orchestration
  classify.js         Live website verification (the core differentiator)
  facebook.js         Facebook cross-sourcing (best-effort)
  emailRecovery.js     Email recovery for site-less leads
  scoring.js          0-100 lead scoring
  dedupe.js           Cross-run deduplication via a named key-value store
test/
  run-unit-tests.js   Unit tests for the network-independent logic
Dockerfile
package.json
```

### How it works, end to end

1. You give it a `category` (e.g. "plumber") and one or more `locations` (e.g.
   `["Austin, TX"]`).
2. It calls a base Google Maps scraper Actor (default: `compass/crawler-google-places`,
   a well-established public Actor) to get the raw list of businesses. **You need your
   own Apify credits to cover that call** -- this Actor doesn't reimplement Maps
   scraping itself, it adds a verification and enrichment layer on top of an existing
   scraper, which is both faster to build correctly and easier to keep working if
   Google changes Maps' page structure.
3. For each business, it classifies the website status, scores the lead, attempts
   Facebook cross-sourcing and email recovery, and skips anything already delivered to
   this same customer in a prior run.
4. Qualified leads are written to the Actor's dataset with a lead score and a
   plain-English reason for the classification.

### Setting this up for real (steps you need to do -- I can't do these from here)

1. **Create a free Apify account** at apify.com if you don't have one (no card
   required, $5/month in free platform credit).
2. **Install the Apify CLI** locally: `npm install -g apify-cli`
3. **From this project folder**, run:
   ```
   apify login
   apify push
   ```
   This uploads and builds the Actor under your account.
4. **Open it in Apify Console** and go to the Publication tab to fill in the Store
   listing (title, description, category tags) -- see "Launch checklist" below.
5. **Set up monetization** (Publication tab -> Monetization -> Pay per event):
   - Add an event named exactly `business-checked` -- suggested price **$0.001**
   - Add an event named exactly `qualified-lead-delivered` -- suggested price
     **$0.012-$0.015**
   - These event names must match the strings used in `src/main.js`
     (`Actor.charge({ eventName: ... })`) exactly, or billing won't fire.
   - Note: significant pricing changes take a 14-day notice period on Apify before
     they apply, so set your initial pricing carefully.
6. **Enable a free trial** (a handful of free qualified leads) in the Monetization
   settings -- with zero reviews at launch, letting people see real output before
   paying is your main trust signal.
7. **Submit for publish.** Apify reviews new Actors before they appear in Store search.

### Known issue found and fixed during real testing (read this)

The first real test run against Google Maps data for "plumber" in Austin, TX
flagged 4 leads out of 20 checked. Manual verification found **3 of those 4
actually have real, working websites** (aaa-auger.com, rooterman.com,
radiantplumbing.com) -- only 1 (a business whose only linked presence was a
Facebook page) was a genuinely correct lead. That's a 75% false-positive
rate on that batch, traced to this cause:

Small business websites are increasingly protected by bot-detection services
(Cloudflare and similar) that block automated requests from cloud servers
like Apify's, even though the site works completely normally for a real
visitor in a browser. The original code treated any failed automated fetch
as `UNREACHABLE` (a qualifying lead), which is wrong -- a bot-block and a
truly dead site look similar to a simple fetch, but they are not the same
thing.

**The fix**, already applied in this version:

- Added a new `COULD_NOT_VERIFY` status, used when a fetch fails with a
  status code (403/429/503) or page content commonly associated with bot
  detection/challenge pages, rather than a real dead-site signal.
- `COULD_NOT_VERIFY` is hard-blocked from ever counting as a qualified lead
  in `scoring.js` itself (not just via the input schema), so it can't be
  miscounted even by a direct API call that bypasses the Console UI.
- Each classification attempt now retries once with a different simulated
  browser before giving up, since a single failed request against a real
  live site is common and often transient.
- `UNREACHABLE` was removed from the **default** qualifying statuses --
  it's still available as an opt-in, but given what this test found, treat
  it as a weaker, higher-false-positive signal than the others until you've
  validated it further.

**This does not mean the false-positive problem is now fully solved** --
it means the most identifiable cause found in one real test batch was
addressed. Re-run the same manual verification process (see below) against
the new build before trusting it with real customers, and expect to keep
tightening this over time as new failure patterns turn up.

### Testing before you rely on it

I validated everything I could without live access to Google Maps or arbitrary
business websites (this sandbox's network is restricted to a fixed allowlist). What
was verified:

- All unit tests pass (`npm test`) -- covers every classification branch that doesn't
  require reaching an arbitrary external site (empty website, Facebook/Instagram/Yelp
  short-circuit detection), plus all of the scoring logic.
- The live HTTP fetch pipeline itself (request, redirect handling, timeout/error
  handling, HTML parsing) was verified end-to-end against real reachable URLs and
  correctly produces `HEALTHY` for a working page and `UNREACHABLE` for a
  non-existent domain.

What was **not** and **could not** be verified from here:

- Real classification accuracy against actual Google Maps data and real business
  websites -- you should run a small test batch (10-20 businesses in your own city)
  right after deploying and manually check a sample of the classifications before
  opening it up to customers.
- The Facebook cross-sourcing module's real-world hit rate. Facebook aggressively
  rate-limits and blocks non-browser traffic; this module is deliberately
  best-effort and fails closed. Expect a real miss rate, and expect to revisit it
  as Facebook's page structure changes.
- The `compass/crawler-google-places` base actor's current exact output field names.
  I've defensively mapped a couple of common variants in `main.js`, but confirm the
  actual field names in a real run and adjust the mapping if needed.
- Parked-domain and DIY-builder detection patterns will need periodic maintenance as
  hosting providers and parking-page templates change over time -- this is a known,
  expected maintenance task, not a one-time build.

### Pricing rationale

Existing competitors charge $1.50-$5.00 per **1,000 raw results**, regardless of how
many are actually qualified -- and since typically only 1-5% of businesses lack a
website, buyers end up effectively paying far more than the sticker price per usable
lead. Charging per **verified qualified lead** instead means the price you show is the
price they actually pay per usable result, which is both more transparent and (per
the research) a real, provable gap versus every incumbent found.

### Legal / compliance notes (not legal advice)

- Scraping public, logged-out Google Maps listing data has supportive precedent for
  the "not a computer-crime" question (*hiQ v. LinkedIn*; *Meta Platforms v. Bright
  Data*), but it still violates Google's own Maps Platform Terms of Service. The
  practical risk is IP blocking/rate-limiting, not criminal liability -- but don't
  represent this Actor as fully ToS-compliant with Google.
- The real legal exposure sits downstream, with the buyer's outreach: CAN-SPAM and
  TCPA penalties are real and significant. Consider adding a short, visible
  disclaimer in your Store listing about buyer responsibility for compliant outreach.
- This Actor does not send any messages itself -- it only produces a data list. Keep
  it that way for v1; sending messages is a materially different compliance
  situation.

### Explicitly out of scope for this version

- No cold-email or SMS sending
- No CRM integration
- No AI-generated outreach message drafting
- No guaranteed Facebook detection or email discovery -- both are best-effort by
  design, documented above

### Extending this later

- Swap the search-guessing approach in `facebook.js` for a more thorough Facebook
  Page lookup once you've validated demand for it.
- Add a vertical-specific mode (e.g. contractor license lookups) as a separate
  higher-priced pay-per-event tier rather than folding it into the base price.
- Add a CRM/Zapier/Make output integration once you have paying customers asking
  for it.

# Actor input Schema

## `category` (type: `string`):

What kind of business to search for, e.g. "plumber", "hair salon", "roofing contractor".

## `locations` (type: `array`):

One or more city/region strings to search in, e.g. \["Austin, TX", "Round Rock, TX"]. Splitting a city into a few neighborhoods usually finds more results than searching the whole city once, because Google Maps caps each search at roughly 120 results.

## `maxPlacesPerLocation` (type: `integer`):

Upper bound on how many businesses to fetch from the base Maps search, per location, before filtering. This is what drives your Apify platform-usage cost, so keep it modest while testing.

## `baseMapsActorId` (type: `string`):

The Apify Actor used to pull the raw business listings from Google Maps. The default is a well-established public scraper. You need your own Apify account credits to run it -- this Actor calls it on your behalf.

## `verifyLiveSites` (type: `boolean`):

If ON (recommended), every business that has a website field on Maps gets that URL fetched and classified as healthy, outdated, a dead link, a parked domain, a directory listing, or a social-only page. If OFF, only the Maps "no website listed" field is used, which produces far more false positives.

## `attemptEmailRecovery` (type: `boolean`):

Best-effort only. For businesses with no live website, tries to pull a contact email from whatever public page is available (their social page, or fields already present in the Maps listing). This will not find an email for every lead -- many simply don't have one publicly listed anywhere.

## `qualifyingStatuses` (type: `array`):

Only businesses classified into one of these statuses are counted as qualified leads (and billed at the qualified-lead rate). HEALTHY and COULD\_NOT\_VERIFY businesses are never included, regardless of this setting. UNREACHABLE is off by default: manual testing found that failed automated checks are often a live site blocking bots, not an actually-dead site, so it produced false positives -- only enable it if you've validated that tradeoff for your use case.

## `dedupeKey` (type: `string`):

Any stable string identifying you as the customer, e.g. your email or a project name. When set, this run will skip businesses already returned to you in a previous run with the same key, and update its memory for next time. Leave blank to disable cross-run deduplication.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for a candidate website to respond before classifying it as UNREACHABLE.

## Actor input object example

```json
{
  "category": "plumber",
  "locations": [
    "Austin, TX"
  ],
  "maxPlacesPerLocation": 120,
  "baseMapsActorId": "compass/crawler-google-places",
  "verifyLiveSites": true,
  "attemptEmailRecovery": true,
  "qualifyingStatuses": [
    "NO_WEBSITE",
    "SOCIAL_ONLY",
    "PARKED_DOMAIN",
    "DIRECTORY_ONLY"
  ],
  "requestTimeoutSecs": 10
}
```

# Actor output Schema

## `results` (type: `string`):

The default dataset containing every qualified lead found during the run, with business details, contact info, verification status, and a 0-100 lead score.

# 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 = {
    "category": "plumber",
    "locations": [
        "Austin, TX"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("adam261987/no-website-leads-verified").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 = {
    "category": "plumber",
    "locations": ["Austin, TX"],
}

# Run the Actor and wait for it to finish
run = client.actor("adam261987/no-website-leads-verified").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 '{
  "category": "plumber",
  "locations": [
    "Austin, TX"
  ]
}' |
apify call adam261987/no-website-leads-verified --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,adam261987/no-website-leads-verified"
        }
    }
}

```

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/WdDJFXNA4Nogbht1R/builds/ivmjO3euNGena5g1W/openapi.json
