> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mavera.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Business Discovery → Market Research

> Map the local competitive landscape using Yelp business search and Mave Agent analysis

## Scenario

You're opening a new location or launching a competitive product. Before investing, you need to understand the local competitive landscape — who's already there, how they're rated, what they offer, and where the gaps are. You use Yelp's business search to map competitors by category and location, then send the landscape data to Mave for strategic analysis.

**Flow:** Yelp `GET /businesses/search` (category + location) → Aggregate: ratings, review counts, price ranges, categories → Mavera `POST /mave/chat` → Competitive landscape report

## Architecture

```mermaid theme={"dark"}
flowchart LR
    A["Yelp GET /businesses/search"] --> B["Aggregate market data"]
    B --> C["POST /api/v1/mave/chat"]
    C --> D["Market entry report"]
```

## Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, time
  from collections import defaultdict

  YELP = os.environ["YELP_API_KEY"]
  MV = os.environ["MAVERA_API_KEY"]
  YELP_BASE = "https://api.yelp.com/v3"
  YELP_H = {"Authorization": f"Bearer {YELP}"}
  MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  # 1. Search businesses by category and location
  CATEGORY = "coffee"
  LOCATION = "Austin, TX"

  businesses = []
  for offset in range(0, 200, 50):
      r = requests.get(f"{YELP_BASE}/businesses/search",
          headers=YELP_H,
          params={
              "term": CATEGORY,
              "location": LOCATION,
              "limit": 50,
              "offset": offset,
              "sort_by": "review_count",
          })
      if r.status_code == 429:
          time.sleep(2)
          continue
      r.raise_for_status()
      batch = r.json().get("businesses", [])
      if not batch:
          break
      businesses.extend(batch)
      time.sleep(0.5)

  print(f"Found {len(businesses)} {CATEGORY} businesses in {LOCATION}")

  # 2. Aggregate market data
  price_tiers = defaultdict(list)
  categories_seen = defaultdict(int)
  neighborhoods = defaultdict(int)

  for biz in businesses:
      price = biz.get("price", "N/A")
      rating = biz.get("rating", 0)
      reviews = biz.get("review_count", 0)
      price_tiers[price].append({"rating": rating, "reviews": reviews, "name": biz["name"]})

      for cat in biz.get("categories", []):
          categories_seen[cat.get("alias", "")] += 1

      loc = biz.get("location", {})
      hood = loc.get("city", LOCATION)
      neighborhoods[hood] += 1

  # 3. Build analysis context
  market_summary = []
  for price, bizes in sorted(price_tiers.items()):
      avg_rating = sum(b["rating"] for b in bizes) / len(bizes)
      avg_reviews = sum(b["reviews"] for b in bizes) / len(bizes)
      top = sorted(bizes, key=lambda x: -x["reviews"])[:3]
      top_names = ", ".join(b["name"] for b in top)
      market_summary.append(
          f"- {price} tier: {len(bizes)} businesses, avg {avg_rating:.1f}/5, "
          f"avg {avg_reviews:.0f} reviews. Leaders: {top_names}"
      )

  cat_summary = ", ".join(f"{k} ({v})" for k, v in
      sorted(categories_seen.items(), key=lambda x: -x[1])[:10])

  biz_block = "\n".join(
      f"- {b['name']}: {b.get('rating',0)}/5, {b.get('review_count',0)} reviews, "
      f"Price: {b.get('price','N/A')}, Categories: {', '.join(c.get('title','') for c in b.get('categories',[]))}"
      for b in sorted(businesses, key=lambda x: -x.get("review_count", 0))[:15]
  )

  # 4. Mave competitive analysis
  analysis = requests.post("https://app.mavera.io/api/v1/mave/chat",
      headers=MV_H,
      json={"message": f"""Analyze the competitive landscape for "{CATEGORY}" in {LOCATION}.

  MARKET OVERVIEW:
  {len(businesses)} businesses found

  BY PRICE TIER:
  {chr(10).join(market_summary)}

  CATEGORY OVERLAP:
  {cat_summary}

  TOP 15 BY REVIEW VOLUME:
  {biz_block}

  Produce:
  1. Market saturation assessment (oversaturated / balanced / underserved)
  2. Competitive clusters (which businesses compete directly)
  3. White space opportunities (underserved niches, price gaps, location gaps)
  4. Positioning recommendations for a new entrant
  5. Differentiator suggestions based on what existing players lack
  6. Pricing strategy recommendation"""}).json()

  print(f"\n=== {CATEGORY.title()} Market in {LOCATION} ===")
  print(analysis.get("content", "")[:2000])
  ```

  ```javascript JavaScript theme={"dark"}
  const YELP = process.env.YELP_API_KEY;
  const MV = process.env.MAVERA_API_KEY;
  const YELP_BASE = "https://api.yelp.com/v3";
  const YELP_H = { Authorization: `Bearer ${YELP}` };
  const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  const CATEGORY = "coffee";
  const LOCATION = "Austin, TX";

  // 1. Search businesses
  const businesses = [];
  for (let offset = 0; offset < 200; offset += 50) {
    const params = new URLSearchParams({
      term: CATEGORY, location: LOCATION, limit: "50", offset: String(offset), sort_by: "review_count",
    });
    const res = await fetch(`${YELP_BASE}/businesses/search?${params}`, { headers: YELP_H });
    if (res.status === 429) { await new Promise((r) => setTimeout(r, 2000)); continue; }
    if (!res.ok) throw new Error(`Yelp ${res.status}`);
    const batch = (await res.json()).businesses || [];
    if (!batch.length) break;
    businesses.push(...batch);
    await new Promise((r) => setTimeout(r, 500));
  }

  console.log(`Found ${businesses.length} ${CATEGORY} businesses in ${LOCATION}`);

  // 2. Aggregate
  const priceTiers = {};
  for (const biz of businesses) {
    const price = biz.price || "N/A";
    (priceTiers[price] ??= []).push({ name: biz.name, rating: biz.rating || 0, reviews: biz.review_count || 0 });
  }

  const marketSummary = Object.entries(priceTiers)
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([price, bizes]) => {
      const avgR = bizes.reduce((s, b) => s + b.rating, 0) / bizes.length;
      const avgRv = bizes.reduce((s, b) => s + b.reviews, 0) / bizes.length;
      const top = bizes.sort((a, b) => b.reviews - a.reviews).slice(0, 3).map((b) => b.name).join(", ");
      return `- ${price}: ${bizes.length} biz, avg ${avgR.toFixed(1)}/5, avg ${Math.round(avgRv)} reviews. Leaders: ${top}`;
    }).join("\n");

  const bizBlock = businesses
    .sort((a, b) => (b.review_count || 0) - (a.review_count || 0)).slice(0, 15)
    .map((b) => `- ${b.name}: ${b.rating}/5, ${b.review_count} reviews, ${b.price || "N/A"}`).join("\n");

  // 3. Analysis
  const analysis = await fetch("https://app.mavera.io/api/v1/mave/chat", {
    method: "POST", headers: MV_H,
    body: JSON.stringify({
      message: `Competitive landscape for "${CATEGORY}" in ${LOCATION}:\n\n${businesses.length} businesses\n\nBY TIER:\n${marketSummary}\n\nTOP 15:\n${bizBlock}\n\nProduce: 1) Saturation 2) Clusters 3) White space 4) Positioning 5) Differentiators 6) Pricing strategy`,
    }),
  }).then((r) => r.json());

  console.log(`\n=== ${CATEGORY} Market in ${LOCATION} ===`);
  console.log((analysis.content || "").slice(0, 2000));
  ```
</CodeGroup>

## Example Output

```text theme={"dark"}
=== Coffee Market in Austin, TX ===

