A newer version of the Gradio SDK is available: 6.22.0
title: Etsy Listing Optimizer
emoji: 🏷️
colorFrom: yellow
colorTo: pink
sdk: gradio
sdk_version: 5.12.0
app_file: server.py
pinned: false
license: mit
Etsy Listing Optimizer
A micro-SaaS tool that generates SEO-optimized Etsy listing titles, tags, and descriptions from a plain-language product description — with every AI output programmatically validated (character limits, exact tag count, zero duplicate words, no truncation) before it's ever shown to a user.
Built on an entirely free tier: Gradio + FastAPI on Hugging Face Spaces, Supabase for auth/database, Groq (with Gemini fallback) for AI generation, Stripe for billing.
New here? Read DEPLOY.md first — it's the step-by-step guide to
getting every account/API key and going live. This README is the map of
what's in the codebase.
Quickstart (local, no API keys needed)
The app runs in demo mode — in-memory storage, a deterministic mock AI provider — whenever real credentials aren't configured, so you can try the whole flow before signing up for anything.
pip install -r requirements.txt
uvicorn app.main:app --reload --port 7860
Open http://localhost:7860 — sign up with any email/password (10+ chars, letters + numbers), generate a listing, poke around the tabs. Data resets every time you restart the process.
To go live with real accounts/AI/payments, follow DEPLOY.md.
Project structure
server.py HF Spaces (Gradio SDK) entry point - runs the app on port 7860
app/
main.py FastAPI app: wires up all routers + mounts Gradio UI
ui.py Gradio Blocks UI (calls the FastAPI routes in-process)
config.py Tier/pricing definitions, feature flags, demo-mode detection
security.py Input sanitization / prompt-injection hygiene
llm/
prompts.py System prompt + few-shot examples (edit wording here)
provider.py Groq / Gemini / Mock provider abstraction + fallback
validation.py Programmatic validation of generated output
generator.py Orchestrates generate -> validate -> retry -> critic pass
auth/
service.py Supabase Auth wrapper + in-memory demo auth
deps.py FastAPI dependency: resolve current user from Bearer token
db/
store.py Supabase-backed store + in-memory demo store (same interface)
payments/
stripe_service.py Checkout session creation + webhook verification/handling
routes/
auth_routes.py /auth/signup, /auth/login, /auth/logout, /auth/password-reset
generate.py /generate — the core feature, tier/quota-gated
brand_voice.py /brand-voice CRUD
history.py /history, /history/export.csv
account.py /account/export, /account/delete (GDPR)
payments.py /billing/checkout/{tier}, /billing/webhook
bulk.py /bulk/generate (Business tier, CSV upload)
legal/
pages.py /legal/privacy, /legal/terms, /legal/refunds (draft text)
middleware/
rate_limit.py Per-IP (auth) and per-account (/generate) rate limits
tests/
test_validation.py Unit tests for the validation logic (run: pytest tests/ -q)
supabase_schema.sql Run this in the Supabase SQL editor - tables + RLS policies
requirements.txt Pinned dependency versions
.env.example Every environment variable, with a comment on where to get it
DEPLOY.md Step-by-step deployment + API key walkthrough
How generation is kept honest
/generate never trusts the model's output at face value:
llm/generator.pycalls the LLM (Groq, falling back to Gemini on a 429).llm/validation.pychecks, in code: exact tag count (13), character limits (140 for titles, 20 for tags), no duplicate words across tags/title, no mid-word/mid-sentence truncation, and the API's ownfinish_reasonisn't"length"(which would mean the response was cut off by the token limit).- If anything fails, the generator re-prompts the model with the specific violation, up to 2 retries.
- Once formally valid, a second cheap "critic" LLM call checks whether each tag is actually relevant to the submitted product (catches hallucinated tags) — if any are flagged, only those specific tags are regenerated, not the whole listing.
All of this is exercised by tests/test_validation.py and was manually
regression-tested end-to-end (signup, generation, tier gating, quota
enforcement, rate limiting, GDPR export/delete, Stripe error handling) before
being handed off.
Tiers
Defined in one place, app/config.py — nowhere else in the codebase
hardcodes a limit or a price. See the table there (or the Account tab in the
UI) for the current Free/Starter/Pro/Business feature matrix.
Security & compliance notes
- Passwords never touch application code — Supabase Auth handles hashing
(bcrypt); the demo-mode fallback used when Supabase isn't configured hashes
with a salt purely as a sandbox safeguard and is explicitly not
production-grade (see the warning in
auth/service.py). - Row Level Security is enabled on every table in
supabase_schema.sql. - Every generation request is checked against the monthly quota before calling the LLM, so rejected requests never burn API spend.
- Stripe webhooks are rejected outright if signature verification fails.
/account/deleteremoves both the database rows and the underlying auth user/session — a deleted account's token stops working immediately.- See
DEPLOY.mdfor the GDPR / legal-pages disclaimer: the shipped Privacy Policy / Terms / Refund Policy are a starting checklist, not legal advice — have them reviewed before real customers sign up.
Known v1 limitations (by design, per the "keep it simple" brief)
- Rate limiting is in-memory (
slowapi) — fine for a single HF Space instance, won't work correctly if you scale to multiple instances without switching to a Redis-backed store. - Bulk generation (
/bulk/generate) processes rows synchronously with a 25-row cap per request — adequate for a v1 CPU-tier deployment, not meant for large batch jobs. - Resend (transactional email) is stubbed, not wired up — Supabase's default auth emails cover signup confirmation/password reset for free in the meantime.