The Haunted Experience · Write-up

Building a Retail Operating System for a Halloween Store

A handheld scanner, a store terminal, a local server, and two-way sync with a Square register and an eBay storefront. Written in one season, under live trading.

Cormac LydonOctober 2026About 15 minutes

IntroductionOne stock pool, two storefronts, one season

I built a complete operating system for a seasonal Halloween costume store: a handheld scanner app, a store terminal, a local server, and two-way sync with a Square register and an eBay storefront. It runs about 15,000 units of stock across nearly fifty rack faces and more than 5,000 live eBay listings, and it was written in one season, under live trading.

A Halloween shop has an unusual shape. Almost all of the year’s revenue arrives in about eight weeks, the last five days of October are worth as much as some whole months, and every costume comes in a run of sizes from several makers. Stock left on the first of November is carried, written down, or sold at a loss.

Off-the-shelf retail software assumes a steady year and a single channel. This store needed something else: one stock pool selling at a counter and online at once, prices that follow cost, competition and the calendar, and a back room that can find, pick and post a parcel in minutes during the rush.

The thesis of this write-up is simple. A small retailer does not have to choose between generic tools and doing everything by hand. A system shaped exactly to the shop, with its rules written down as code, can carry a season that would otherwise need several more people.

ScaleBy the numbers

PartSize
Code (JavaScript, HTML, CSS)about 64,000 lines in 193 files
Operational tools (repricing, publishing, audits)75 command-line tools
Automated test suites63 suites, about 2,900 individual checks
Stock on hand, counted by scannerabout 15,000 units
Live eBay listingsabout 3,200 listings covering 5,300 SKUs
Vendor reference catalogueabout 80,000 barcodes from more than 80 makers
Architecture3 floor-side parts, 1 host, 4 outside services

ArchitectureThe system at a glance

Three parts work the shop floor, one server on the shop’s laptop keeps the record, and four outside services are reached only through that server.

SHOP FLOORHANDHELDZebra TC57count · recount · pickTERMINALback-room laptoppack · time clock · dashboardsSCHEDULED TOOLS75 command-line toolsreprice · publish · audit · photo huntHOST · THE SHOP’S LAPTOPlocal serverone append-only logone stock poolone rules fileoutbox with per-channel ackspricing.js · floorplan.jscatalogue decoding · floorsthe only holder of credentialsthe only caller of outside servicesOUTSIDE SERVICESSQUAREregister and catalogueEBAYlistings, orders, offers, adsLABEL SERVICErates, labels, trackingMAKERS’ SITESphotos and namesThe handheld and the terminal never talk to eBay or Square directly. Everything passes through the host.
Figure 1. Three floor-side parts, one host, four outside services. The scheduled tools run on the same laptop as the host, so there is one log, one set of rules, and the credentials never leave the building.

The handheld and the terminal never talk to eBay or Square directly; everything they do passes through the host. The scheduled tools run on the same laptop, so there is one log, one set of rules, and the credentials never leave the building.

The floorCounting the store

The stocktake is a walk, not a spreadsheet. A staff member carries a Zebra TC57 handheld round the shop, chooses the rack they are standing at on the screen, then scans every costume hanging there. The racks carry no barcodes. The shop knows its own racks by heart, so the handheld offers them as buttons in counting order, with the next rack on the walk one tap away and the store map there to tap instead. Each scan is a small record, barcode, rack, time, device, appended to one log on the store’s laptop.

The floor plan is code. The shop’s own drawing was transcribed into a model of every fixture: sixteen double-sided C racks, the S and A racks, the perimeter walls (wigs, toddler, masks, plus, sexy, pets), the weapons block, the dressing rooms and the counter. Coordinates stay in the drawing’s units, about ten to the foot, so the plan is the authority and feet are derived only where a person reads them.

Faces and thirds. Every freestanding rack has an A face and a B face, counted as separate locations. Walking round to the other side is one button, which names the face you are going to, and the handheld confirms the switch, so a whole face’s count cannot land on the wrong side by a slip. Along a face, position is kept as thirds: a costume scanned fourth of twelve sits in the first third of the walk. Thirds claim nothing about hook counts that nobody measured.

The handheld does the bookkeeping on the spot. Hanging a costume on a dollar-deals rack marks it as an online deal. Scanning onto the accessory racks files it as an accessory and prices it by the accessory rule. A recount task compares a rack against what it held before and moves items between racks only when the whole rack is finished, so a half-done count never scatters stock.

