The night I lost three hours of work, I was on my third re-run of a batch upload. My CSV had the wrong column count for Vecteezy, four columns instead of the five I'd copy-pasted from an Adobe template, and every row got rejected. Then I fixed it, re-ran, and realized I had no idea which files from the previous attempt had actually gone through. I was about to double-post to Adobe, which is the fastest way I know to get an account flagged. That was the moment I stopped writing new tools and started deleting them.

I ended up with four scripts. They packaged 39 finished products, 856 MB of output, on a server with 1 vCPU, 2 GB RAM, and no GPU. Here is what I learned about why fewer tools shipped more.

Metadata is a formatting problem, not a creative one

All three of the stock channels I submit to reject a wrong CSV header instantly. Adobe wants its 5-column layout, Vecteezy wants 4 columns, Dreamstime wants 15. There is no partial credit. If the header is wrong, the whole upload fails before a human ever looks at your art.

So make_metadata.py does one thing: it walks a folder of files and writes the exact header each platform demands, nothing else. It runs on the Python standard library alone, no install. You can pass --titles titles.json to supply real titles and keywords, and if you don't, it title-cases the filename as a first pass. That first pass is fine for testing and not fine for submission, and I say that in the script's own docs. Replace it before you submit.

The lesson: I wasted weeks trying to build a universal metadata format. The platforms don't want universal. They want exact.

Double-posting is the failure mode nobody plans for

Every batch pipeline has this bug eventually: you re-run the job, and it posts the same assets twice. Reviewers notice. Accounts get flagged.

upload_tracker.py is a ledger, also standard library only. Three commands:

python upload_tracker.py pending items.txt python upload_tracker.py done "asset-001.jpg" python upload_tracker.py status 

Before every run I check pending. If a file is already marked done, it doesn't go out again. It is about forty lines of code and it has prevented more account risk than anything else I wrote. The fancy tools in my pipeline never protected me from this. The dumb ledger did.

A document is not a product until it looks like one

I had a 300-page research corpus behind my pipeline rules. It sat as Markdown for months because turning it into something sellable felt like a separate project. It wasn't. It was two commands.

md2pdf.py takes a Markdown file and renders a clean, styled PDF with headings, tables, lists, and links, using reportlab. That is one of only two third-party packages in the whole kit. pack_product.py is the other half: it zips the files and renders a cover at 1280x720 plus a thumbnail at 600x600, using Pillow. It writes the zip, cover, and thumb into a _system folder, and you can redirect that with --dir or the PACK_ROOT environment variable without editing anything.

A zip with a real cover sells. A bare file does not. I confirmed this the lazy way: my first few listings without covers sat at zero, and the packaged ones did not.

The tiny server forced good habits

Everything ran on 1 vCPU and 2 GB RAM. That sounds like a limitation. It was a discipline. The habits that made it work:

  • One process at a time. Never fan out image jobs in parallel on a single core.
  • Call gc.collect() after each asset so memory actually frees between items.
  • Measure peak RSS, not average. resource.getrusage(RUSAGE_SELF).ru_maxrss gives you the real ceiling, and the real ceiling is what crashes you.
  • One file per subprocess for heavy render steps, so memory returns to the OS.
  • Inspect previews at 60 dpi instead of rendering at 300. You do not need full resolution to check whether an image is garbage.

None of these are clever. All of them came from crashing first.

The folder layout matters more than the code

One structure, fixed: assets/ for raw output, output/ for ready-to-submit files plus metadata CSVs, packs/ for finished zips and covers, research/ for notes and sources, scripts/ for the tools. Keep research/ forever. I deleted mine once during a cleanup pass and immediately needed the scrape logs for a follow-up product. That mistake will not happen twice.

Where this ended up

The parent project is about 19,000 lines of pipeline code, but the packaging layer is just these four scripts, two dependencies, and a folder structure. That ratio is the whole philosophy. The making is the fun part, and the four stall points, metadata, double-posting, doc-to-product, and packaging, are all solved by small boring tools that do one thing exactly right.

I bundled these exact scripts, the folder layout, and the tiny-server habits into the ai sticker factory playbook, alongside the prompt and packaging workflow behind those 39 products. If you want the tools instead of the theory, that is where they live.

Get ai sticker factory playbook

Building digital products? Free guides & kits for digital product sellers.