INSPIRATION

I kept reading the same story in owner-operator forums, told a hundred different ways: a driver takes a load because the broker said "$2.60 a mile", and three days later he cannot work out why the bank account went down.

The reason turns out to be structural, not personal. Rate per mile is the only number brokers quote, and it hides three things that decide whether a load is actually any good:

It hides deadhead. $2.50/mi with 400 empty miles to the shipper is $1.70/mi, and those empty miles still burn fuel and tyres.

It hides the clock. Hours-of-service law, not distance, decides how many days a load takes. A 600-mile run that strands you on a 34-hour restart costs three days. The same 600 miles run clean costs one. Identical rate, one third of the business.

It hides what a day costs. The truck note and the insurance are billed whether the wheels turn or not.

And there is a worse failure than a thin load: accepting one you cannot legally deliver on time. The appointment is sixteen hours out, the run needs nineteen hours of legal driving, and you discover this at hour fourteen — parked, with the receiver on the phone.

Nobody is doing bad maths. They're doing correct maths on a number that was never designed to tell them the truth. I wanted to build the ninety-second sanity check that exists between the broker's call and the word "yes".

WHAT IT DOES

You paste the offer — lane, rate, appointments, and where your ELD clock currently sits. In under a second HaulCheck returns BOOK IT / NEGOTIATE / WALK AWAY, and tells you why.

True cost accounting. Fuel at your MPG and your fuel price, maintenance and tyres per mile, fixed costs amortised across the days the load actually takes, factoring, dispatch, tolls, lumper, reefer burn. A real FMCSA §395 clock simulator. The 11-hour driving limit, the 14-hour window, the 30-minute break, the 60/70-hour cycle and the 34-hour restart — simulated event by event into an hour-by-hour timeline and a legal ETA. Appointment feasibility. "Legal ETA Tue 06:34 — 22.6 hours LATE." Before you accept, not after. The deadhead reveal. Side by side: what the broker quotes (rate ÷ loaded miles) versus what you actually earn (rate ÷ every mile you turn). Committed-time model. A load is charged for the recovery rest it forces on you, so a 14-hour, 500-mile turn is a full day of the truck's earning capacity — not 0.6 of one. Cycle-hour economics. Net per on-duty hour, because the 70-hour cycle is the one input an owner-operator cannot buy more of. A counter-offer, written for you. Break-even rate, target rate, and a copy-paste message that argues from your numbers instead of your mood. A board, ranked honestly. Every load you checked, sorted by net profit per committed day. The highest rate-per-mile is very often not the top row.

Four built-in scenarios demonstrate the four real shapes of the decision:

The hours trap — $2.95 a loaded mile, the best-looking load on any board, and 22.6 hours undeliverable. The money was never the problem. The deadhead trap — $2.34 a loaded mile that is $1.59 all-in once you count the 435 empty miles to get there. The cheap turn — 235 miles, home tonight, easy, and it eats ten hours of a 70-hour cycle you cannot buy more of. A load worth booking — no deadhead, real hours, real money.

HOW I BUILT IT

Vanilla HTML, CSS and JavaScript. No framework, no build step, no backend, no dependencies. The whole app is about 63 KB of static files.

That was a decision, not a shortcut. The user is in a truck cab, on a phone, sometimes with one bar of signal. Every kilobyte of framework is a second of load time and a chance of failure in a place where there is no help. A static bundle loads on a cheap Android in a parking lot in Wyoming.

Architecture — the one decision everything else follows from:

assets/engine.js ← PURE. no DOM, no storage, no fetch. the whole decision layer. assets/app.js everything impure: DOM, localStorage, formatting assets/data.js 170 North American freight hubs, lat/lon tests/engine.test.js 45 assertions against engine.js

Quarantining engine.js as pure functions meant the entire hours-of-service and accounting layer is unit-testable in plain Node with zero mocking — node tests/engine.test.js, 45 assertions, 40 milliseconds, nothing to install. That let me iterate on federal rest rules with confidence instead of hope, which mattered enormously on a solo build against a clock.

The HOS simulator is event-driven, not formula-driven. It advances a clock through drive / on-duty / break / reset / restart events, checking four simultaneous limits at every step and inserting whatever rest the law requires, until the load is delivered. It returns the timeline the app renders.

Distances are computed offline — haversine over a bundled 170-city freight-hub dataset, with a distance-dependent circuity factor (1.35× under 60 miles down to 1.15× over 900) to convert great-circle to practical road miles. A routing API would mean a key, a bill, a rate limit and a dead app where there's no signal. ±5–8% is far inside the noise on a rate negotiation, and every number stays user-overridable.

Storage is localStorage, wrapped in try/catch. No account, no server, no analytics. A driver's cost structure is commercially sensitive and none of it leaves his device. A service worker caches the shell, so the app works parked.

CHALLENGES I RAN INTO

