Last March I lost a whole evening to a CSV header. I had 40 vectors ready for Adobe Stock, I exported my metadata the way I thought it should look, hit upload, and the review check bounced the whole batch in seconds. Not "your art is bad". Not "keyword spam". The file never even got parsed, because the header row was wrong. Adobe's uploader expects a specific 5-column layout, and it does not negotiate. I fixed that one, then found out Vecteezy wants 4 columns and Dreamstime wants 15. Three platforms, three headers, zero forgiveness.
That night I nearly started building a small framework. A config file, plugins per platform, a nice CLI. I'm glad I didn't. Here is what I learned shipping the boring version instead.
The failure mode is packaging, not art
Making the assets is the part I enjoy, and it's the part that goes fast. What actually stalled me, repeatedly, was the four steps after the art exists:
- Metadata CSVs that pass each platform's header check on the first try.
- Not re-uploading things I already posted, because running a batch twice is the fastest way to get an account flagged.
- Turning a Markdown document into something that looks like a product, not a text dump.
- Bundling it all into a zip with a cover that makes someone click.
Every one of these is small. Together they are why most pipelines die as a folder of half-finished files. My parent pipeline is around 19,000 lines of Python now, and the part that earns money is maybe 600 of them, split across four standalone scripts.
Why four scripts won
A framework wants to own your whole flow: your config, your folder layout, your plugin API. Every time a platform changed a rule, I'd be editing an abstraction instead of a file. With standalone scripts, the fix is one line in one file, run it, done.
Two of them don't even need an install. make_metadata.py and upload_tracker.py are pure standard library. The other two pull in exactly two third-party packages: Pillow for covers, reportlab for PDFs. That's the whole dependency tree. No cloud account, no API keys, no runtime to keep alive.
make_metadata.py walks a folder and writes the exact header each platform demands: the 5-column Adobe layout, the 4-column Vecteezy one, the 15-column Dreamstime one. It takes --platform adobe and nothing else to think about. If you don't have titles yet, it title-cases the filename as a placeholder, which is fine for a first pass and should be replaced before you submit. The point is that the header is never your failure point again.
upload_tracker.py is the least glamorous and most valuable file I own. It's a ledger. pending items.txt shows what hasn't been posted, done "asset-001.jpg" marks one as finished, status gives you the overview. A re-run of the batch can no longer double-post, because the tracker remembers. When you sell on three platforms and review queues take days, "did I already submit this?" is a real question worth making impossible to get wrong.
md2pdf.py takes a Markdown file and renders a clean styled PDF with headings, tables, lists and links, in one command. pack_product.py zips the files and renders a 1280x720 cover plus a 600x600 thumbnail. A zip with a real cover sells. A bare file does not. I know because I sold both kinds.
The server is tiny, so the code has to be honest
All of this runs on 1 vCPU, 2 GB RAM, no GPU. That constraint shaped the code more than any design decision did. The habits that made it work:
- One process at a time. Never fan out image jobs in parallel on a single core. It doesn't go faster, it just runs out of memory in a more interesting way.
- Call
gc.collect()after each asset so memory actually comes back between items. - Measure peak RSS, not average.
resource.getrusage(RUSAGE_SELF).ru_maxrsstells you the real ceiling. Average memory lying to you is how you discover OOM kills at 2 AM. - Run heavy render steps one file per subprocess so the OS reclaims the memory fully.
- Render at the resolution you need. Inspecting a preview at 60 dpi instead of 300 is the difference between fitting and not fitting.
A framework hides all of this from you, which sounds nice until you need to know exactly where your 2 GB went.
The folder layout matters more than the code
One non-code thing that saved me: a fixed structure. assets/ for raw output, output/ for ready-to-submit files and metadata, packs/ for finished zips and covers, research/ for notes and sources, scripts/ for the tools. Without it, a catalogue of 39 products becomes a junk drawer you're afraid to open.
And keep research/ forever. I once cleaned up my scrape logs and notes to "tidy up" and then needed them two weeks later for a follow-up product. That raw material is the input to the next thing you make. Deleting it is a mistake I made once and won't repeat.
Where this landed
The full run produced 39 finished products, 856 MB of output, built on top of about 300 pages of research into platform rules, and validated on that same 1 vCPU / 2 GB box. The four scripts are the packaging layer of that system. They're not clever. They're the part that had to work every single time, so I made them boring on purpose.
I packaged them up as the pipeline starter kit, with the scripts, the install notes, the quick start commands, the folder layout and the RAM habits written down the way I wish someone had written them down for me. If you're at the stage where your art exists but your products don't, that's the stage these were built for.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)