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: YelpGET /businesses/search (category + location) → Aggregate: ratings, review counts, price ranges, categories → Mavera POST /mave/chat → Competitive landscape report
Architecture
Code
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])
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));
Example Output
=== 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
Search result cap
Search result cap
Yelp limits search results to 1,000 total (offset + limit ≤ 1000). For dense markets, narrow by neighborhood or category.
Free tier limits
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.
Missing price data
Missing price data
Not all businesses have
price set. The code groups these as "N/A". Filter or exclude for cleaner tier analysis.