I still have the rejection email open in a browser tab from last March. I had spent a weekend generating a set of vector assets, written what I thought was a careful CSV for Adobe Stock, and uploaded 40 files in one batch. Every single one came back rejected before review even started. Not because of the art. Because my metadata CSV had the wrong header. Adobe's uploader expects an exact 5-column format, and it checks that header before anything else. One wrong column name and the whole batch dies.
That was the moment I stopped treating the art as the hard part.
The metadata is the product, technically
Here is what nobody tells you when you start selling on stock platforms: each one has its own CSV dialect, and none of them forgive improvisation.
Adobe wants 5 columns. Vecteezy wants 4. Dreamstime wants 15. If you sell on all three, and you should, because the same asset earns in three places, you are maintaining three different file formats by hand, or you are writing a script that does it for you.
I wrote make_metadata.py for exactly this. You point it at a folder of files, tell it the platform, and it emits the CSV with the exact header that platform's uploader demands. Nothing extra, nothing missing. The header check is the first gate every upload passes through, so getting it wrong costs you a full batch cycle, which on Adobe can mean waiting a day to find out you failed.
The script has a --titles titles.json option so you can supply real titles and keywords. Without it, it title-cases the filename. That is fine for a first pass, but replace it before you submit. A filename-based title reads like a filename, and reviewers notice.
Double-posting is how accounts get flagged
My second failure was quieter. I re-ran a batch job after a partial failure, and I posted duplicates. The platforms do not always reject duplicates loudly. Sometimes they just flag the pattern, and a flagged account gets slower reviews and more scrutiny on everything after.
So I wrote upload_tracker.py, which is honestly the least interesting and most valuable script in the set. It is a ledger. You mark an asset as done, and a re-run shows you only what is pending. Four commands: pending, done, status. Standard library only, no install. It makes double-posting structurally impossible instead of something you remember to avoid at 1am.
The parts that are not stock platforms
Selling only through stock platforms means sharing revenue with them. The same asset set can also become standalone products: a PDF guide, a bundled pack with a cover. That is where the other two scripts come in.
md2pdf.py turns a Markdown file into a styled PDF using reportlab. Headings, tables, lists, links all render properly. One command: python md2pdf.py content.md product.pdf "My Product Title". The difference between a raw document and something that looks like a product you paid for is mostly typography, and reportlab handles it once you set it up. I set it up once so nobody has to again.
pack_product.py bundles the files into a zip and renders a cover at 1280x720 plus a thumbnail at 600x600, using Pillow. A zip with a real cover sells. A bare file does not. That is not a marketing claim, it is just what listing pages look like. The output goes into a _system folder by default, and you can redirect it with --dir or the PACK_ROOT environment variable, so no script editing.
The whole thing has exactly two third-party dependencies: Pillow and reportlab. The metadata and tracker scripts need nothing but Python 3.9 or newer. If you only care about uploads, skip the install entirely.
The server is tiny, so the habits matter
None of this ran on a dev machine. It ran on a 1 vCPU, 2 GB RAM box with no GPU, because that is what I could justify paying for while validating the idea. That constraint shaped the code more than any feature request would have.
The habits that made it work:
One process at a time. Never fan out image jobs in parallel on a single core. It does not speed anything up and it will exhaust your memory.
Free memory between items. Call gc.collect() after each asset. Python will not do it for you on a schedule you control.
Measure peak RSS, not average. resource.getrusage(RUSAGE_SELF).ru_maxrss gives you the real ceiling. Average memory tells you nothing about whether you will get OOM-killed at file 200.
One file per subprocess for heavy render steps, so the OS actually reclaims the memory instead of trusting the interpreter.
Render at the resolution you need. Inspecting a preview at 60 dpi instead of 300 is the difference between fitting in RAM and not.
What the numbers ended up being
One prompt engine, one asset methodology, 39 finished products across the three platforms, 856 MB of packaged output. Around 19,000 lines of pipeline code in the parent project, and over 300 pages of research behind the rules the scripts enforce. The four scripts are the packaging layer that sits on top of that.
The honest part: the scripts did not make the art good. They made everything around the art reliable. The rejections stopped because the CSVs were right. The duplicates stopped because the ledger remembered. The products looked like products because the covers and PDFs were generated instead of skipped.
If you are sitting on a folder of assets and stalling at exactly the gates I stalled at, wrong headers, duplicate fear, no packaging, that is the whole problem this solved. I packaged the four scripts, plus the folder layout that keeps a catalogue from becoming a junk drawer, as the vector prompt pack. It assumes a structure like assets/, output/, packs/, research/, scripts/, and one rule I learned the hard way: keep your research folder forever. I deleted mine once during a cleanup pass and lost the raw material for three future products.
The pack is the tool built from this work, not a course about it.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)