Ryan Smith
production2026-08

The Lots That Sold for $100

Finds Brevard County vacant lots that changed hands for almost nothing and emails one buyer a worked list every Friday

1.9 millionSale records screened
159 lotsFound in first 60 days
Under 2 minutesWeekly build runtime

The Problem

A small buyer in Brevard County places manufactured homes on vacant lots, one lot at a time, and had no list to work from. The lots she wants show up in the county's transfer history as sales recorded at $0 to $1,500: quit claims, family transfers, estate deeds, the deals nobody advertised. That history is published only as a 1.9 GB Access database refreshed daily, and whether a manufactured home is even allowed on a given lot lives in a separate zoning map. Her alternative was calling the Planning Department one parcel number at a time.

What It Does

The whole county property roll is loaded into a database and checked before it replaces the last copy. Every week the feed pulls vacant residential parcels that changed hands for $0 to $1,500 since the last batch, skips anything already delivered, and drops retired parcel IDs into an exclusion log instead of silently losing them. Each lot gets its zoning district from the county map, and each owner with a real person's name is run through a skip trace for phones and emails. A workbook is built with a Fits Criteria column and a Why Not column against her size, price and flood rules, then a verifier checks the file before anyone sees it. Thursday night an email lands in Ryan's inbox with six stat tiles and the workbook attached. Friday he opens a second workflow, confirms the date and recipient, and it sends the file to her, stamps the ledger so those lots never come back, and logs the delivery.

A real estate buyer who acquires individual vacant lots for manufactured home placement and works from a printed list with letters, postcards and phone calls. The feed is exclusive to one subscriber.

Outcome

The first 60-day batch surfaced 159 nominal-consideration vacant lots, 34 of which fit her buy box, from a county file no buyer could read by hand. The weekly feed picks up where that batch stopped. The first Friday file went out on 2026-09-04 with 6 parcels; the following week the build ran, found nothing new, and said so instead of sending an empty file. The first weeks are slim because the county publishes transfers about six weeks after they happen.

Screenshots

The Thursday approval email for the first delivered file: six tiles, one warning explained as a vendor miss rather than a join failure, and the two choices, send it or do nothing.
The Thursday approval email for the first delivered file: six tiles, one warning explained as a vendor miss rather than a join failure, and the two choices, send it or do nothing.

The Thursday approval email for the first delivered file: six tiles, one warning explained as a vendor miss rather than a join failure, and the two choices, send it or do nothing.

Two Thursday runs at 21:00. The first built a 6-row file in under two minutes; the second found nothing new and finished in 43 seconds.
Two Thursday runs at 21:00. The first built a 6-row file in under two minutes; the second found nothing new and finished in 43 seconds.

Two Thursday runs at 21:00. The first built a 6-row file in under two minutes; the second found nothing new and finished in 43 seconds.

The 2026-09-04 file as she received it: six lots, four marked YES against her criteria, the two that missed told why, with flood zone, zoning district and a map pin on every row.
The 2026-09-04 file as she received it: six lots, four marked YES against her criteria, the two that missed told why, with flood zone, zoning district and a map pin on every row.

The 2026-09-04 file as she received it: six lots, four marked YES against her criteria, the two that missed told why, with flood zone, zoning district and a map pin on every row.

How It's Built

Trigger:Two workflows. Workflow 1 runs on cron `0 21 * * 4`, Thursdays at 21:00 Central, timezone pinned to America/Chicago in the workflow settings; it computes the next Friday as the send date. Workflow 2 is a manual trigger Ryan runs on Friday after reading the approval email. Upstream of both, the county roll is downloaded by hand and loaded into Postgres.
Components:24

The county appraiser publishes the whole roll as a zip holding an Access database. A loader script unzips it, refuses to run if the column layout changed, stages both tables as text, compares row counts and date parse rates against the live tables, then swaps them in one transaction and rebuilds a per-parcel sales summary. Workflow 1 opens an SSH session to the server and runs a shell wrapper that chains the weekly extract SQL, a zoning lookup against county and Palm Bay GIS, skip trace prep and submission to the trace vendor's API, the workbook builder and the verifier. The extract joins transfers to parcels on property ID, keeps the five vacant land use codes, drops inactive parcels to an exclusion log, and pulls only rows the delivered ledger has no delivered timestamp for, stamping them as staged. A code node parses the wrapper's final RESULT line, throws if there is none, separates NOTE lines from warnings, and lifts the row, phone, fit and trace counts out of the log. An IF node routes OK to a second SSH call that base64-encodes the workbook, then a code node builds an HTML approval email with six tiles and sends it through Resend to Ryan; EMPTY routes to a plain nothing-new email. Workflow 2 starts from a Set node holding the send date and recipient, re-runs the verifier over SSH, reads the email body from a text file and base64-encodes the file in the same call, refuses to continue unless the verifier printed ALL CHECKS PASSED and line one of the body starts with Subject:, sends through Resend, captures the message ID, then branches: a live recipient stamps the ledger, parses the stamp result, logs the delivery to an Airtable Deliveries table and emails Ryan a confirmation; any other recipient is test mode, which sends with a [TEST] subject and leaves the ledger and Airtable untouched.

