Every week, thousands of US government agencies sell off stuff they no longer want: a school district dumps 40 Dell monitors, a county sheriff sells a pallet of Motorola radios, a university lab clears out an oscilloscope that lists for $8,000 new, a city fleet auctions a Toro mower with 300 hours on it. They are legally required to dispose of surplus in a public, open way, so they post it on surplus auction sites like GovDeals, GSA Auctions, Public Surplus and Municibid.

Here is the edge: the agency does not care what it sells for. The clerk who posts the listing writes a vague title (“MISC ELECTRONIC EQUIPMENT - LOT 14”), uploads three photos taken with a phone, and sets a closing time on a Wednesday afternoon. The audience is a few local bidders who happen to be within driving distance. The same items, cleaned up, tested and listed properly, can sell on eBay to a national audience for well above the hammer price, sometimes several times it, often not enough to be worth the trip.

Nothing about this is a loophole in the illegal sense. It is a public auction, open to anyone, and buying to resell is completely normal (a large share of surplus buyers are resellers). The edge is that most bidders still browse their local area manually. Some resellers already use saved searches and third-party alert tools, so you are not alone, but coverage plus disciplined pricing still beats casual browsing. A script that watches every new listing for keywords, prices each one against eBay, and pings you only when the math works is a real advantage.

This guide builds that scanner, then walks through the money, the capital, the logistics and the ways people lose money doing this.

How the money works

Every surplus flip has these numbers:

  1. Hammer price: your winning bid.
  2. Buyer premium: a percentage the auction site adds on top of your bid. On GovDeals it is shown on each listing page (often up to 12.5%), so read it every time. Other sites, including GSA Auctions, set their own terms per sale; check the listing or its terms before bidding.
  3. Sales tax: charged by many sellers unless you file a resale or exemption certificate with them. Rules vary by state and agency.
  4. Getting it home: surplus is almost always pickup only. Your gas and time, or a freight broker or local courier if it is far away. This is the cost beginners ignore.
  5. Resale: what the item actually sold for on eBay recently (sold, not asking). eBay final value fee is around 13.6% of the total sale amount (item + shipping + sales tax) plus $0.40 per order ($0.30 on orders of $10 or less) for most US categories at time of writing. Some categories differ and Store subscribers pay less; check eBay’s fee page. Plus outbound shipping and packing.

The scanner computes a maximum bid per listing:

net_resale = resale - resale x (1 + tax) x 0.136 - 0.40 - ship_out
max_bid    = (net_resale - min_profit - haul) / ((1 + premium) x (1 + tax_buy))

Worked example (illustrative numbers): a lot of 10 used Cisco IP phones and 2 managed switches from a school district. You check eBay sold listings: the phones sell around $25 each and the switches around $120 each, so resale is about $490 total if you list them separately. Assume 8% tax on the eBay side, $12 average outbound shipping per item (12 items, $144), so net resale is about $490 - $71.97 - $4.80 - $144 = $269. You want at least $100 profit for a half day of work, and the pickup costs you $30 in gas. With a 10% buyer premium and a resale certificate on file (no purchase tax), max bid is ($269 - $100 - $30) / 1.10 = $126. Whether a lot like this closes under that number depends entirely on local competition; vague titles and bad photos help, but plenty of lots get bid past any sensible max.

Notice how much shipping hurts: twelve separate shipments eat more than the eBay fee. That is why the best surplus flips are single items worth $200 to $3,000 (test equipment, pro audio, power tools, network gear, lab instruments, commercial kitchen gear, e-bikes and scooters from police property rooms), not boxes of cheap stuff.

What you need

ItemCostNotes
GovDeals buyer accountFreegovdeals.com. New bidders start on a probation with lower bid limits until they complete a few purchases or post a deposit; check current rules
GSA Auctions accountFreegsaauctions.gov, federal surplus, identity check at registration (one account per SSN)
Public Surplus / Municibid accountsFreeMore agencies, less competition
eBay seller accountFreeOlder account with feedback is better; new sellers get limits and payout holds
eBay Developers Program keysetFreedeveloper.ebay.com, Production App ID + Cert ID for the Browse API
Python 3.10+ and PlaywrightFreeInstalled in a virtualenv in Step 4
VPS or always-on PC$0 to $6/monthPlaywright with Chromium wants at least 1 GB RAM, 2 GB is safer
Telegram botFreeCreate with @BotFather for alerts
A vehicle (or a man-with-a-van contact)You have it or you do notThis business is physical. No car, no pickups
Resale certificateFree in most statesRegister for a sales tax permit in your state first
Working capital$2,000 to $5,000 to startCash is tied up 2 to 6 weeks per item: pay, pick up, test, list, sell, get paid

