| --- |
| 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. |
|
|