Corrections, not edits. A count can be corrected, “this face holds 3, not 5”, and the correction replaces every read up to it while later reads still add. The laptop, the handheld and the host all derive counts with that one rule, because three screens disagreeing silently is worse than any one of them being wrong.

The handheld works offline. Reads queue on the device and send when the laptop is reachable, and it carries its own copy of the 80,000-row vendor catalogue so a barcode can be named and priced without a connection.

InventoryOne pool, two storefronts

Every costume exists once, in one stock pool, and is offered in two places: the Square register at the counter and an eBay store. The design promise is one sentence.

First sale wins, the other channel decrements.

A sale at the till is read from Square’s change stream and relayed to eBay. An eBay order is read from the fulfilment API and relayed to Square. Neither is ever sent back to where it came from. Every outbox entry records which channels have acknowledged it and leaves only when all of them have.

The hard parts of two-way inventory sync, and what the system does about each:

ProblemWhat goes wrongThe answer
EchoA pushed sale is read back as a new sale and counted twiceEvery write carries a reference id the puller recognises and drops
Lost updatesDiffing two count snapshots races any sale made in betweenSync from the change stream as deltas, never from counts
DuplicatesSquare delivers at least once, and retries add moreEvery change has an id, and an id already applied is skipped
Offline salesThe card reader takes payments with no network for up to 72 hoursApplied when they land, but flagged, since the pool was wrong meanwhile
Stale historySquare refuses any change dated more than 24 hours backAn entry that missed its day is sent as a correction to the pool, stamped now
Missing linksAn eBay sale lands before the listing’s link is recordedMatched by barcode, since the seller SKU is the barcode

The last two rows were learned the hard way. A child’s costume sold online, was parked as unknown because its link was not yet written, and was pushed back up to eBay, where the same buyer bought the same, already shipped, costume a second time. The fix was not to patch that listing but to close every path that could produce it, and then sweep every SKU sold that season to prove none was still on offer. That became a rule: correct the pool, not just the channel.

The sync loop orders its work by what stops an oversell first: collect orders, then push quantities, then publish new listings. Each step is fenced on its own, so a slow publish can never delay the order pull that protects the stock count.

RulesPricing as policy

Every price in the store comes from a written rule, and every rule lives in one file shared by the terminal, the handheld, the host and the tools. When the owner changes a policy, it changes in one place and every channel follows.

In store. The till rings cost times a category multiplier, two for costumes, three for wigs and accessories, four for make-up, always rounded up: to the next 50 cents under $10, to the next $5 above, shown as .99. A price somebody typed is a decision, so it rings the higher of the typed price and the rule. Nothing ever rounds down.

On eBay, the ask is built in layers, checked in order.

THE EBAY ASK · CHECKED IN ORDER, TOP TO BOTTOM1Dollar-deal racksrack price, or the market price when competitors show buyers pay more; buyer pays postage; the till is never touched2Accessories kept onlinea floor that still makes money on one item in its own parcel after fees, ads and a store discount3Ride-on costumesnever below $79.99 or the cost of posting a large box across the country4Costumesthe repricer’s price, lifted to a $19.99 floor, postage folded in so the listing can show free shipping5The owner’s floorcost back plus a minimum return after fees and postage · dead stock may recover 75% · MAP bindsUnder everything sits the owner’s floor. Nothing ever rounds down.
Figure 2. The eBay ask in five layers. Each layer applies to a class of stock; the owner’s floor applies to everything.
  1. Dollar-deal racks list at the rack’s price, or at the market price when a competitor’s listing shows buyers will pay more, with the buyer paying calculated postage. The till is never touched, because the same costume may also hang on a full-price rack.
  2. Accessories kept online carry a floor that still makes money on one item, in its own parcel, after fees, the advertising rate and a further store discount.
  3. Ride-on costumes never ask less than $79.99 or what a large box costs to post across the country.
  4. Costumes ask the repricer’s price, lifted to a $19.99 floor, with postage folded into the ask so the listing can show free shipping, which eBay ranks higher.
  5. Under everything sits the owner’s floor: cost back plus a minimum return after fees and postage, with dead stock allowed to recover 75% of cost, and any maker’s minimum advertised price always binding.

The repricer reads competitors’ landed prices, price plus postage, since that is what a buyer compares, and positions each listing by its market: undercut a crowded field, hold a thin one, ask a premium where the shop is the only seller. A calendar multiplier lifts prices towards the last weeks of October and drops them after Halloween.