By the numbers

transfer rows in the county roll
1,917,407
parcels in the county roll
393,772
county roll file size
1.91 GB Access database, 235 MB zipped
roll load runtime
about 10 minutes
nominal price band
$0 to $1,500
recorded sale price on first-batch rows
$100 on all 159
first backfill window
2026-05-28 to 2026-07-28, 159 transfers
first batch by deed type
98 quit claim, 53 warranty, 5 estate, 3 foreclosure
first batch fitting her buy box
34 of 159
nominal vacant-land transfers per month, 0.20 to 2.00 acres
about 90
county publication lag
4 to 6 weeks
zoning coverage on the 90-day sample
137 of 158 parcels
first weekly file, delivered 2026-09-04
6 parcels, 4 fitting her criteria, 1 traceable owner, 0 phones
second weekly build, 2026-09-10
nothing new, no file sent
build workflow runtime, 2026-09-03 run with 6 rows
1 minute 51 seconds
build workflow runtime, 2026-09-10 empty run
43 seconds
verifier checks printed as passed on the 2026-09-04 file
15
skip trace cost per successful hit
2 cents
approval email stat tiles
6
n8n nodes across both workflows
24
build workflow proven runs before first delivery
5
build schedule
Thursdays 21:00 Central
delivery cadence
weekly, Fridays, one subscriber

Key Decisions

Split build and send into two workflows with a human in between

The Thursday build can run unattended because nothing leaves the house until Ryan has read the stat tiles and the run log. Doing nothing is the safe default: rows stay unstamped and roll into next week on their own.

Build Thursday at 21:00 and send Friday morning

She wanted her new data on Fridays. Pulling as late as Thursday night gives her the freshest possible file, and the send waits for the next morning so a person has looked at it.

Set the nominal band ceiling at $1,500

An old habit. Different counties record non-market transfers at different placeholder amounts, sometimes $0, sometimes $100, so $1,500 is wide enough to catch whatever a given county uses without ever missing one.

Key the delivered ledger on book, page and property ID, and stamp delivered only from an approved send

One recorded instrument can convey several parcels, so the instrument alone is not unique. Staging and delivering are separate stamps so a rejected build self-heals into the next one instead of losing rows.

Test mode is triggered by the recipient address, and the default recipient is Ryan

Forgetting to change the field costs a test email to himself, not an unrecallable email to the client. Stamping the ledger on a test would mean those parcels never reach her, so test mode never stamps.

Throw when the wrapper prints no RESULT line, when the base64 attachment size disagrees with the reported byte count, or when the verifier does not print ALL CHECKS PASSED

A silent partial run is worse than a failed one. Each check turns a quiet bad file into a loud stopped workflow before an email goes out.

Count trace matched separately from trace hits, and say which one is zero in the email

A join failure and a vendor with no phone for a decedent look identical in the phone count and mean completely different things. The tile now reads either JOIN FAILED or joined fine, vendor had none.

Keep NOTE lines out of the warning count

One informational line contained the phrase not a join failure, which the warning regex swept up and counted as an alarm on a clean week.

Make the ledger stamp failure loud after the email is already gone

If the send succeeds and the stamp does not, the file is with the client but the ledger still thinks it is pending. The workflow throws with the exact command to run by hand rather than logging a delivery it cannot back.

Let the Airtable log fail without stopping the confirmation

The delivery already happened and the ledger is already stamped by that point. A logging hiccup should not look like a failed send.

Flag her criteria in a Fits Criteria and Why Not column instead of filtering rows out

She asked to see everything, including parcels over two acres, so the feed carries no acreage filter and lets her decide.

Drop inactive parcels to an exclusion file with a reason rather than silently

A retired parcel ID dead-ends on the appraiser site, but a count of what was excluded and why belongs in the approval email.

Send an email even when there is nothing to send

A quiet Thursday is indistinguishable from a broken one unless the run says so. The empty notice states that nothing was built, that the county is weeks behind, and that the ledger still holds everything undelivered.

What broke & how it was fixed

The first n8n runs all aborted before doing any work. The shell wrapper guarded against overlapping runs with pgrep on its own name, and when n8n launched it the wrapper shell also carried the script path, so the count was two and it exited. It was replaced with flock on a lock file and tested against four cases. Separately, the trace step warned on a normal week: it treated zero phone hits as a join failure when the join had worked and the vendor simply had no phone for a deceased owner. The fix counts matched rows apart from hits, and the approval email now says which one is zero. Earlier, appending the SSH public key with cat welded it onto an existing line with no trailing newline, which broke both keys and let sshd fall back to password auth without the test noticing; every key test now forces batch mode with password auth off.

Where else this applies

The shape is a bulk government dataset that is too big to read, filtered to a narrow signal, deduped against a ledger of what was already sent, enriched from two more public sources, and delivered on a schedule with a human approval gate in front of the send. Any county roll, court docket or licensing file that is published whole and refreshed daily fits it. The two-workflow split, build unattended then send on approval, is the part that transfers most cleanly: it lets a one-person operation run nightly automation against a paying client without ever sending something unread.