Every online shop has the same graveyard: products photographed nicely, priced right, and described with three words. “Hoodie bagus premium.” “Best quality.” Or nothing at all, because writing 40 descriptions felt like homework.
Meanwhile, marketplaces rank partly on listing completeness, and buyers convert on listings that answer their questions. Descriptions are boring to write and cheap to generate — which makes them a real service you can sell, not a hypothetical one.
This guide gives you the working pipeline: a spreadsheet of product facts goes in, a checked listing comes out, for about $0.002 per product. Then the part most tutorials skip: how to turn it into paying clients and a monthly retainer.
Why this works as a service (and for whom)
The buyer isn’t a marketing director. It’s one of these:
- A marketplace seller (Shopee, Tokopedia, Amazon, Etsy) with 50–500 SKUs whose old listings are thin
- A Shopify/WooCommerce shop owner migrating products or adding a new line
- A wholesaler/catalog business that needs reseller-ready product sheets
Their pain is concrete: blank description fields they never get around to, and listings that lose to competitors who bothered. The deliverable is concrete too: every product with a title, description, bullet points and specs, in their voice, with nothing invented.
What you’re selling is not “AI writing”. It’s a description writer who reads their actual product data and never makes things up — plus the hours they didn’t spend.
What you need
| Item | Cost | Notes |
|---|---|---|
| Python 3.10+ | $0 | Any computer |
| Gemini API key | $0 to start | aistudio.google.com → “Get API key” |
google-genai, pydantic | $0 | pip install |
| Client’s product sheet | $0 | CSV/Excel export from their marketplace or store admin |
Pricing as of October 2026 (verify at ai.google.dev/pricing; Google changes models and prices often):
- Gemini 3.5 Flash-Lite (paid tier): $0.30 per 1M input tokens, $2.50 per 1M output tokens
- Free tier: fine for testing with invented products. Don’t send a real client’s catalog through it — Google’s terms allow free-tier content to be used to improve its products.
How the cost works out
Per product, the script sends: the client’s style guide and example listing (~350 tokens), the product’s facts row (~120 tokens), and gets back a listing (~450 output tokens, including Flash-Lite’s minimal thinking).
- Input: ~470 × $0.30/1M = $0.00014
- Output: ~450 × $2.50/1M = $0.0011
- Total: about $0.0013 per product, call it $0.002
So 500 products ≈ $1. Even 5,000 products is under $10. Your API cost is noise; the value is in the setup and the review.
Step 1: The input format
Everything the writer needs goes in one CSV. You build this with the client from their store export:
sku,name,facts,keywords,style_notes
HD-001,Hoodie Black,fleece interior; unisex; sizes M-XXL; cotton blend 330gsm,hoodie hitam; jaket hoodie,casual young
TS-204,T-Shirt White Oversize,combed cotton 30s; oversize fit; pre-shrunk,kaos oversize; kaos putih,streetwear
MUG-11,Ceramic Mug 350ml,dishwasher safe; lead-free glaze; gift box included,mug kado; cangkir,instagrammable
The columns that matter:
- facts — only true things, from the client. The AI never adds facts; it only arranges them. This is the difference between a service and a liability.
- keywords — what buyers actually search (get these from the seller’s marketplace search suggestions, not from a keyword tool).
- style_notes — one or two words on voice per product line, plus one global style block in the script.
Step 2: The script
Save as descriptions.py:
import argparse, csv, os, sys, time
from pathlib import Path
from google import genai
from google.genai import errors, types
from pydantic import BaseModel
MODEL = os.environ.get("DESC_MODEL", "gemini-3.5-flash-lite")
STYLE_BLOCK = """Shop voice: friendly, concrete, no hype words (no "best", "premium quality",
"super"). Short sentences. Bahasa Indonesia, casual but polite. Never invent a fact,
measurement, material or certification that is not in FACTS. If FACTS is thin, write a
shorter description instead of padding."""
class Listing(BaseModel):
title: str # marketplace title, <= 100 chars, keywords included naturally
description: str # 2-3 short paragraphs
bullets: list[str] # 3-5 factual bullet points, each <= 12 words
specs: dict[str, str] # only specs present in FACTS
invented: list[str] # anything you were tempted to add but couldn't source
def build_prompt(style, row):
return f"""You write marketplace product listings.
{style}
Shop-wide style:
{STYLE_BLOCK}
Rules:
- title: include the product name and at most 2 keywords, no keyword stuffing, no ALL CAPS.
- description: paragraphs only, no emoji, no "Buy now!!".
- bullets: only facts from FACTS, rephrased for scanning.
- specs: key/value pairs strictly from FACTS (e.g. "Material: cotton blend").
- In "invented", list anything missing that a buyer would ask about (size chart, material).
This list helps the shop owner, so be specific.
- FACTS contains everything you may claim. Nothing else.
FACTS: {row['facts']}
KEYWORDS: {row['keywords']}
STYLE NOTES: {row['style_notes']}
PRODUCT NAME: {row['name']}
SKU: {row['sku']}"""
def generate(prompt):
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=Listing,
thinking_config=types.ThinkingConfig(thinking_level="minimal"),
temperature=0.4, # a little variety between products, not chaos
),
)
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(lst: Listing, row) -> list[str]:
"""Python-side checks. The most important one: no invented facts in the visible copy."""
problems = []
facts_words = {w.lower().strip(";,.") for w in row["facts"].replace(";", " ").split()}
name_words = {w.lower() for w in row["name"].split()}
# every spec value must trace back to the facts or the product name
for key, val in lst.specs.items():
tokens = [t for t in val.lower().replace(",", " ").split() if len(t) > 3]
if tokens and not any(t in facts_words or t in name_words for t in tokens):
problems.append(f"spec '{key}: {val}' not traceable to facts")
if len(lst.title) > 100:
problems.append("title too long")
if not lst.bullets:
problems.append("no bullets")
return problems
def main():
ap = argparse.ArgumentParser()
ap.add_argument("products")
ap.add_argument("--out", default="listings.csv")
args = ap.parse_args()
client = genai.Client() # reads GEMINI_API_KEY
with open(args.products, newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
out_rows, problems_total = [], 0
for row in rows:
lst = generate(build_prompt(STYLE_BLOCK, row))
if lst is None:
out_rows.append({"sku": row["sku"], "status": "REVIEW",
"problems": "API failed", "title": "", "description": "",
"bullets": "", "specs": ""})
problems_total += 1
continue
problems = check(lst, row)
status = "REVIEW" if problems else "OK"
problems_total += bool(problems)
out_rows.append({
"sku": row["sku"], "status": status, "problems": "; ".join(problems),
"title": lst.title, "description": lst.description,
"bullets": " | ".join(lst.bullets),
"specs": "; ".join(f"{k}: {v}" for k, v in lst.specs.items()),
"missing_info": "; ".join(lst.invented),
})
print(f"{status:6} {row['sku']:12} {lst.title[:48]}")
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{len(rows) - problems_total}/{len(rows)} OK, {problems_total} need review")
print(f"Open {args.out} and the 'missing_info' column: that's the client's homework list.")
if __name__ == "__main__":
main()
Run it:
python descriptions.py products.csv
Two outputs matter:
listings.csv— ready-to-paste titles, descriptions, bullets and specs, sorted by status.- The
missing_infocolumn — the model listing what a buyer would ask but the facts don’t answer. In practice this column is the most valuable thing you deliver, because it’s the client’s own product-data gap list, generated for free.
Step 3: Quality control (the part that keeps clients)
- Check every listing for the first batch. Look for one thing: claims not in the facts. The spec check catches most; your eyes catch the rest.
- Get one example listing approved. Before running 300 products, have the client approve a single listing. Lock that tone. It’s much cheaper than rewriting a whole catalog.
- Batch by category. Running hoodies together keeps the voice consistent across the batch.
- Upload for them, once. Marketplace bulk-upload templates (Shopee and Tokopedia both have them) take a CSV. Filling their template from yours and uploading is 15 minutes of work that makes you look like a magician.
Honest cost breakdown
500-product catalog, one-time project:
| Item | Cost |
|---|---|
| Gemini API, 500 × ~$0.002 | ~$1 |
| Re-runs after client edits facts | ~$0.50 |
| Your time: building the facts CSV with the client | 3–6 h |
| Your time: review pass + upload | 2–3 h |
| Total cash cost | under $3 |
The client is paying for your hours and judgment, which are the only scarce inputs. The AI is a $1 component.
How to Sell This as a Service
The demo is the pitch. Take 5 real products from the prospect’s own shop (public listings are public). Run them through your pipeline with facts scraped from their own listing. Send back a before/after: their three-word description vs. the full listing. This takes you twenty minutes and converts far better than any pitch document.
Offer structure:
- Catalog cleanup (one-off): all current products rewritten, delivered as a filled marketplace upload template or store import CSV. Price by catalog size — as a test zone for an Indonesian seller, something like Rp 2,000–5,000 per listing for a 100+ SKU catalog, with a floor of around Rp 500,000 for small jobs. Adjust to your market; there’s no established rate card.
- Monthly retainer: new products get described within 48 hours of the client sending raw facts + photos. This is the better business: it’s recurring and the client’s effort stays near zero. As a starting range, Rp 300,000–750,000/month covers up to ~50 new listings.
- Language add-on: the same pipeline translates listings to English (or back) for cross-border selling. Nearly free for you to run — same script, one prompt change.
Where to find clients: seller communities (Facebook groups for Shopee/Tokopedia sellers, local UMKM WhatsApp groups), Shopify store owners in a niche you know, and print-on-demand or wholesale businesses with catalog-sized problems. One wholesaler with resellers is worth ten single shops, because their catalog never stops growing.
What to promise: “Every product described from your real product data, with a list of what information you’re still missing.” Never promise “SEO-optimized rankings” — you can’t guarantee marketplace rank, and overpromising is how service businesses die.
Get the facts sign-off in writing. One line in the invoice: “Descriptions generated strictly from product facts provided by client.” It protects you when the client’s own facts were wrong.
Reality check: what does NOT work
- Fully automatic publishing. Never push generated copy to a live store without a human pass. One hallucinated “medical-grade” claim can cost a client their account.
- Inventing specs to fill space. The
invented/missing_infomechanism exists so the model reports gaps instead of filling them. If your output reads longer but thinner than the facts support, tighten the prompt. - Selling “AI content” instead of deliverables. Nobody buys AI. They buy “my 400 products done by Friday”. Talk in deliverables and deadlines.
- Competing on price per word. Race-to-the-bottom territory. Compete on the upload-template delivery and the missing-info report — things a $5/month AI writing tool doesn’t do.
- Thin facts in, thin listing out. If a client won’t give you real product data, the output will be fluff and they’ll blame the tool. Walk away or charge extra for the research.
- Free tier for client catalogs. Client product lines are business-sensitive. Paid tier only.
Final checklist
- One example listing approved by the client before the batch run
- Facts CSV built from the client’s data, not the internet
- Every
REVIEWrow resolved, spot-check 1 in 10OKrows -
missing_infocolumn delivered as the client’s homework list - Paid-tier API key, client’s data protected
- Delivered as an upload template the client can actually import
Try it on your own products first, or on a friend’s shop for free. If the before/after makes them say “can you do the rest?”, you have a service.