Last March I had a folder of 240 finished vector assets and nothing uploaded. I sat down on a Sunday to batch-submit to Adobe Stock, pasted my CSV, and the upload page rejected the whole file in about two seconds. Wrong header row. Adobe wants exactly five columns in a specific order. Vecteezy wants four. Dreamstime wants fifteen. I had built one generic CSV with a header I invented, and all three platforms bounced it. That was the moment I stopped treating metadata as an afterthought and started building the pipeline that eventually turned one prompt engine into 39 sellable products, all of it running on a 1 vCPU, 2 GB server with no GPU.
The metadata problem is a formatting problem, not a content problem
Every stock platform gives you a bulk upload CSV template, and every one of them rejects a wrong header instantly. No partial credit. Adobe's template has 5 columns, Vecteezy has 4, Dreamstime has 15. If the header doesn't match exactly, the file is dead on arrival.
So the first script I wrote does one thing: read a folder of files, write the exact header each platform demands, and nothing else. make_metadata.py takes --platform adobe, --platform vecteezy, or --platform dreamstime and emits the right shape. Filenames get title-cased into titles if you don't supply a titles.json, but that's a first pass only. Replace generated titles with real ones before you submit, reviewers notice keyword-stuffed filenames and it hurts approval rates.
The honest part: the CSV gets you in the door. Approval still depends on titles, keywords, and the art itself. The script just removes the failure mode that wastes an afternoon.
Double-posting is the fastest way to get flagged
My second real failure was running a batch twice. I re-ran an upload script after a crash, forgot which items had gone through, and posted duplicates. Duplicate submissions are one of the quickest ways to get an account flagged on the major platforms, and cleaning up duplicates manually is worse than doing it right the first time.
The fix is dumb and it works: a ledger. upload_tracker.py is a tiny standard-library tool with three commands:
python upload_tracker.py pending items.txt
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
pending shows what has not been posted. Mark items done as you go, and a re-run can never double-post because the ledger is checked before anything is queued. No database, no install. If you use nothing else from this post, use this pattern.
One prompt engine, many products
The 39 products did not come from 39 ideas. They came from one prompt engine producing raw vector output, then a packaging layer that turned output into things people buy. The math looked like this: roughly 19,000 lines of pipeline code in the parent project, 300+ pages of research behind the rules the scripts enforce, and 856 MB of finished output across the 39 products.
The lesson I keep relearning: making the art is the fun part and it is maybe 30% of the work. The other 70% is the four things that stall everyone:
- Metadata in the platform's exact format.
- Not double-posting.
- Turning a document into something that looks like a product.
md2pdf.pyconverts a Markdown file into a styled PDF, headings, tables, lists, links, in one command. That is how my guides and pattern books became PDFs instead of text dumps. - Packaging. A zip with a real cover sells, a bare file does not.
pack_product.pybundles the files and renders a 1280×720 cover and a 600×600 thumbnail. Those two dimensions matter: covers get cropped on Gumroad and marketplace previews, and I settled on those sizes after testing what displayed without ugly letterboxing.
Running this on 2 GB of RAM
The whole thing ran on a single-core box with 2 GB of RAM and no GPU. That constraint shaped the architecture more than any feature request did. The habits that made it work:
- One process at a time. Never fan out image jobs in parallel on a 1-core box. Parallelism on 1 vCPU just swaps.
- Call
gc.collect()after each asset. Python holds references longer than you expect in loops. - Measure peak RSS, not average.
resource.getrusage(RUSAGE_SELF).ru_maxrsstells you the real ceiling. Average memory lying to you is how you get OOM-killed at item 180 of 240. - One file per subprocess for heavy render steps, so memory is actually returned to the OS between items.
- Render at the resolution you need. Inspecting a preview at 60 dpi instead of 300 cut my render memory by a factor I stopped feeling guilty about.
For image generation costs, keep it in the single digits of dollars per product batch if you are using paid API credits, or run local generation on a different machine and only use the tiny server for packaging. The 2 GB box is for orchestration, metadata, PDFs, and zips, not for rendering 4K rasters.
Keep your research folder
One mistake I made once and will not repeat: I deleted my research/ folder to "clean up" and lost the scrape logs and notes that were the raw material for the next four products. The folder layout that scales is boring and fixed:
project/
assets/ raw output
output/ ready-to-submit files + metadata.csv
packs/ finished zips + covers
research/ notes and sources, keep forever
scripts/ the tools
Where the tools live
Everything above is packaged as the Pipeline Starter Kit: four standalone Python scripts, two third-party dependencies total (Pillow and reportlab), validated on 1 vCPU / 2 GB RAM. The metadata builder, the upload ledger, the Markdown to PDF converter, and the product packager are the exact ones that produced the 39 products. If you want the prompt side too, the vector prompt pack is the prompt engine that fed this pipeline, with the same rules baked in.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)