Outside the US? The same model exists elsewhere (UK: councils and police use sites like Bumblebee Auctions and various government disposal contractors; Australia: GraysOnline handles a lot of government disposal; Indonesia: DJKN runs lelang.go.id for state asset auctions). The scanner below targets US sites and eBay US, but the logic carries over. For Indonesian readers, lelang.go.id plus Tokopedia or Shopee sold-price checks is the same play, though lots there skew toward vehicles and property, which need a lot more capital.

Step 1: Pick categories that actually flip

The scanner watches keywords. Pick them using three filters:

  • Value per pound: $200+ items that fit in a box you can ship via UPS Ground. A $400 oscilloscope that weighs 15 lb is great. A $400 office desk is a trap.
  • Clear model numbers: anything you can price exactly on eBay sold listings by model (Fluke 87V, Tektronix TDS2024, Shure SM58, Milwaukee M18, Cisco Catalyst 2960, Hobart slicer, Zebra ZT410).
  • Agency blind spots: things government buyers bought expensive and value at nothing. Lab gear (universities), pro audio and AV (schools, theaters, churches using government contracts), network and server gear (IT refreshes), test equipment (utilities, public works), commercial kitchen gear (schools, prisons), unclaimed bikes and e-bikes (police).

Avoid as a beginner: phones, tablets, laptops and especially Chromebooks from schools. They are often enterprise-enrolled or activation-locked, and a locked device is worth parts price. Also avoid vehicles until you have done 20 smaller flips: titles, towing and mechanical surprises are their own business.

Use eBay Seller Hub, Research (Terapeak) to check sold prices and sell-through. It is free for eBay sellers. You want items that sell multiple times a week at a stable price.

Step 2: Set up eBay API access

  1. Sign in at developer.ebay.com, join the Developers Program, and create a Production keyset. You get an App ID and a Cert ID.
  2. For a read-only tool that stores no eBay user data, apply for the Marketplace Account Deletion exemption in the keyset settings.
  3. Test it:
export EBAY_APP_ID="YourApp-xxxx-PRD-xxxx"
export EBAY_CERT_ID="PRD-xxxx"
curl -s -X POST "https://api.ebay.com/identity/v1/oauth2/token" \
  -u "$EBAY_APP_ID:$EBAY_CERT_ID" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&scope=https://api.ebay.com/oauth/api_scope"

You should get an access_token valid for 7,200 seconds. The default Browse API quota is 5,000 calls per day, far more than this scanner needs.

One honest limitation: the public Browse API only returns active listings, not sold ones. eBay sold-data API (Marketplace Insights) is restricted, and the old findCompletedItems call is gone. So the scanner uses the median active “Buy It Now” price of used items as a rough ceiling, then applies a haircut (default 25%). Before you bid on anything, you confirm with Terapeak sold data by hand. The script is a filter, not an oracle.

Step 3: Build the watchlist

Create watch.json. ebay_q is the eBay search, keywords are the words that must appear in a surplus listing title or description:

[
  {"keywords": ["oscilloscope", "tektronix"], "ebay_q": "tektronix oscilloscope", "ship_out": 35},
  {"keywords": ["fluke 87"], "ebay_q": "fluke 87v multimeter", "ship_out": 12},
  {"keywords": ["shure", "wireless mic"], "ebay_q": "shure wireless microphone system", "ship_out": 15},
  {"keywords": ["catalyst", "cisco switch"], "ebay_q": "cisco catalyst switch", "ship_out": 25},
  {"keywords": ["milwaukee m18"], "ebay_q": "milwaukee m18 tool", "ship_out": 18},
  {"keywords": ["hobart slicer"], "ebay_q": "hobart slicer", "ship_out": 90}
]

Start with 10 to 20 entries in categories you can actually test. An untested oscilloscope sells for half of a tested one.

Step 4: Capture surplus listings with Playwright