Dead stock is a first-class idea. A costume is dead if it was bought two or more seasons ago, appears on no current vendor order form, or comes from a maker that has closed. Dead stock gets deeper discounts and the lower recovery floor, because carrying it another year costs more than selling it near cost. Analysis showed unorderable stock was 38% of units on hand but only 15% of what sold, which is why it is priced to move.

OnlineListing at scale

A costume counted on a rack becomes an eBay listing without anyone typing a description. The pipeline runs from a barcode to a published, priced, photographed and advertised listing, rack by rack, every night.

The reference catalogue. Spreadsheets, order forms and price lists from more than 80 makers and distributors were merged into one catalogue of about 80,000 barcodes. Each row carries a description, maker, style number, size, age group, cost and an orderable flag. When sources disagree, a direct maker’s price beats a distributor’s, and the newest list beats the oldest.

Size runs. A style number is the costume; the sizes are the same garment with a different label. Sizes are grouped into one eBay listing with a size selector, which cut one rack’s listings by more than half and pooled the sales history that ranks a listing. Plus-size runs and dollar-deal sizes key apart, because a group shares one shipping policy and a plus run written over an adult run ended a listing every night until it was caught.

Photographs. A listing without a photo is not a listing. The system takes the maker’s own picture first, then the same costume in another size, then a photo taken in the shop. It rejects anything under eBay’s 500-pixel minimum and never mixes picture sources, which eBay refuses. A background job hunts makers’ and dealers’ sites every two hours for photos and names of rows that have none.

Titles and categories. Titles are written from the catalogue with age words (Kids, Adult, Toddler) drawn from the rack a costume hangs on. Categories follow age and gender into eBay’s costume tree, with pet costumes in Pet Supplies, wigs in Wigs, and noses, horns and costume jewellery in Costume Accessories.

Review before publishing. Nothing goes in front of a stranger until it passes review: a name, a size, a photo, a price, nothing that reads as a weapon. Publishing is gated; quantity is not, because a stale count on a listing that already exists only ever sells something that is not there.

Promotion. A cost-per-sale ad campaign covers every live listing, re-checked nightly, and a daily job sends private offers to buyers watching a listing, never below its floor.

The back roomFulfilment

An online order becomes a parcel in two steps: a pick walk on the handheld, then a packing bench on the terminal. Both are built for October volume, when twenty or thirty racks can be on one run.

One walk for every order. Pressing Start plans a single loop through every order waiting, rather than one trip per order. The floor plan is turned into a graph of aisles, the gaps between fixtures are the only places a person walks, and each stop (a rack face and a third of it) is joined to the aisle beside it, never reached through another rack. The walk starts and ends at the back door, where parcels are packed.

Planning the route. Up to eight stops are solved exactly by trying every order. Beyond that a nearest-neighbour route is refined by two classic moves, undo any two legs that cross, and lift a short run of stops to wherever it is cheapest, until neither improves. On random eight-rack runs this lands within 0.2% of the true shortest walk on average, and forty racks plan in about 25 milliseconds. On one real day, eight orders walked as 280 feet in one loop against 734 feet as separate trips.

Scanning into bags. Each order gets a bag number in the order the walk first reaches it. Every row shows the costume’s photo, size, rack, third, and the names hanging either side of it. A scan confirms the item and says which bag it belongs in. Nothing can be ticked off by hand. Tapping the photo enlarges it; tapping the neighbours opens the whole rack in walking order, scrolled to the item.

The packing bench. The terminal shows each order with what the packer needs and nothing they must not see:

  • the photo, rack and size of every item;
  • the price the buyer paid beside the store’s price, with a flag to peel the sticker when the sticker is lower;
  • whether the handheld has picked it, how long it has waited, and other orders to the same buyer to pack together;
  • what the buyer was promised, against each carrier’s quoted arrival date;
  • a warning that big boxes over a cubic foot are billed by size.

The packer measures the parcel, picks a rate, and buys and prints a 4×6 label. The tracking number goes back to eBay in the same step, which marks the order shipped. Cost and profit never appear on these screens, because anyone on the floor can pack.

The officeRunning the business

The same terminal that counts and ships also runs the people and the numbers, so the owner can see the shop in one place and check it against the bank.

Time clock and payroll. Staff clock in at the terminal with a personal PIN. Every punch is kept as a record, not just a running total. Payroll is read from that punch ledger: a day’s worked time less punched breaks, with an automatic 30-minute meal deduction when a shift runs past five hours with no meal break on record, marked so it can be checked. Pay is reported by pay week, with a start day the manager sets, overtime at 1.5 times past 40 hours a week, and an eight-week table of gross pay per person built for checking against a manual reconcile.

