Real estate agents write. A lot. Every listing needs a description — portal rules, buyer expectations, and the agent’s own marketing all demand it — and every photo set needs work: straightening, brightening, background cleanup, watermarks. For an agent closing a few properties a month across rent and sale listings, this is 10–20 hours of work nobody entered the business to do.
Both halves are now nearly free to produce. That combination — recurring need, real hours, cheap automation — is exactly what a monthly service is made of. This guide builds the pipeline: a listing’s facts and photos go in, a checked description and cleaned photos come out, for about $0.02 per listing in AI costs. Then, how to sell it as a retainer that survives month after month.
What the pipeline does
For each listing:
- Description. The agent’s fact sheet (or their existing half-written draft) goes to Gemini, which writes a portal-ready description in the agent’s voice — and a Python checker verifies every claim traces to the facts, because a made-up “24-hour security” line in a property ad is a lawsuit-shaped problem.
- Photos. Open-source
rembgremoves cluttered backgrounds for feature shots; Pillow handles the boring fixes portals care about: brightness, straightening, sizing to spec, watermark placement. - Everything lands in one folder per listing, ready to upload.
The agent’s work drops to: fill in the fact sheet, glance at the output, upload.
What you need
| Item | Cost | Notes |
|---|---|---|
| Python 3.10+ | $0 | Any computer; 8 GB RAM is enough for rembg |
| Gemini API key | $0 to start | aistudio.google.com → “Get API key” |
google-genai, pydantic, rembg, pillow | $0 | pip install (first rembg run downloads a ~170 MB model) |
| The agent’s fact-sheet habit | $0 | A one-page template per listing — see below |
Pricing as of October 2026 — verify at ai.google.dev/pricing:
- Gemini 3.5 Flash-Lite (paid tier): $0.30 per 1M input tokens, $2.50 per 1M output tokens
- Free tier: testing only, with invented listings. Real property details are client business data; paid tier only.
Cost per listing
- Description: ~900 input tokens (facts + style + instructions), ~450 output → about $0.0015
- Ten photos through rembg: $0 (runs locally on your CPU)
- Total: roughly $0.002 per listing, call it $0.02 with re-runs and a generous photo set. Fifty listings a month is under $1.
Step 1: The fact sheet
Every claim in the description must come from this. Template the agent fills in (5 minutes per listing):
listing_id,type,facts,highlights,language
R-2041,rent_house,"2 bedrooms; 1 bathroom; carport 1 car; semi-furnished; 120m2 land 90m2 building; Jl. Kenanga, Bandung; 15 min to Pasteur toll; AC 2 units",near Cihampelas Walk; quiet gang,id
S-1187,shopfront,"5m frontage; 60m2; mezzanine; 3-phase electricity; busy one-way street; parking 2 cars",high visibility; food business ready,id
highlights is the agent’s own selling angle — the AI uses it, but never invents beyond it.
Step 2: The script
Save as listing.py. It writes the description with claim-checking, then processes every photo in the listing’s folder.
import argparse, csv, os, sys, time
from pathlib import Path
from google import genai
from google.genai import errors, types
from PIL import Image, ImageEnhance, ImageOps
from pydantic import BaseModel
MODEL = os.environ.get("LISTING_MODEL", "gemini-3.5-flash-lite")
class Description(BaseModel):
title: str # portal title <= 70 chars
body: str # 3-4 short paragraphs
bullet_points: list[str]
missing_info: list[str] # what buyers will ask that the facts don't answer
DESC_INSTRUCTIONS = """You write property listings for portals ( Rumah123 / Lamudi style ).
Rules:
- Use ONLY facts and highlights provided. Never invent amenities, security, views,
distances, or legal status. If a buyer would want a fact that's missing, list it
in "missing_info" instead of guessing.
- Warm, concrete, no hype ("mewah megah eksklusif" spam is out). Match the agent's language.
- body: 3-4 short paragraphs. First paragraph = what and where. Last = practical next step.
- bullet_points: 4-6 factual bullets.
- title: property type + area + key spec, <= 70 characters.
"""
def write_description(client, row):
prompt = f"""{DESC_INSTRUCTIONS}
FACTS: {row['facts']}
AGENT HIGHLIGHTS: {row['highlights']}
LANGUAGE: {row['language']}
TYPE: {row['type']}"""
for attempt in range(3):
try:
resp = client.models.generate_content(
model=MODEL, contents=prompt,
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=Description,
thinking_config=types.ThinkingConfig(thinking_level="minimal"),
temperature=0.4,
),
)
except errors.APIError as e:
print(f" API error {e.code} (attempt {attempt+1})", file=sys.stderr)
time.sleep(5 * (attempt + 1))
continue
if resp.parsed:
return resp.parsed
return None
def check(desc: Description, row) -> list[str]:
"""Every bullet must trace to the facts/highlights text."""
source_words = {w.lower().strip(";,.") for src in (row["facts"], row["highlights"])
for w in src.replace(";", " ").split() if len(w) > 3}
problems = []
for b in desc.bullet_points:
tokens = [t.lower().strip(";,.") for t in b.split() if len(t) > 3 and not t.isdigit()]
if tokens and not any(t in source_words for t in tokens):
problems.append(f"bullet not traceable: '{b[:40]}...'")
if len(desc.title) > 70:
problems.append("title too long")
return problems
# ---- photos ---------------------------------------------------------------
def fix_photo(img: Image.Image) -> Image.Image:
"""The fixes portals and buyers actually notice."""
img = ImageOps.exif_transpose(img) # correct phone rotation
img = ImageEnhance.Brightness(img).enhance(1.12) # interiors are always dark
img = ImageEnhance.Contrast(img).enhance(1.06)
return img
def process_photos(client, listing_dir: Path):
out = listing_dir / "processed"
out.mkdir(exist_ok=True)
for p in sorted(listing_dir.glob("*.jpg")) + sorted(listing_dir.glob("*.jpeg")) + \
sorted(listing_dir.glob("*.png")):
img = fix_photo(Image.open(p).convert("RGB"))
name = p.stem.lower()
if "front" in name or "facade" in name or "eksterior" in name:
img.thumbnail((1600, 1600))
img.save(out / p.name, quality=88)
continue # keep the facade as-is: context sells
if "room" in name or "interior" in name:
# interior detail shots: drop the cluttered background for a clean cutout
try:
from rembg import remove
cut = remove(img)
cut.save(out / f"{p.stem}_cutout.png")
except Exception as e:
print(f" rembg failed on {p.name}: {e}", file=sys.stderr)
img.thumbnail((1600, 1600))
img.save(out / p.name, quality=88)
print(f" photo: {p.name}")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("facts_csv")
ap.add_argument("--photos-dir", default="photos") # photos/<listing_id>/*.jpg
ap.add_argument("--out", default="descriptions.csv")
args = ap.parse_args()
client = genai.Client() # reads GEMINI_API_KEY
with open(args.facts_csv, newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
out_rows = []
for row in rows:
desc = write_description(client, row)
if desc is None:
out_rows.append({"listing_id": row["listing_id"], "status": "REVIEW",
"problems": "API failed", "title": "", "body": "", "bullets": ""})
continue
problems = check(desc, row)
status = "REVIEW" if problems else "OK"
out_rows.append({"listing_id": row["listing_id"], "status": status,
"problems": "; ".join(problems), "title": desc.title,
"body": desc.body, "bullets": " | ".join(desc.bullet_points),
"missing_info": "; ".join(desc.missing_info)})
print(f"{status:6} {row['listing_id']} {desc.title[:50]}")
pdir = Path(args.photos_dir) / row["listing_id"]
if pdir.exists():
process_photos(client, pdir)
with open(args.out, "w", newline="", encoding="utf-8-sig") as f:
w = csv.DictWriter(f, fieldnames=out_rows[0].keys())
w.writeheader()
w.writerows(out_rows)
print(f"\n-> {args.out}; check missing_info: that's the agent's list of unanswered buyer questions")
if __name__ == "__main__":
main()
python listing.py facts.csv
Run it on one real listing first and read the description as a buyer would. If it sounds like the agent, ship it. If it sounds like a brochure, tighten DESC_INSTRUCTIONS — the fix is in the instructions, never in rewriting by hand.
Honest cost breakdown
One agent, 40 listings/month, ~10 photos each:
| Item | Monthly |
|---|---|
| Gemini descriptions, 40 × ~$0.002 + re-runs | ~$0.15 |
| rembg + Pillow | $0 (local CPU) |
| Your time: fact-sheet template setup, review, delivery | 3–5 h one-time + 1–2 h/month |
| Total cash cost | under $1/month |
The agent’s side: 10–20 hours of writing and photo fiddling becomes roughly 1–2 hours of fact sheets and uploads. That gap is what the retainer prices against.
How to Sell This as a Service
Real estate is a retainer business by nature — agents don’t have one listing, they have a flow, and they’ll happily pay monthly for a back office that never lets a listing go up thin.
The offer:
- First listing free (the demo). Take one of the agent’s current listings, run the pipeline, deliver the description plus cleaned photos within a day. Compare it live against what they posted. This converts.
- Monthly retainer: every new listing gets a description in the agent’s voice within 24 hours of the fact sheet, photos processed to portal spec, and a monthly report of the
missing_infopatterns across their listings (e.g. “you never specify electricity capacity — buyers ask every time”).
Pricing. Anchor on their hours and their deal size. A single commission dwarfs any plausible monthly fee, which means the real decision criterion for the agent is “does my listing go up faster and look better” — so price for easy yes. As a test zone: Rp 500,000–1,500,000/month for up to ~20 listings (Indonesian market), scaling with volume; international clients (US/UK agents) sustain $150–400/month for the same work. Validate against local rates and the agent’s actual listing volume; the first agent sets your real price.
Who to approach: agents who post often and thin — Instagram/portal listings with two blurry photos and one sentence. Property management offices with rental flows. Newer agents who need every listing to compete against established names.
What to promise: “Your listings up faster, described from your facts, photos cleaned to portal spec.” Never “AI-generated listings that sell faster” — you can’t promise sales outcomes, and an agent who feels oversold on listing #1 won’t be a client in month #2.
Reality check: what does NOT work
- Inventing features. The claim check exists because a hallucinated “swimming pool” or “24-hour security” in a property ad isn’t a typo — it’s misrepresentation with the agent’s name on it. Never skip the
check(). - Fully AI-generated interiors. Generating or heavily “beautifying” rooms beyond brightness/contrast crosses from enhancement into misrepresentation, and portals increasingly ban it. Fix lighting and rotation; don’t repaint the house.
- Over-cutting photos.
rembgcutouts suit feature/room detail shots; the facade must stay real — buyers use it to judge the building, and a floating house on white looks like something is being hidden. - Skipping the agent’s voice. Deliver one sample description for approval before batch-running. An agent whose listings all suddenly sound the same will churn; one whose voice got cleaner stays.
- Bad input photos. A phone photo taken against a bright window will still be a bad photo after processing. The service includes a one-page photo checklist (daylight, landscape, corner-to-corner, no mirrors-with-people).
- Free tier for client listings. Property details and addresses are client business data. Paid tier, always.
Final checklist
- Fact-sheet template agreed with the agent, one sample listing approved
- Claim check passing on every bullet;
missing_infodelivered as homework - Facades untouched, interiors brightened, cutouts only for detail shots
- Paid-tier key, budget alert set
- Portal specs (image size, description length) verified for each portal used
- Delivery SLA agreed: fact sheet in → output within 24 h
Do one listing for an agent you know, free, this week. When they ask “can you do this for everything I post?”, the retainer conversation has already started — and the pipeline behind it costs you about a dollar a month to run.