The day I almost double-posted 58 assets
Last spring I had a folder of finished vectors and a CSV I thought was ready for Adobe Stock. I ran my upload script twice because the first run timed out and I assumed it failed. Adobe's reviewer queue does not care that it was an accident. Duplicate submissions are one of the fastest ways to get an account flagged, and I spent an evening manually withdrawing submissions before review caught it.
That was the moment I stopped thinking of "the art file" as the product. A stock asset is not finished when the render is done. It is finished when it has correct metadata, a tracking record so it never gets posted twice, and packaging that makes it sellable somewhere else. Here is what actually works, learned the slow way across 1,700 assets made and 58 live on Adobe, with a first sale of $0.99 that I screenshot and keep.
Metadata is where submissions die
All three major channels validate the CSV header before anything else. Adobe wants a 5-column format. Vecteezy wants 4 columns. Dreamstime wants 15. Get one column wrong and the upload rejects instantly, before a human ever sees your work.
So the rule I follow now is simple: never write a metadata file by hand, and never let a script guess the header. I built make_metadata.py to write exactly the header each platform demands and nothing else. It walks a folder of files and emits the right CSV for the platform you name:
python make_metadata.py ./my-assets --platform adobe --out metadata.csv --ext .jpg
One honest caveat: without a titles file it title-cases the filename as a placeholder. That is fine for a first pass, but replace generic titles before you submit. Reviewers notice keyword-stuffed filenames, and a title like "Abstract Background 47" does not help search ranking on any platform.
The ledger that prevents the worst mistake
The double-post scare changed how I think about batch jobs. Any process that can run twice needs memory. Not a spreadsheet you update manually, because you will forget, exactly when you are tired and batch-running at midnight.
upload_tracker.py is a plain ledger with three commands:
python upload_tracker.py pending items.txt
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
pending shows what is not done yet. A re-run consults the ledger first, so the same file never goes up twice. It uses the Python standard library alone, no install, which matters when you are hopping between machines or recovering a broken environment at 1 a.m.
Turning a document into a product
Stock art is one income line. Documents are another. I had research notes, hundreds of pages of them, sitting in Markdown doing nothing. The gap between "notes" and "product" was purely cosmetic: a clean PDF with real headings, tables, lists, and working links.
md2pdf.py closes that gap in one command:
python md2pdf.py content.md product.pdf "My Product Title"
It uses reportlab, one of only two third-party packages in the whole toolkit. The other is Pillow. Everything else is standard library. I held that line deliberately, because every extra dependency is another thing that breaks on a small server.
Packaging is what makes a file sell
A bare zip does not sell. A zip with a real cover image does. The difference in perceived value between a naked file listing and a product card with a 1280×720 cover and a 600×600 thumbnail is the whole difference between a listing and a product.
pack_product.py bundles it all:
python pack_product.py my-product "My Product" "A short subtitle" 9
That writes the zip, the cover, and the thumbnail. Output goes to a _system folder by default, and you can redirect it with --dir or the PACK_ROOT environment variable without editing the script.
This is the exact pipeline that packaged 39 products, 856 MB of finished output, from a parent codebase of about 19,000 lines backed by 300+ pages of research.
The tiny server habits that keep it running
None of this needs a real machine. The whole thing runs on 1 vCPU, 2 GB RAM, no GPU. But a box that small punishes bad habits, so I follow five:
- One process at a time. Never fan out image jobs in parallel on a single core.
- Call
gc.collect()after each asset so memory does not creep. - Measure peak RSS, not average, with
resource.getrusage(RUSAGE_SELF).ru_maxrss. Average lies, the peak is your real ceiling. - Run heavy render steps as one file per subprocess so memory returns to the OS.
- Render at the resolution you need. Inspecting a preview at 60 dpi instead of 300 saves more than you expect.
The folder structure that stops the junk drawer
The last piece is boring and important. A catalogue without a fixed layout becomes a junk drawer within weeks. The structure this assumes:
project/
assets/ raw output
output/ ready-to-submit files + metadata
packs/ finished zips + covers
research/ notes and sources
scripts/ the tools
Keep research/ forever. I once deleted mine to "clean up" and lost raw material that would have become three more products. Never again.
Where to get the tools
Everything above is the packaging layer of a larger system I call digital asset factory. It contains these four scripts, the folder conventions, the small-server memory notes, and how they plug into a free stack on top: an orchestrator for scheduling and retries, and a local LLM proxy, all at $0 in tooling cost. The scripts ran on a 2 GB server and produced 39 finished products. If you are a maker who keeps stalling after the art is done, this is the part that comes after the art file.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)