Scenario
You’re a consumer brand deciding where to place your product, or a franchise evaluating which competitor model to emulate. You pull competitor business profiles from Yelp — their descriptions, photos, ratings, price tiers — and use them as stimulus material for a Mavera Focus Group. Synthetic consumer personas rank the competitors and explain their preferences, revealing which attributes drive consumer choice. Flow: YelpGET /businesses/search → Top competitors → Mavera POST /personas (local consumer archetypes) → POST /focus-groups (Ranking: “Which would you pick?”) → Consumer preference data
Code
import os, requests, time
YELP = os.environ["YELP_API_KEY"]
MV = os.environ["MAVERA_API_KEY"]
YELP_H = {"Authorization": f"Bearer {YELP}"}
MV_BASE = "https://app.mavera.io/api/v1"
MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
# 1. Pull top competitors
CATEGORY = "pizza"
LOCATION = "Brooklyn, NY"
r = requests.get("https://api.yelp.com/v3/businesses/search",
headers=YELP_H,
params={"term": CATEGORY, "location": LOCATION, "limit": 10,
"sort_by": "review_count"})
r.raise_for_status()
competitors = r.json().get("businesses", [])
# 2. Get detailed profiles
profiles = []
for biz in competitors[:6]:
detail = requests.get(f"https://api.yelp.com/v3/businesses/{biz['id']}",
headers=YELP_H).json()
revs = requests.get(f"https://api.yelp.com/v3/businesses/{biz['id']}/reviews",
headers=YELP_H).json().get("reviews", [])
profiles.append({
"name": detail.get("name", ""),
"rating": detail.get("rating", 0),
"reviews": detail.get("review_count", 0),
"price": detail.get("price", "N/A"),
"categories": ", ".join(c.get("title", "") for c in detail.get("categories", [])),
"hours": "Open" if not detail.get("is_closed") else "Closed",
"neighborhood": detail.get("location", {}).get("city", LOCATION),
"excerpt": revs[0].get("text", "")[:200] if revs else "No reviews",
})
time.sleep(0.5)
# 3. Create local consumer personas
CONSUMER_ARCHETYPES = [
{"name": "Busy Parent", "desc": "Family of 4, ordering takeout 3x/week. Values speed, portion size, kid-friendliness. Budget: $30-50/order."},
{"name": "Foodie Millennial", "desc": "Late 20s, Instagram-active. Values quality, ambiance, unique menu items. Willing to pay premium."},
{"name": "Budget College Student", "desc": "NYU student. Eats out daily. Values price, late hours, portion size. Budget: under $15."},
{"name": "Local Regular", "desc": "Lives in the neighborhood 10+ years. Values consistency, knowing the staff, supporting local. Goes weekly."},
]
persona_ids = []
for arch in CONSUMER_ARCHETYPES:
p = requests.post(f"{MV_BASE}/personas", headers=MV_H, json={
"name": f"Yelp Consumer: {arch['name']}",
"description": f"{arch['desc']} Location: {LOCATION}.",
"demographic": {"location": LOCATION},
"psychographic": {"dining_persona": arch["name"]},
}).json()
persona_ids.append({"id": p["id"], "name": arch["name"]})
time.sleep(0.2)
# 4. Build stimulus
competitor_cards = "\n\n".join(
f"({chr(65+i)}) {p['name']}\n"
f" Rating: {p['rating']}/5 ({p['reviews']} reviews) | Price: {p['price']}\n"
f" Categories: {p['categories']}\n"
f" Neighborhood: {p['neighborhood']}\n"
f" Recent review: \"{p['excerpt']}\""
for i, p in enumerate(profiles)
)
# 5. Focus Group
fg = requests.post(f"{MV_BASE}/focus-groups", headers=MV_H, json={
"name": f"{CATEGORY.title()} Preference Study — {LOCATION}",
"persona_ids": [p["id"] for p in persona_ids],
"questions": [
{"type": "ranking", "text": f"Rank these {CATEGORY} places from first choice to last:\n\n{competitor_cards}"},
"Explain why you ranked your #1 choice first. What specific attribute sealed it?",
"What would make you switch from your current favorite to a new option?",
"Describe your ideal ordering experience for this category in 2 sentences.",
"If a new place opened in your neighborhood, what ONE thing would make you try it?",
],
"context": f"Consumer preference study: {CATEGORY} in {LOCATION}.\n\n{competitor_cards}",
"responses_per_persona": 3,
}).json()
# 6. Poll
for _ in range(20):
time.sleep(5)
data = requests.get(f"{MV_BASE}/focus-groups/{fg['id']}", headers=MV_H).json()
if data.get("status") == "completed":
break
print(f"=== {CATEGORY.title()} Preference Study — {LOCATION} ===")
for resp in data.get("responses", []):
persona = next((p["name"] for p in persona_ids if p["id"] == resp.get("persona_id")), "?")
print(f"\n[{persona}] {resp.get('question','')[:60]}")
print(f" → {resp.get('answer','')[:300]}")
const YELP = process.env.YELP_API_KEY;
const MV = process.env.MAVERA_API_KEY;
const YELP_H = { Authorization: `Bearer ${YELP}` };
const MV_BASE = "https://app.mavera.io/api/v1";
const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
const CATEGORY = "pizza";
const LOCATION = "Brooklyn, NY";
// 1. Top competitors
const competitors = (await fetch(
`https://api.yelp.com/v3/businesses/search?term=${CATEGORY}&location=${encodeURIComponent(LOCATION)}&limit=10&sort_by=review_count`,
{ headers: YELP_H }
).then((r) => r.json())).businesses || [];
// 2. Profiles
const profiles = [];
for (const biz of competitors.slice(0, 6)) {
const detail = await fetch(`https://api.yelp.com/v3/businesses/${biz.id}`, { headers: YELP_H }).then((r) => r.json());
const revs = await fetch(`https://api.yelp.com/v3/businesses/${biz.id}/reviews`, { headers: YELP_H })
.then((r) => r.json()).then((d) => d.reviews || []);
profiles.push({
name: detail.name, rating: detail.rating, reviews: detail.review_count,
price: detail.price || "N/A",
categories: (detail.categories || []).map((c) => c.title).join(", "),
excerpt: revs[0]?.text?.slice(0, 200) || "No reviews",
});
await new Promise((r) => setTimeout(r, 500));
}
// 3. Personas
const archetypes = [
{ name: "Busy Parent", desc: "Family of 4. Values speed, portions, kid-friendly. $30-50." },
{ name: "Foodie Millennial", desc: "Late 20s, Instagram. Quality, ambiance, unique menu." },
{ name: "Budget Student", desc: "Eats out daily. Price, late hours, portions. Under $15." },
{ name: "Local Regular", desc: "10+ years in neighborhood. Consistency, community, weekly." },
];
const personaIds = [];
for (const arch of archetypes) {
const p = await fetch(`${MV_BASE}/personas`, {
method: "POST", headers: MV_H,
body: JSON.stringify({
name: `Yelp: ${arch.name}`, description: `${arch.desc} Location: ${LOCATION}.`,
psychographic: { dining_persona: arch.name },
}),
}).then((r) => r.json());
personaIds.push({ id: p.id, name: arch.name });
await new Promise((r) => setTimeout(r, 200));
}
// 4. Stimulus + Focus Group
const cards = profiles.map((p, i) =>
`(${String.fromCharCode(65 + i)}) ${p.name}\n ${p.rating}/5 (${p.reviews}) | ${p.price}\n "${p.excerpt}"`
).join("\n\n");
const fg = await fetch(`${MV_BASE}/focus-groups`, {
method: "POST", headers: MV_H,
body: JSON.stringify({
name: `${CATEGORY} Preference — ${LOCATION}`,
persona_ids: personaIds.map((p) => p.id),
questions: [
{ type: "ranking", text: `Rank these:\n\n${cards}` },
"Why is your #1 first?",
"What would make you switch?",
"Describe your ideal ordering experience.",
"What ONE thing would make you try a new place?",
],
context: cards,
responses_per_persona: 3,
}),
}).then((r) => r.json());
let data;
for (let i = 0; i < 20; i++) {
await new Promise((r) => setTimeout(r, 5000));
data = await fetch(`${MV_BASE}/focus-groups/${fg.id}`, { headers: MV_H }).then((r) => r.json());
if (data.status === "completed") break;
}
for (const resp of data.responses || []) {
const name = personaIds.find((p) => p.id === resp.persona_id)?.name || "?";
console.log(`\n[${name}] ${(resp.question || "").slice(0, 60)}`);
console.log(` → ${(resp.answer || "").slice(0, 300)}`);
}
Example Output
{
"study": "Pizza Preference — Brooklyn, NY",
"rankings_by_persona": {
"Busy Parent": ["Di Fara Pizza", "L&B Spumoni Gardens", "Juliana's"],
"Foodie Millennial": ["Roberta's", "Juliana's", "Di Fara Pizza"],
"Budget Student": ["L&B Spumoni Gardens", "Best Pizza", "Di Fara Pizza"],
"Local Regular": ["Di Fara Pizza", "Totonno's", "L&B Spumoni Gardens"]
},
"key_insights": [
{
"persona": "Busy Parent",
"why_first": "Di Fara — consistent quality, large slices kids love, and they don't mind if my 4-year-old makes a mess. The wait is the only downside."
},
{
"persona": "Foodie Millennial",
"why_first": "Roberta's — the wood-fired crust, the Bushwick vibe, the seasonal toppings. It photographs well and tastes even better."
},
{
"persona": "Budget Student",
"why_first": "L&B — a square slice is $3.50 and it's HUGE. Best dollar-to-pizza ratio in Brooklyn."
},
{
"persona": "Local Regular",
"switch_trigger": "If a new place opened within walking distance with consistent quality and the owner actually remembered my name? I'd give it a shot."
}
]
}
Error Handling
3 review excerpt limit
3 review excerpt limit
Yelp API returns at most 3 review excerpts. For richer stimulus, supplement with Yelp business description and category data. Never scrape additional reviews.
Business detail rate limits
Business detail rate limits
Each
GET /businesses/{id} counts against your quota. For 6 competitors with details + reviews, that’s 12 calls. Cache results if running repeatedly.Location ambiguity
Location ambiguity
Yelp’s
location param accepts city names, zip codes, or addresses. Use zip codes for precision in dense metro areas with overlapping neighborhood names.Ranking too many options
Ranking too many options
Focus Groups work best with 4-6 ranking options. More than 8 creates decision fatigue. Filter to top competitors by review count before running.