Labour against takings. Wages for the day or the week are set beside what the till took in the same span, giving a labour ratio, sales per paid hour and sales per square foot of floor.

Sales. The sales dashboard reads eBay’s orders, its fee records and the postage actually bought, and works out net per order, per item, per size run, per franchise, per maker, per day and per ship-to state. A KPI view adds the till, payouts that reached the bank, labour, logged expenses and progress toward a season goal.

Sales by rack. Every barcode belongs to the rack it hangs on, so each rack shows stock, units and dollars sold in store and online, sell-through, and how much of it is live on eBay. Tapping a rack lists the items behind its numbers. In-store sales are tied to barcodes through the register’s own catalogue, and anything rung as a custom amount is reported apart rather than hidden.

Audits and forecasts. Command-line tools audit everything live on eBay against the rules: prices that drifted from what the rules say, listings asking less online than the till rings, missing or undersized photos, wrong shipping policies or categories, oversell risk, and ad coverage. The same data drives a season forecast with low, middle and high cases, built from the shop’s own daily run-rate and last year’s October curve.

How it was builtEngineering practice

The system was built in plain JavaScript with no framework, no database server and no cloud, on one laptop in the shop. I led its design and managed the project from first sketch to live trading: I decided what each part had to do and how the parts fit together, set every business rule, prioritised the work against the season’s calendar, tested each change on the shop floor and decided what shipped. I built it with an AI coding assistant, Claude Code, as a pair programmer that turned that direction into code and tests.

Principles that held up:

  • One rule, one file. Pricing, the floor plan, catalogue decoding and the eBay floors are each written once and loaded by the handheld, the terminal, the host and the tools alike. Two copies of a rule is how two screens start disagreeing.
  • An append-only log as the source of truth. Every scan, correction and edit is a line in one log. Counts are derived from it, so nothing is ever silently overwritten and any figure can be traced to the reads behind it.
  • Offline first. The handheld keeps working with no network and catches up when the laptop is back. The register already does this, so the sync treats late-arriving sales as normal, not as errors.
  • Credentials stay on the laptop. The handheld and terminal ask the local host to talk to eBay, Square and the label service; no device on the floor holds a key.
  • A typed entry is a decision. A price or barcode a person typed is treated as deliberate and never overwritten by a bulk rule.
  • Comments explain why. Each rule in the code carries the incident or instruction that produced it, with the date, so the reasoning survives the person who remembers it.

IncidentsLessons learned, newest first

DateWhat happenedThe lesson
30 SepA pick sorted racks by name, so C10 came before C2Use the floor’s geometry, not its labels
27 SepUpdating a listing’s description silently stripped its photosA full replace must carry everything it replaces
26 SepA costume sold twice to the same buyerFix the stock pool, not just the listing
26 SepChanges older than 24 hours were refused by the register for daysWatch for queues that stop draining, not only for errors
25 SepA bulk accessory rule lowered prices a person had setRules may raise a typed price, never lower it
24 SepPrices rounded down to the nearest stepRound up, always; the rule is a floor
26 AugA shell command re-encoded source files and corrupted themNever rewrite source through tools that guess encodings

Every incident in this table left behind a test, so it cannot come back unnoticed.

OutcomeResults and reflection

By October the store was counted rack by rack on a handheld, its stock sold in two places from one pool, and more than 5,000 SKUs were listed online without a description typed by hand. Pricing, picking, packing, payroll and reporting all ran from the same records.

What it changed, concretely:

  • Stock became visible. Every unit has a rack, a face and a third, so a costume sold online can be found in a shop of 15,000 units in seconds.
  • Online became a second shop. About 60% of the counted SKUs went live on eBay, priced by rule, photographed from makers’ catalogues, and advertised, with the backroom able to pick a day’s orders in one loop.
  • Policy became code. The owner’s instructions, round up, never down; free shipping stays; deals are online only; weapons stay off eBay, are enforced everywhere at once, and each carries the date and reason it was set.
  • Mistakes became rules. Every incident in the table above left behind a test, so it cannot come back unnoticed.

What I would do differently. Version control from day one: without it, a bad write could only be undone from a backup. Place every fixture on the floor plan before relying on it; the dollar-deal racks and the top wall still have no position, so the route sends them to the end of the walk. And measure real parcels early; large boxed costumes cost about twice the postage their prices assumed until their prices were floored to match.

What comes next. Steadier repricing, so overnight and morning runs stop undoing each other. An after-Halloween plan that turns the dead-stock analysis into markdowns. And a reorder list for next season drawn from what actually sold, rack by rack and size by size.