U.S. Ghost Kitchen & Virtual Brand Detector
Pricing
$100.00 / 1,000 analyzed locations
U.S. Ghost Kitchen & Virtual Brand Detector
Review likely delivery-only competition in one U.S. market or a bounded batch of U.S. markets. Inspect public delivery-platform evidence, optional Yelp storefront context, per-location billing status, and coverage notes for partial, failed, skipped, duplicate, or inconclusive sources.
Pricing
$100.00 / 1,000 analyzed locations
Rating
0.0
(0)
Developer
Critical Distinction
Maintained by CommunityActor stats
0
Bookmarked
3
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
Reveal likely delivery-only competition before you launch, pitch, lease, invest, or expand.
Most market scans treat each delivery-app listing as a separate business. That can make a market look cleaner, more fragmented, or less competitive than it really is. This Actor helps you see where multiple brands may share a kitchen, where delivery-only concepts may be concentrated, and where known virtual brands may already be active.
Enter one U.S. city, ZIP code, or street address, or provide a batch of market requests. The Actor attempts public-source collection from DoorDash, Uber Eats, and Grubhub for each unique market, then returns flagged brands, the evidence behind each flag, and a run summary that makes partial, skipped, failed, duplicate, and charge-limited locations visible.
What This Actor Is
A U.S. market intelligence tool for finding likely ghost kitchens and virtual brands.
A ghost kitchen is a kitchen that mainly prepares food for delivery, often without a dine-in storefront. A virtual brand is a delivery-only brand that operates under a separate name, often from an existing restaurant or shared kitchen.
Why Teams Use It
Traditional market research often counts delivery-app listings at face value. That can hide shared-kitchen activity and overstate how many truly independent operators are in a market.
Use this Actor when you need a clearer read on:
- whether a neighborhood is already crowded with delivery-only brands
- whether several brands near a property appear to come from the same kitchen
- whether a trade area shows meaningful ghost-kitchen or virtual-brand activity before outreach, expansion, or underwriting
Example: a sales team pitching commissary kitchen services in downtown Chicago can use the Actor to see whether several delivery brands appear to trace back to a small number of addresses.
How It Works
- Enter one U.S. city, ZIP code, or address, or use
locations[]for a batch of market requests. - For each unique location and scan-control combination, the Actor attempts to collect public listings from DoorDash, Uber Eats, and Grubhub.
- It standardizes addresses, groups brands that appear to operate from the same place, checks for delivery-only patterns, and compares names against known virtual-brand families.
- It assigns an evidence-based classification and explains the strongest signals in plain English.
- It writes dataset rows and an
OUTPUT.locations[]summary so every requested market remains visible, including duplicate aliases, spend-limit skips, all-source blackouts, and charge outcomes.
How To Judge The Output
This is not a black-box label.
Each result includes the reason it was flagged, such as:
- multiple brands at the same address
- signs the brand is delivery-only
- a match to a known virtual-brand family
- supporting or conflicting storefront signals
The Actor also tells you when a platform returned only partial data or failed before useful listings were collected. That helps you separate a genuinely quiet market from a thin, blocked, or interrupted collection run.
For extra storefront context, you can add an optional Yelp Fusion API key. That enrichment can bring in ratings, review counts, photos, website presence, and other signals that may help distinguish an established storefront restaurant from a delivery-first concept. It is optional, unofficial, and not a guarantee that every storefront has been verified.
Important Notes
This Actor is built for market research, commercial planning, and due diligence. It is not an official business registry and it does not make a legal determination.
It currently supports U.S. locations only. It relies on public delivery-platform listings rather than official platform feeds or government records. Address matching is strong but not perfect because platforms do not always format locations the same way.
When coverage is incomplete, the Actor surfaces that clearly in the run summary instead of hiding the uncertainty.
What You Can Spot Quickly
- Shared-kitchen clusters β multiple brands that appear to operate from the same address
- Known virtual brands β delivery-only brands linked to established parent companies
- Delivery-only concepts β brands with little evidence of a normal storefront
- Local saturation β areas where ghost-kitchen activity appears concentrated around a property or neighborhood
Who Uses It
Consultants and strategy teams β Size a market more accurately before advising on expansion, positioning, or virtual-brand strategy.
Sales and business development teams β Prioritize markets and properties where shared-kitchen activity may signal demand for kitchen space, restaurant services, or delivery-focused infrastructure.
Investors, brokers, and real estate teams β Evaluate whether a property sits in an area with meaningful ghost-kitchen density and hidden delivery competition.
Researchers and journalists β Turn a hard-to-see market trend into structured, evidence-backed findings.
Input Parameters
Provide either location for one market or locations[] for a batch.
Do not provide both in the same input. The rest help you narrow the
scan, go deeper, tune batch execution, or make the output more
conservative.
| Field | Default | What it does |
|---|---|---|
location | none | Single-market input. Use a U.S. city and state, ZIP code, or street address. Examples: Austin, TX, 90210, 123 Main St, Chicago, IL. Leave this empty when using locations[]. |
locations | none | Batch input. Provide up to 50 requested location objects, with at most 25 unique location and scan-control fingerprints. Exact duplicate aliases remain visible in OUTPUT.locations[] and are not separate charge candidates. |
radiusMiles | 5 | How far around the point to search. Range: 1 to 25. If a platform supplies distance data, listings outside the radius are removed. If it does not, the Actor keeps the candidate and records that limit in the run diagnostics. |
cuisineFilter | none | Restrict the scan to one category, such as pizza or wings, when you want a more focused segment view. |
maxRestaurants | 200 | Maximum number of deduplicated candidates to review in depth. Range: 10 to 1000. Use a higher number for a broader candidate set when source coverage allows it. |
confidenceThreshold | 0.5 | Minimum score required for a result to be included. Range: 0.0 to 1.0. Higher values are more conservative. Lower values surface more borderline cases. |
includeTraditionalRestaurants | false | Include results classified as traditional or unknown, not just likely ghost kitchens and virtual brands. Use this when you want a broader candidate map. |
batchConcurrency | 1 | For batch inputs, maximum number of unique market scans to run at once. Range: 1 to 3. Dataset rows, OUTPUT.locations[], checkpoints, and charges remain deterministic and charges remain serialized. |
yelpApiKey | none | Optional Yelp Fusion API key for extra storefront context signals such as ratings, review counts, photos, and website presence. |
Each locations[] item may override radiusMiles, cuisineFilter,
maxRestaurants, confidenceThreshold, and
includeTraditionalRestaurants. The Yelp key stays global and secret.
Good first run: start with one location, the default 5-mile
radius, the default confidence threshold, the default 200-restaurant
limit, and batchConcurrency: 1. For a first batch run, use two or
three locations before expanding the request.
Agentic And API Use
Apify Store currently exposes this Actor for agentic-payment discovery.
Agents and API clients should still treat each run as bounded market
research, not as a guaranteed detection service. Start with one market
or a small locations[] batch, set a maxTotalChargeUsd run cap that
matches the number of unique market charges you are willing to allow,
and keep batchConcurrency at 1 until you have inspected the output
shape.
Use the output schema links to read both the default dataset and
OUTPUT. The dataset holds restaurant-level classifications;
OUTPUT.locations[], OUTPUT.sourceHealth, partialData, and charge
statuses explain whether a market was charged, skipped, duplicate,
partially covered, or blacked out before source work completed. Do not
chain a quiet market into an automated decision until those run-level
fields show whether the sources were actually reached.
Output Format
The Actor writes two primary outputs:
- Dataset results β one record per business reviewed.
OUTPUTsummary β a structured run summary with aggregate counts, batch location rows, platform coverage, billing status, and diagnostics.
For automation, follow both links. Dataset rows alone do not explain coverage, spend-limit, duplicate, or charge-state context.
Dataset Results
Each result record is built to answer three questions quickly:
- What brand is this?
- What did the Actor conclude?
- Why did it conclude that?
Common fields include:
| Field | What it tells you |
|---|---|
schemaVersion, batchRunId | Batch-capable output contract and output-safe run identifier |
requestIndex, locationIndex, locationId, requestLocation, resolvedLocation | Which requested market produced this row, including the canonical unique-location index |
locationStatus, chargeStatus, resultOrdinal | Per-location batch state and deterministic result order |
name, address, platformUrl, sources | The business identity, where it was seen, and a source link |
classification, confidenceScore | The label and how strong the local evidence looks on a 0.0 to 1.0 scale |
evidenceSummary | A plain-English explanation of why the result was flagged |
evidence.sharedAddressBrands, evidence.sharedAddressCount | Other brands seen at the same address and how many there are |
evidence.deliveryOnly, evidence.hasDineIn | Signals that support or weaken a delivery-only interpretation |
evidence.isKnownVirtualBrand, evidence.knownParentChain | Whether the brand matches a known virtual-brand family |
evidence.foundOnPlatforms, evidence.foundOnPlatformCount | Which delivery apps showed the brand |
evidence.externalRating, evidence.externalReviewCount, evidence.hasIndependentWebsite, evidence.hasPhotos | Extra storefront signals when Yelp enrichment is available |
addressClusterId, brandsAtThisAddress | Whether the brand sits in a larger address cluster |
scrapedAt, dataSource | When the record was collected and which source produced the final record |
A typical result looks like this:
{"schemaVersion": "ghost-kitchen-batch-output-v1","batchRunId": "gkb_1234abcd5678ef00","requestIndex": 0,"locationIndex": 0,"locationId": "loc_1234abcd5678ef00","requestLocation": "Austin, TX","resolvedLocation": "Austin, TX","requestedRadiusMiles": 3,"requestedMaxRestaurants": 25,"locationStatus": "charged","chargeStatus": "charged","resultOrdinal": 0,"name": "Wings Factory","address": "123 Main St, Austin, TX 78701","sources": ["doordash", "ubereats"],"classification": "confirmed_ghost_kitchen","confidenceScore": 0.85,"evidenceSummary": "3 other brands operate from this address. Delivery-only operation. No dine-in option observed.","evidence": {"sharedAddressBrands": ["Burger Barn", "Pasta Palace", "Taco Town"],"sharedAddressCount": 3,"foundOnPlatforms": ["doordash", "ubereats"],"foundOnPlatformCount": 2,"deliveryOnly": true,"hasDineIn": false,"isKnownVirtualBrand": false,"knownParentChain": null,"distanceMiles": 1.2,"cuisineType": "American","priceRange": "$$","rating": 4.2,"reviewCount": 87},"addressClusterId": "addr_123_main_street_austin_tx_78701","brandsAtThisAddress": 4,"scrapedAt": "2026-03-20T15:30:00Z","dataSource": "doordash"}
OUTPUT Run Summary
The OUTPUT record helps you judge the run itself, not just the listings.
It includes:
- the batch output contract version and output-safe
batchRunId - requested, unique, duplicate, charged, zero-event, and spend-limited location counts
- the
location-analyzedevent name,$0.10local event price, and estimated maximum charge for unique non-duplicate locations maxTotalChargeUsdwhen Apify provides a run spending cap to Actor code- how many businesses were analyzed
- how many were flagged as ghost kitchens, virtual brands, traditional storefronts, or unknown
- the location, radius, and result limit used
- whether any platform returned only partial data
- diagnostic notes explaining things like missing distance data or interrupted pagination
- which platforms worked, struggled, or failed
- a
sourceHealtharray with stable source status and failure classes - a
locations[]row for every requested market, including duplicate aliases and rows skipped before source work - total run duration
A typical summary looks like this:
{"schemaVersion": "ghost-kitchen-batch-output-v1","batchRunId": "gkb_1234abcd5678ef00","requestedLocationCount": 3,"uniqueLocationCount": 2,"duplicateLocationCount": 1,"chargedLocationCount": 1,"zeroEventLocationCount": 0,"skippedSpendLimitCount": 1,"estimatedMaxChargeUsd": 0.2,"maxTotalChargeUsd": 0.1,"eventName": "location-analyzed","eventPriceUsd": 0.1,"totalAnalyzed": 3,"candidatesConsidered": 4,"ghostKitchens": 2,"virtualBrands": 1,"traditional": 0,"unknown": 0,"location": "Austin, TX","requestedRadiusMiles": 3,"requestedMaxRestaurants": 25,"partialData": true,"diagnostics": ["Radius filtering remained best-effort for 1 listing because source distance data was unavailable."],"scrapersUsed": ["doordash", "ubereats", "grubhub"],"scrapersPartial": ["ubereats"],"scrapersFailed": ["doordash", "grubhub"],"scraperDiagnostics": [{"name": "ubereats","status": "partial","restaurantsFound": 3,"message": "pagination stopped after page 1"},{"name": "doordash","status": "failed","restaurantsFound": 0,"message": "DoorDash blocked the request with HTTP 403."},{"name": "grubhub","status": "failed","restaurantsFound": 0,"message": "Grubhub required authentication with HTTP 401."}],"sourceHealth": [{"source": "ubereats","status": "partial","restaurantsFound": 3,"failureClass": "partial_pagination","stage": "pagination","retryable": true,"httpStatus": 429,"pagesFetched": 1,"retryCount": 0,"message": "Uber Eats returned useful feed rows before pagination stopped."},{"source": "doordash","status": "failed","restaurantsFound": 0,"failureClass": "anti_bot_403","stage": "search","retryable": false,"httpStatus": 403,"retryCount": 0,"message": "DoorDash blocked the request with HTTP 403."},{"source": "grubhub","status": "failed","restaurantsFound": 0,"failureClass": "auth_required_401","stage": "search","retryable": false,"httpStatus": 401,"retryCount": 0,"message": "Grubhub required authentication with HTTP 401."}],"runDurationSeconds": 2.13,"locations": [{"requestIndex": 0,"locationIndex": 0,"locationId": "loc_1234abcd5678ef00","duplicateOfLocationIndex": null,"requestLocation": "Austin, TX","resolvedLocation": "Austin, TX","requestedRadiusMiles": 3,"requestedMaxRestaurants": 25,"status": "charged","chargeStatus": "charged","chargeEligible": false,"chargeEligibility": "already_charged","chargedEventName": "location-analyzed","idempotencyKeyHash": "ik_1234abcd5678ef00","totalAnalyzed": 3,"candidatesConsidered": 4,"partialData": true,"scrapersUsed": ["doordash", "ubereats", "grubhub"],"scrapersPartial": ["ubereats"],"scrapersFailed": ["doordash", "grubhub"],"sourceHealth": [],"diagnostics": [],"runDurationSeconds": 2.13},{"requestIndex": 1,"locationIndex": 0,"locationId": "loc_1234abcd5678ef00","duplicateOfLocationIndex": 0,"requestLocation": "Austin, TX","resolvedLocation": "Austin, TX","requestedRadiusMiles": 3,"requestedMaxRestaurants": 25,"status": "duplicate_skipped","chargeStatus": "duplicate_skipped","chargeEligible": false,"chargeEligibility": "ineligible_duplicate","chargedEventName": null,"idempotencyKeyHash": null,"totalAnalyzed": 3,"candidatesConsidered": 4,"partialData": true,"scrapersUsed": ["doordash", "ubereats", "grubhub"],"scrapersPartial": ["ubereats"],"scrapersFailed": ["doordash", "grubhub"],"sourceHealth": [],"diagnostics": [],"runDurationSeconds": 2.13},{"requestIndex": 2,"locationIndex": 1,"locationId": "loc_abcdef1234567890","duplicateOfLocationIndex": null,"requestLocation": "Chicago, IL","resolvedLocation": "Chicago, IL","requestedRadiusMiles": 3,"requestedMaxRestaurants": 25,"status": "skipped_spend_limit","chargeStatus": "skipped_spend_limit","chargeEligible": false,"chargeEligibility": "ineligible_spend_limit","chargedEventName": null,"idempotencyKeyHash": "ik_abcdef1234567890","totalAnalyzed": 0,"candidatesConsidered": 0,"partialData": false,"scrapersUsed": [],"scrapersPartial": [],"scrapersFailed": [],"sourceHealth": [],"diagnostics": ["Skipped before source work because local spend-limit accounting had no remaining $0.10 location-analyzed charge slot under maxTotalChargeUsd 0.10."],"runDurationSeconds": 0.0}]}
scraperDiagnostics is the legacy compatibility field. Use
sourceHealth when you need machine-readable coverage status. Its
failureClass values distinguish cases such as empty_market,
partial_pagination, auth_required_401, anti_bot_403,
rate_limited_429, decode_error, schema_drift,
timeout_transport, and aggregate all_source_blackout.
If every upstream platform is blocked before any trustworthy listing is
collected, the run still fails. When storage is available, the Actor
writes a zero-result OUTPUT summary first so you can see what
happened instead of interpreting an empty result as a clean market. In
that case, sourceHealth includes source-specific failed rows plus an
aggregate source: "all" row with
failureClass: "all_source_blackout". A quiet but successfully reached
market is different: it uses empty_market and does not include the
aggregate blackout row.
Batch Location Statuses
OUTPUT.locations[] is the quickest place to audit batch billing
semantics:
| Status | Meaning |
|---|---|
charged | Durable inspectable output was published and the location-analyzed charge completed. |
duplicate_skipped | The request was an exact duplicate alias of an earlier location and was not charged separately. |
skipped_spend_limit | Source work did not start because the remaining maxTotalChargeUsd budget could not cover another possible charge. |
zero_event_blackout | All source coverage failed before trustworthy listings were collected; output explains the blackout and no charge is attempted. |
zero_event_pre_output_failure | The location failed before durable useful output, such as geocoding or timeout-before-output, and no charge is attempted. |
charge_failed_after_output | Useful output exists, but the charge call failed after output publication. Inspect the row before rerunning. |
charge_limit_reached | Apify charge-limit feedback stopped further charging. Later unstarted rows are skipped. |
output_published_charge_eligible or charge_pending | Durable output exists and a charge is eligible or in progress at the retry boundary. |
How To Read The Classifications
| Classification | What it means in plain English |
|---|---|
confirmed_ghost_kitchen | Strong shared-address and operating evidence supports a ghost-kitchen call. Treat this as the strongest local evidence tier, not an official or legal confirmation. |
likely_ghost_kitchen | Meaningful ghost-kitchen indicators are present, but the evidence is not strong enough for the confirmed tier. |
virtual_brand | The name matches a known virtual-brand family, but there is not enough address or operating evidence to call it a ghost kitchen by itself. |
traditional | Positive storefront signals, such as dine-in, an established external listing, or an independent website, support a standard restaurant interpretation. |
unknown | The listing was found, but the available signals are too limited or contradictory for a responsible call. |
confidenceThreshold filters the numeric evidence-strength score. It is not a verified accuracy score, and traditional or unknown still depend on what evidence exists, not just the score alone.
Example Use Cases
Market Pressure Review Before Expansion
A restaurant consultant compares several Dallas submarkets before recommending a virtual-brand launch. Running the Actor by neighborhood surfaces shared-address clusters, known virtual-brand parents, and delivery-only patterns so the team can decide which areas deserve manual follow-up.
Site Selection Or Underwriting
A real-estate or commissary operator evaluating a Chicago property runs the Actor with a 2-mile radius and includeTraditionalRestaurants: true. The results show which nearby addresses already host multiple delivery brands, where delivery-only clusters are concentrated, and whether the trade area looks crowded or under-served.
Limitations
- The Actor currently attempts public-source collection from DoorDash, Uber Eats, and Grubhub; those sources can return partial data, rate limits, blocked requests, or no usable listing data for a run.
- Yelp enrichment is optional. Without a Yelp Fusion API key, website presence, photos, and some stronger storefront signals may remain unknown rather than being treated as false.
- Address matching is strong but not perfect because delivery apps do not always format locations the same way.
- The Actor is limited to U.S. locations.
- Batch mode supports up to 50 requested rows and at most 25 unique location and scan-control fingerprints. Exact duplicates stay visible as aliases instead of separate charge candidates.
- Radius handling is only as precise as the source data. When a platform omits listing distance, the Actor keeps the candidate and records a best-effort radius note in
OUTPUT. - Platform access can be rate-limited or restricted. Partial coverage is surfaced in the final status message and in
OUTPUT.sourceHealth. A total upstream blackout writes a structured diagnosticOUTPUTsummary and fails the run instead of returning a misleading empty dataset.
Disclaimer
This is an unofficial Actor. It is not affiliated with, endorsed by, or associated with DoorDash, Uber Eats, Grubhub, or Yelp. All product names, trademarks, and registered trademarks are the property of their respective owners.
Permissions
This Actor is designed to run with limited permissions. It uses only its own default dataset and key-value store, plus outbound HTTP requests to delivery platforms, Nominatim geocoding, and the optional Yelp Fusion API. It does not require access to your other Apify storages or account resources.
Pricing
This Actor currently uses Pay Per Event pricing at $0.10 per charged
market analysis attempt.
A chargeable unit is a unique requested location and scan-control fingerprint that reaches durable, inspectable output. Exact duplicate aliases, spend-limit skips, all-source blackouts, and pre-output failures are not separate charged units.
An honestly reached empty market can still be a charged unit because
the output is useful market evidence. A partial-source success can also
be charged when the run publishes inspectable results and
OUTPUT.sourceHealth explains the missing or partial source coverage.
If Apify shows the same rate as $100.00 / 1,000 analyzed locations, that is the platform's per-1,000 display of the same $0.10 per-location event charge.
There are no separate pass-through compute or platform-usage charges added on top of that event price.
The charge is for the requested market analysis attempt with durable
output, not a guarantee of full delivery-platform coverage or a
confirmed ghost-kitchen count. Check OUTPUT.locations[], coverage
diagnostics, sourceHealth, and result classifications before treating
a quiet or mostly unknown market as a conclusion.
For batch runs, Apify's maxTotalChargeUsd run option can limit how
many potentially chargeable unique locations start. Rows skipped by
that local spend-limit accounting remain visible as
skipped_spend_limit.
For agentic and API runs, set maxTotalChargeUsd before execution when
you need a hard budget. One completed unique market costs $0.10; a
cap below another full event leaves later unique rows skipped before
source work starts.
FAQ
How long does a typical run take? A standard city search with the default 200-restaurant limit usually finishes in 2 to 5 minutes. Larger searches, such as a 25-mile radius with a 1000-restaurant cap, can take longer.
Can I analyze multiple cities in one run?
Yes. Use locations[] and leave location empty. Each item needs a
location value and may override the scan controls for that market.
Exact duplicate aliases remain visible but are not charged separately.
For stable first runs, keep batchConcurrency at 1; advanced batch
runs can raise it up to 3 while output and charges remain serialized
and deterministic.
How should I think about accuracy?
The strongest signal is multiple brands at the same address. Known virtual-brand matching is exact when a match exists. Delivery-only and no-dine-in signals are treated as supporting evidence, not proof by themselves. When the evidence is mixed or too thin, a listing can remain unknown instead of being forced into a confident label.
How often should I rerun it? That depends on how actively you monitor a market. Monthly runs are a good baseline for market tracking. Weekly runs make more sense when you are watching an active target area or supporting ongoing sales outreach.
Release History
See ./CHANGELOG.md for version-by-version release notes and migration guidance.