Scenario
Customers at your downtown café are different from customers at your suburban location. Downtown draws office workers during lunch; suburbs get families on weekends. You extract review text with location context, identify distinct customer profiles per location, then create geo-specific Mavera personas. These local personas power hyper-targeted marketing for each location. Flow: Google reviews + location data → Identify per-location customer profiles → MaveraPOST /personas → Geo-specific personas
Code
import os, requests, time, re
from collections import Counter
GOOG = os.environ["GOOGLE_ACCESS_TOKEN"]
ACCT = os.environ["GOOGLE_ACCOUNT_ID"]
MV = os.environ["MAVERA_API_KEY"]
GB_BASE = "https://mybusiness.googleapis.com/v4"
MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
GB_H = {"Authorization": f"Bearer {GOOG}"}
STAR_MAP = {"ONE": 1, "TWO": 2, "THREE": 3, "FOUR": 4, "FIVE": 5}
# 1. Locations + reviews
locations = requests.get(f"{GB_BASE}/{ACCT}/locations",
headers=GB_H, params={"pageSize": 100}).json().get("locations", [])
persona_ids = []
for loc in locations[:8]:
loc_id = loc.get("name", "")
loc_name = loc.get("locationName", "Unknown")
city = loc.get("address", {}).get("locality", "Unknown")
state = loc.get("address", {}).get("administrativeArea", "")
category = loc.get("primaryCategory", {}).get("displayName", "Business")
r = requests.get(f"{GB_BASE}/{loc_id}/reviews",
headers=GB_H, params={"pageSize": 50})
if r.status_code != 200:
continue
reviews = r.json().get("reviews", [])
if len(reviews) < 5:
continue
# 2. Extract customer signals from review text
all_text = " ".join(rev.get("comment", "") for rev in reviews).lower()
signals = {
"family": len(re.findall(r'\b(family|kids|children|stroller)\b', all_text)),
"business": len(re.findall(r'\b(meeting|office|work|lunch break|colleague)\b', all_text)),
"tourist": len(re.findall(r'\b(tourist|visiting|trip|vacation|hotel)\b', all_text)),
"local_regular": len(re.findall(r'\b(regular|every week|always come|our spot)\b', all_text)),
"date_night": len(re.findall(r'\b(date|anniversary|romantic|evening)\b', all_text)),
}
dominant_segment = max(signals, key=signals.get) if any(signals.values()) else "general"
avg_rating = sum(STAR_MAP.get(r.get("starRating", "FIVE"), 5) for r in reviews) / len(reviews)
# 3. Create geo-specific persona
sample_quotes = [rev.get("comment", "")[:150] for rev in reviews if rev.get("comment")][:3]
p = requests.post("https://app.mavera.io/api/v1/personas", headers=MV_H, json={
"name": f"GBP: {loc_name} — {dominant_segment.replace('_', ' ').title()}",
"description": (
f"Customer of {loc_name} in {city}, {state}. Category: {category}. "
f"N={len(reviews)} reviews, avg {avg_rating:.1f}/5. "
f"Dominant segment: {dominant_segment}. "
f"Signal scores: {', '.join(f'{k}: {v}' for k, v in sorted(signals.items(), key=lambda x: -x[1])[:3])}. "
f"Sample: \"{sample_quotes[0][:100]}...\""
),
"demographic": {
"location": f"{city}, {state}",
"customer_type": dominant_segment,
},
"psychographic": {
"visit_context": dominant_segment,
"satisfaction": "high" if avg_rating >= 4 else "mixed" if avg_rating >= 3 else "low",
},
}).json()
persona_ids.append({"id": p["id"], "location": loc_name, "city": city, "segment": dominant_segment})
print(f"Persona: {p['id']} — {loc_name} ({city}) → {dominant_segment}")
time.sleep(0.5)
print(f"\nCreated {len(persona_ids)} local personas")
const GOOG = process.env.GOOGLE_ACCESS_TOKEN;
const ACCT = process.env.GOOGLE_ACCOUNT_ID;
const MV = process.env.MAVERA_API_KEY;
const GB_BASE = "https://mybusiness.googleapis.com/v4";
const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
const GB_H = { Authorization: `Bearer ${GOOG}` };
const STAR_MAP = { ONE: 1, TWO: 2, THREE: 3, FOUR: 4, FIVE: 5 };
const locations = (await fetch(`${GB_BASE}/${ACCT}/locations?pageSize=100`, { headers: GB_H })
.then((r) => r.json())).locations || [];
const personaIds = [];
for (const loc of locations.slice(0, 8)) {
const locId = loc.name || "";
const locName = loc.locationName || "Unknown";
const city = loc.address?.locality || "Unknown";
const state = loc.address?.administrativeArea || "";
const res = await fetch(`${GB_BASE}/${locId}/reviews?pageSize=50`, { headers: GB_H });
if (!res.ok) continue;
const reviews = (await res.json()).reviews || [];
if (reviews.length < 5) continue;
const allText = reviews.map((r) => r.comment || "").join(" ").toLowerCase();
const signals = {
family: (allText.match(/\b(family|kids|children)\b/g) || []).length,
business: (allText.match(/\b(meeting|office|work|lunch break)\b/g) || []).length,
tourist: (allText.match(/\b(tourist|visiting|trip|vacation)\b/g) || []).length,
local_regular: (allText.match(/\b(regular|every week|always come)\b/g) || []).length,
};
const dominant = Object.entries(signals).sort(([, a], [, b]) => b - a)[0]?.[0] || "general";
const avg = reviews.reduce((s, r) => s + (STAR_MAP[r.starRating] || 3), 0) / reviews.length;
const p = await fetch("https://app.mavera.io/api/v1/personas", {
method: "POST", headers: MV_H,
body: JSON.stringify({
name: `GBP: ${locName} — ${dominant.replace(/_/g, " ")}`,
description: `Customer of ${locName} (${city}). N=${reviews.length}, avg ${avg.toFixed(1)}/5. Segment: ${dominant}.`,
demographic: { location: `${city}, ${state}`, customer_type: dominant },
psychographic: { visit_context: dominant },
}),
}).then((r) => r.json());
personaIds.push({ id: p.id, location: locName, city, segment: dominant });
console.log(`Persona: ${p.id} — ${locName} (${city}) → ${dominant}`);
await new Promise((r) => setTimeout(r, 500));
}
console.log(`\nCreated ${personaIds.length} local personas`);
Example Output
{
"personas_created": 5,
"local_profiles": [
{ "location": "Downtown Café", "city": "Austin", "segment": "business", "avg_rating": 4.5,
"insight": "Office workers on lunch. Care about speed, Wi-Fi, quiet spaces." },
{ "location": "Suburban Store", "city": "Round Rock", "segment": "family", "avg_rating": 4.1,
"insight": "Families on weekends. Care about kid-friendliness, parking, portion sizes." },
{ "location": "Airport Kiosk", "city": "Austin", "segment": "tourist", "avg_rating": 3.7,
"insight": "Travelers in a hurry. Care about grab-and-go, clear pricing, speed." },
{ "location": "University Ave", "city": "Austin", "segment": "local_regular", "avg_rating": 4.6,
"insight": "Regulars who come weekly. Care about consistency, loyalty, and knowing the staff." }
]
}
Error Handling
Review access per location
Review access per location
Each location requires separate review API calls. Batch across locations with delays to stay within 300 req/min.
Sparse review text
Sparse review text
Some Google reviews are ratings-only with no text. Filter
comment: null reviews before persona creation.Regex limitations
Regex limitations
Simple keyword matching for segments is approximate. For better accuracy, send review text to Mave for classification.