Search “bengkel motor” or “klinik gigi” in Google Maps in any Indonesian city and tap through the results. Next to the top three you will find profiles with no opening hours, no phone number, two blurry photos, a name that is spelled differently from the signboard, and four reviews from 2021. The owner has no idea, because they never search for their own business.

Those profiles lose calls every day to competitors who simply filled in the form properly. Fixing that is a service you can sell this week: find weak profiles in bulk with the Google Places API, send the owner a short message with their own gaps, then do the setup for a fixed fee and keep them on a small monthly plan.

It is not glamorous, but it is real work businesses already pay for, the tools cost close to $0, and most of the job is repeatable.

In this guide:

  • what actually moves a Google Maps listing (and what doesn’t)
  • a Python script that scans 60 businesses per search and scores how badly each profile needs help
  • a Gemini step that writes the pitch and the 750-character description without inventing numbers
  • pricing, costs, realistic earnings
  • the risks: suspended profiles, the keyword-stuffing temptation, fake reviews, and WhatsApp bans

What Google says ranks a local listing

Google’s own help page (“Tips to improve your local ranking on Google”) names three factors: relevance, distance, and prominence. Distance you can’t change. Relevance comes from the categories, description, services and the information you fill in. Prominence comes from reviews (count and rating), links and general web presence.

Google also says plainly that businesses with complete and accurate information are easier to match to searches. That is the honest core of this service: complete, accurate, active profiles. Say it to clients exactly like that. Never promise “#1 on Google Maps.” A clinic 6 km from the searcher won’t beat one 500 m away just because you added photos.

One more fact that creates demand: in March 2024 Google shut down the free websites built with Business Profile (the business.site pages). Lots of small businesses lost the only “website” they had, and many profiles still have no website at all.

What people pay

Search Fiverr for “google business profile optimization” or “google my business setup.” You will typically see cheap basic-setup gigs in the tens of dollars and pricier “full optimization” packages (posts, photos, citations) in the low hundreds. Check current listings yourself; prices vary widely and the market moves.

Fiverr is a race to the bottom with sellers from everywhere. Local clients you can visit or call are easier and pay for trust. My pricing logic for Indonesian small businesses (not a market survey):

PackageWhat’s includedSuggested price
Profile auditWhatsApp message + 1-page report with their gapsFree (it’s your pitch)
Setup / fixClaim or recover access, verification, categories, hours, services, description, 15-30 photos, products, Q&A seedRp 500k-1.5 juta
Monthly care4 posts/month, new photos, review replies, hours for holidays, monthly insights screenshotRp 250-500k/month

The monthly plan is where it turns semi-passive: your scanner and Gemini do the drafting, you spend 1-2 hours per client per month.

What you need

ItemCostNotes
Google Cloud project with Places API (New) enabled + billing account$0 within the free capBilling must be on, but the free monthly usage cap covers prospecting (see below)
Python 3.10+, requests, google-genai$0Any laptop
Gemini API key$0 to startaistudio.google.com
A Google account for client access$0You will be added as Manager, never owner
Phone with a decent camera$0For on-site photos and video verification help

Places API pricing (check developers.google.com/maps/billing-and-pricing before relying on it): since March 2025 Google replaced the old $200 monthly credit with free usage caps per SKU. The fields we need (rating, review count, website, phone, opening hours) put Text Search in the Enterprise tier: about $35 per 1,000 requests with the first 1,000 requests per month free. Each request returns up to 20 places, so 1,000 free requests is up to 20,000 businesses scanned. Set a budget alert anyway.

Gemini pricing (check ai.google.dev/pricing): Gemini 3.5 Flash-Lite is $0.30 per 1M input tokens and $2.50 per 1M output tokens. A pitch plus a description is about 1,500 tokens in and 500 out, roughly $0.0017 per business.

Step 1: Set up the API keys

  1. console.cloud.google.com, create a project (e.g. gbp-prospecting), attach a billing account.
  2. APIs & Services, Library, enable Places API (New).
  3. Credentials, Create credentials, API key. Restrict it to Places API (New).
  4. Billing, Budgets & alerts: create a $5 budget alert so a bug in a loop can’t surprise you.
mkdir gbpgig && cd gbpgig
python3 -m venv .venv && source .venv/bin/activate
pip install requests google-genai

