Last March I had 40 assets sitting in a folder, ready to go up on Adobe Stock. I built the CSV by hand in a spreadsheet, pasted it into the uploader, and got a rejection in under a minute. Wrong header row. Adobe wants a specific 5-column format, and one extra column in the wrong place means the whole file bounces. I fixed it, re-uploaded, hit another error on the keywords field. That night I lost two hours to a spreadsheet.
That was the moment it clicked. I wasn't failing at stock art. I was failing at process. The product was fine. The system around it did not exist.
I want to walk through what I changed, because the numbers after that change are the only argument I trust: since then, one prompt engine has produced 39 finished products, 856 MB of output, run entirely on a 1 vCPU, 2 GB RAM server with no GPU. Here is what actually worked.
Metadata is a format problem, not a creative one
Every stock platform has a rigid upload CSV, and every one of them rejects a wrong header instantly. No human review, no partial credit. Adobe uses 5 columns. Vecteezy uses 4. Dreamstime wants 15. If your header does not match exactly, nothing else about your submission matters.
So I stopped hand-building CSVs and wrote make_metadata.py. It takes a folder of files and writes the exact header each platform demands, nothing more. It runs on the Python standard library alone, no installs. Optional flag lets me pass a JSON file of real titles and keywords. Without it, it title-cases the filename, which is fine for a first pass but I always replace it before submitting, because a filename-based title reads like a filename.
The point is not the script. The point is that metadata was never going to scale as a manual task. Forty assets times three platforms times hand-typed headers is a full afternoon. A script does it in seconds, and it never typos a header.
Double-posting is the fastest way to get flagged
Here is a mistake I made exactly once: re-running a batch job that had partially completed, and posting duplicate submissions. Reviewers notice. Platforms notice. That is how accounts get flagged.
The fix is boring and essential. upload_tracker.py is a tiny ledger that remembers what I have already posted. Before any batch run I check pending to see what is actually left. After posting I mark items done. A re-run now physically cannot double-post, because the tracker sits between me and the queue.
If you take one habit from this post, take this one. Batch jobs fail partway through all the time. The failure is normal. The duplicate is what costs you.
Packaging is where a file becomes a product
A bare zip of files does not sell. A zip with a real cover image does. I learned this watching my own listing pages: the products with a proper 1280x720 cover and a 600x600 thumbnail looked like something a person made on purpose. The rest looked like homework.
pack_product.py bundles the files, renders the cover and thumbnail, and drops everything into a _system folder. You can redirect output with --dir or the PACK_ROOT environment variable, so no script editing. It uses Pillow, which is one of exactly two third-party packages in my whole packaging layer.
The other one is reportlab, for md2pdf.py, which turns a Markdown file into a styled PDF with headings, tables, lists, and links. One command. I use it for every product that started as a document, which is most of them.
The folder structure is the product you keep
A catalogue without a fixed layout turns into a junk drawer. Mine did, once. This is the structure I settled on and have not changed since:
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 rule I would tattoo on my own desk: keep research/ forever. I deleted my notes once during a cleanup, and then needed them six weeks later when I was extending a product line. Scrape logs and research notes are raw material for future products. Deleting them is the mistake I made once and will not repeat.
Small servers force good habits, and the habits are worth it
The whole pipeline runs on 1 vCPU and 2 GB RAM. No GPU. That constraint taught me more than a beefier machine ever would:
- One process at a time. Fanning out image jobs in parallel on a single core just thrashes.
- Call
gc.collect()after each asset to free memory between items. - Measure peak RSS, not average.
resource.getrusage(RUSAGE_SELF).ru_maxrssgives you the real ceiling your process hits. - Run heavy render steps one file per subprocess, so memory actually returns to the OS.
- Render at the resolution you need. Inspecting a preview at 60 dpi instead of 300 saves more memory than any clever code.
My AI image generation costs stay low partly because of this. I am not paying to over-render previews or re-run crashed parallel jobs.
Why "build a system" is the only advice that holds
The parent project behind all this is about 19,000 lines of pipeline code, backed by over 300 pages of research into the platform rules. But the four scripts that did the daily work are small, standalone, and boring, which is exactly why they survived. No framework, no cloud account, two pip packages total.
Products are outputs. Some sell, some do not, and you cannot predict which in advance. The system is the thing that lets you find out cheaply, forty times, without burning an evening on a CSV header. Every hour I spent on the scripts paid itself back by the second product.
I packaged these four scripts, with the folder layout and the server notes, into the Pipeline Starter Kit. It is the exact tooling from this work, not a tutorial that ends at "here is how you would do it". If you are at the stage I was, staring at a rejection email over a hand-built spreadsheet, it will save you that night.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)