Last Tuesday I got a support message about a product I had listed. The buyer said the listing promised vectors and asked where they were. They were right that the listing said "vector pack". They were wrong about what they had bought, because what I actually shipped was the pack with a 1280×720 cover image on top, and the cover was the thing that made the sale in the first place. That message stung, but it also settled an argument I had been having with myself for weeks: do I hold a product back until every asset is perfect, or do I package what exists and ship it?
I shipped. Here is what I learned from doing it the imperfect way, and what the numbers look like.
The cover does more work than the assets
When I package a product with pack_product.py, it writes three things: a zip of the files, a 1280×720 cover, and a 600×600 thumbnail. The 600×600 thumbnail is what shows up in grid views and search results on most marketplaces. The 1280×720 cover is what the buyer sees when they click through.
For weeks I treated those renders as an afterthought and spent my time on the assets themselves. That was backwards. A zip of files with no cover does not sell. The same files with a clean title, a subtitle, and a real cover looks like a product. Same content, different outcome, and the only difference was about forty lines of Pillow code that renders text onto a canvas.
The first sale I ever made was $2.80, for the Digital Asset Factory product itself. Not a life-changing number. But it happened on a listing where the cover existed and the packaging was done, not on any of the half-finished packs sitting in my packs/ folder with placeholder images.
Metadata headers are pass or fail, not a quality score
The other reason I stopped waiting for perfection: the stock platforms do not care about my feelings, they care about CSV headers. Adobe wants a 5-column header. Vecteezy wants 4 columns. Dreamstime wants 15. If one column is wrong or in the wrong order, the upload rejects instantly. There is no partial credit.
So make_metadata.py writes the exact header each platform demands and nothing else. When I generate a CSV from a folder, I know the first row will pass. What I do not know yet is whether my titles and keywords are good, which is why the script takes an optional titles.json. Without it, the filename gets title-cased and used as the title. That is fine for a first pass, and I replace it before submitting. The point is that the mechanical part is solved so I can spend my judgment on the part that needs judgment.
Double-posting is the fastest way to get flagged
This is the mistake that would have hurt the most. When you run a batch upload twice, the platforms see duplicate submissions, and duplicate submissions are one of the quickest ways to get an account flagged. Early on I almost re-ran a batch after a partial failure, and I realized I had no idea which files had actually gone through.
upload_tracker.py is a plain ledger for this. pending shows what is not done, done "asset-001.jpg" marks one file, status prints the summary. It uses nothing but the standard library, so there is nothing to install or break. A re-run can never double-post because the ledger is checked before anything is queued. Boring tool, and the single most important one in the kit.
Small servers force good habits
The whole pipeline runs on 1 vCPU and 2 GB RAM with no GPU. That constraint turned out to be a feature. On a box like that you cannot fan out image jobs in parallel, so you run one process at a time. You call gc.collect() after each asset because memory does not free itself. You measure peak RSS with resource.getrusage(RUSAGE_SELF).ru_maxrss instead of guessing, because average memory tells you nothing about the ceiling. You render previews at 60 dpi for inspection instead of 300, and you render at the resolution you actually need.
The result is that packaging a product takes minutes on hardware that costs almost nothing to run. 39 products packaged, 856 MB of output, on a server that would fall over if I had written it the "modern" way with parallel workers.
What shipping the imperfect pack actually bought me
Back to the buyer with the vector complaint. I answered honestly, explained what was in the pack, and offered the vector files as soon as the batch finished rendering. Meanwhile the listing stayed up, because a listing that is up and 90 percent right earns more than a listing that is perfect and unpublished. Of 1,860 assets made so far, 1,077 are ready or submitted. The gap between those two numbers is not failure, it is the pipeline doing its job while I sleep.
The lesson I keep relearning: the bottleneck is never the art. It is metadata, packaging, tracking, and the discipline to ship before everything is polished.
The tool this became
Everything above came out of building one system, and I packaged the working parts of it as the Digital Asset Factory. It includes the four scripts I described, the folder layout, the server notes, and the metadata rules, validated on that same 1 vCPU / 2 GB box. If you want the tools instead of just the story, it is here:
Building digital products? Free guides & kits for digital product sellers.