## Market Saturation: BALANCED with NICHE GAPS

183 coffee businesses. Downtown oversaturated (42 within 1 mile).
East Austin and South Congress underserved relative to foot traffic.

## Competitive Clusters
- **Specialty/Third Wave** (28 businesses): Houndstooth, Fleet, Merit.
  Avg 4.5/5, $$ pricing. Competing on quality and ambiance.
- **Chain/Drive-thru** (34): Starbucks, Dutch Bros. Avg 3.8/5, $.
  Competing on speed and consistency.
- **Café + Food** (45): Cenote, Epoch. Avg 4.3/5, $$.
  Competing on experience and dwell time.

## White Space Opportunities
1. **Specialty + drive-thru** — no Austin player combines third-wave
   quality with drive-thru convenience. Nearest: Dutch Bros (not specialty).
2. **East Austin specialty** — 3 specialty shops vs 12 downtown.
   Growing residential density with no quality option.
3. **Evening coffee** — 80% close by 6pm. Night-market concept untested.

## Positioning Recommendation
Enter the $$ specialty tier in East Austin with drive-thru capability.
Differentiate on: speed (under 3 min), local roasting, and evening hours.
```

## Error Handling

<AccordionGroup>
  <Accordion title="Search result cap">Yelp limits search results to 1,000 total (offset + limit ≤ 1000). For dense markets, narrow by neighborhood or category.</Accordion>
  <Accordion title="Free tier limits">The free tier allows only 5,000 total API calls. Business search is the most expensive — each call counts as 1. Cache results aggressively.</Accordion>
  <Accordion title="Missing price data">Not all businesses have `price` set. The code groups these as `"N/A"`. Filter or exclude for cleaner tier analysis.</Accordion>
</AccordionGroup>
