<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sum Lau</title>
    <description>The latest articles on DEV Community by Sum Lau (@sum_lau_b6f2b27a5f2b30806).</description>
    <link>https://dev.to/sum_lau_b6f2b27a5f2b30806</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4186361%2Fe5d1ae02-5cd9-44ac-96d1-cabebb464262.png</url>
      <title>DEV Community: Sum Lau</title>
      <link>https://dev.to/sum_lau_b6f2b27a5f2b30806</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sum_lau_b6f2b27a5f2b30806"/>
    <language>en</language>
    <item>
      <title>I just saved 25 hours a month…</title>
      <dc:creator>Sum Lau</dc:creator>
      <pubDate>Fri, 16 Oct 2026 15:32:08 +0000</pubDate>
      <link>https://dev.to/sum_lau_b6f2b27a5f2b30806/i-just-saved-25-hours-a-month-51db</link>
      <guid>https://dev.to/sum_lau_b6f2b27a5f2b30806/i-just-saved-25-hours-a-month-51db</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I just saved &lt;strong&gt;25 hours a month&lt;/strong&gt; — and a $20/mo ChatGPT subscription — by pointing a 3.4 GB open-source model at my laptop instead of a paid API.&lt;/p&gt;

&lt;p&gt;If you run a small shop on Etsy or eBay, you already know the real bottleneck isn't the product — it's the listing. Titles that rank, descriptions that sell, and the right 10–13 search tags. Do that for 100 listings a month and it eats 25 hours. That's a full-time job's worth of copywriting hiding inside a side hustle.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Listing Copilot&lt;/strong&gt; takes a one-line product keyword — &lt;code&gt;handmade ceramic mug, minimalist, gift&lt;/code&gt; — and returns a ready-to-paste Etsy/eBay listing: a keyword-loaded title under 140 chars, a benefit-driven 2–4 paragraph description, and 10–13 lowercase search tags. All of it is generated &lt;strong&gt;on my machine&lt;/strong&gt; by Google's open-source &lt;strong&gt;Gemma 3 (4B)&lt;/strong&gt; running through &lt;strong&gt;Ollama&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There is no API call to OpenAI or Anthropic, no per-token billing, and no inference leaving the machine. A single line in the UI says it: &lt;code&gt;100% on-device · Gemma 3 · $0 API&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Optional, and all graceful-degrade: competitor pricing via &lt;strong&gt;SerpApi&lt;/strong&gt;, and a save backend via &lt;strong&gt;MongoDB Atlas&lt;/strong&gt; — when the keys aren't there, the app quietly falls back (pricing panel hidden, saves to local JSON), so the core demo never breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Open the app, type keywords, hit &lt;strong&gt;Generate&lt;/strong&gt;, and the listing types itself out word by word in the terminal-style UI:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F54wtvjfb0uj3cnt04wkd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F54wtvjfb0uj3cnt04wkd.png" alt="Listing Copilot UI: a generated Etsy listing for a ceramic mug, with the fixed badge reading 100% on-device, Gemma 3, $0 API in the bottom right corner" width="800" height="787"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The model is a 3.4 GB pull. After warm-up a generation takes about 6–7 seconds; a cold start is 15–20 seconds. Here is &lt;code&gt;/health&lt;/code&gt; on my own machine. &lt;code&gt;model_ready: true&lt;/code&gt; for &lt;code&gt;gemma3:4b&lt;/code&gt;, and the last two fields are the ones I actually care about as evidence: &lt;code&gt;"storage": "mongodb"&lt;/code&gt; and &lt;code&gt;"serpapi_enabled": true&lt;/code&gt;. Those two flags flip to &lt;code&gt;local-json&lt;/code&gt; and &lt;code&gt;false&lt;/code&gt; the moment the keys are missing, so they cannot be faked by editing a README:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5hff4146gy1u16jfxfbv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5hff4146gy1u16jfxfbv.png" alt="Terminal output of curl health on localhost port 8000, pretty-printed JSON showing model_ready true, storage mongodb and serpapi_enabled true" width="800" height="619"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;Full stack is deliberately boring and open: Python 3.13 + FastAPI for the backend, Ollama's &lt;code&gt;/api/chat&lt;/code&gt; endpoint with &lt;code&gt;format:"json"&lt;/code&gt; and temperature 0.7 for generation, and a single no-build HTML file on the front end — no framework, no &lt;code&gt;node_modules&lt;/code&gt;, no deploy pipeline. You clone it, &lt;code&gt;pip install -r requirements.txt&lt;/code&gt;, &lt;code&gt;ollama pull gemma3:4b&lt;/code&gt;, and run &lt;code&gt;uvicorn app.main:app&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Generation is a single structured call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemma3:4b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;# system forces valid JSON output
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;options&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num_predict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;700&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt instructs the model to return &lt;em&gt;only&lt;/em&gt; &lt;code&gt;{"title", "description", "tags"}&lt;/code&gt; with hard rules (title ≤ 140 chars with the most-searched keywords; tags 10–13, each ≤ 20 chars, lowercase, no &lt;code&gt;#&lt;/code&gt;). A tolerant parser strips markdown fences and recovers the JSON even if the model adds chatter.&lt;/p&gt;

&lt;p&gt;One rule looks pedantic until you've sold on Etsy: &lt;strong&gt;no &lt;code&gt;#&lt;/code&gt; in tags.&lt;/strong&gt; Etsy tags are plain search keywords capped at 20 characters each, and a &lt;code&gt;#&lt;/code&gt; spends one of those characters without adding anything Etsy will match on. eBay doesn't even have a tags field (its search runs on the title and item specifics), so a &lt;code&gt;#&lt;/code&gt; is pure dead weight in a listing's most valuable real estate.&lt;/p&gt;

&lt;p&gt;The prompt hard-codes &lt;code&gt;no '#'&lt;/code&gt;, and a normalization pass strips any stray &lt;code&gt;#&lt;/code&gt; the model might still emit. Tags render as &lt;code&gt;#chips&lt;/code&gt; in the UI — that's how people read them — but the data underneath is exactly what a seller pastes into Etsy.&lt;/p&gt;

&lt;p&gt;Repo (MIT-licensed): &lt;strong&gt;&lt;a href="https://github.com/SS-LAU/sidehustle-listing-copilot" rel="noopener noreferrer"&gt;SS-LAU/sidehustle-listing-copilot&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;The core loop is under 100 lines of Python. I wrapped Ollama in a client that turns every failure mode — service down, model not pulled, timeout — into a friendly &lt;code&gt;OllamaError&lt;/code&gt; instead of a 500, and the front end catches that and shows a hint plus a &lt;strong&gt;Demo&lt;/strong&gt; button that loads a bundled offline sample so the UI never white-screens. That last detail matters more than it sounds: a side-hustle tool has to work on the seller's flaky wifi, not just on a dev machine.&lt;/p&gt;

&lt;p&gt;On the open-source side, I made &lt;strong&gt;swapping the model a one-line env var&lt;/strong&gt; (&lt;code&gt;OLLAMA_MODEL&lt;/code&gt;). Today that's &lt;code&gt;gemma3:4b&lt;/code&gt;; tomorrow it's &lt;code&gt;gemma3:1b&lt;/code&gt; on a weaker laptop or a bigger model on a GPU box. The prompt, tone presets, and tag rules are plain text you can edit for your own brand — that's the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Because for a side hustle, "open" isn't a value statement — it's the business model.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero marginal cost → near-100% gross margin.&lt;/strong&gt; A hosted API bill scales with every listing you generate; a flat $20/mo subscription just caps how much you can do by hand. Gemma running locally makes the 10,000th listing cost exactly $0 more than the first. My cost curve is flat; a proprietary API's is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No prompt ever reaches a hosted LLM.&lt;/strong&gt; A seller's product keywords and copy are their trade secrets. Local inference means no "we may use your prompts for training" fine print, which lowers the trust barrier with the exact audience — small, privacy-wary sellers. The two optional integrations are the only outbound calls, and both stay off until you add keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-host + edit the prompt = a moat.&lt;/strong&gt; Because it's open, a user can re-tune the tone, inject their own brand voice, and point the whole thing at any model. You can't lock someone into a moat built on a closed API; you &lt;em&gt;can&lt;/em&gt; build a moat on tooling around open models that a competitor has to rebuild, not just resubscribe to.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;On the numbers: 100 listings × 15 min each = 25 hours a month back. At a $2/mo self-hosted price point, 1,000 sellers is roughly $2,000 MRR on a cost structure where the AI itself is free to run. That flat cost curve is the whole thesis — and it's only possible because the model is open.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;I built this with an AI coding agent driving the loop: it scaffolded FastAPI, wrote the Ollama client and JSON parser, and — crucially — wrote the offline fallback so the demo survives a model outage. The agent also ran the app and caught that a cold start (~15s) looked like a hang to a first-time user, which is why the warm-up + offline-Demo path exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — the core engine. Every listing in this project is generated by &lt;code&gt;gemma3:4b&lt;/code&gt; on-device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of SerpApi&lt;/strong&gt; — the competitor-pricing layer (auto-hides when no key is set).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render&lt;/strong&gt; — the app deploys from a checked-in &lt;code&gt;render.yaml&lt;/code&gt; Blueprint and is live at &lt;a href="https://sidehustle-listing-copilot.onrender.com" rel="noopener noreferrer"&gt;sidehustle-listing-copilot.onrender.com&lt;/a&gt;. Render serves the FastAPI front end, a &lt;code&gt;/health&lt;/code&gt; endpoint that always returns 200 so the health check never flaps, and the bundled offline sample; a scheduled GitHub Action pings &lt;code&gt;/health&lt;/code&gt; to keep the instance warm, though GitHub throttles scheduled jobs, so the first visit after a quiet spell is still a cold start (up to a minute). There's no GPU or Ollama in the cloud, so hosted &lt;strong&gt;Generate&lt;/strong&gt; deliberately fails fast and honestly rather than pretending.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of MongoDB Atlas&lt;/strong&gt; — save backend via &lt;code&gt;MONGODB_URI&lt;/code&gt; (falls back to local &lt;code&gt;demo/listings.json&lt;/code&gt; when unset).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claims are cheap, so before submitting I run a six-assertion integration check against the live server — &lt;code&gt;scripts/verify_integrations.py&lt;/code&gt;. It fails loudly if SerpApi stops returning real prices or if saves stop landing in Atlas, which is why I trust the two partner categories above:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftdzl8jn02twwa69jx0bg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftdzl8jn02twwa69jx0bg.png" alt="Terminal output of scripts/verify_integrations.py showing six PASS lines, with real SerpApi competitor prices and listings written to MongoDB Atlas" width="799" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosure
&lt;/h2&gt;

&lt;p&gt;I built this with an AI coding agent in the loop, and I used it to help draft this post too. Everything factual above was verified against the running app before publishing: the screenshots are real terminal output, the competitor prices are live SerpApi results, and the Render deploy is live at the link above. If a claim couldn't be backed by a check, I cut it.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
  </channel>
</rss>