export MAPS_KEY="your-places-api-key"
export GEMINI_API_KEY="your-gemini-key"

# sanity check: should print a JSON list of places
curl -s -X POST "https://places.googleapis.com/v1/places:searchText" \
  -H "Content-Type: application/json" \
  -H "X-Goog-Api-Key: $MAPS_KEY" \
  -H "X-Goog-FieldMask: places.displayName,places.userRatingCount" \
  -d "{\"textQuery\": \"bengkel motor Sleman\", \"pageSize\": 3}"

The X-Goog-FieldMask header decides which fields you get and which SKU you pay for. Ask only for what you use.

Step 2: Scan a niche and score the gaps

Pick one niche in one city: “klinik gigi Bandung,” “bengkel mobil Bekasi,” “laundry kiloan Depok.” Results and photos stack into a portfolio, and the pitch gets sharper after the tenth business.

Save as scan.py:

import csv, os, sys, time
import requests

KEY = os.environ["MAPS_KEY"]
URL = "https://places.googleapis.com/v1/places:searchText"
FIELDS = ",".join([
    "places.id", "places.displayName", "places.formattedAddress",
    "places.businessStatus", "places.primaryType", "places.rating",
    "places.userRatingCount", "places.websiteUri", "places.nationalPhoneNumber",
    "places.regularOpeningHours", "places.photos", "places.googleMapsUri",
    "nextPageToken",
])

def search(query, max_pages=3):
    """Text Search returns max 20 places per page and max 60 per query."""
    body = {"textQuery": query, "pageSize": 20, "languageCode": "id", "regionCode": "ID"}
    for _ in range(max_pages):
        r = requests.post(URL, json=body, timeout=30,
                          headers={"X-Goog-Api-Key": KEY, "X-Goog-FieldMask": FIELDS})
        r.raise_for_status()
        data = r.json()
        yield from data.get("places", [])
        if "nextPageToken" not in data:
            break
        body["pageToken"] = data["nextPageToken"]
        time.sleep(2)

def score(p):
    gaps, pts = [], 0
    photos = len(p.get("photos", []))  # the API returns at most 10 photo refs
    reviews = p.get("userRatingCount", 0)
    if "nationalPhoneNumber" not in p:
        gaps.append("no phone number"); pts += 3
    if "regularOpeningHours" not in p:
        gaps.append("no opening hours"); pts += 3
    if photos < 3:
        gaps.append(f"only {photos} photos"); pts += 3
    elif photos < 10:
        gaps.append(f"only {photos} photos"); pts += 1
    if "websiteUri" not in p:
        gaps.append("no website link"); pts += 2
    if reviews < 20:
        gaps.append(f"only {reviews} reviews"); pts += 2
    if p.get("rating") and p["rating"] < 4.0:
        gaps.append(f"rating {p['rating']}"); pts += 1
    return pts, gaps

if __name__ == "__main__":
    query = sys.argv[1]
    rows = []
    for p in search(query):
        if p.get("businessStatus") != "OPERATIONAL":
            continue
        pts, gaps = score(p)
        rows.append({
            "score": pts,
            "name": p["displayName"]["text"],
            "address": p.get("formattedAddress", ""),
            "rating": p.get("rating", ""),
            "reviews": p.get("userRatingCount", 0),
            "phone": p.get("nationalPhoneNumber", ""),
            "gaps": "; ".join(gaps),
            "maps": p.get("googleMapsUri", ""),
            "place_id": p["id"],
        })
    if not rows:
        sys.exit("no operational businesses found, try another query")
    rows.sort(key=lambda r: r["score"], reverse=True)
    out = query.replace(" ", "_") + ".csv"
    with open(out, "w", newline="") as f:
        w = csv.DictWriter(f, fieldnames=rows[0].keys())
        w.writeheader(); w.writerows(rows)
    print(f"{len(rows)} businesses -> {out}")
    for r in rows[:10]:
        print(r["score"], r["name"], "|", r["gaps"])

Run it:

python scan.py "klinik gigi Bandung"
# example output (your numbers will differ):
# 58 businesses -> klinik_gigi_Bandung.csv
# 11 Klinik Gigi Sehat ... | no phone number; no opening hours; only 2 photos; no website link; only 4 reviews
# 9 drg. ... | no opening hours; only 1 photos; only 7 reviews

