---
name: find-businesses-without-a-website
description: Build a source-backed website-agency prospect queue for a local category and city, separating no owned website found from unresolved identity and search failures.
---

# Find local businesses without a website

Produce a CSV of local businesses where no owned website was found in the
checked Maps and Search evidence, with the business phone listed on Maps.
This is a qualification queue, not proof that a website does not exist.

Two endpoints do the work. Google Maps search lists the businesses. Google
Search checks for associated sites. Each request is one call at $0.15 per
1,000 calls.

Before making API requests, ask the user: "Which city or area should I search?"
Skip this question only if the user has already supplied the location. Do not
infer it from the examples below. Ask whether they want a particular business
category; otherwise search for local businesses in their chosen location.
A listed phone is required for this task unless the user explicitly changes that
requirement. Also establish an attempt cap and any ownership or category exclusions.
The Austin food-truck requests below are examples, not defaults; substitute the
user's location and chosen category in every Maps and Search query.
Independent ownership is unknown unless evidence supports it; a small-looking
listing alone is not proof. Keep unknown ownership separate if independence is
a hard requirement.

## 1. Get an API key

1. Use `LITESCRAPE_API_KEY` if it is set.
2. Otherwise send the person to https://ecll-litescrape-v3--site-api-web.modal.run/#demo for a key and have
   them set it in their environment, not paste it into an output file.
3. Send the key only to HTTPS `api.litescrape.com` as `Authorization: Bearer <key>`.
4. Check the balance before you start:

```bash
curl -s https://ecll-litescrape-v3--site-api-web.modal.run/api/keys/status \
  -H "Authorization: Bearer $LITESCRAPE_API_KEY"
```

`remaining_calls` must cover one call per Maps page plus one call per candidate
in step 4. Three pages of 20 places with a third lacking a website is about 23
calls.

## 2. List the businesses from Google Maps

One call per page of 20 places. Page with `start` in steps of 20.

```bash
curl -s "https://ecll-litescrape-v3--site-api-web.modal.run/api/google/maps" \
  -H "Authorization: Bearer $LITESCRAPE_API_KEY" \
  --data-urlencode "q=food trucks in Austin, TX" \
  --data-urlencode "type=search" \
  --data-urlencode "start=0" -G
```

```python
from litescrape_sdk import GoogleMaps, scrape  # pip install litescrape-sdk

pages = scrape(
    [GoogleMaps(q="food trucks in Austin, TX", type="search", start=s) for s in (0, 20, 40)],
    api_key=API_KEY,
)
places = [place for page in pages for place in page.raise_for_error().get("local_results", [])]
```

- Each item in `local_results` carries `title`, `phone`, `address`, `rating`,
  `reviews`, `type`, `place_id` and `website`.
- Keep one row per `place_id`. Maps repeats a few places across pages; retain
  conflicting source values with their timestamps instead of hiding them.
- Build the Maps link as `https://www.google.com/maps/place/?q=place_id:<place_id>`.
- Stop after the page you were asked for, after three pages if no limit was
  given, or when a page returns fewer than 20 rows.

## 3. Check the website field

A nonempty `website` is a supplied website candidate, not proof of ownership.
Recognize these common third-party hosts as social, directory or ordering pages:

```
facebook.com instagram.com linktr.ee tiktok.com x.com twitter.com youtube.com
yelp.com tripadvisor.com yellowpages.com mapquest.com bbb.org nextdoor.com
foursquare.com manta.com chamberofcommerce.com angi.com homeadvisor.com
thumbtack.com houzz.com porch.com ubereats.com doordash.com grubhub.com
favordelivery.com toasttab.com opentable.com singleplatform.com allmenus.com
menupix.com restaurantji.com eater.com wanderlog.com google.com wheree.com
restaurants-us.com roaminghunger.com streetfoodfinder.com bestfoodtrucks.com
```

- Match the host exactly or as a subdomain: `m.facebook.com` counts as
  `facebook.com`.
- Do not match substrings: `discadatx.com` is a real website, not `x.com`.
- Exclude supported first-party website listings from the no-website queue.
  An unrecognized host is not automatically first-party: use the returned name,
  geography and website context. Route ambiguous websites to identity review.
- Keep missing/social/directory-only website rows as candidates. Apply the
  requested category, location and phone requirements before spending Search
  calls. Food courts, aggregators, relocation/closure conflicts and ownership
  unknowns must not silently become qualified independent food-truck leads.
