Last March I lost a whole weekend to a CSV header. I had 60 assets ready for Adobe Stock, generated a metadata file with my own column order, hit submit, and the portal rejected the batch in seconds. Wrong header, five columns required, mine had six. I fixed it, resubmitted to Vecteezy the same night with the same file, and got rejected again because Vecteezy wants four columns. That was the moment I stopped hand-editing spreadsheets and started writing scripts instead.
Eight months later the pipeline has packaged 39 finished products, about 856 MB of output, running on a VPS with 1 vCPU, 2 GB RAM, and no GPU. Here is what actually worked, and what wasted my time.
Metadata is a formatting problem, not a creative one
Every stock channel has a fixed upload CSV shape. Adobe wants 5 columns, Vecteezy wants 4, Dreamstime wants 15. Get the header wrong and the file is rejected before anyone looks at your art. There is no partial credit.
So I wrote make_metadata.py. You point it at a folder of files, pick a platform, and it writes the exact header that channel demands, nothing more. It uses only the Python standard library, so there is nothing to install and nothing to break on a small server. You can pass a titles.json with real titles and keywords. Without it, the script title-cases the filename, which is fine for a first pass but you should replace it before submitting. Filenames are not keywords.
The lesson: the platforms do not reward cleverness here. They reward matching their spec exactly, every time.
Double-posting is the fastest way to get flagged
Early on I ran a batch twice because I lost track of what I had already uploaded. Nothing bad happened that time, but I read enough contributor forum threads to know that repeated duplicate submissions are a real path to account review. On a small account you cannot afford that risk.
upload_tracker.py is a tiny ledger. You mark an item done, and a re-run of pending simply never shows it again. It also uses the standard library alone. Two of my four scripts need zero pip installs, which matters when your server has 2 GB of RAM and you do not want a dependency tree resolving at midnight.
A 2 GB box forces good habits
The whole pipeline runs on 1 vCPU and 2 GB RAM with no GPU. That sounds like a limitation. It is actually a discipline. The habits that made it work:
- One process at a time. Never fan out image jobs in parallel on a single core. Parallelism on 1 vCPU just means everything finishes slower and memory spikes stack.
- Call
gc.collect()after each asset. Python holds references longer than you expect, and on 2 GB the difference between a 400 MB and a 900 MB working set is the difference between running and being OOM-killed. - Measure peak RSS, not average.
resource.getrusage(RUSAGE_SELF).ru_maxrssgives you the real ceiling. Average memory tells you nothing about whether tonight's batch survives. - Run heavy render steps one file per subprocess, 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 one step's memory use dramatically. Rendering pixels nobody will see is the most expensive thing a tiny server does.
For AI image generation I keep that off the server entirely. Generation costs a few cents per image on hosted APIs, and my VPS only ever handles post-processing: resizing, zipping, covers, PDFs. Pillow and reportlab handle all of that comfortably in 2 GB.
Turning content into a product is its own step
I had Markdown documents full of real research, over 300 pages of it behind the rules in my pipeline, and no way to sell them. Copy-pasting into a word processor by hand does not scale past product number two.
md2pdf.py takes a Markdown file and renders a clean styled PDF with headings, tables, lists, and links, in one command. It uses reportlab, one of exactly two third-party packages in the whole kit.
Then pack_product.py bundles the product: it zips the files and renders a cover at 1280 by 720 plus a thumbnail at 600 by 600 using Pillow. A zip with a real cover sells. A bare file does not. I learned that from watching my own listing numbers before and after I started shipping covers.
The script writes output into a _system folder by default, and you can redirect it with --dir or a PACK_ROOT environment variable. No editing the script, no hardcoded paths.
The folder layout that saved me
A catalogue without a fixed structure becomes a junk drawer. This is the layout the whole thing assumes:
project/
assets/ raw output, one file per asset
output/ ready-to-submit files plus metadata.csv
packs/ finished product zips and covers
research/ notes and sources
scripts/ the tools
One warning from experience: keep research/ forever. I deleted mine once to clean up, and lost raw material I still reference for new products. Disk space is cheaper than re-research.
Where this all lives now
Everything above is packaged as the ai sticker factory playbook. It is not a tutorial that stops at "here is how you would do it". It contains the four working scripts that packaged those 39 products, the RAM discipline notes for running on 1 vCPU and 2 GB, and the platform metadata rules they enforce, drawn from about 19,000 lines of pipeline code in the parent project. If you want the same packaging layer without rebuilding it from your own rejected CSVs, it is there.
Get ai sticker factory playbook
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)