# Local Market Saturation & Competitor Density Analyzer (`shelvick/local-market-saturation`) Actor

Analyze how saturated a local market is. Give a business category and a location; get an aggregated Google Maps competitive landscape: competitor count, rating/review/price distributions, incumbent strength, and a 0-1 saturation score. Aggregate stats, not a lead list.

- **URL**: https://apify.com/shelvick/local-market-saturation.md
- **Developed by:** [Scott Helvick](https://apify.com/shelvick) (community)
- **Categories:** Business, Developer tools, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $127.50 / 1,000 market analyzeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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

## Local Market Saturation & Competitor Density Analyzer

How crowded is a local market? Give a business category and a location and get back an aggregated competitive-landscape read from Google Maps local listings — how many competitors there are, how good and how established they are, what they charge, and a single 0-1 saturation score — instead of a raw dump of map pins you have to analyze yourself.

### What this does

- **Category + location in, a saturation report out** — pass markets like `{"category": "coffee shops", "location": "Austin, TX"}`; get one aggregated report per market. Built for the question "how saturated is this market / where is it underserved" when sizing up competition or deciding where to open.
- **Competitor count + density** — how many businesses match the category in that location (deduplicated across result pages), with a flag when the sample is capped.
- **Rating distribution** — mean, median, and a bucketed spread of star ratings, so you can see whether incumbents are strong or mediocre.
- **Review-volume distribution** — total and median review counts plus a bucketed spread, a proxy for how established and entrenched the field is.
- **Price-tier mix** — how the market splits across `$`–`$$$$`.
- **Incumbent strength** — the fraction of competitors rated ≥ 4.5, the fraction with ≥ 1,000 reviews, and a count of strong incumbents (well-rated *and* review-deep).
- **A transparent saturation score (0-1)** — `low` / `moderate` / `high`, with its component signals (density, incumbent entrenchment, review depth) and the exact formula returned alongside it. Not a black box.
- **Aggregate-only by design** — the report is statistics plus a short most-reviewed-incumbents sample (name, rating, review count). It is **not** a per-business lead list: no addresses, phone numbers, or review text.

Use cases:

- An AI agent assessing "should my user open a bakery in this neighborhood" or "which of these five metros is least saturated for HVAC."
- Market research / site selection: compare competitor density and incumbent strength across candidate locations.
- Competitive landscape sizing for a category in a city without manually counting pins on a map.
- Feeding a downstream model clean, aggregated market structure instead of tens of thousands of tokens of raw listings.

### Why an aggregate-first design

The raw data — local business listings — is already widely scraped. The problem this solves isn't *access* to pins; it's that a list of 60 businesses with ratings and review counts is not, by itself, an answer to "how saturated is this market." Someone still has to aggregate it: bucket the ratings, sum the reviews, judge how entrenched the incumbents are, and turn that into a comparable read across locations.

This Actor does that aggregation deterministically and returns the *answer shape* — distributions, fractions, a saturation score — not the raw inputs. The saturation score is a transparent, documented composite (weighted density + incumbent entrenchment + review depth) with its components exposed, so you can audit how the number was produced and re-weight it yourself if you disagree. It is explicitly a Maps-derived heuristic, not a population-based index — there's no census join here.

### How it compares

| Approach | Output | Aggregated | Saturation score | Per-business PII | Built for agents |
|---|---|---|---|---|---|
| Raw Maps/places scraper | list of pins | No — you aggregate | No | often yes | partial |
| Manual counting on a map | a number in your head | by hand | No | n/a | No |
| Enterprise location-intelligence platform | dashboards + forecasts | Yes | proprietary | varies | No (seat-based SaaS) |
| **This Actor** | aggregate report per market | Yes | Yes, transparent | No (aggregate-only) | Yes |

A raw scraper hands you the soup; an enterprise platform is a seat-licensed dashboard. This is the middle: a cheap, callable, aggregate market read with a transparent score.

### Input

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `markets` | array | yes | — | Markets to analyze. Each item is `{"category": "...", "location": "..."}`: category is the business type to count (e.g. `coffee shops`, `plumbers`, `dentists`); location is a city/area (e.g. `Austin, TX`). 1–20 per run; each becomes one report. |
| `maxPlaces` | integer | — | `60` | How many local results to aggregate per market (paginated ~20/page). Higher = more representative distributions but slower. 10–80. |
| `language` | string | — | `en` | Google interface language code (`hl`). |
| `country` | string | — | `us` | Two-letter Google country code (`gl`), e.g. `us`, `gb`, `ca`. |

### Output

One dataset record per market.

```json
{
  "market": "coffee shops in Austin, TX",
  "category": "coffee shops",
  "location": "Austin, TX",
  "status": "completed",
  "competitorCount": 58,
  "resultsCapped": false,
  "rating": { "mean": 4.42, "median": 4.5, "distribution": { "<3.0": 1, "3.0-3.9": 6, "4.0-4.4": 19, "4.5-5.0": 32 } },
  "reviews": { "total": 184320, "median": 410, "distribution": { "<10": 4, "10-99": 14, "100-999": 22, "1000+": 18 } },
  "priceMix": { "$": 7, "$$": 28, "$$$": 11, "$$$$": 2, "unknown": 10 },
  "incumbentStrength": { "fractionRatingGe45": 0.552, "fractionReviewsGe1000": 0.31, "strongIncumbents": 16 },
  "topCompetitors": [
    { "name": "Mozart's Coffee Roasters", "rating": 4.5, "reviewCount": 11000 },
    { "name": "Cosmic Pickle", "rating": 4.7, "reviewCount": 4300 }
  ],
  "saturation": { "score": 0.74, "band": "high", "components": { "density": 1.0, "entrenchment": 0.276, "reviewDepth": 0.41 }, "formula": "0.5*density + 0.3*entrenchment + 0.2*reviewDepth (densityRef=50)" },
  "notice": "Aggregated from public Google Maps local business listings at request time; ... not advice.",
  "error": null
}
```

| Field | Type | Description |
|---|---|---|
| `market` / `category` / `location` | string | The analyzed market and its echoed inputs. |
| `status` | string | `completed` (report produced, charged) or `failed` (no usable results; see `error`; not charged). |
| `competitorCount` | integer | null | Distinct businesses aggregated; a lower bound when `resultsCapped` is true. |
| `resultsCapped` | boolean | null | True if the `maxPlaces` cap was hit (more competitors likely exist). |
| `rating` / `reviews` | object | null | Mean/median + bucketed distributions for star ratings and review counts. |
| `priceMix` | object | null | Count of competitors per price tier (`$`–`$$$$`, plus `unknown`). |
| `incumbentStrength` | object | null | Fractions rated ≥ 4.5 and with ≥ 1,000 reviews, plus the strong-incumbent count. |
| `topCompetitors` | array | Up to 5 most-reviewed incumbents (name, rating, review count only). |
| `saturation` | object | null | 0-1 score, band, component signals, and the formula. |
| `notice` | string | Standing data-source + disclaimer note on every record. |
| `error` | string | null | Reason when `status` is `failed`; `null` on success. |

### Example

```json
{ "markets": [{ "category": "coffee shops", "location": "Austin, TX" }, { "category": "coffee shops", "location": "Portland, OR" }], "maxPlaces": 60 }
```

```bash
curl -X POST "https://api.apify.com/v2/acts/shelvick~local-market-saturation/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"markets":[{"category":"coffee shops","location":"Austin, TX"}]}'
```

### Calling from an AI agent

#### Apify MCP server

The Actor is a callable tool on `mcp.apify.com`. The input schema is self-documenting — an LLM can build a correct call from the field names and descriptions alone. Pay per call via x402 USDC on Base or Skyfire managed tokens.

#### Apify SDK (Python)

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_TOKEN")
run = client.actor("shelvick/local-market-saturation").call(
    run_input={"markets": [{"category": "coffee shops", "location": "Austin, TX"}]}
)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["market"], item["competitorCount"], item["saturation"]["score"], item["saturation"]["band"])
```

#### REST API

```
POST https://api.apify.com/v2/acts/shelvick~local-market-saturation/run-sync-get-dataset-items?token=YOUR_TOKEN
```

### Pricing

Pay-per-event, billed only on success: one charge per market (category × location) successfully analyzed, after the report is pushed — never for a market that returns no usable results. Multi-market runs are billed per completed market; cap a whole run with `maxTotalChargeUsd`. The **Pricing** tab on this Store page is authoritative for the current per-market rate and any subscriber discounts.

### Behavior

**Run-level failures (rare):** invalid input fails the run before any work — empty `markets`, more than 20, a blank `category`/`location`, or `maxPlaces` out of range (10–80). Nothing is charged.

**Per-market outcomes:**

- `completed` — an aggregated report was produced (charged). Check `competitorCount`, `resultsCapped`, and the `saturation` components for confidence.
- `failed` — `no-results` (the location/category returned nothing usable) or a fetch error. Never charged.

**Performance:** each market paginates the local listings (~20 results/page) up to `maxPlaces`; markets are processed concurrently. A handful of markets complete in well under a minute; large batches stay within the run timeout.

**Telemetry:** to improve coverage and reliability, this Actor reports anonymous usage metrics and diagnostic events to the developer - run outcome counts, sample-size distributions, and failure classes. Customer market, category, and location text is never collected. No account identifiers are collected, and telemetry never affects a run.

### FAQ

**Is the saturation score a real index?**
It's a transparent heuristic, not a population-based saturation index. The score combines competitor density, incumbent entrenchment, and review depth with documented weights, and every component is returned so you can audit or re-weight it. There's no census/population join.

**How many competitors does it actually see?**
Up to `maxPlaces` (default 60, max 80), drawn from the top-ranked local results — a representative sample of the competitive set, not a guaranteed census of every business. `resultsCapped` tells you when there are likely more.

**Does it return business contact details?**
No. The output is aggregate statistics plus a small most-reviewed-incumbents sample (name, rating, reviews). No addresses, phones, or review text — it's a market-research tool, not a lead list.

**Can I compare several locations at once?**
Yes — pass multiple markets in one run (e.g. the same category across five cities) and get one comparable report each.

### What this doesn't do

- **No per-business lead lists** — no addresses, phone numbers, emails, or websites; aggregate statistics only.
- **No review text** — review *counts* and ratings, never the review content.
- **No foot-traffic, demographics, or revenue forecasting** — this is competitive structure from listings, not a location-intelligence platform.
- **No historical trend** — a point-in-time snapshot at request time, not a time series.
- **No guaranteed census** — a top-ranked sample, capped at `maxPlaces`.

Use a raw places/Maps scraper if you actually want the per-business list with contact details. Use an enterprise location-intelligence platform if you need foot-traffic panels or revenue forecasts. Use this when you want a cheap, callable, aggregate read on how crowded and how entrenched a local market is.

***

Design notes: [www.scotthelvick.com/tools/local-market-saturation](https://www.scotthelvick.com/tools/local-market-saturation)

# Actor input Schema

## `markets` (type: `array`):

Markets to analyze. Each item is a {"category", "location"} object: category is the business type to count (e.g. "coffee shops", "plumbers", "dentists"); location is a city/area (e.g. "Austin, TX", "Brooklyn, NY"). 1-20 markets per run. Each market becomes one aggregated saturation report.

## `maxPlaces` (type: `integer`):

How many local results to aggregate per market (the local finder returns ~20 per page; this paginates up to the cap). Higher = more representative distributions but slower. 10-80.

## `language` (type: `string`):

Google interface language code (hl), e.g. "en". Affects result language/formatting.

## `country` (type: `string`):

Two-letter Google country code (gl), e.g. "us", "gb", "ca". Biases results to that country.

## Actor input object example

```json
{
  "markets": [
    {
      "category": "coffee shops",
      "location": "Austin, TX"
    }
  ],
  "maxPlaces": 60,
  "language": "en",
  "country": "us"
}
```

# Actor output Schema

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

Aggregated saturation reports for this run (one per market).

# 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 = {
    "markets": [
        {
            "category": "coffee shops",
            "location": "Austin, TX"
        }
    ],
    "maxPlaces": 60,
    "language": "en",
    "country": "us"
};

// Run the Actor and wait for it to finish
const run = await client.actor("shelvick/local-market-saturation").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 = {
    "markets": [{
            "category": "coffee shops",
            "location": "Austin, TX",
        }],
    "maxPlaces": 60,
    "language": "en",
    "country": "us",
}

# Run the Actor and wait for it to finish
run = client.actor("shelvick/local-market-saturation").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "markets": [
    {
      "category": "coffee shops",
      "location": "Austin, TX"
    }
  ],
  "maxPlaces": 60,
  "language": "en",
  "country": "us"
}' |
apify call shelvick/local-market-saturation --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=shelvick/local-market-saturation",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

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