GovDeals and most surplus sites are JavaScript apps: the HTML is an empty shell and the listings arrive as JSON from a background API call. Instead of parsing fragile HTML, the scanner opens your saved search in headless Chromium and records the JSON responses the page itself loads, then walks them looking for listing-like objects.

Set up the project on your VPS or PC (Ubuntu/Debian shown):

mkdir -p ~/surplus && cd ~/surplus
python3 -m venv venv
venv/bin/pip install playwright requests
venv/bin/playwright install --with-deps chromium   # --with-deps installs the system libraries Chromium needs (uses sudo)

For Telegram: create a bot with @BotFather to get TG_TOKEN, send your bot any message, then open https://api.telegram.org/bot<TG_TOKEN>/getUpdates and copy the chat.id number as TG_CHAT.

Do one search manually in your browser first (for example GovDeals, keyword “oscilloscope”, sorted by newest, your state or nationwide), and copy the URL from the address bar into searches.txt, one per line. This way the script does not depend on guessing URL formats, which change.

Save as surplus_scan.py:

#!/usr/bin/env python3
"""Watch surplus auction searches, price hits against eBay, alert to Telegram."""
import base64
import json
import os
import re
import sqlite3
import statistics
import time

import requests
from playwright.sync_api import sync_playwright

APP_ID = os.environ["EBAY_APP_ID"]
CERT_ID = os.environ["EBAY_CERT_ID"]
TG_TOKEN = os.environ["TG_TOKEN"]
TG_CHAT = os.environ["TG_CHAT"]

FEE_RATE, FEE_FIXED = 0.136, 0.40   # eBay US, most categories; check yours
TAX_SELL = 0.08        # buyer sales tax, part of the eBay fee base
TAX_BUY = 0.0          # 0 if your resale certificate is on file, else local rate
PREMIUM = 0.125        # assume worst case; real value is on each listing
HAIRCUT = 0.25         # active asking prices run above sold prices
MIN_PROFIT = 80
HAUL = 40              # default pickup cost (gas + time), override per listing

TITLE_KEYS = re.compile(r"title|name|desc", re.I)
PRICE_KEYS = re.compile(r"current_?bid|^bid$|price|amount", re.I)


def find_listings(obj, found):
    """Recursively collect dicts that look like auction listings."""
    if isinstance(obj, dict):
        keys = list(obj.keys())
        t = next((k for k in keys if TITLE_KEYS.search(k) and isinstance(obj[k], str)), None)
        p = next((k for k in keys if PRICE_KEYS.search(k) and isinstance(obj[k], (int, float))), None)
        if t and p:
            found.append(obj)
        for v in obj.values():
            find_listings(v, found)
    elif isinstance(obj, list):
        for v in obj:
            find_listings(v, found)


def capture(url):
    payloads = []
    with sync_playwright() as pw:
        browser = pw.chromium.launch(headless=True)
        page = browser.new_page()
        page.on("response", lambda r: payloads.append(r)
                if "json" in r.headers.get("content-type", "") else None)
        page.goto(url, wait_until="networkidle", timeout=60000)
        page.wait_for_timeout(3000)
        found = []
        for r in payloads:
            try:
                find_listings(r.json(), found)
            except Exception:
                pass
        browser.close()
    return found


def norm(item):
    """Pull title, bid and an id out of whatever field names the site uses."""
    title = next(str(v) for k, v in item.items() if TITLE_KEYS.search(k) and isinstance(v, str))
    bid = next(float(v) for k, v in item.items() if PRICE_KEYS.search(k) and isinstance(v, (int, float)))
    ident = next((str(v) for k, v in item.items()
                  if re.search(r"^id$|_id$|^(asset|item|listing|lot|account)id$", k, re.I)), title)
    return ident, title, bid, json.dumps(item)[:400]


def ebay_token():
    auth = base64.b64encode(f"{APP_ID}:{CERT_ID}".encode()).decode()
    r = requests.post("https://api.ebay.com/identity/v1/oauth2/token",
                      headers={"Authorization": f"Basic {auth}",
                               "Content-Type": "application/x-www-form-urlencoded"},
                      data={"grant_type": "client_credentials",
                            "scope": "https://api.ebay.com/oauth/api_scope"}, timeout=20)
    r.raise_for_status()
    return r.json()["access_token"]