That run costs 3 requests. Do 20 neighbourhoods x 5 niches and you have used 300 of your 1,000 free requests.

Before you pitch, open each profile yourself. The API is a filter, not a verdict. Some “no phone” businesses list WhatsApp in the description or photos; some profiles show details on Maps that the API response left out. Skip businesses with a score below 5: they are already fine and won’t feel the pain.

Note the Google Maps Platform terms: you may store place_id long-term, but don’t build and resell a database of the other fields. Use the CSV for your own outreach and refresh it, don’t sell it.

Step 3: Let Gemini draft the pitch (with a number check)

Save as pitch.py. It reads the CSV, asks Gemini for a short Indonesian WhatsApp message, and rejects any draft that contains a number not present in the input data. LLMs love inventing “70% of customers search on Maps.”

import csv, os, re, sys
from google import genai

MODEL = os.environ.get("PITCH_MODEL", "gemini-3.5-flash-lite")
client = genai.Client()  # reads GEMINI_API_KEY

PROMPT = """Tulis pesan WhatsApp singkat (maks 80 kata, Bahasa Indonesia santai-sopan)
untuk pemilik usaha "{name}". Sebutkan kekurangan profil Google Maps mereka HANYA dari
data ini: {gaps}. Rating: {rating}, jumlah ulasan: {reviews}.
Aturan: jangan janjikan ranking atau peringkat 1. Jangan tulis angka atau statistik
yang tidak ada di data. Tawarkan cek gratis, tanpa harga. Tanda tangan: {me}."""

def numbers(text):
    return set(re.findall(r"\d+(?:[.,]\d+)?", text))

rows = list(csv.DictReader(open(sys.argv[1])))
me = os.environ.get("MY_NAME", "Andi")
for r in rows[: int(sys.argv[2]) if len(sys.argv) > 2 else 10]:
    if int(r["score"]) < 5:
        continue
    facts = PROMPT.format(name=r["name"], gaps=r["gaps"], rating=r["rating"] or "-",
                          reviews=r["reviews"], me=me)
    for attempt in range(3):
        text = (client.models.generate_content(model=MODEL, contents=facts).text or "").strip()
        invented = numbers(text) - numbers(facts)
        if not invented:
            break
    else:
        text = f"[CHECK MANUALLY, invented numbers: {invented}]\n" + text
    print(f"=== {r['name']} ({r['phone'] or 'no phone, find on signboard'})\n{text}\n")
python pitch.py klinik_gigi_Bandung.csv 15 > pitches.txt

Read every message and send them by hand, from your normal WhatsApp, a few per day. Adding your own line (“saya lewat depan klinik kemarin”) does more for replies than any prompt trick.

Step 4: Deliver the setup

When someone says yes, the actual work, roughly 2-4 hours per profile:

  1. Access first. The owner adds you via Business Profile, Menu, Business Profile settings, People and access as Manager. If nobody knows who owns the profile (often an ex-employee or the old web agency), the owner requests access through Google’s ownership request flow. That can take a week or more; price it in.
  2. Verification. Google increasingly asks for video verification: one continuous video showing the signboard or street, the inside of the business, and proof of management (keys, cash register, tools). Help the owner record it; it must be their real location.
  3. Name, address, phone exactly as the signboard. Consistency beats cleverness.
  4. Categories. One primary plus up to 9 additional. The primary category matters most; check what the top 3 competitors use.
  5. Hours, special hours (Lebaran, Natal, tanggal merah), services, products with photos, attributes.
  6. Description. Max 750 characters, no links, no promotional offers. Draft it with Gemini from facts the owner gives you, then edit.
  7. 15-30 real photos: exterior from the street (helps people find the door), interior, team, products. No stock images.
  8. Review link. Share the short “Ask for reviews” link from the profile and print it as a QR at the cashier.

For the description, a one-liner with the same client works fine:

from google import genai
facts = open("client_facts.txt").read()  # services, years open, area, what makes them different
text = genai.Client().models.generate_content(
    model="gemini-3.5-flash-lite",
    contents="Write a Google Business Profile description in Bahasa Indonesia, max 700 characters, "
             "no links, no prices, no superlatives like terbaik/termurah, only these facts:\n" + facts,
).text
print(len(text), "chars\n", text)

Costs and realistic earnings

