About This Code Showcase
This showcase highlights the core logic of Banana Lab's 3-agent automation pipeline — how Claude is called to research and write 50-prompt packs, how the site is rebuilt automatically, and how the weekly report is compiled from local state.
API credentials and Gmail passwords are stored in config.json (not shown). The state file (state.json) is the only database — no SQL server required.
Project File Structure
Banana Lab/
├── main.py ← Scheduler + CLI entry point
├── config.json ← API keys (not committed)
├── state.json ← Live data: products, sales (auto-managed)
├── requirements.txt ← anthropic, schedule, requests
│
├── agents/
│ ├── builder_agent.py ← Monday: research niche + generate 50 prompts
│ ├── site_agent.py ← Monday: generate product page + rebuild homepage
│ └── reporter_agent.py ← Sunday: email weekly P&L report
│
├── utils/
│ ├── state_manager.py ← CRUD wrapper for state.json
│ └── email_sender.py ← Gmail SMTP delivery
│
├── products/ ← Generated each Monday (auto-populated)
│ ├── {slug}.md ← Prompt pack (owner converts to PDF)
│ └── {slug}-listing.txt ← Ready-to-paste Gumroad listing copy
│
└── website/ ← Banana Lab showroom (auto-rebuilt)
├── index.html ← Homepage product grid
├── about.html ← About page
├── style.css ← Banana yellow + black theme
└── products/ ← Individual product landing pages
Builder Agent — Niche Research & Prompt Generation
The core of Banana Lab: three Claude API calls per product cycle. First, pick a unique niche. Second, plan five categories. Third, generate 50 prompts in two batches to stay within token limits.
def _research_idea(self, state):
existing = [p["title"] for p in state.get("products", [])]
prompt = f"""You are the product researcher for Banana Lab.
Existing products (do not duplicate):
{json.dumps(existing, indent=2) if existing else "None yet"}
Pick ONE high-demand prompt pack idea for small business owners.
Return ONLY valid JSON, no markdown fences:
{{
"title": "e.g. 50 ChatGPT Prompts for Restaurant Owners",
"slug": "url-friendly-slug-here",
"description": "2-3 sentence listing description that sells the pack",
"niche": "target niche label",
"price": 9.00,
"tags": ["tag1", "tag2", "tag3", "tag4", "tag5"],
"what_inside": ["bullet 1", "bullet 2", "bullet 3", "bullet 4"]
}}"""
msg = self.client.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
messages=[{"role": "user", "content": prompt}]
)
raw = msg.content[0].text.strip()
if raw.startswith("```"):
raw = raw.split("```")[1]
if raw.startswith("json"):
raw = raw[4:]
return json.loads(raw.strip())
def _generate_prompts(self, idea):
"""Generate 50 prompts in two batches of 25 to avoid output token limits."""
cat_msg = self.client.messages.create(
model="claude-sonnet-4-6",
max_tokens=256,
messages=[{"role": "user", "content":
f"List exactly 5 category names for a ChatGPT prompt pack targeting "
f"{idea['niche']} owners. Return ONLY a JSON array of 5 strings, no markdown."
}]
)
categories = json.loads(cat_msg.content[0].text.strip())
def _batch(cats, start_number):
batch_cats = "\n".join(f"- {c} (10 prompts)" for c in cats)
p = f"""Create ChatGPT prompts for: {idea['title']}
Write 10 prompts for EACH of these categories:
{batch_cats}
Rules: niche-specific, use [PLACEHOLDERS IN CAPS], number from {start_number}.
Return ONLY a valid JSON array: [{{"number": N, "category": "...", "prompt": "..."}}]"""
msg = self.client.messages.create(
model="claude-sonnet-4-6",
max_tokens=8000,
messages=[{"role": "user", "content": p}]
)
raw = msg.content[0].text.strip()
if raw.startswith("```"):
raw = raw.split("```")[1].lstrip("json").strip().rstrip("```")
return json.loads(raw.strip())
all_prompts = _batch(categories[0:3], 1)
all_prompts += _batch(categories[3:5], len(all_prompts) + 1)
return all_prompts
Site Agent — Auto-Rebuild Homepage
After every Builder run, Site Agent reads all products from state.json and rewrites index.html from scratch — newest products first. Each registered product gets its own landing page too.
def _rebuild_homepage(self, products):
cards = ""
for p in reversed(products):
cards += f"""
<div class="product-card">
<div class="card-badge">AI Prompt Pack</div>
<h3><a href="products/{p['slug']}.html">{p['title']}</a></h3>
<p>{p['description']}</p>
<div class="card-footer">
<span class="price">${p['price']}</span>
<a href="{p['gumroad_url']}" class="btn-buy-small">Get It Now</a>
</div>
</div>"""
html = f"""<!DOCTYPE html>
...
<section class="products-grid">
<h2>All Packs ({len(products)} available)</h2>
<div class="grid">{cards}</div>
</section>
..."""
path = os.path.join(self.website_dir, "index.html")
with open(path, "w", encoding="utf-8") as f:
f.write(html)
print(f"[Site] Homepage rebuilt: {path}")
Reporter Agent — Weekly P&L Email
Every Sunday the Reporter reads state.json, calculates revenue and costs entirely from local data (no Gumroad API), and emails a plain-text weekly summary.
def _build_report(self, products, week):
live = [p for p in products if p.get("status") == "live"]
pending = [p for p in products if p.get("status") == "pending_upload"]
new_week = [p for p in products if self._days_ago(p.get("created_date","")) <= 7]
revenue_gross = sum(p.get("sales",0) * p.get("price",0) for p in live)
gumroad_fees = revenue_gross * 0.10
api_cost_myr = len(new_week) * 0.40
net_myr = (revenue_gross - gumroad_fees) * 4.7 - api_cost_myr
lines = [
f"Banana Lab — Week {week} Report",
"=" * 45,
f"Products live: {len(live)}",
f"Pending upload: {len(pending)}",
f"Total sales (all time): {sum(p.get('sales',0) for p in live)}",
f"Gross revenue: ${revenue_gross:.2f}",
f"Gumroad fee (10%): -${gumroad_fees:.2f}",
f"Est. API cost: -RM {api_cost_myr:.2f}",
f"Net profit: RM {net_myr:.2f}",
]
for u in live:
if u.get("weeks_live",0) >= 3 and u.get("sales",0) == 0:
lines.append(f"Low performer: \"{u['title']}\" — consider price cut or removal")
return "\n".join(lines)
Main Scheduler — Weekly Automation
main.py wires the three agents to a weekly schedule using the Python schedule library, and exposes CLI flags for manual testing and product registration.
schedule.every().monday.at("09:00").do(run_monday_pipeline, config=config)
schedule.every().sunday.at("18:00").do(run_reporter, config=config)
while True:
schedule.run_pending()
time.sleep(30)
if "--register" in args:
slug = args[idx + 1]
url = args[idx + 2]
if BuilderAgent(config).register(slug, url):
run_site(config)