def ebay_ceiling(token, q):
    r = requests.get("https://api.ebay.com/buy/browse/v1/item_summary/search",
                     headers={"Authorization": f"Bearer {token}",
                              "X-EBAY-C-MARKETPLACE-ID": "EBAY_US"},
                     params={"q": q, "limit": 100,
                             "filter": "buyingOptions:{FIXED_PRICE},conditions:{USED}"},
                     timeout=30)
    r.raise_for_status()
    prices = [float(i["price"]["value"]) for i in r.json().get("itemSummaries", [])
              if i.get("price")]
    if len(prices) < 5:
        return None
    return statistics.median(prices) * (1 - HAIRCUT)


def max_bid(resale, ship_out):
    net = resale - resale * (1 + TAX_SELL) * FEE_RATE - FEE_FIXED - ship_out
    return round((net - MIN_PROFIT - HAUL) / ((1 + PREMIUM) * (1 + TAX_BUY)), 2)


def tg(text):
    requests.post(f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
                  data={"chat_id": TG_CHAT, "text": text}, timeout=20)


def main():
    db = sqlite3.connect("seen.db")
    db.execute("CREATE TABLE IF NOT EXISTS seen (id TEXT PRIMARY KEY, ts INTEGER)")
    watch = json.load(open("watch.json"))
    urls = [u.strip() for u in open("searches.txt") if u.strip()]
    token, ceilings = ebay_token(), {}

    for url in urls:
        for raw in capture(url):
            try:
                ident, title, bid, blob = norm(raw)
            except StopIteration:
                continue
            if db.execute("SELECT 1 FROM seen WHERE id=?", (ident,)).fetchone():
                continue
            db.execute("INSERT INTO seen VALUES (?, ?)", (ident, int(time.time())))
            text = (title + " " + blob).lower()
            for w in watch:
                if not any(k in text for k in w["keywords"]):
                    continue
                q = w["ebay_q"]
                if q not in ceilings:
                    ceilings[q] = ebay_ceiling(token, q)
                resale = ceilings[q]
                if not resale:
                    continue
                mb = max_bid(resale, w["ship_out"])
                if bid < mb:
                    tg(f"SURPLUS HIT\n{title}\nCurrent bid: ${bid:.2f}\n"
                       f"eBay ceiling (after haircut): ${resale:.0f}\n"
                       f"Max bid: ${mb:.0f}\nSearch: {url}\n"
                       f"Check Terapeak sold + pickup distance before bidding.")
                break
        db.commit()
        time.sleep(20)   # be polite: one search page at a time


if __name__ == "__main__":
    main()

First run, add a print(raw) inside the loop to see which fields the site actually returns. If the generic matcher grabs the wrong field (say a “reserve” amount instead of the current bid), tighten PRICE_KEYS to the exact key name you see. Sites change their JSON every so often; when alerts suddenly stop, that is the first thing to check.

Some sites run bot protection that blocks headless browsers outright. If a search URL returns nothing even with print(raw), the scanner will not work on that site; do not try to defeat the protection, just watch that site by hand or with its own saved-search email alerts.

Cron does not see variables you exported in your shell, so put them in a file ~/surplus/.env:

export EBAY_APP_ID="YourApp-xxxx-PRD-xxxx"
export EBAY_CERT_ID="PRD-xxxx"
export TG_TOKEN="123456:ABC..."
export TG_CHAT="123456789"

Test once by hand (cd ~/surplus && . ./.env && venv/bin/python surplus_scan.py), then run it every two hours with cron:

crontab -e
# add:
0 */2 * * * cd /home/you/surplus && . ./.env && venv/bin/python surplus_scan.py >> scan.log 2>&1

Every two hours is enough. Surplus auctions run for days, often a week or more. You do not need speed here; you need coverage.

Step 5: From alert to profit

An alert is a lead, not a buy. For each one:

  1. Open the listing and read everything: buyer premium, pickup deadline (often a short window set by the agency), payment deadline, removal conditions (“buyer must bring help and equipment”), and whether it is sold “as is, where is”. Almost all surplus is.
  2. Check Terapeak sold data for the exact model. Replace the script estimate with the real number.
  3. Price in the pickup. A $150 profit that needs a 3-hour round trip is a $30/hour job. Batch pickups: win several items in the same metro area, then do one run.
  4. Bid your max once, late. GovDeals uses proxy bidding and many listings have a soft close (bids near the end extend the auction), so last-second sniping helps less than on eBay. Just put in your real maximum and walk away.
  5. Test, clean, photograph, list. Tested-working with a photo of the device powered on is often worth 30 to 50% more than “untested”. Put the model number in the title and the specs in item specifics.

