Written October 2026, looking back at Systems5 min read
The terminal and the engine
Joining Print.World to work on the Rust trade backend under a crypto trading terminal, and the first problems that taught me how it really behaves.
From Print.World · Aug 2025 – Present
I joined Print.World as a software engineer in August 2025. It's a crypto trading terminal, remote from Toronto, and I'm doing it alongside school. The terminal is what users see. I work on the engine underneath it: quotes, routing, and execution, mostly in the Rust backend that builds and sends trades on Solana.
These are the first few problems I took on there, because each one taught me something about how the system actually behaves rather than how I assumed it would.
Building swaps across many DEXs
A trade on-chain isn't one API call. You pick a venue, read its pool state, derive the right accounts, and build an instruction in that venue's exact format. The backend supports swap building across roughly eight DEX families, and each has its own account layout and quirks.
Most of the work is careful plumbing. The fun part is that "careful" has a very literal meaning here: get one account wrong and the transaction fails on-chain, after you've already told the user it's on its way.
A cache that verifies itself
That leads to my favourite design on the backend so far. We cache pool data so we don't hit RPC on every quote. Some of the cached fields are used to derive on-chain addresses. If one of those values is plausible but wrong, you build a transaction that looks completely valid, passes every local check, and then reverts on-chain.
That's the worst kind of bug: no error until it costs the user something.
Making every read go to chain would defeat the point of the cache. So instead, after every cache write, a background task re-reads that account from chain and compares. The rules for that task mattered more than the idea:
- it never blocks the caller; the trade path doesn't wait for verification
- it only repairs when the on-chain value actually differs
- if the RPC call errors, it writes nothing, because a failed read is not evidence the cache is wrong
- it's debounced per key, so a burst of writes to the same entry costs one verification, not one RPC call each
One subtlety I'd tighten: a slow verification could read the chain at an older slot than the value already in the cache. Every RPC response carries the slot it was read at, so a repair should only apply when its read is at least as new as the cached value. Otherwise the verifier itself could make the cache wrong.
Making a gap measurable
We consume a live stream of chain updates. Streams disconnect; that's normal. The problem was that, in the version we ran, the stream couldn't resume from a specific slot. After a reconnect, there was a window of updates we'd simply never seen, and nothing told us how big it was.
I couldn't close the gap with what the stream supported at that version. What I could do was make it measurable. On reconnect, we compare the last slot we processed with the first slot we receive and record an upper bound on how many slots we might have missed. That number feeds alerting. A small gap is noise. A large one means the cache may be stale and someone should look.
It's not a fix, and I'm upfront about that internally. But "we lose some updates sometimes" and "we lost at most N slots at this time" are very different statements to operate against.
Where did the milliseconds go?
Users notice when a trade feels slow, and "it feels slow" isn't actionable. So I added a per-trade latency breakdown, for both the quote and the submit path. Every RPC call and cache call gets timed, cache misses are recorded, and we note where the blockhash came from. The result reads roughly like:
quote: cache_pool 0.x ms (hit) | rpc_account N ms (miss) | build N ms
submit: blockhash (cached) | sign N ms | send N ms
Before that, latency debates were opinions. After, you could point at the line that was slow.
Freshness versus cost
A lot of the backend is balancing how fresh data needs to be against how much RPC you're willing to spend. Two examples:
- the recent blockhash is polled into a cache in the background rather than fetched per trade, so submit doesn't pay for that round-trip
- negative results, like "this account doesn't exist", get a shorter TTL than positive ones, because they're the ones most likely to change soon and the most damaging to keep wrong
Neither is clever. Both came from looking at the breakdown and asking why a particular number was there.
What I've taken from the first month
Every fix above started with measuring, not guessing. The cache bug was invisible until I asked what happens when a value is plausible but wrong. The stream gap was invisible until I tried to quantify it. The latency complaints went nowhere until each trade could explain its own milliseconds.
The latency breakdown made every later conversation shorter, and it's the first thing I build on any new path now. Later work at Print took me into perps aggregation and bridging, and the same habit carried over.