Misc · Build log · Jul 2026–now
The watchlist.
Nellis Auction sells Amazon returns out of warehouses in Mesa and Phoenix: thousands of lots, every one starting at $0, every one on a 24-hour clock. I wanted to stop scrolling it. So I wrote standing orders: type what you want once, and a bot watches both warehouses and emails you what matched, with a call on whether it's worth a bid.
- Running since
- Jul 2, 2026
- Standing watches
- 50
- Lots matched
- 3,616
- Delivered in digests
- 10,829
Why
Nellis is a liquidation auction. Returns and overstock come in by the truckload, get a photo, a condition grade and an "estimated retail" number, and go up at $0. Bids step up a dollar at a time and every lot closes 24 hours after it opens. Pickup only.
The interesting lots close with no bids at all. A sealed $100 item can go for $1 because nobody local happened to be looking. The business is being the one who was looking, for exactly the things you know how to resell, without spending the day refreshing a website.
It also turned out to be the cheapest parts store in the Valley for the engine work. Half the watchlist is Stirling hardware: bearings, sprag clutches, brushless outrunners, tri-clamp fittings, linear bearings, helium tanks.
How it finds things
The bot runs the same kind of search you would on the website, asking only for lots that are still open and match the watch, across both warehouses.
Site search matches loosely; a watch should not. Each term is parsed into required words, exact phrases and exclusions, and every title is re-checked locally so gear doesn't match headgear, and the designer Coach doesn't pull in a baseball coach's whistle.
baitcasting reel -spincast # every word must match, -word excludes
"Floor Length Mirror" # exact phrase
tri clamp -gasket -valve # Stirling parts, not plumbing kits
gucci >150 # per-watch retail floor
hub motor @[email protected] # route this watch to its own inboxSearch results don't carry everything. The live bid and the retail estimate need a closer look at each lot, which is the slow part, so the bot only does it for lots that already matched a watch, soonest-closing first, keeps it gentle, and caches what it learns. Thousands of open lots cost a handful of searches; only the few dozen that match get a closer look.
Is it worth a bid?
Nellis's "estimated retail" is optimistic. The bot discounts it by condition (what returns actually fetch: 50% sealed, 38% new, 28% opened), adds the real cost of winning (15% buyer's premium plus sales tax), and subtracts everything it costs to resell: eBay's 13.25% + $0.40, shipping by weight, and $2 of box and tape. What's left is a net, and the net makes the call.
def resale_call(h):
if damaged or missing_parts or not_functional:
return "PASS"
tier = "sealed" if in_package else ("new" if "new" in cond else "open")
value = retail * RECOVERY[tier] # 50% / 38% / 28% of Nellis retail
landed = bid * 1.15 * (1 + TAX) # 15% buyer's premium + sales tax
net = value * (1 - 0.1325) - 0.40 - ship(weight) - 2.00 - landed
roi = net / landed
if net >= 20 and roi >= 1.0: return "FLIP"
if net >= 12: return "MAYBE"
return "PASS"Three emails a day, never the same lot twice
Emailing on every find would bury the inbox in a day. So the board refreshes in the background and email goes out on a fixed schedule: 8 AM, 3 PM and 7 PM, Arizona time, which never shifts for daylight saving.
Every lot-and-inbox pair is remembered. A lot that matched this morning won't show up again tonight, no matter how many watches it matches. Inboxes that would get identical digests share one send.
EMAIL_HOURS = [8, 15, 19] # Arizona time, no DST
def next_digest(now):
lt = time.localtime(now)
times = [time.mktime((lt.tm_year, lt.tm_mon, lt.tm_mday + d, h, 0, 0, 0, 0, -1))
for d in (0, 1) for h in EMAIL_HOURS]
return min(t for t in times if t > now)
while True:
nxt = next_digest(time.time())
while time.time() < nxt: # quiet refreshes keep the board fresh
time.sleep(min(nxt - time.time(), 6 * 3600))
if nxt - time.time() > 60:
scan(send_email=False)
scan(send_email=True) # the one sweep that emailsfor h in matches:
for addr in recipients_for(h["term"]):
key = f"{h['id']}::{addr}" # one lot, one inbox, once ever
if key in already_emailed:
continue
addr_hits[addr].append(h)
# inboxes receiving the identical set of lots share one send
groups = {}
for addr, hits in addr_hits.items():
sig = tuple(sorted(h["id"] for h in hits))
groups.setdefault(sig, {"to": [], "hits": hits})["to"].append(addr)The digest itself is built for a phone in a parking lot: grouped by warehouse so a pickup run reads top to bottom, each card showing photo, bid, retail, percent off, a closing countdown, a green FLIP badge with the estimated net, and a one-tap eBay sold-listings search to check the comp before bidding.
The boring parts that make it reliable
- Atomic files. Every JSON store is written to a temp file and swapped in with os.replace, with a one-generation backup of the watches. A crash mid-write never leaves a corrupt watchlist.
- Data next to the script. Paths resolve from the script's own folder, so a shortcut, a new terminal or the startup folder all find the same watches.
- A small control panel. A simple board to add a watch, route it to an inbox, dismiss a lot or send a test email. A new watch searches for just that term right away, in the background.
- Second chances. Lots whose page fetch failed get one retry before the digest goes out, so emails don't ship with blank prices. Failures are never cached.
- A watchdog. If the bot ever stops, something outside it notices and starts it again.
def save_watches(ws):
shutil.copy2(PATH, PATH + ".bak") # one-generation backup
with open(PATH + ".tmp", "w") as f:
json.dump(ws, f, indent=2)
os.replace(PATH + ".tmp", PATH) # atomic: never a half-written fileWhat I learned
- Search is cheap; detail is expensive. Filter as much as possible on the index before touching a single product page.
- The edge is attention, not price. The winning lots aren't mispriced; nobody saw them. A bot that sees everything is the whole advantage.
- Discount the sticker. Liquidation "retail" is a ceiling. A model that starts from 28–50% of it is the only one that matched what things actually sold for.
- Email is a product. Schedule, dedupe, group and badge it, or it gets ignored by the third day.
Figures from the live bot's files on Oct 18, 2026. Resale constants are the ones in the code; tune them to your own fees and shipping. A sibling project, a full-catalog scanner, is a story for another post. Questions: Discord.