File size: 6,549 Bytes
7da356c
16e1aa7
 
 
 
7da356c
16e1aa7
 
7da356c
16e1aa7
7da356c
 
16e1aa7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
---
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.