A reseller photographs forty things a week and then writes forty listings by hand. That is the tedious part of the side hustle — not the sourcing, not the shipping, the copy. Existing tools fix it by charging per listing and shipping your inventory to somebody else's cloud. I wanted to see how far a local open-weight model could get instead.
So I built Listing Studio: describe an item once and it writes a platform-shaped title, description, tags and price range for Etsy, eBay, Depop or Vinted — running entirely on your own machine.
🔗 Code: https://github.com/sank8-2/listing-studio (MIT)
Who would actually pay for this
The buyer is a one-person reseller: someone clearing a house, selling vintage finds, or running a small craft business. Their economics are small margins and large volume, and their two real costs with cloud listing tools are:
- Per-listing pricing. When you list 200 items a month, a subscription or credit pack is a real line item. A local model costs electricity.
- Handing over commercially sensitive data. Your stock list, your supplier names and your margins are the business. Uploading them to generate ad copy is a strange trade.
Neither of those is a technical argument. They are the reason a local tool exists at all.
How it works
flowchart LR
A[item notes] --> B[Python: build prompt<br/>+ platform rules]
B --> C[Ollama<br/>open-weight model]
C --> D[extract JSON]
D --> E[Python: clamp title,<br/>normalise tags, price band]
E --> F[(SQLite history)]
E --> G[listing + CSV export]
The pipeline has one deliberate rule: the model writes prose, Python decides every hard limit.
- Etsy: 140-character title, 13 tags × 20 chars
- eBay: 80-character title, no tags
- Depop/Vinted: shorter titles, 5 tags
The model is never asked to count characters, because small models cannot count. finalise() clamps titles at a word boundary, lowercases and de-duplicates tags, caps their length, and reorders the price band if the model gets it backwards.
It is stdlib Python and one HTML file. No pip install, no build step, no account:
ollama pull llama3.2:1b
python3 app.py # → http://localhost:8004
What it actually produced
Input:
item: vintage wooden birdhouse · category: garden decor · materials: pine wood · condition: good, some weathering · details: roof lifts off for cleaning; hanging hook on the back; 28 cm tall
First run, ~49 seconds on CPU, unedited:
Title: Vintage Weathered Pine Wood Birdhouse by Local Carpenter (56/140 chars)
Tags (12 returned, 13 allowed): garden decor · birdhouse · weathered pine wood · wooden birdhouse · hanging hook · pine wood birdhouse · local carpenter · handmade garden deco · garden accessory · wooden bird house · bird house for birds · decorative wooden (clipped to Etsy's 20-char tag limit mid-phrase — you can see the limit doing its work)
Price: GBP 25–40
Description: the model returned this one empty — so the deterministic fallback composer wrote it instead. That safety net is not a nice-to-have; it is the difference between an empty listing and a sellable one.
Then a second run on the identical input produced a shorter title (24 chars), only 8 tags, and a GBP 5–10 band — that is the screenshot above, and it is the honest headline: a 1B model is a drafting assistant, not an appraiser. Two runs, a 4× spread on price. The seller still sets the price. What the tool removes is the blank page, and it does that at zero marginal cost.
The part that did not work
I wanted the photo to be the input — point the camera at the item, get the listing. It is wired up, and it is not the default, for reasons worth sharing:
- On this machine (Ollama 0.18.2, CPU-only)
gemma3:1bandqwen2.5:1.5bboth produced degenerate output — literal token loops like1 1 1 1 1even at temperature 0. Re-pulling did not help.llama3.2:1bworked fine, so the runtime is healthy and those blobs are not. -
moondream:latestdescribed a portrait correctly but returned symbol salad on real product photos, at 130–229 seconds per image. - With ~4 GB RAM free, holding a vision model and a text model at once made the model server return HTTP 500.
So I shipped text-first and left the photo field in place behind VISION_MODEL. The honest version of this project is less magical than the pitch, and it still saves the seller real time. I would rather write that down than show a screenshot of a vision pipeline I never got working reliably.
Why open matters here specifically
-
The privacy claim is structural, not a policy. There is no API key and no third-party endpoint; the app talks to
127.0.0.1:11434. You can verify it by pulling the network cable out and watching it keep working. -
Model choice stays yours.
WRITER_MODEL=llama3.1:8bis the entire upgrade path from "usable draft" to "genuinely good copy". No plan tier, no vendor deciding what you are allowed to run this month. - The price cannot be changed out from under you. A tool whose running cost is your own electricity does not get a pricing-page redesign that triples your bill.
For a business tool aimed at people with thin margins, those are not nice-to-haves. They are the product.
Try it
git clone https://github.com/sank8-2/listing-studio
cd listing-studio && ollama pull llama3.2:1b && python3 app.py
If you sell things on the side: I would genuinely like to know whether the output survives contact with a real listing, and where the copy falls apart. That feedback is more useful to me than a star.
Built for the Hacktoberfest Open-Source AI Challenge, Week 2 — Side Hustle.

Top comments (0)