This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle
TL;DR ListingLift turns a seller's rough notes and a product photo into a ready-to-paste Etsy, Amazon, eBay, Meesho, Shopify or Instagram listing. The model is open-weight Google Gemma, running inside the browser tab through WebLLM and WebGPU. No server. No API key. No per-listing bill. And a Fact Guard that catches the claims a small model invents before a buyer ever reads them.
What I Built
My mother makes Kantha-stitch dupattas in West Bengal. Every piece is a few layers of old saris, hand-quilted with a running stitch. Every piece is one of a kind. And every piece needs a listing: a title that fits the marketplace's character limit, five bullet points, a description, a dozen search tags, a meta line.
Multiply that by thirty products and four marketplaces and you get the real bottleneck of a small online craft business. It isn't making the thing. It's describing it, over and over, in the exact shape each platform wants.
ListingLift is a listing writer for that seller. You:
- Drop a product photo. ListingLift reads its dominant colours on your device (a tiny k-means pass on a 64×64 canvas; the pixels never leave memory).
- Type the notes you'd tell a friend: what it is, who made it, size, material, care.
- Pick a marketplace, a tone and a language.
- Hit Generate. Gemma streams a full listing token by token.
- ListingLift then fits it to the platform (trims titles to the limit, de-duplicates and caps tags, strips prompt echoes) and runs the Fact Guard, which highlights every claim that isn't in your notes.
- Copy, download CSV or JSON, done.
Who would pay for it
Indian marketplace sellers (Meesho, Amazon.in, Etsy India), Instagram shops, and the long tail of handmade sellers everywhere. Today they either write every listing by hand, pay a freelancer, or pay a monthly subscription to a cloud "AI listing generator" that charges per generation and ships their unreleased product details to someone else's server.
The business model I'd actually run:
| Plan | Price | What you get |
|---|---|---|
| Free | ₹0 | Unlimited listings in the browser. Your hardware does the work, so this costs me nothing to give away. |
| Seller Pro | ₹299/month (≈ $3.50) | Bulk mode (CSV in, CSV out), saved brand voices, marketplace-specific rule packs (Etsy 13-tag rules, Amazon style guide checks), custom risky-claim lists. |
| Craft collective | ₹1,499/month | Shared brand voice and rule packs for a co-op of artisans, one admin. |
The interesting part: because inference runs on the customer's device, my marginal cost per listing is zero. A cloud competitor has to charge per generation to cover GPU bills. I can charge for the workflow instead, and the core product can stay free forever. That pricing is only possible because the model is open.
Demo
🎬 The studio, end to end (real recorded Gemma 3 1B run: stream, platform fit, Fact Guard, score):
When you open the app, the first run downloads the model once into your browser cache (Gemma 3 1B is roughly 700 MB). After that it works offline: turn off Wi-Fi and generate again. If your browser has no WebGPU, the studio switches to replay mode and plays back real recorded Gemma runs (clearly labelled), so you still see the whole flow.
The studio
Six marketplaces, each with its own rules baked into the prompt and the post-processor:
| Marketplace | Title limit | Tags | Bullets | Style the prompt asks for |
|---|---|---|---|---|
| Etsy | 140 chars | 13 × 20 chars | 5 | warm, story-led, long-tail search phrases |
| Amazon | 200 chars | 7 search terms | 5 | benefit-first, factual, no subjective hype |
| eBay | 80 chars | 8 | 4 | keyword-dense title, specifics up front |
| Meesho | 100 chars | 8 | 5 | simple, value-focused |
| Shopify | 70 chars | 10 | 5 | brand voice, SEO meta |
| 90 chars | 20 hashtags | 3 | casual caption, community hashtags |
A finished listing, with the Fact Guard doing its job
This is a real Gemma 3 1B output for my mother's dupatta. Look at bullet 4: "Made from 100% organically grown cotton." My notes never said organic. They never said 100%. A buyer who reads that and receives recycled sari cotton has a legitimate complaint, and the seller has a refund and a bad review.
The Fact Guard caught four things: "luxurious", "100%", "organic" and the tag "fair trade". Each one is highlighted in place, in red, so the seller can delete it in two seconds.
The Network Witness: privacy you can check, not just read about
Every privacy page says "we don't upload your data". ListingLift shows you. The Network Witness panel reads the browser's own PerformanceObserver resource log and lists every request the page makes. Model shards and the WebLLM library come down from public CDNs. Then every outgoing URL is checked against the text of your notes. If any request ever contained them, the big green 0 would turn red.
Code
ListingLift is deliberately tiny: one HTML file, no build step, no backend, no framework. Open the live app and use View Source: every line is there, readable and commented.
Here is the core of it.
1. Loading Gemma in the browser
async function getEngine(model) {
if (engine && engineModel === model) return engine;
const webllm = await import("https://esm.run/@mlc-ai/web-llm@0.2.86");
engine = await webllm.CreateMLCEngine(model, {
initProgressCallback: (r) => {
bar.style.width = Math.round(r.progress * 100) + "%";
label.textContent = r.text;
},
});
engineModel = model;
return engine;
}
The model picker offers three open-weight Gemma builds that WebLLM ships prebuilt:
| Model ID (WebLLM) | Why you'd pick it |
|---|---|
gemma3-1b-it-q4f16_1-MLC |
Fastest, smallest download. Default. |
gemma-2-2b-it-q4f16_1-MLC |
Fewer invented claims in my tests (see benchmarks). |
gemma-4-E2B-it-q4f16_1-MLC |
Newest Gemma generation, bigger download. |
2. Streaming the listing
const stream = await engine.chat.completions.create({
messages: [{ role: "user", content: buildPrompt(opts) }],
temperature: 0.6,
stream: true,
max_tokens: 900,
});
for await (const chunk of stream) {
raw += chunk.choices[0]?.delta?.content || "";
consoleEl.textContent = raw; // live token stream
}
const result = process(raw, opts); // fit + guard + score
WebLLM exposes an OpenAI-compatible API, so the code reads exactly like a cloud call. The difference is where it runs.
3. A platform-aware prompt
export function buildPrompt({ notes, platform, tone, lang, colors }) {
const p = PLATFORMS[platform];
const col = colors?.length
? `\nDominant photo colours (detected on-device): ${colors.join(", ")}.` : "";
return `You are ListingLift, an expert ${p.name} copywriter for small independent sellers.
Write a ${p.name} listing in ${lang}. Tone: ${tone}. Platform style: ${p.style}.
Use ONLY facts from the seller notes. Never invent materials, percentages, certifications or sizes.
Seller notes: ${notes}${col}
Return ONLY JSON with keys:
"title" (max ${p.titleMax} characters),
"bullets" (exactly ${p.bullets} short benefit bullets),
"description" (90-130 words),
"tags" (exactly ${p.tagCount} unique search tags, each max ${p.tagLen} characters),
"seo_meta" (one sentence, max 155 characters).`;
}
Notice the line "Never invent materials, percentages, certifications or sizes." Gemma 3 1B still wrote "100% organically grown cotton". Prompting is necessary, and it is not sufficient. That's why the next part exists.
4. The Fact Guard
const RISKY = ["100%", "organic", "fair trade", "certified", "genuine leather",
"pure silk", "silk", "waterproof", "hypoallergenic", "vegan", "non-toxic",
"premium", "luxury", "luxurious", "antique", "vintage", "heirloom",
"guaranteed", "warranty", "eco-friendly", "biodegradable",
"high-temperature", "lead-free", "weather-resistant", /* … */];
export function factGuard(listing, notes, lang = "English") {
const src = notes.toLowerCase();
const flags = [];
for (const [field, text] of fieldsOf(listing)) {
const t = text.toLowerCase();
// 1. risky claims that the seller never made
for (const w of RISKY)
if (t.includes(w) && !src.includes(w)) flags.push({ field, claim: w, kind: "unsupported claim" });
// 2. numbers with units that aren't in the notes (invented sizes, weights, burn times)
for (const m of t.matchAll(/(\d+(?:\.\d+)?)\s?(%|cm|mm|m|inch|inches|g|kg|ml|hours?)\b/g))
if (!src.includes(m[1])) flags.push({ field, claim: m[0], kind: "number not in notes" });
// 3. script leaks, e.g. Arabic characters inside a Bengali listing
if (/[\u0600-\u06FF]/.test(text) && !["Arabic", "Urdu"].includes(lang))
flags.push({ field, claim: "Arabic-script text", kind: "script leak" });
}
// tags get checked too, and the description must actually be in the requested script
...
return dedupe(flags);
}
It's intentionally boring: a lexicon, a regex, and Unicode ranges. No second model, no extra download, runs in under a millisecond. It turns "I hope the AI didn't lie" into a short, highlighted to-do list.
5. Fitting the output to the marketplace
export function fitToPlatform(raw, platform) {
const p = PLATFORMS[platform];
// title: cut at the last word boundary under the limit
// bullets: drop prompt echoes like "Return only JSON with keys:", cap the count
// tags: lowercase, strip '#', shorten to tagLen, de-duplicate, cap to tagCount
// instagram: turn tags into #hashtags
// meta: trim to 155 chars on a word boundary
return { listing, fixes }; // every fix is reported back to the UI
}
Small models are great at content and sloppy at constraints. Gemma 3 1B returned 16 tags for a 13-tag Etsy slot (two of them duplicates), and once returned nine bullets when I asked for five, the last one being a copy of my own prompt. Deterministic code fixes that for free, and the UI lists every fix it made so nothing happens silently.
How I Built It
Stack
- Model: Google Gemma (open weights): Gemma 3 1B IT by default, Gemma 2 2B IT and Gemma 4 E2B selectable. 4-bit quantised MLC builds.
- Runtime: WebLLM 0.2.86 (MLC AI, Apache-2.0) on WebGPU. OpenAI-style streaming API in the browser.
-
App: a single static HTML file, vanilla JS, no framework, no bundler. Canvas for the aurora background and colour extraction, CSS for every animation,
IntersectionObserverfor scroll reveals,PerformanceObserverfor the Network Witness.localStoragefor history (with a graceful fallback where storage is blocked). -
Benchmark harness: the same prompt builder ported to Python, driving Ollama with
gemma3:1bandgemma2:2bon a 2-core ARM CPU with about 8 GB RAM and no GPU. -
Demo film: a 1920×1080 HTML "film" with a deterministic
seek(t)timeline, rendered frame by frame with headless Chromium (1,837 frames at 24 fps), narrated with the open-source Piper TTS voice, and encoded with FFmpeg. Every tool in the video pipeline is open source, too.
Architecture
┌──────────────── your browser tab ─────────────────┐
│ │
│ photo ──► canvas k-means ──► colour names ─┐ │
│ ▼ │
│ notes ───────────────────────────► buildPrompt() │
│ │ │
│ WebLLM + WebGPU ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ Gemma (q4f16, cached in browser) │ │
│ └──────────────────────────────────────┘ │
│ │ streamed JSON │
│ ▼ │
│ fitToPlatform() ─► factGuard() ─► score() │
│ │ │
│ ▼ │
│ copy · CSV · JSON · local history │
│ │
│ PerformanceObserver ─► Network Witness (0) │
└────────────────────────────────────────────────────┘
▲ one-time download of model weights (public CDN)
There is no box outside the tab that ever sees the seller's data.
Benchmarks: what I actually measured
I didn't want to write "it's fast" and "it's accurate" without numbers, so I ran the exact same prompts through the same post-processor on a deliberately weak machine: 2 CPU cores, about 8 GB RAM, no GPU at all. A laptop with WebGPU will be faster. These are floor numbers.
| Product | Model | Marketplace · language | Speed (CPU only) | Fact Guard flags | Score |
|---|---|---|---|---|---|
| 🧵 Kantha dupatta | Gemma 3 1B | Etsy · English | 26.3 tok/s | luxurious, 100%, organic, fair trade | 80 |
| 🧵 Kantha dupatta | Gemma 2 2B | Etsy · English | 16.0 tok/s | none | 96 |
| 🐎 Bankura terracotta horse | Gemma 3 1B | Amazon · English | 26.4 tok/s | high-temperature, antique | 90 |
| 🐎 Bankura terracotta horse | Gemma 2 2B | Amazon · English | 16.1 tok/s | weather-resistant (×2) | 78 |
| 🕯️ Chai soy candle | Gemma 3 1B | Instagram · English | 26.5 tok/s | none | 90 |
| 🇧🇩 Kantha dupatta | Gemma 3 1B | Meesho · Bengali | 20.6 tok/s | Arabic-script leak | 79 |
| 🇧🇩 Kantha dupatta | Gemma 2 2B | Meesho · Bengali | 16.1 tok/s | eco-friendly ×3, description not in Bengali | 68 |
What I take from it:
- Gemma 3 1B is fast enough on a CPU (about 26 tokens/s; a full listing in roughly 15–19 s) but invents marketing claims. With the Fact Guard on top, it's usable.
- Gemma 2 2B is slower and more faithful to the notes in English: zero flags on the dupatta, but it still called an unglazed terracotta horse "weather-resistant". No small model is trustworthy on its own, which is exactly why the guard exists. Swapping between them is a dropdown.
- Bengali is hard for both small models. Gemma 3 1B wrote real Bengali but leaked a stray Arabic word and padded the bullets with nine items, including an echo of my own instructions. Gemma 2 2B mostly answered in English. The Fact Guard flagged both. This is the honest frontier: regional-language commerce needs bigger or fine-tuned open models, and because the weights are open, anyone can do that fine-tune.
(Gemma 3 1B, Bengali, Meesho. Note the Fact Guard catching the Arabic-script word in bullet 1, and the auto-fixes trimming nine bullets down to five.)
(Gemma 2 2B on the same dupatta: every claim traced back to the notes.)
Decisions I'd defend
- Deterministic guard rails over a second LLM. A "critic model" would double the download and the latency on exactly the hardware my users have. A lexicon plus regex catches the claims that cause refunds.
-
Strict JSON, parsed leniently. I ask for JSON and then extract the first
{…}block, because small models like to add a friendly sentence before it. - Replay mode instead of a broken page. Many phones and some desktop browsers still lack WebGPU. Rather than an error, the studio replays recorded Gemma runs from my benchmark, labelled as recordings, so judges and sellers can see the full flow anywhere.
- One file. A seller's nephew can save it, put it on any static host, and it works. No build chain to rot.
Limitations
- First load is a large download (roughly 700 MB to 1.4 GB depending on the model). After that it's cached.
- Needs WebGPU for live generation (recent Chrome or Edge on desktop, newer Android Chrome). Safari support is improving.
- The Fact Guard is a lexicon. It catches the common lies, not every subtle one. It's a seatbelt, not an autopilot.
- Regional-language quality from 1B–2B models is not production-grade yet (see the Bengali rows).
Why Does Open Innovation Matter?
Because for this product, a closed API isn't a slightly worse option. It's a different, worse product.
1. Privacy becomes a property of the architecture, not a promise. A seller's unreleased designs, supplier notes and pricing are business secrets. With an open-weight model I can run inference inside the user's own browser, and then prove it with the Network Witness. With a closed API, the best I could offer is a privacy policy.
2. Zero marginal cost changes who can afford AI. A closed API bills per token, forever, so every listing has a price and every rewrite costs more. For a craft seller earning a few hundred rupees per piece, that adds up. With open weights the user's own GPU does the work: my cost per listing is ₹0, so the core product can stay free and I charge for workflow features instead.
3. Offline is a feature in Bharat. Mobile data drops. Power cuts happen. Once the model is cached, ListingLift keeps working in airplane mode. No cloud product can say that.
4. Model choice is a dropdown, not a contract. My benchmark shows Gemma 2 2B makes fewer invented claims than Gemma 3 1B. Switching took one line. When a better open model ships tomorrow, sellers get it the same day. When one model is weak in Bengali, the open weights mean anyone can fine-tune a better one and drop it in.
5. It's auditable end to end. Open weights (Gemma), an open runtime (WebLLM, Apache-2.0), and an app that is one readable HTML file. A seller, or a judge, can inspect every step between their notes and the listing. That's what lets me make the privacy claim at all.
My mother doesn't care what a transformer is. She cares that the listing is right, that it costs nothing, and that her designs stay hers. Open models are the only way I know to give her all three.
Prize Categories
- Best Use of Gemma: the whole product is built around open-weight Gemma running locally (Gemma 3 1B default, Gemma 2 2B and Gemma 4 E2B selectable), with measured comparisons between Gemma generations.
Built by @aniruddhaadak during Hacktoberfest 2026. Thanks to the Gemma team for open weights, the MLC/WebLLM team for making them run in a browser, and the Piper and FFmpeg communities whose tools made the demo film.








Top comments (0)