Honest cost breakdown

CostMonthly
VPS (2 GB)$6
eBay, GovDeals, GSA, APIs, Telegram$0
Terapeak$0 (included for eBay sellers)
Packing supplies$30 to $80
Gas for pickups$100 to $400 (the big one)
eBay feesabout 14 to 15% of sales, effectively
Buyer premiumsvaries per listing, budget around 10% of hammer prices
Working capital$2,000 to $5,000, recycled

Realistic earnings

These are estimates from the unit economics, not promises:

  • Part-time (10 to 15 hours/week), $2,000 capital: 6 to 12 flips a month at $60 to $150 profit each, so roughly $500 to $1,500/month. The first two months are usually worse while you learn what sells.
  • Serious side hustle (25 to 30 hours/week), $5,000 capital, a van or SUV: 20 to 30 flips at an average of $100 to $200, so $2,500 to $5,000/month. People at this level usually specialize (test equipment, network gear, restaurant equipment) because specialists price faster and test better.
  • Beyond that you are running a small business: storage unit or garage, helper for pickups, sometimes buying big IT lots and parting them out. That level needs $20k+ in capital, space and real labor. Automation does not remove the physical work; it only removes the browsing.

The scanner changes the hit rate, not the nature of the job. Without it, you find a few deals in your area. With it, you see matching listings from every search you saved within about two hours.

Risks: what can actually go wrong

  • “As is, where is” means no refunds. Dead items, missing power supplies, locked devices, wrong model in the photo. You eat it. Budget for 1 in 10 flips to lose money.
  • Non-payment or no-show penalties. If you win and do not pay or do not pick up on time, sites can suspend your account and some agencies block you permanently. Never bid on something you cannot collect.
  • Scraping and ToS. Most auction sites restrict automated access in their terms. The scanner above loads a handful of search pages every two hours, like a person refreshing a browser, which is very different from crawling the whole site. Still, if a site blocks your IP or account, that is their right. Keep the request rate low, do not hammer it, and never automate bidding.
  • eBay seller risk. New accounts get selling limits and payment holds. Not-as-described returns on used electronics are common; describe flaws honestly and photograph serial numbers so you can fight swapped-item returns.
  • Restricted items. Some surplus has resale rules: certain military items (export controlled under ITAR/EAR), medical devices that eBay restricts, firearms parts, items with government data on drives. Wipe or destroy any storage you buy and do not resell data. Read the listing for end-use or export restrictions; buying a demilitarization-required item and flipping it overseas is how people get into real legal trouble.
  • Taxes. This is business income. Track every purchase, premium and gas receipt. Without a resale certificate you pay sales tax on purchases and cannot get it back.

Reality check: what does not work

  • Bidding on boxes of cheap stuff. “Lot of 30 keyboards” looks like a steal at $20 until you realize each keyboard sells for $12 and ships for $10.
  • Driving 200 miles for one $100 profit. Distance is the silent margin killer. Set a radius, or only go far for big items or batched pickups.
  • Trusting asking prices. Active eBay listings sit unsold for months at fantasy prices. That is why the script applies a haircut, and why you check sold data every time.
  • Furniture and big appliances unless you have local buyers on Facebook Marketplace lined up. They cannot be shipped economically.
  • Chasing vehicles first. Big tickets, big surprises. Learn on small items.
  • Expecting passive income. The alerting is automated. Pickups, testing, photos and shipping are not. Plan on about 1 to 2 hours of hands-on work per flip.

The bottom line

Government surplus is an arbitrage pool where the seller cares less about price than a private seller would and the audience is often small and local. A $6/month VPS and a Python script turn it from random browsing into a nationwide deal feed. The money is real if you are willing to drive, test and ship: a few hundred a month part-time, a few thousand a month if you treat it like a business. Start this week: open accounts on GovDeals and GSA Auctions, pick five models you know how to test, save the searches, and let the scanner run.