TL;DR
- The work between “I own this thing” and “money arrived” is mostly paperwork and printing, and it’s where the hours actually go.
- Listings get built through the marketplace API, not by driving a browser — structured item specifics as JSON beat fighting a typeahead widget every time.
- Drafts are created automatically. Publishing is always manual. Publish is a money-moving, buyer-facing, irreversible action; it gets a human every time.
- Barcode labels render from the inventory record itself: ZPL → PDF → print job to a thermal printer, triggered from the item’s page. No separate label app, no retyping serials.
- Pack slips print on a schedule, and the job verifies the printer’s page counter moved before marking the slip printed. Optimistic “it printed” flags are how orders ship without paperwork.
- Anything bulk or financial — repricing, printing five-plus labels — sits behind an explicit confirmation. Cheap friction in exactly the right places.
The boring half is the expensive half
Sourcing gets all the attention. I’ve written about the funnel that finds lots and the data model that tracks the units. Both were interesting problems.
Neither is where the hours go. The hours go here:
Photograph the thing. Write a title that people search for. Fill in twenty item specifics. Set a price. Pick a shipping policy. Publish. Wait. Answer a question about whether it includes the cable. Sell it. Print a label. Print a pack slip. Find the item on the shelf. Box it. Match the payout against the sale.
That’s per unit. Multiply by forty units from one pallet. This is the part that makes people quit, and it’s almost entirely mechanical, which means almost all of it can be automated — with one very deliberate exception.
Listings: API over browser
For a while I built listings by driving a logged-in browser. It works and I don’t recommend it. Marketplace sell-flows are built for humans: item specifics are typeahead widgets that reject values they suggested themselves, rich-text description fields silently drop pasted markup, and photo uploads double-submit if you look at them wrong. Every one of those is a flaky selector waiting to break on a UI refresh.
The API path is better in every way that matters. The lifecycle is three calls:
- Create an inventory item — the product itself: title, condition, quantity, aspects, image URLs. Aspects go in as clean JSON. No typeahead, no autocomplete, no guessing which of four spellings the widget will accept.
- Create an offer — price, category, business policies, location. This returns an offer ID and is not yet live.
- Publish the offer — this one makes it real.
Images need to be public HTTPS URLs, so they get pushed to the marketplace’s own image service first and the returned URLs get used in step one. That also means I’m not hosting anything the marketplace needs, which removes a dependency I’d otherwise have to keep up.
Steps one and two run automatically. Step three does not, ever.
Why publish stays manual
Publish is the line. On one side: reversible, private, cheap to get wrong. On the other: a live listing with a price, visible to buyers, with a binding obligation attached the moment someone clicks buy.
So drafts accumulate in an unpublished state — which is exactly why the inventory status machine has a drafted state distinct from listed — and I review them in a batch. The review takes about ninety seconds per item and catches things a model reliably gets wrong:
- Price sanity. The estimate came from comparable sales; comparable sales can be thin, or can be for a variant with a critical difference.
- Condition honesty. “Good” from a photo and “good” in my hand are different judgements, and the second one is the one a buyer will hold me to.
- What’s actually in the box. Missing cables, missing rails, missing calibration certificates. Photos don’t reliably show absence.
- The title. Automated titles are accurate and lifeless. Search doesn’t reward accuracy, it rewards matching the phrase a buyer types.
That last one is worth dwelling on: the drafts are consistently good enough to publish and consistently worse than a thirty-second human edit. Ninety seconds of review on a $400 instrument is an obviously good trade, and it’s why I have no interest in closing this gap.
Channels, and horses for courses
Not everything belongs on the big marketplace.
- The main marketplace for anything shippable with an established comp history. Highest fees, highest liquidity.
- Local pickup channels for anything heavy, fragile, or low-value-per-pound. A 90-pound instrument that sells for $180 is a loss after freight and a profit at the curb. These channels don’t have usable APIs, so this is the one place browser automation still earns its keep — and it’s tolerable because the volume is low and a failure just means I post it by hand.
- Industry-specific marketplaces for lab and clinical equipment. Far smaller audiences, far better buyers: people searching for a specific instrument by model number, who know what it’s worth and aren’t looking for a bargain. Fees are structured completely differently — some flat-fee-per-listing, some free tiers.
- A plain direct-sale page of my own, fed by a generated product feed. No marketplace fees, and no marketplace traffic either. It’s not a volume channel; it’s the channel where someone who found a specific instrument another way can actually buy it.
Routing is mostly rules: weight and dimensions, category, and value band. The interesting exception is lab gear, where the specialist channels beat the general one badly enough that it’s worth the slower sale.
Labels come from the record
The physical-world integration that pays for itself fastest: printing a barcode label directly from the inventory record.
The item’s page has a print action. It renders label markup for the thermal printer, converts it to PDF, and submits a print job to a networked label printer over the standard print protocol. The label carries the SKU, model, serial or service tag, condition grade, and a QR code that opens that item’s page in the tracker.
Nothing about that is impressive. What makes it valuable is what it eliminates: nobody types a serial number twice. The serial is captured once — often automatically, when a machine checks itself in after imaging — and everything downstream reads it from the record. Transcription errors on serial numbers are the single most annoying category of inventory bug, because you don’t find out until you’re matching a payout against a unit that doesn’t exist.
The QR code matters more than I expected. A physical shelf of eighty units, each with a QR that opens its own record on a phone: what it cost, what it’s listed for, whether it has an open repair ticket. That closed the loop between the shelf and the database in a way no amount of good UI did.
One guard: printing more than a handful of labels at once asks for confirmation first. Thermal labels are cheap but a runaway loop that eats a 500-label roll while nobody’s watching is a real thing that could happen, and the confirmation costs one keystroke.
Pack slips, and the trust-but-verify flag
Pack slips print on a schedule — a job that runs twice an hour, finds sales that need paperwork, renders a slip, and sends it to the office laser printer double-sided in black and white.
The part worth stealing is the flag handling. The naive version stamps pack_slip_printed_at right after submitting the print job. That’s a lie roughly as often as printers are printers: out of paper, offline, queue jammed, someone power-cycled it. The order then ships without a slip, or the slip never prints and nothing ever tells you.
So the job reads the printer’s own page counter before and after, and only stamps the flag if the counter moved. If it didn’t, the sale stays unstamped and gets picked up on the next run.
That’s a two-line change that converts an optimistic write into a verified one, and it generalises well beyond printing: if you’re recording that a side effect happened, check that it happened. Submitting a job is not the same as the job completing, and the gap between those two facts is where a whole genre of silent failure lives.
Reconciliation is where the truth is
The last state a unit reaches is reconciled, and it exists because a sale record and a payout record are different claims.
A sync job pulls the marketplace’s order and payout data on a schedule and matches it against my sales: fees actually charged versus fees expected, quantity-N orders mapped to individual units, refunds, partial refunds, shipping charged versus shipping paid. Discrepancies get flagged rather than auto-corrected, because a mismatch usually means my model of the fee structure is wrong, and silently overwriting my number with theirs would hide that.
Fees are the thing this catches most often. Rates differ by category, promoted-ad rates differ per listing, store subscription costs get amortised — and every one of those drifts when the marketplace changes terms. Until reconciliation existed I believed my margin numbers. Now I know which categories were quietly costing three points more than I thought.
Gotchas
Publish is not a draft. Treat it like a commit. Any tooling that can publish should require an explicit, separate action with the word “publish” in it. Building the offer and publishing the offer must not be one function call.
Bulk price changes need a confirmation prompt. Repricing forty listings is one command and forty real financial decisions. Mine refuses to run in bulk without an explicit confirmation, and I’ve been grateful for it once, which is exactly once more than never.
Don’t hand out direct object-storage URLs for photos. Serve them through the app with proper caching headers. Otherwise every photo URL you’ve ever put in a listing is a permanent public read on your bucket, and you can never move storage.
Item specifics are the SEO. Buyers filter on them. A listing missing “Model” doesn’t appear in the filtered search, no matter how good the title is. This is the single most common reason an automated listing underperforms a hand-written one.
Local channels don’t have APIs and that’s fine. Don’t burn a month building a robust automation for a channel you use twice a week. Browser automation with a human watching is the right amount of engineering there.
Where this sits
Drafts built by API and reviewed in batches, four channels with rule-based routing, labels printed from the record with a QR back to it, pack slips that verify they actually printed, and a reconciliation pass that keeps my fee model honest.
Everything is automated except the two actions that move money in a direction I can’t undo: publishing a listing and changing a price in bulk. Those cost me a couple of minutes a day and they’re the reason I sleep fine leaving the rest of it running on a schedule.