CostPer month
Places API (within 1,000 free Enterprise requests)$0
Gemini (300 pitches + 10 descriptions + 40 posts)under $1
Phone data / transport to visit clientsRp 100-300k
Totalroughly Rp 300k at most

Time: about 3 hours per setup, 1-2 hours per care client per month, plus 3-5 hours a week of outreach early on.

Realistic numbers for someone starting from zero in one city (my estimate, not a promise): reply rates on cold WhatsApp messages to businesses are low, so expect something like 1-3 paying clients from every 50 well-targeted messages. Sending a few messages a day, month 1 is more like 1-3 setups (roughly Rp 0.5-3 juta), and less if your first clients are discounted for testimonials (see the end of this guide). If you keep that pace and about half take monthly care at Rp 300k, after three months you might have Rp 0.5-1.5 juta recurring for under 10 hours of work a month, on top of new setups. Some months you will close nothing. It is a solid side income, not a quit-your-job number until you have 20+ care clients or sell to agencies.

Risks: suspensions, grey tactics, and the line you don’t cross

Suspended profiles. Google sometimes suspends profiles after big edits: name changes, address changes, category swaps, many edits at once. A suspended profile disappears from Maps, and your client will blame you. Make changes gradually, keep everything matching the real signboard, take screenshots before and after, and know the reinstatement request form. Tell the client about this risk in writing before you start.

Keyword-stuffed names (the grey area). You will see “Klinik Gigi Murah Terbaik Bandung 24 Jam” ranking well. It violates Google’s guidelines (the name must match the real-world name), and it works for a while in many cities. But any competitor can “Suggest an edit,” and Google can suspend the profile. If the business actually renames its signboard and documents, that’s legitimate. If not, don’t sell it, and certainly don’t do it without written consent.

Virtual offices and fake locations. Profiles at a virtual office, coworking desk or an address where no staff work are not eligible. Service-area businesses (plumbers, AC servicing) working from home must hide the address. Creating fake locations to “cover the city” is a fast route to mass suspension. Don’t.

Fake or bought reviews: hard no. Writing reviews, buying them, paying customers per review, or “review gating” (only asking happy customers) all break Google’s policy. Google removes them, and in some markets now shows warning banners on profiles caught doing it. In the US, the FTC rule on fake reviews (in force since October 2024) allows civil penalties over $50,000 per violation. Asking every customer for an honest review is allowed and is the whole strategy.

Never own the client’s profile. The client is primary owner, you are manager. Holding a profile “hostage” when someone stops paying is how agencies earn bad reputations, and Google will side with the business.

WhatsApp bans. Blasting hundreds of unsaved numbers with the same text gets accounts restricted quickly. Send a handful a day, personalise each, and stop if someone says no. Indonesia’s Personal Data Protection Law (UU PDP No. 27/2022) is in force; using a public business number for one relevant offer is low risk, building and selling contact lists is not.

Reality check

Optimization doesn’t beat distance. A complete profile 5 km away still loses to a mediocre one next door for “near me” searches. Set expectations around more calls and direction requests, not rank.

Profiles with 3 reviews won’t jump just from photos. The review system you set up (QR at the cashier, asking every customer) matters more over months than anything you do in week one.

Posts are for customers, not rankings. Weekly posts keep the profile looking alive and help conversion; don’t sell them as an SEO lever.

Cheap clients churn. Many warungs won’t pay Rp 300k/month. Target businesses where one customer is worth a lot: dental clinics, workshops, wedding vendors, physiotherapy, kos-kosan.

Show results with their own numbers. The Performance tab shows calls, direction requests and website clicks. Screenshot it before you start and every month after. That is your renewal argument and your portfolio.

Model names change. If gemini-3.5-flash-lite is retired, set PITCH_MODEL to the current Flash-Lite model.

Final checklist

  • Places API (New) key restricted, $5 budget alert on
  • One niche, one city, scanned and manually checked
  • Pitches reviewed and sent by hand, a few per day
  • You are Manager, client is owner, risks explained in writing
  • NAP matches the signboard, changes made gradually, before/after screenshots
  • No stuffed names, no fake locations, no fake or incentivised reviews
  • Monthly Performance screenshot sent with each invoice

Start with three businesses you can walk into this week. Fix their profiles cheaply in exchange for a testimonial and before/after screenshots, then start charging full price from client four.