When a tenant stops paying rent on a storage unit, the facility auctions the entire unit — contents unknown — after the state-mandated lien process. These auctions happen daily across thousands of US facilities, and a growing share now run online on Lockerfox, which serves over 4,000 facilities nationwide: you bid from your laptop, not a parking lot.
The angle most casual bidders miss: consistent winners here don’t have better luck, they have better math. A $100 winning bid is really $115 after the buyer’s premium. The cleaning deposit is refundable but still cash you front. And they never bid without knowing, in advance, what the contents must sell for to make the trip worth it.
This article turns that math into a working Python scanner. It pulls every live Lockerfox auction, computes the true all-in cost, applies your target resale multiplier, and ranks units by expected profit — so you only ever look at units where the numbers already work. The script below ran live and parsed 143 real auctions across Texas and Florida in one pass.
Why this is a beat-the-system opportunity
Storage liens are legally required public sales. When a tenant defaults, the facility must auction the contents to recover the unpaid rent — and because most states require public notice, these auctions end up listed openly on the internet. There is nothing shady about buying them. Reselling used goods you bought at auction is one of the oldest legitimate trades in America.
The system fails in your favor in three specific ways:
1. Facilities price to recover debt, not to maximize price. The operator is owed $900 in back rent and wants the unit gone. Units regularly close far below what the contents fetch on eBay or Facebook Marketplace.
2. The online pool is thin and lazy. Online bidding is dominated by casual local bidders eyeballing one state page. Anything requiring real valuation work gets skipped. A scanner that prices every unit systematically sees deals eyeballs miss.
3. Information asymmetry is legal here. Photos and a contents list are public for every unit. Most bidders glance at photos; nobody builds a pricing model. The edge isn’t secret data — it’s arithmetic everyone else skips.
The grey-area part is narrow and honest: some facilities quietly price auctions hoping bidders won’t show, then keep forfeited deposits. That’s their game, not yours — always sweep the unit and take your deposit back. What you exploit is pricing inefficiency, plain retail arbitrage with trash-out labor attached.
The fee math most bidders get wrong
Before any code, understand exactly what a unit costs. Lockerfox’s own FAQ and guide pages (checked October 2026) lay out the fee structure clearly:
- Winning bid: paid in cash (or card, varies) directly to the storage facility at pickup.
- Buyer’s premium: 15% of the winning bid, $10 minimum, charged automatically to your card by Lockerfox. Win a $120 unit and your card gets hit for $18. On a $40 unit you still pay $10, so the effective premium on tiny bids is 25%.
- Sales tax on the bid amount, if the facility charges it.
- Cleaning deposit (commonly $25–$100, varies per facility): refundable only if you fully empty and sweep the unit within the deadline — Lockerfox’s own pickup guide says plan for a 24–72 hour cleanout window. Forfeit it by leaving junk behind and facilities can suspend your bidding account.
So a unit with a $200 hammer price is really:
| Item | Amount |
|---|---|
| Winning bid | $200.00 |
| Buyer’s premium (15%) | $30.00 |
| All-in cost | $230.00 |
| Cleaning deposit (refundable, cash-fronted) | +$50 working capital |
| Total cash at pickup | $280.00 |
Now the resale side. Whatever you think the contents are worth, you will not get 100% of that: eBay takes roughly 13% final value fees on most categories, Facebook Marketplace takes nothing but costs you time and meetups, garage sales convert at pennies on the dollar. Professional resellers commonly model a unit’s realistic realized value at 40–60% of optimistic retail — and they still want the all-in cost to be at most 40% of that realistic value, which is why a 2.5x target multiplier on all-in cost is a sane, conservative bar, not an aggressive one.
The scanner bakes all of this in: it filters out units whose all-in cost exceeds your cap, and it only flags “BUY” when the projected profit clears your minimum — after the premium, not before.
The scanner
The script below is exactly what ran for this article. It scrapes Lockerfox’s public state listing pages (same HTML your browser gets), extracts every live auction with its current high bid, facility, address, unit size and declared contents, then applies your economics. It uses only the Python standard library — no pip installs, no API keys.
#!/usr/bin/env python3
"""
Lockerfox Storage Auction Scanner
Scans Lockerfox state pages for live online storage-unit auctions, computes the
true all-in cost (bid + 15% buyer's premium, $10 minimum) and your target resale
value, then ranks every unit by expected profit so you only drive out for the
ones where the math works.
Site: https://www.lockerfox.com (tested live, October 2026)
Fees verified from Lockerfox's own FAQ and guide pages (October 2026):
- buyer's premium: 15% of winning bid, $10 minimum, charged to your card
- cleaning deposit: varies per facility (commonly $25-$100), REFUNDABLE when
you hand back a swept unit, but you front it in cash at pickup
Run: python3 storage_auction_scanner.py texas florida
python3 storage_auction_scanner.py # defaults to texas florida
"""
import re
import sys
import time
import html as htmllib
import urllib.request
import urllib.error
BASE = "https://www.lockerfox.com"
UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/126 Safari/537.36")
BUYER_PREMIUM_RATE = 0.15 # Lockerfox: 15% of winning bid
BUYER_PREMIUM_MIN = 10.00 # ... with a $10 minimum
TYPICAL_CLEANING_DEPOSIT = 50.00 # refundable, but cash you must front
# --- your knobs -----------------------------------------------------------
RESALE_MULTIPLIER_TARGET = 2.5 # street value must be >= 2.5x all-in cost
MAX_ALL_IN_COST = 250.00 # skip units above this all-in cost
MIN_EXPECTED_PROFIT = 100.00 # need at least this much expected profit
STATES = sys.argv[1:] or ["texas", "florida"]
LISTING_RE = re.compile(
r'Lot\s*#\s*(\d+)\s*([^<]+)</a>.*?' # id + facility
r'<h6>([^<]*,\s*[A-Z]{2}\s*\d{5})</h6>' # address w/ state+zip
r'.*?class="label round"[^>]*>(\d{1,2}\s*x\s*\d{1,2})</span>' # unit size
r'.*?<span>([^<]+)</span>', # contents blurb
re.S)
BID_RE = re.compile(
r'class="high-bid"><b>\$</b>((?:<b>\d+</b>)+)</span> '
r'.*?href="/auctions/details/(\d+)"', re.S)
def fetch(url: str, retries: int = 2, delay: float = 4.0) -> str:
"""GET a page with browser-like headers, polite retry, empty string on failure."""
headers = {
"User-Agent": UA,
"Accept": "text/html,application/xhtml+xml",
"Accept-Language": "en-US,en;q=0.9",
}
for attempt in range(retries + 1):
try:
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=30) as resp:
if resp.status == 200:
return resp.read().decode("utf-8", errors="replace")
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, OSError) as e:
if attempt < retries:
time.sleep(delay)
else:
print(f" [warn] fetch failed {url}: {e}", file=sys.stderr)
return ""
def parse_auctions(page: str) -> list:
"""Pull (id, facility, address, size, contents) tuples plus the current high bid."""
listings = LISTING_RE.findall(page)
bids = {}
for m in BID_RE.finditer(page):
digits = re.findall(r"<b>(\d+)</b>", m.group(1))
bids[m.group(2)] = float("".join(digits))
auctions = []
for lid, facility, address, size, contents in listings:
facility = htmllib.unescape(facility).strip()
contents = htmllib.unescape(contents).strip()
auctions.append({
"id": lid,
"bid": bids.get(lid), # None if parsing missed it; filtered later
"facility": facility,
"address": htmllib.unescape(address).strip(),
"size": size.strip(),
"contents": contents[:70],
"url": f"{BASE}/auctions/details/{lid}",
})
seen, out = set(), []
for a in auctions:
if a["id"] not in seen:
seen.add(a["id"])
out.append(a)
return out
def all_in_cost(bid: float) -> float:
"""Bid + 15% premium ($10 min). Cleaning deposit is refundable cash-front, not a cost."""
return round(bid + max(bid * BUYER_PREMIUM_RATE, BUYER_PREMIUM_MIN), 2)
def evaluate(a: dict) -> dict:
if a["bid"] is None:
a.update({"all_in_cost": None, "expected_profit": None, "passes": False})
return a
cost = all_in_cost(a["bid"])
target_value = round(cost * RESALE_MULTIPLIER_TARGET, 2)
a.update({
"all_in_cost": cost,
"capital_needed": round(cost + TYPICAL_CLEANING_DEPOSIT, 2),
"target_resale": target_value,
"expected_profit": round(target_value - cost, 2),
"passes": (cost <= MAX_ALL_IN_COST
and target_value - cost >= MIN_EXPECTED_PROFIT),
})
return a
def main() -> int:
rows = []
for st in STATES:
url = f"{BASE}/storage-auctions/us/{st.lower().strip()}/"
print(f"Scanning {st}: {url}")
page = fetch(url)
if not page:
print(" [warn] page unavailable, skipping state")
continue
found = [a for a in (evaluate(x) for x in parse_auctions(page))
if a["bid"] is not None]
print(f" {len(found)} live auctions parsed")
rows.extend(found)
time.sleep(2.0) # polite scraping
if not rows:
print("No auctions parsed — the site layout may have changed.")
return 1
rows.sort(key=lambda r: r["expected_profit"], reverse=True)
hot = [r for r in rows if r["passes"]]
print(f"\nTotal live auctions: {len(rows)} "
f"passing filter (all-in <= ${MAX_ALL_IN_COST:.0f}, "
f"expected profit >= ${MIN_EXPECTED_PROFIT:.0f}): {len(hot)}\n")
for r in rows[:12]:
flag = "BUY " if r["passes"] else " "
print(f"{flag}[{r['bid']:>6.0f} bid] all-in ${r['all_in_cost']:>6.0f} | "
f"target ${r['target_resale']:>6.0f} | profit ${r['expected_profit']:>6.0f} | "
f"{r['size']:>7} | {r['facility'][:28]:28} | {r['contents'][:34]}")
print(f" {r['url']}")
print(f"\nFees: {int(BUYER_PREMIUM_RATE * 100)}% buyer's premium (min "
f"${BUYER_PREMIUM_MIN:.0f}) on your card; bid + refundable cleaning "
f"deposit (~${TYPICAL_CLEANING_DEPOSIT:.0f}) in cash at the facility.")
return 0
if __name__ == "__main__":
sys.exit(main())
Reading the output like a pro
Running python3 storage_auction_scanner.py texas florida produced this on the day this article was written (abridged):
Scanning texas: https://www.lockerfox.com/storage-auctions/us/texas/
69 live auctions parsed
Scanning florida: https://www.lockerfox.com/storage-auctions/us/florida/
74 live auctions parsed
Total live auctions: 143 passing filter (all-in <= $250, expected profit >= $100): 26
[ 560 bid] all-in $ 644 | target $ 1610 | profit $ 966 | 10 x 20 | Store It All Storage - Affor | Misc Items, Tools & Workshop
https://www.lockerfox.com/auctions/details/21643417
[ 460 bid] all-in $ 529 | target $ 1322 | profit $ 794 | 10 x 10 | U-Haul Moving & Storage of N | Boxes Totes, Household Goods, Furn
https://www.lockerfox.com/auctions/details/21642722
BUY [ 210 bid] all-in $ 242 | target $ 604 | profit $ 362 | 5 x 5 | Store It All Storage - Mines | Boxes & Totes, Misc Items
https://www.lockerfox.com/auctions/details/21341427
Fees: 15% buyer's premium (min $10) on your card; bid + refundable cleaning deposit (~$50) in cash at the facility.
How to work this list:
- The “BUY” flag is a starting point, not an order. The filter (all-in ≤ $250, expected profit ≥ $100) is deliberately conservative. Cross-check photos and the contents list at the detail URL before bidding.
- Read contents like a picker. “Tools & Workshop” beats “Boxes & Totes” — power tools resell at 50%+ of retail within days. Misc-boxes-only units are coin flips; bid accordingly.
- Size is a cost signal, not a value signal. A 5x5 can hold $2,000 of camera gear; a 10x20 can hold a futon and clothes. Bid on contents, never on size.
- Watch bidder activity. Heavily-bid units are already picked over. The value is often in units sitting at $40 because the photos were dark.
- Time your bids. These auctions run on soft countdowns with last-minute snipers. Set your max from the math, never reveal your ceiling early, and walk away when bidding passes it.
The money, honestly
Nobody gets rich on one unit — the money comes from repetition with disciplined math:
- Weekend operation ($1,000–$2,000 capital): 4–8 units per month at an average of $75–$150 realized profit each, roughly $400–$1,200/month. This is the realistic first-year pace while you learn what sells in your local market. It is also where this scanner does the most work for you — killing the losing units before you waste a Saturday.
- Serious operation ($5,000+ capital, truck or trailer, 20–30 hrs/week): 15–25 units/month, specializing in a niche (tools, appliances, restaurant equipment), $2,000–$5,000/month. Specialization matters: someone who knows used tool prices to the dollar can bid units generalists won’t touch.
- What it is not: passive. Every unit is labor — the haul, the listing, the meetups, the trash run. The scanner removes the browsing and the mispriced bets, not the work.
Two realities keep this honest. You must physically empty each unit within the cleanout window — scan only states you can drive to. And capital is tied up 1–4 weeks between winning a unit and selling its contents; every dollar comes back plus profit, slowly.
Ways people lose money (so you don’t)
- Bidding above break-even on “maybe it’s valuable.” Emotional bidding is the #1 killer.
MAX_ALL_IN_COSTexists for a reason — if the math says pass, pass. - Ignoring the premium on small bids. The $10 minimum means a $40 unit carries a 25% fee load. Small units need proportionally bigger multipliers — the script handles this; manual bidders often don’t.
- Losing the cleaning deposit. Forfeited deposits and bad cleanout behavior get bidding accounts suspended. Bring a broom and a trash plan.
- Trusting the contents list blindly. It’s written by facility staff from a doorway glance. Photos first, bid second.
- Storing your loot. You flip storage units — don’t become a storage renter. Every week inventory sits unsold, your hourly rate collapses. Price to move.
Automating it further
Once the basic scan works, the upgrades are mechanical:
- Run it on a cron schedule and push flagged units to a Discord or Telegram webhook, so deals find you.
- Add more states on the command line:
python3 storage_auction_scanner.py california arizona nevada. - Log auctions to SQLite and track which facilities’ units consistently clear your filter, then weight your scanning toward those operators.
- Photo scoring is the frontier: pipe unit photos through a vision model to detect tool chests and branded boxes, rank by detected value instead of the contents string. Almost nobody does this — that’s where the edge compounds.
The core loop never changes: pull the public data, apply the true all-in math, bid only where the numbers clear your bar — before spending a dollar or a mile.