Local Market Saturation & Competitor Density Analyzer
Pricing
from $127.50 / 1,000 market analyzeds
Local Market Saturation & Competitor Density Analyzer
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.
Pricing
from $127.50 / 1,000 market analyzeds
Rating
0.0
(0)
Developer
Scott Helvick
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 hours ago
Last modified
Categories
Share
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.
{"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
{ "markets": [{ "category": "coffee shops", "location": "Austin, TX" }, { "category": "coffee shops", "location": "Portland, OR" }], "maxPlaces": 60 }
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)
from apify_client import ApifyClientclient = 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). CheckcompetitorCount,resultsCapped, and thesaturationcomponents 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.
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