The 14-hour window does not pause for your break. My first simulator paused the 14-hour clock during the mandatory 30-minute break. It does not. The 14 is consecutive clock time — taking the break the law requires costs you window you can never get back. My ETAs were optimistic by exactly the amount that puts real drivers short of the receiver. I made the same mistake the industry makes, and only caught it by hand-simulating a 450-mile run against the regulation text.

I was dividing by the wrong thing, twice. Version one divided profit by miles, like every calculator out there — it declared a $900 turn excellent. So I made the clock simulator feed the accounting layer and divided by days instead. Better, but still wrong: a 14-hour turn scored as 0.6 of a day, which quietly assumes you can grab another load the moment you unhook. You can't — your clock is spent and you are legally parked for ten hours. Loads now carry the recovery rest they force on you. That change flipped three of my four demo scenarios from green to amber, and every reversal was correct.

Solving backwards is much harder than solving forwards. "What rate do I need?" is not cost plus margin, because factoring, dispatch and percentage-based driver pay all scale with the rate. It's R = (target + fixedCosts) / (1 − k). My naive version was off by $140. I only found it because a test asserted that quoting the app's own break-even rate must produce exactly $0 net.

Simulation loops can hang. An early version spun forever on a load whose delivery window made progress impossible. Real users would have seen a frozen phone. There is now an iteration guard that fails loudly with a readable error instead of locking the tab.

Honest scope discipline under a deadline. Split sleeper-berth and a true rolling 8-day recap are genuinely hard and I did not have the hours. Rather than fake them, I left them out and documented exactly where the model is conservative. Knowing what not to build was as important as anything I did build.

ACCOMPLISHMENTS THAT I'M PROUD OF

The insight, more than the code. An owner-operator does not sell miles — he sells days, and how many days a load takes is decided by federal law, not by distance. That inversion is the product. Every load calculator divides money by miles; this one runs the legal clock first and divides money by the days the law says it will cost. It is the difference between a calculator and a decision tool.

A hours-of-service simulator that a real driver would recognise. Not a checkbox — an actual event-driven clock producing a timeline: drive 8.0h → 30-minute break → drive 3.0h → 10-hour reset. Four interacting limits, correct on the rules that most people (including my first draft) get wrong.

45 passing tests on a hackathon project. Including adversarial ones: that quoting the app's own break-even rate yields exactly $0 net, that a 3,000-mile haul terminates, that the 14-hour window binds before the 11-hour limit does.

Nothing to break. No API key, no backend, no database, no environment variables, no build step. It deploys in forty seconds and works with the phone in aeroplane mode.

It says WALK AWAY. The commercially obvious move is a tool that helps you book more loads. This one tells you to hang up, and shows its working. That is the only version a driver would trust twice.

WHAT I LEARNED

Domain knowledge was the hard part, not the code. I could write the JavaScript in an hour. Understanding why the 14-hour window is the limit that actually bites, why deadhead is the silent killer, and why a 70-hour cycle is a scarcer resource than a fuel tank — that was the work, and it is what makes the output worth reading.

The metric everyone uses can be the wrong metric. Rate per mile is universal in trucking and it is measuring the wrong thing. Noticing that was worth more than any feature I could have added.

Tests find bugs you'd defend. The break-even solver looked right. It was wrong by $140 and I would have shipped it, because it produced a plausible number. A test that asserted a mathematical identity — quote the break-even rate, get zero — caught it in seconds.

Purity is a practical decision, not an academic one. Keeping engine.js free of the DOM wasn't tidiness. It is the only reason I could test federal rest rules at all on a one-day solo build.

Constraints improved the product. No backend forced an offline distance dataset — which turned out to be a genuine feature for people who work where there is no signal. No framework meant no build step, which meant deployment cost forty seconds instead of an hour I did not have.

A tool that tells you "no" is more valuable than one that tells you "yes". I built HaulCheck to help book loads. Its most useful output is the one that stops you.

WHAT'S NEXT

Rolling 8-day recap and split sleeper-berth (8/2, 7/3) — together these remove the two biggest sources of conservatism in the model and would let skilled drivers see the legal ETA they can actually achieve. Canadian daily limits as a first-class mode — 13/14/16 alongside the US 11/14, so cross-border planning is exact rather than careful. Paste a rate confirmation or a load-board screenshot and have the fields fill themselves. The ninety-second check becomes a ten-second one. Truck-legal routing and live state fuel prices, the moment there is a budget for an API key. The offline estimate stays as the fallback, because signal doesn't. A "what would make this work" solver — the earliest pickup, latest delivery and minimum rate that turn a red verdict green, so the driver walks into the negotiation with the exact ask. A fleet view. Everything here scales from one truck to twenty without changing the engine: the same verdict, per truck, against each truck's real clock.

Built With

Share this project:

Updates

Submission history