haul: Route Planner for EVE Online
I built haul (Hyperspace Asset Unloading and Loading) because EVE really is spreadsheets in space, and apparently my reaction to that joke was to make a better spreadsheet. The game is just an excuse; the real fun was gluing together a route planner that could out-nerd me when I am tired and still in love with the tooling.
What started as a weekend experiment with a shortest-path library turned into a playground for the parts of engineering I enjoy: modelling messy systems, caching everything that moves, and feeding a frontend with data it never expected. This write-up walks through the bits of haul that still make me grin when I open the repo.
Making the map behave
The core of haul is a navigation graph that refuses to stay abstract. I let
the NavigationGraph
class soak up ship quirks, warp curves, and
my personal collection of "don't go there" systems. Instead of
showing raw distances, each edge is weighted by how it feels to fly it. Once
the graph learns those preferences I cache the resulting paths so the server
can answer questions instantly, even when I am spamming route requests out of
curiosity.
Working on this layer was equal parts math and playing whack-a-mole with weird EVE map data. Stations that share a grid needed custom rules, stargate hops deserved their own timing, and I slipped in logging that reads like a travel diary when a route surprises me. It is how I test new ideas: change a weight, watch the logs, tweak again.
Keeping the pipes lively
The backend leans on FastAPI because I wanted something snappy while I experimented. Asynchronous calls pull market orders, solar-system metadata, and whatever else the ESI API lets me touch that week. An asyncio pipeline cleans that firehose: filter by cargo space, sanity-check prices, hand the survivors to the pathfinder, and push the results over server-sent events to React. If a good trade appears, the UI lights up without a single manual refresh.
I wrote a Rust sidecar for the hot path just to see if it would help, and it stayed because the latency drop was obvious the second the market API started sulking. More importantly, the rewrite forced me to tighten the data model so the Python and Rust halves share exactly the same shape. The Rust service now sits beside the main app, ready to take over whenever I want to stress-test the pipeline.
Letting the code pick favourites
My favourite loop lives in the pathfinder. It takes noisy trade matches, filters out anything the graph cannot reach, asks how long the detour would really take, and quietly throws away the ones that turn a profit only in my imagination. The trimmed version looks like this:
def pathfinder(order_matches, graph, ship):
order_matches = filter_order_matches_not_in_graph(order_matches, graph)
order_matches = filter_order_matches_same_region(order_matches, graph)
order_matches = calculate_net_profit(order_matches, graph, ship)
return [match for match in order_matches if match.net_profit and match.net_profit > 0]
It is small, but it captures everything I wanted from haul: code that quietly handles the bookkeeping so I can geek out about the next tweak. When the logger announces that only three trades survived the gauntlet, I know the tool did the thinking for me.
There is still plenty I want to play with--automated risk-map updates, a fully Rust backend, maybe live route rewrites when markets drift--but the current build already scratches the itch that started the project. haul is my favourite kind of side project: the game stays a backdrop while the code gets to be the main act.
For the nerds
I like EVE because everything in space has to be built by someone, but I love haul because it let me fiddle with that ecosystem on my terms. This was never the path to infinite ISK; it was an excuse to automate away the boring bits and keep the crunchy decisions.
I flew a Sunesis almost exclusively for these runs. Low-volume, high-value loot made more sense than hauling mountains of tritanium, so most nights were spent sweeping the regions around The Forge and quietly feeding Jita. Fire sales were my favourite targets. Some players would sell items at low prices and haul would flag them before the market bots woke up. I tuned for align time and warp speed, then bolted on just enough armour to shrug off the stray smart bombs that would have vaporized a lesser frigate. Two-second align, roughly seven AU per second, about seven hundred cubic metres of cargo space: nerd stats, but they mattered.
Risk calculations mixed whatever I was carrying with how sketchy the route felt. The classic choice between Jita and Amarr was either the spicy five minute sprint through Ahbazon or the sensible fifteen minute detour. With twenty million ISK in the hold I usually gambled; with two hundred million I let the planner nudge me toward the scenic route.
I kept mostly to high-sec, dipped into low-sec when the rewards were silly, and only daydreamed about null-sec after reading killmails that told me I would not make it home. On the rare occasions I needed to move a freighter's worth of items, PushX got the contract. There is something thrilling about watching a courier move everything you own while your monitoring script quietly refreshes in the background.
So yes, the theme was always the same: move everything to Jita, but do it with enough automation that the space spreadsheets finally felt like play.