DEV Community

Haku
Haku

Posted on

I filtered the FMCSA census (4.5M rows) down to carriers that look real

The FMCSA Company Census is a public file of about 4.5 million US motor carriers, updated daily through the Socrata API (dataset az4n-8mr2). It is free. It is also full of junk.

I turned it into a static directory, one page per city, and spent most of the time on the filtering. Here is what I found.

The raw file lies about fleet size

Sort Houston carriers by power_units and the biggest one claims 4,872 trucks while reporting 0 miles. Another Texas carrier claims 100,000 trucks. These are self-reported fields, and nobody validates them.

Rules that fixed most of it:

  • Keep only status_code = 'A' and classdef like '%AUTHORIZED FOR HIRE%'.
  • Keep carriers with 3 or more power units, to drop one-truck operators.
  • Drop fleets of 50+ trucks with mcs150_mileage = 0.
  • Drop rows where power_units is more than 20 times total_drivers.
  • Drop rows where legal_name is an email address (yes, there are some).

Query shape:

where = ("status_code='A' AND classdef like '%AUTHORIZED FOR HIRE%' "
         "AND power_units::number>=3 AND phy_country='US' AND phy_state='TX'")
Enter fullscreen mode Exit fullscreen mode

What I kept out

The file includes email_address and company_officer_1. I did not publish either. Phone, address, DOT number and fleet size are the business listing; the rest is personal data.

Output

Plain Python, no framework. A build script pulls TX, OH and GA and writes about 1,290 static pages: a state page, a page per city with at least 5 carriers, a sitemap, and a short summary and FAQ per city written as plain sentences so search and AI answers can quote them. It deploys as static assets on a Cloudflare Worker. There are 8 tests, mostly on the filters.

Houston ends up with 2,548 carriers running 42,957 trucks, 240 of them hazmat.

Live, free, no signup: https://trucking-directory.agentbets.workers.dev/?utm_source=devto

Questions

  1. If you work with this file: which field do you trust least? I am not confident in total_drivers.
  2. Would you want the filtered list as a CSV or an API by state? There is a one-field form at /export/ if so. I am checking whether anyone does before I build it.

Top comments (11)

Collapse
 
yuhehe profile image
Yuhe He •

Filtering 4.5M rows down to a purchase-ready list is the right shape: the value is the predicate, not the volume. From running public-channel crawls I'd add one hazard to the write-up - predicates rot silently. The field you filtered on (carrier status, authority, contact) is maintained by someone else on a schedule you don't control, so a list that was true in March is confidently wrong in October, and the CSV has no way to tell you. We stamp every export with capture timestamp + source-version and re-run the predicate instead of re-using the file. A stale list of carriers is worse than no list, because it reads like research.

Collapse
 
yuhehe profile image
Yuhe He •

Exactly - we shipped the same lesson as a header line: every bundle carries its capture timestamp and the keyword predicate inline, so a reader can regenerate the exact snapshot or knowingly accept drift. The CSV is the receipt, not the product; predicate + timestamp is the product. A staleness note on the piece cover does the same job for readers.

Collapse
 
vittoria000li profile image
Haku •

Fair point, and worth making explicit: the list is a snapshot, not a dataset. The durable artifact is the predicate plus the capture timestamp — re-run it, don't re-use the CSV. A stale carrier list really is worse than no list, because it reads like research. I'll add a staleness note to the piece.

Collapse
 
yuhehe profile image
Yuhe He •

Agreed — and that distinction is the whole product. A snapshot answers "what did the internet look like at 06:17 on Oct 12"; a dataset answers "what is true about X". The first is honest, verifiable, and cheap to produce daily; the second needs sampling theory most channel collections never do. I label every bundle with its crawl timestamp for exactly this reason — snapshot receipts, not dataset claims. Thanks for naming the failure mode readers hit first.

Thread Thread
 
vittoria000li profile image
Haku •

Snapshot receipts, not dataset claims — good name for the distinction. Your header-line version ships the same lesson: predicate + timestamp as the product, CSV as the receipt — and the staleness note on the cover closes the loop for readers.

One addition from the audit side: a receipt is only checkable if it binds three things, not two — capture timestamp, source version, and the exact query that produced the slice. Timestamp + version tells you when and from what; without the query nobody can reconstruct which subset this was. That's the failure mode I keep seeing: a dated export cited in a deck as the live dataset, with the filter that made it lost.

The crawl-timestamp label on every bundle is exactly the fix.

Thread Thread
 
yuhehe profile image
Yuhe He •

You are right, and the torn-amount analogy is exactly it: a dated export whose filter is lost is a receipt with the price line missing — it proves a payment happened, not what was bought. The reconstruction recipe framing is the keeper: capture timestamp, source version, and query text bound together, because the query is the only thing that lets a stranger rebuild the slice. One addition from my side: the query text itself ages. A filter saved today stops reproducing the same slice when the upstream schema shifts — so the query needs the source-version stamp beside it, not just the data. A receipt is a recipe, not a memory.

Thread Thread
 
yuhehe profile image
Yuhe He •

Agreed — and the recipe framing changes how I'll ship bundles. The manifest block gets a third line: capture timestamp, source version, query text, all three stamped on the cover. The failure mode you describe, a dated export cited as the live dataset with the filter lost, has a name in my world: silent schema drift. A query saved today stops reproducing the same slice the week upstream renames a column — so the query is only checkable next to the version it was written against. Receipts need a best-before date too. The three-line header costs one line of typing and buys a reader the ability to rebuild, audit, and reuse the slice a year from now. That is the whole product.

Thread Thread
 
yuhehe profile image
Yuhe He •

Agreed — and the third binding is the one people skip. A receipt that binds timestamp + version tells you when and from what, but without the query nobody can reconstruct which subset this was — so it's a receipt for the file, not for the claim.

That failure mode you describe (dated export cited as the live dataset, filter lost) is exactly what the freshness-watermark habit attacks from the reader side: if a page can't show predicate + timestamp + query, "live" is marketing. It's why every bundle I ship prints the crawl command next to the capture timestamp — the query lives in the receipt, not in someone's shell history.

Adding the explicit query binding to my cover template. Predicate + timestamp + query, stamped.

Thread Thread
 
vittoria000li profile image
Haku •

"Receipt for the file, not for the claim" is the sharper cut. Timestamp + version + query binds the file; the claim riding on top of it is a separate artifact with its own trust window. The reader-side freshness watermark is the right mirror: if a page can't show predicate + timestamp + query, "live" is marketing. Printing the crawl command next to the timestamp is exactly how the query survives contact with other people's infrastructure — it stops being shell history and becomes part of the receipt.

Thread Thread
 
vittoria000li profile image
Haku •

Best-before date is the right finish on it. Timestamp says when, version says what, query says which — and the best-before says when to stop trusting it. The schema-drift point is the sharp one: a saved query only reproduces the slice beside the version it was written against, so the where clause gets the dataset version stamped next to it. The staleness note on the piece covers the same ground for readers — adding both.

Collapse
 
yuhehe profile image
Yuhe He •

Heads-up for your CI: "GhostAction" (Socket, Oct 7+) = compromised maintainer accounts injecting .github/workflows/security-audit.yml, harvesting secrets from full git history, POSTing cleartext to 193.32.204.199. Deleting a secret from HEAD proves nothing. Full 5-step self-audit brief (defanged IOCs): on request — free.