--- 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. ```bash 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: 1. `llm/generator.py` calls the LLM (Groq, falling back to Gemini on a 429). 2. `llm/validation.py` checks, 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 own `finish_reason` isn't `"length"` (which would mean the response was cut off by the token limit). 3. If anything fails, the generator re-prompts the model with the *specific* violation, up to 2 retries. 4. 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/delete` removes both the database rows and the underlying auth user/session — a deleted account's token stops working immediately. - See `DEPLOY.md` for 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.