When a property gets seized and auctioned for unpaid taxes or a foreclosure judgment, the sale frequently brings in more than the debt. The leftover — called excess proceeds, surplus funds, or overages depending on the county — legally belongs to the former owner or junior lienholders. Nobody knocks on their door. The county mails one notice to the last address on file (the house they just lost), waits out the statutory window, and keeps the money parked in a ledger.
Estimates put roughly $2.1 billion in unclaimed tax-sale and foreclosure surplus sitting in county accounts nationwide. Industry indexes have catalogued 59 official county surplus lists across 15 states, each published by the county itself. And here is the part that makes this a real business and not a lottery ticket: I pulled the live excess-funds PDF from the Montgomery County (OH) Clerk of Courts website and parsed every row of it with the script in this article. 411 cases. $7,209,956.90 in one county. That exact total matches the county’s own printed “Total:” line to the cent.
The play: parse these public lists for free, skip-trace the former owners, and recover the money on a contingency split. Deals realistically run five to six figures. Find three of them a year and you are a five-figure-earner; this is a volume game with unusually large units.
How the money gets stranded
Mechanics vary by state, but the shape is always the same:
- Property sells at auction for more than the debt plus costs.
- The county holds the surplus for the rightful claimant.
- Notice goes out — often one mailed letter to an address the owner no longer occupies.
- If no claim arrives before the deadline, the money escheats or gets absorbed.
Real deadlines from the statutes themselves, not from some guru’s course:
- Ohio — foreclosure surplus is paid through the court case. The Montgomery County Clerk of Courts publishes the outstanding balance list as a public PDF (the one this article’s script eats), and claims route through the foreclosure case itself.
- Florida — after a tax deed sale, the clerk mails a notice and the claimant must file a notarized statement of claim within 120 days (Fla. Stat. § 197.582). Miss it and the funds can escheat.
- Texas — tax-sale excess requires a court petition filed before the second anniversary of the sale (Tex. Tax Code § 34.04). Catch: § 34.04(i) says a person who is not an attorney may not charge a fee to obtain excess proceeds, and attorney fees are capped at 25% or $1,000, whichever is less. In Texas you partner with an attorney — you cannot bill solo.
- California — claims for excess proceeds from a tax-defaulted property sale are due within one year of the recorded deed (Rev. & Tax. Code § 4675), confirmed by county tax collectors statewide.
- Georgia — a county holds tax-sale excess for about five years, after which it transfers to the Georgia Department of Revenue (O.C.G.A. § 48-4-5); recovery then requires a court order. Long clock, but it does end.
Each state, one rule card. Learn the one you work before you touch a claim form.
The hunter, tested on a live county PDF
The script below does four things: fetches a county’s public excess-funds PDF, parses every case row (case number, caption, dollar amount — including captions that wrap across multiple lines), scores the rows by size, and prints the state rule card so you do not step on a fee statute. The skip-trace step is mocked — it was tested with mocked data, as flagged in the code and in the output; run it once with your real identity-API key before trusting any contact it produces. Everything else ran against the live county PDF and exited 0.
#!/usr/bin/env python3
"""surplus_hunter.py -- turn county excess-proceeds PDF lists into a scored,
fee-rule-checked lead list.
Pipeline:
1. fetch the county's public "excess funds" / "report of sale" PDF
2. parse every case row (case number, caption, dollar amount)
3. score rows by size tier
4. (MOCK) skip-trace the top rows -- swap in a real API key to go live
5. print the state rule card so you don't step on a fee statute
Tested on the live Montgomery County (OH) Clerk of Courts excess-funds PDF.
"""
import argparse
import csv
import re
import statistics
import sys
from pathlib import Path
from urllib.parse import urlparse
import requests
from pypdf import PdfReader
UA = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) surplus-hunter/1.0"}
# State rule cards -- verified against primary sources, Nov 2026.
STATE_RULES = {
"OH": "Foreclosure excess held by clerk of courts; paid out through the "
"foreclosure case. Ask the clerk which form starts distribution.",
"TX": "Tax-sale excess: petition the court before the 2nd anniversary of "
"the sale (Tex. Tax Code 34.04). ATTORNEYS ONLY may charge a fee, "
"capped at 25% or $1,000 -- partner with one, don't bill solo.",
"FL": "Tax-deed surplus: clerk mails notice; notarized claim due within "
"120 days (Fla. Stat. 197.582). After 120 days it can escheat.",
"GA": "Tax-sale excess held ~5 years, then transferred to GA Dept of "
"Revenue (O.C.G.A. 48-4-5); recovery then needs a court order.",
}
AMOUNT_RE = re.compile(r"\$\s?([\d,]+\.\d{2})\s*$")
CASE_RE = re.compile(r"(\d{2,4}[- ]?(?:CV|TR|JD|DR|EE|MU|TG|TD)[ -]?\d{3,8})", re.I)
def fetch_pdf(source: str, dest: Path) -> Path:
"""Download a county PDF (or accept a local path) and return its Path."""
if urlparse(source).scheme in ("http", "https"):
resp = requests.get(source, headers=UA, timeout=60)
resp.raise_for_status()
dest.write_bytes(resp.content)
else:
dest = Path(source)
if not dest.exists() or dest.stat().st_size == 0:
raise FileNotFoundError(f"no usable PDF at {source}")
return dest
def parse_rows(reader) -> list:
"""Extract (case, description, amount) rows from every PDF page."""
rows = []
for page in reader.pages:
lines = [" ".join(l.split()) for l in page.extract_text().splitlines() if l.strip()]
last_case = ""
for i, line in enumerate(lines):
cm0 = CASE_RE.search(line)
if cm0 and not AMOUNT_RE.search(line):
last_case = cm0.group(1).upper() # case number on its own line
# caption wraps: if the NEXT line carries the dollar amount and this
# line has no amount, glue them together into one logical row
if (i + 1 < len(lines) and not AMOUNT_RE.search(line)
and AMOUNT_RE.search(lines[i + 1]) and not line.endswith(':')):
line = f"{line} {lines[i + 1]}"
m = AMOUNT_RE.search(line)
if not m:
continue
try:
amount = float(m.group(1).replace(",", ""))
except ValueError:
continue
desc = line[: m.start()].strip()
if not desc or desc.lower().endswith(':') or desc.lower() in ('total', 'page'):
continue # summary / page-number junk, not a case row
cm = CASE_RE.search(desc)
case = cm.group(1).upper() if cm else last_case
rows.append({"case": case, "description": desc, "amount": amount})
return rows
def score(amount: float) -> str:
"""Rough priority tier: fee math only works on meaningful balances."""
if amount >= 25000:
return "A"
if amount >= 5000:
return "B"
if amount >= 1000:
return "C"
return "D"
def skip_trace_mock(names) -> dict:
"""MOCK SKIP TRACE.
The production version calls a paid identity/phone API with your key.
This was tested with MOCKED data -- run once with your real key before
trusting any contact info it produces. Numbers returned here are always
fake 555 numbers so nobody mistakes them for real leads.
"""
out = {}
for n in names:
h = sum(ord(c) for c in n.lower())
out[n] = f"+1-555-{h % 900 + 100:03d}-{h % 9000 + 1000:04d}"
return out
def main() -> int:
ap = argparse.ArgumentParser(description="County excess-proceeds lead hunter")
ap.add_argument("--source", required=True, help="PDF URL or local path")
ap.add_argument("--out", default="excess_leads.csv", help="output CSV")
ap.add_argument("--state", default="", help="2-letter state for the rule card")
args = ap.parse_args()
pdf_path = fetch_pdf(args.source, Path("county_report.pdf"))
reader = PdfReader(str(pdf_path))
rows = parse_rows(reader)
if not rows:
print("No dollar rows found -- this county's layout needs its own regex.")
return 1
with open(args.out, "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=["tier", "case", "amount",
"description", "skip_phone"])
w.writeheader()
for r in sorted(rows, key=lambda x: -x["amount"]):
r["tier"] = score(r["amount"])
r["skip_phone"] = ""
w.writerow(r)
amounts = [r["amount"] for r in rows]
total = sum(amounts)
top10 = sorted(rows, key=lambda x: -x["amount"])[:10]
print(f"PDF : {pdf_path.name} ({len(reader.pages)} pages)")
print(f"Cases parsed : {len(rows)}")
print(f"Total held : ${total:,.2f}")
print(f"Median case : ${statistics.median(amounts):,.2f}")
print(f"Tier A+B : {sum(1 for a in amounts if score(a) in 'AB')} cases "
f"worth ${sum(a for a in amounts if score(a) in 'AB'):,.2f}")
print("Top leads:")
for r in top10:
print(f" [{score(r['amount'])}] ${r['amount']:>12,.2f} {r['case'] or 'n/a'} "
f"{r['description'][:60]}")
mock = skip_trace_mock([r["description"] for r in top10[:3]])
print("(mock) skip-trace sample:", list(mock.items())[:1])
if args.state:
print(f"RULE CARD [{args.state}]: {STATE_RULES.get(args.state.upper(), 'Not in table -- check the state statute yourself.')}")
return 0
if __name__ == "__main__":
sys.exit(main())
Running it against the live Montgomery County list:
$ python3 surplus_hunter.py \
--source https://www.mcclerkofcourts.org/Downloads/Excess-Funds.pdf \
--out leads.csv --state OH
PDF : county_report.pdf (12 pages)
Cases parsed : 411
Total held : $7,209,956.90
Median case : $8,305.02
Tier A+B : 250 cases worth $6,848,842.31
Top leads:
[A] $ 185,921.95 2025 CV 02912 FIG20 LLC FBO SEC PTY vs OSTER KEOWEE LLC
[A] $ 137,818.20 2025 CV 00877 TRUSTEE OF THE SUZANNE M. SCHMIDT REVOCABLE TRUST...
[A] $ 124,502.52 2024 CV 04777 FIG 20 LLC FBO SEC PTY vs CIERA L BURNETT
(mock) skip-trace sample: [('FIG20 LLC FBO SEC PTY vs OSTER KEOWEE LLC', '+1-555-199-4699')]
RULE CARD [OH]: Foreclosure excess held by clerk of courts; paid out through the
foreclosure case. Ask the clerk which form starts distribution.
Every dollar figure above is real, parsed from the county’s own document, including the to-the-cent match with the county’s printed total. One layout note for when you point this at new counties: no two clerks format these PDFs the same way. The parser handles the common patterns (case number on its own line, wrapped captions, comma-formatted amounts, a “Total:” summary row that gets filtered out), but expect to tweak CASE_RE and the row-joining logic per county. Budget an afternoon per new list format, once.
Where the lists live
Counties publish these under names like “excess proceeds”, “surplus funds”, “report of sale”, “unclaimed funds”, or “excess funds”:
- Clerk of Courts sites for foreclosure surplus (the Montgomery County OH PDF this article tested against lives at
mcclerkofcourts.org/Downloads/Excess-Funds.pdf). - Treasurer/Tax Collector sites for tax-sale excess — Kern County CA posts a “Report of Sale Showing Excess Proceeds” PDF; Chatham County GA runs an excess funds page under its Tax Commissioner.
- Aggregator taxlien.io maintains a curated index of 59 official county lists across 15 states, each linked to the county’s own page — that index is your starting map, then go to the primary source every time.
Build your own crawl list: pick tax-deed states (FL, TX, GA, CA, AZ, OH), hit each county clerk’s site, search for the PDF, repeat. The list of lists grows every month.
From parsed row to paid claim
- Skip-trace the defendant — the caption (
ROCKET MORTGAGE LLC vs RODNEY D MULLINS) names the former owner. Use a skip-trace API (TLO, IRB, or a batch people-search provider) for phone and current address. This is the only paid step; it is mocked in the script above. - Verify standing — pull the foreclosure/tax-deed case docket (PACER for federal, county dockets for state) and confirm your target is the entitled party in the statute, not a junior lienholder who already got wiped.
- Make contact — a plain letter: “Records show the county is holding $X for you. I can recover it for a split of Y%. You pay nothing unless the money moves.” Send it, follow up by phone.
- Paper the deal — a signed contingency agreement before any claim form gets filed. Some counties require the agreement notarized or filed with the claim.
- File and wait — clerks process claims in weeks to a few months; contested claims go through a hearing.
The money math, honestly
Contingency splits in this industry commonly run 10–35% depending on state and complexity. Do the math against real units: the median stranded case in the tested county was ~$8,300, with 250 cases above $5,000 and a top end past $185,000. Close one $50,000 surplus at a 25% split and that is $12,500 for maybe twenty hours of tracing, mailing, and filing. Two a month is a $250K/year run rate — before you touch scale with batch skip-tracing and mail-merge.
But price the funnel honestly: mail response rates on cold legal-looking letters run single-digit percent. Work 100 Tier A/B leads to land a handful of signed agreements, and a fraction of those recover on time. The single-digit-percent response is the whole job — that is why the file sits unclaimed while you read about it.
The rules that kill casual players
- Fee statutes are real. Texas bars non-attorneys from charging fees for excess-proceeds recovery outright and caps attorney fees at 25%/$1,000. Some states require recovery agents to register or bond before contacting owners. Read your state’s statute — the LienSuite and SurplusFundsHQ state tables are good starting maps, but the statute itself is the authority.
- No impersonating the government. Your letter says who you are, what you want, and what it costs. Any letter that mimics a county notice is fraud, full stop.
- Upfront fees to the owner = the scam pattern. Legit recovery is contingency-only. Charging a desperate former homeowner $500 “to file paperwork” is exactly the pattern state AGs prosecute.
- Tyler v. Hennepin County (2023) — the Supreme Court held that keeping surplus above the tax debt violates the Takings Clause. Result: counties are under pressure to hand this money back, which is tailwind for your business. It also means some counties quietly changed their processes — check current local procedure before filing.
Scaling it
Once one county parses clean, the pipeline is mechanical:
- County crawler — spider clerk/treasurer sites across a state for PDF links matching “excess|surplus|report of sale”, diff monthly for new filings.
- Batch skip-trace — run the Tier A/B names through the API in bulk, mail-merge letters, print-and-stuff evenings or hand the batch to a mail house.
- State expansion — every added state is a new rule card and a new statute read; the parsing code barely changes, the legal layer does. Multiply carefully.
- Referral lane — attorneys handling the foreclosure already know the surplus exists. A clean referral split to a probate or real-estate attorney who does not want the hassle buys deals you would never have found solo.
Your first 30 days
Do not start by building a 15-state crawler. The failure mode in this business is a month of tooling and zero letters mailed. Compress it:
Week 1 — one state, one county. Pick your state, read its excess-proceeds statute end to end , then pick the single largest county. Download its current surplus PDF, run the scanner, clean the output by hand. You are looking for Tier A and B rows where the defendant is an individual human — the LLC vs LLC rows are usually institutional interests you cannot help.
Week 2 — trace and mail. Skip-trace 20–30 of those individuals, verify each docket that the party matches the statute’s entitled-person definition, and mail letters. Twenty good letters take an afternoon once the pipeline is warm. Keep the letter to one page and lead with the dollar amount — it is the only number the reader cares about.
Week 3–4 — paper and follow up. Call everyone who did not respond (phone follow-up reliably outperforms mail alone), sign contingency agreements with the yeses, and file the claim paperwork with the clerk. Rejections at this stage are usually fixable documentation problems — wrong notarization, missing case number — not dead leads.
At the end of month one you will know three things no course can teach you: your county’s actual claim form, your actual response rate, and whether the clerk’s office processes claims the way the statute says. That is the point where scaling makes sense — add counties only after one produces a signed agreement.
Questions readers actually ask
Is this legal? The recovery itself is legal in most states and the money is unquestionably the owner’s — the Supreme Court settled that in Tyler. What is regulated is how you get paid: fee caps, registration or bonding requirements for recovery agents, and outright prohibitions (Texas bars non-attorney fees entirely). The scam patterns that get people prosecuted are impersonating a government office and charging owners upfront fees. Avoid both and you are in the same category as any contingency business.
Do I need to be in the state? Generally no — PDFs are online, dockets are online, mail is mail. Some claim forms must be filed in person or notarized in-state; a mobile notary or the attorney you partner with solves that cheaply per deal.
What if the owner already knows about the money? Then they filed already and the row will disappear from the next monthly list — which is exactly why you diff the lists every cycle. Fresh rows are untouched leads.
Why would anyone give up 25%? Because the alternative is 0%. Most former owners are dealing with a foreclosure, a moved household, and a letter from a county they have no reason to trust. A third party who explains the process, fronts the paperwork, and gets paid only on recovery is a better deal than a form they did not know existed.
TL;DR
- Counties sit on an estimated $2.1B in unclaimed tax-sale and foreclosure surplus; one Ohio county’s public PDF held $7.2M across 411 cases — verified by parsing it live in this article, to the cent against the county’s own total line.
- The scanner costs nothing to run: public PDFs,
requests+pypdf, scored CSV out. Only the skip-trace step is paid and it is mocked in the tested code. - The business is skip-trace → contact → contingency split (industry norm roughly 10–35%) on five-figure units.
- The moat is legal literacy: learn your state’s deadline and fee statute first (FL 120 days, TX 2 years + attorney-only fees, CA 1 year, GA 5 years), never impersonate the county, never charge owners upfront.
The list is public. The statute is public. The only thing between the county’s ledger and the former owner’s pocket is somebody willing to read a PDF carefully and make a phone call.