U.S. Ghost Kitchen & Virtual Brand Detector avatar

U.S. Ghost Kitchen & Virtual Brand Detector

Pricing

$100.00 / 1,000 analyzed locations

Go to Apify Store
U.S. Ghost Kitchen & Virtual Brand Detector

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

Critical Distinction

Maintained by Community

Actor 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

  1. Enter one U.S. city, ZIP code, or address, or use locations[] for a batch of market requests.
  2. For each unique location and scan-control combination, the Actor attempts to collect public listings from DoorDash, Uber Eats, and Grubhub.
  3. 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.
  4. It assigns an evidence-based classification and explains the strongest signals in plain English.
  5. 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.

FieldDefaultWhat it does
locationnoneSingle-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[].
locationsnoneBatch 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.
radiusMiles5How 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.
cuisineFilternoneRestrict the scan to one category, such as pizza or wings, when you want a more focused segment view.
maxRestaurants200Maximum 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.
confidenceThreshold0.5Minimum 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.
includeTraditionalRestaurantsfalseInclude results classified as traditional or unknown, not just likely ghost kitchens and virtual brands. Use this when you want a broader candidate map.
batchConcurrency1For 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.
yelpApiKeynoneOptional 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:

  1. Dataset results β€” one record per business reviewed.
  2. OUTPUT summary β€” 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:

FieldWhat it tells you
schemaVersion, batchRunIdBatch-capable output contract and output-safe run identifier
requestIndex, locationIndex, locationId, requestLocation, resolvedLocationWhich requested market produced this row, including the canonical unique-location index
locationStatus, chargeStatus, resultOrdinalPer-location batch state and deterministic result order
name, address, platformUrl, sourcesThe business identity, where it was seen, and a source link
classification, confidenceScoreThe label and how strong the local evidence looks on a 0.0 to 1.0 scale
evidenceSummaryA plain-English explanation of why the result was flagged
evidence.sharedAddressBrands, evidence.sharedAddressCountOther brands seen at the same address and how many there are
evidence.deliveryOnly, evidence.hasDineInSignals that support or weaken a delivery-only interpretation
evidence.isKnownVirtualBrand, evidence.knownParentChainWhether the brand matches a known virtual-brand family
evidence.foundOnPlatforms, evidence.foundOnPlatformCountWhich delivery apps showed the brand
evidence.externalRating, evidence.externalReviewCount, evidence.hasIndependentWebsite, evidence.hasPhotosExtra storefront signals when Yelp enrichment is available
addressClusterId, brandsAtThisAddressWhether the brand sits in a larger address cluster
scrapedAt, dataSourceWhen 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-analyzed event name, $0.10 local event price, and estimated maximum charge for unique non-duplicate locations
  • maxTotalChargeUsd when 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 sourceHealth array 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:

StatusMeaning
chargedDurable inspectable output was published and the location-analyzed charge completed.
duplicate_skippedThe request was an exact duplicate alias of an earlier location and was not charged separately.
skipped_spend_limitSource work did not start because the remaining maxTotalChargeUsd budget could not cover another possible charge.
zero_event_blackoutAll source coverage failed before trustworthy listings were collected; output explains the blackout and no charge is attempted.
zero_event_pre_output_failureThe location failed before durable useful output, such as geocoding or timeout-before-output, and no charge is attempted.
charge_failed_after_outputUseful output exists, but the charge call failed after output publication. Inspect the row before rerunning.
charge_limit_reachedApify charge-limit feedback stopped further charging. Later unstarted rows are skipped.
output_published_charge_eligible or charge_pendingDurable output exists and a charge is eligible or in progress at the retry boundary.

How To Read The Classifications

ClassificationWhat it means in plain English
confirmed_ghost_kitchenStrong 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_kitchenMeaningful ghost-kitchen indicators are present, but the evidence is not strong enough for the confirmed tier.
virtual_brandThe name matches a known virtual-brand family, but there is not enough address or operating evidence to call it a ghost kitchen by itself.
traditionalPositive storefront signals, such as dine-in, an established external listing, or an independent website, support a standard restaurant interpretation.
unknownThe 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 diagnostic OUTPUT summary 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.