- Order eligible candidates deterministically by phone availability, then
  original Maps page/rank and place ID; preserve exclusions and unchecked rows.
- This step spends no calls.

## 4. Check each candidate with Google Search

One call per candidate. Put the business name in quotes and add the city.
`fast_mode=true` returns organic results only.

```bash
curl -s "https://ecll-litescrape-v3--site-api-web.modal.run/api/google/search" \
  -H "Authorization: Bearer $LITESCRAPE_API_KEY" \
  --data-urlencode 'q="Santo Patio Taco Trailer" Austin, TX' \
  --data-urlencode "fast_mode=true" \
  --data-urlencode "gl=us" -G
```

```python
from litescrape_sdk import GoogleSearch, scrape

checks = scrape(
    [GoogleSearch(q=f'"{p["title"]}" Austin, TX', fast_mode=True, gl="us") for p in candidates],
    api_key=API_KEY,
)
```

- Read the first ten `organic_results`.
- Match business identity from title/snippet, geography and available contact
  evidence. A name substring in a domain or subdomain is neither necessary nor
  sufficient. Generic names can have genuine exact-name domains; a menu
  directory can put the entire business name in its subdomain. Record first-party,
  third-party or ambiguous source classification with the evidence URL.
- Use `owned_site_found` when the evidence supports a matching first-party site;
  exclude that business. Use `website_not_found_in_checked_sources` only after a
  successful nonempty search contains matching-business evidence but no
  supported first-party site. That status is still a prospect for qualification.
- Empty, unrelated-only or failed searches are `unverified`, never no-website
  evidence. If the cap permits, make at most one fallback query using the
  normalized business name without quotes plus city or listed phone. Save both
  queries; never replace the first response. If unresolved, keep it in review.
- Conflicting cities, addresses, operating status or business identities go to
  `qualification-review.csv`, with the conflict and next check. Do not export
  such rows as ready prospects. Do not claim an independently owned business
  unless the available evidence supports that separate requirement.
- If independent ownership is required and remains the only missing requirement,
  use at most three remaining attempts on the highest-fit candidates: search the
  matched business name, city and the specific ownership question. First-party
  social/about snippets can support an explicitly labelled self-report; retain
  the exact claim, link, date and matching identity evidence. A family-business
  statement supports `self_reported_family_business`, not verified independence
  or non-franchise status. Attribute the ownership clause to the matched business,
  not another business in the snippet; preserve conflicting date labels. Keep
  those requirements separate. Third-party descriptions, customer
  guesses or absence of a chain label cannot certify ownership. A self-report is
  not independent legal verification; if that stricter standard is required,
  retain the row for review. These checks use the same agreed attempt budget.
- Do not open the businesses' own websites or any other page.

## 5. Write the CSV

Columns, one row per provisional prospect with no qualifying site observed:

```
name,phone,address,rating,reviews,category,maps_link,website_status,ownership_status,evidence_urls,note
```

- Keep no-phone rows in the review queue when phone is required; otherwise
  retain them with `contact_status=no_phone` and report the phone-bearing count.
- Write `qualification-review.csv` for conflicts, unknown required ownership,
  unverified searches and unchecked candidates, and `search-evidence.csv` for
  query/result links and classifications. Escape spreadsheet formula cells.
- Write `manifest.json` with raw/unique places, exclusions, successful checks,
  provisional prospects, review/unchecked counts, attempt cap, attempts,
  successful calls, estimated cost and source IDs. Reused evidence keeps its
  original timestamp and request ID. Never call the total a verified count of
  businesses with no website.

## Cost, limits and stop conditions

- Count all attempts, including retries, against the agreed cap. Stop immediately
  on 401/403/402. Retry only a 429 or transient 5xx once within cap, honoring
  `Retry-After`; other errors and timeouts remain unresolved observations.
- Read `concurrency_limit` from key status and stay within it.
- Stop and report when `remaining_calls` reaches zero (HTTP 402
  `payment_required`), when a page returns fewer than 20 rows, or when you
  reach the requested number of pages. For "all" of a category, stop after
  five pages.
- Ask before spending more than 200 calls.
- Show the CSV path, the totals and the call count.

Endpoint reference: https://ecll-litescrape-v3--site-api-web.modal.run/docs. Billing rules:
https://ecll-litescrape-v3--site-api-web.modal.run/pricing.
