The Free Tool Stack I Use to Sell Digital Products Without a Design Team
The moment that changed how I work was a rejection email at 11pm. I had batch-generated 60 vector assets, built what I thought was a valid Adobe Stock CSV, and uploaded the whole folder. Every single one came back rejected in under an hour. Not for the art. For the metadata. Adobe's CSV format is strict: 5 columns in a fixed order, and if the header row does not match exactly, the upload fails before a human ever sees your work. I had written the CSV by hand in a spreadsheet. Sixty assets, zero accepted, one wasted evening.
That was the night I stopped trying to do this manually and started writing scripts instead. Here is the stack I ended up with, what each piece does, and the numbers behind why it works.
The metadata problem is a format problem, not a content problem
Adobe Stock wants a 5-column CSV. Vecteezy wants 4 columns. Dreamstime wants 15. Each platform rejects a wrong header instantly, which means the failure mode is total: one bad row or one wrong column name and your entire batch bounces.
So I wrote make_metadata.py. It reads a folder of files and writes the exact header each platform demands, and nothing else. You point it at a directory:
python make_metadata.py ./my-assets --platform adobe --out metadata.csv --ext .jpg
It uses only the Python standard library. No pip install, no cloud account. If you pass a titles.json file with real titles and keywords, it uses those. Without it, the filename gets title-cased and used as a placeholder, which is fine for a first pass but you should replace it before submitting. Search ranking on stock platforms depends heavily on titles and keywords, so a filename like IMG_0042.jpg is not doing you any favors.
The time math is simple. Hand-building a 60-row CSV took me about 40 minutes and produced a 0% acceptance rate. The script takes seconds and, once the header is right, my rejections moved to content reasons I could actually fix.
Double-posting is the fastest way to get flagged
My second real failure: I re-ran a batch script on a Monday morning because I could not remember whether I had run it Sunday night. I had. Now the same asset existed twice on the platform, and duplicate submissions are one of those things that quietly damages your account standing.
upload_tracker.py is a plain ledger that remembers what you have posted:
python upload_tracker.py pending items.txt
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
It is also standard library only. It stores state in a local file, so a re-run can never double-post. This is a boring tool that solves an expensive mistake, and boring is what you want in a pipeline you run at 2am.
Turning content into a product people will pay for
Making the art is the fun part. Selling it means it has to look like a product. A raw Markdown file is not a product. A bare zip of loose files does not sell either.
Two scripts handle this. md2pdf.py turns a Markdown file into a styled PDF with proper headings, tables, lists, and links, using reportlab:
python md2pdf.py content.md product.pdf "My Product Title"
pack_product.py bundles everything: it zips the files and renders a 1280x720 cover and a 600x600 thumbnail with Pillow:
python pack_product.py my-product "My Product" "A short subtitle" 9
That is two third-party packages across the whole stack, pillow and reportlab, and both are free. Everything else is standard library. My entire dependency list fits in one short requirements.txt.
The whole thing runs on a 1 vCPU, 2 GB server
I do not own a GPU and I do not rent one. The full pipeline ran on a single-core box with 2 GB of RAM, no GPU, and it packaged 39 finished products, about 856 MB of output. That only worked because of habits I learned the hard way:
- One process at a time. Never fan out image jobs in parallel on a 1-core box. You will thrash, not speed up.
- Call
gc.collect()after each asset to free memory between items. - Measure peak RSS, not average.
resource.getrusage(RUSAGE_SELF).ru_maxrsstells you the real ceiling. Average memory numbers lie. - Run heavy render steps one file per subprocess, so memory is actually returned to the OS when it finishes.
- Render at the resolution you need. Inspecting a preview at 60 dpi beats rendering at 300 when you only need to check composition.
None of this is clever. It is discipline, encoded in scripts so I do not have to remember it.
The folder layout matters more than you think
One structure, fixed early:
project/
assets/ raw output
output/ ready-to-submit files + metadata.csv
packs/ finished zips + covers
research/ notes and sources
scripts/ the tools
The one rule I would tattoo on my own hand: keep research/ forever. I deleted my scrape logs and notes once, to "clean up", and lost raw material that would have fed three later products. Notes you keep are the seed of the next catalogue.
What this adds up to
The parent project behind these scripts is around 19,000 lines of pipeline code, built on top of 300+ pages of research into how these platforms actually work. The four scripts here are the packaging layer, and they plug into a free stack above them: an orchestrator that schedules jobs and retries failures, and a local LLM proxy for generation. All of it costs $0 in tooling. The real costs are time and the mistakes you make before writing things down.
I packaged all of this, the exact scripts that produced those 39 products, into the Pipeline Starter Kit, because the version of me from that 11pm rejection night needed it and could not find it. It is not magic. It is four scripts, two dependencies, and a folder layout, validated on hardware worse than most laptops.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)