Invictus
People who lend their crypto to trading pools often have money in many places at once. Invictus is the terminal that puts all of it on one screen, live. I led the rebuild that took it from an MVP on Robinhood Chain to seven chains.



The short version
Invictus is a terminal for liquidity providers: people who deposit crypto into trading pools and earn a share of the fees when others trade. It shows every position they hold across seven blockchains in one place, updated as the markets move.
It serves 1,000+ concurrent users and has recorded $4M in transaction volume. I lead its engineering: the architecture, the web app, the backend, and the connections to each blockchain.
The problem, in plain words
Imagine keeping savings at seven banks, where every bank prints its statements in a different format and updates them at a different time. Adding up what you own, and whether each account is still earning, becomes a job in itself.
That is the daily life of a liquidity provider. Their positions sit on Solana, Ethereum, Base, Robinhood, BSC, Arc and HyperEVM, and each trading protocol describes a position its own way. Uniswap describes it as money inside a price range; Meteora splits it into small price buckets. So the hard part is not drawing a table. It is deciding what one row in that table means when the rows come from different systems.
Where it started
The first Invictus was an MVP built around Robinhood Chain (Arc came later) and one protocol family, Uniswap v3 and v4. It had a web workstation, a Telegram bot, custodial hot wallets and its own price ingestion, all in one Node package of about 54,000 lines with the web app nested inside it.
It did its job: it proved the product. But the next step was seven chains including Solana, pools of three different shapes, and features that need data at very different depths and speeds. The MVP's structure was not designed for any of that.
Rebuild, not refactor
Refactoring a working product is usually the safer bet. Here it was not: the MVP's structure was built around a few chains and one protocol family, and widening it meant touching most of it. So I rebuilt it as a new workspace beside the MVP, with no code carried over, on top of recorded research.
Two rules kept the rebuild honest. Decisions were made from measurements before features were built, and one feature was built all the way through, from chain to screen, before the rest.
Decisions and trade-offs
A few of the choices that shaped the system, and what each one cost:
- Users' keys never live with us. The MVP held custodial hot wallets; the rebuild uses Privy embedded wallets or the user's own wallet, so a breach of our servers exposes no key. The cost is a dependency on Privy, and key export is the user's way out.
- One server and one image, split only on measured need. Every process (API, workers, one indexer per chain) runs from the same image with a different role. That is one artifact to build and roll back; the single point of failure is accepted and covered by off-site backups.
- Postgres with TimescaleDB instead of a second database. Measured on real Base traffic, a row of pool events costs 192 bytes in plain Postgres, 53 compressed in TimescaleDB and 35 in ClickHouse. Thirty days on six EVM chains drops from about 80 GB to 22 GB. ClickHouse compresses more, but it is a second system to run, back up and keep consistent.
- Refresh by attention. Most cost comes from refreshing data nobody is looking at, so data has temperatures: hot when someone is viewing it, warm when it matters for alerts, cold when it can never change. A chain nobody is using costs nothing.
- Halt, don't fail over, when a chain stops. Chains do stop: Base for 33 minutes in August 2025, HyperEVM dozens of times. When a chain's head stops moving, the terminal shows its last price with "chain halted since" and accepts no new operations, instead of switching providers and showing stale data as live.
- Nothing is lost between the database and the queue. A change and the events it causes are written in one transaction to an outbox; jobs are idempotent and retried with limits; if the queue itself is lost, it is rebuilt from Postgres. A lost queue costs time, not work.
How the data moves
Seven chains, 31 protocol deployments across 15 variants (Uniswap, PancakeSwap, Aerodrome, Meteora and others), translated on the server into one shape that every client reads.
flowchart LR
subgraph sources["On-chain sources"]
evm["EVM chains<br/>Uniswap V3 / V4"]
sol["Solana<br/>Meteora DLMM / DAMM v2"]
end
subgraph backend["Backend, Node.js"]
jobs["Background jobs<br/>BullMQ on Redis"]
db[("PostgreSQL")]
api["API"]
end
subgraph clients["Clients"]
terminal["Terminal<br/>Next.js"]
follow["Follow<br/>copy-LP"]
mcp["MCP server"]
end
evm --> jobs
sol --> jobs
jobs --> db --> api
api --> terminal
api --> follow
api --> mcp
mcp -.-> agents(["AI agents"])What you see on screen
A terminal is something people scan, so the screen is dense on purpose. The Discover view is one sortable table with price, one-day trend, market cap, age, liquidity, volume, fees and yield side by side, and search opens from the keyboard.
The status bar shows the frame rate and how long the last server round trip took. A tool that shows live money should be honest about how fresh its own numbers are.
- Discover: where could I put money to work?
- Positions: what do I hold right now, and is it still earning?
- Portfolio: what is all of it worth together?
- Rewards: what have I earned?
Built on top
Because every position is already translated into one shape, new features are cheap. Follow lets users discover and copy the positions of high-performing wallets. An MCP server lets AI assistants read the same data a person sees, instead of scraping the screen.
For engineers
The stack, for readers who want it.
- TypeScript on Node 24 in a pnpm and Turborepo monorepo: apps for web, API, indexers and workers, and layered domain packages with one owner per concern.
- Fastify with zod, pino and OpenTelemetry; oRPC procedures defined contract-first; one WebSocket per browser tab fed by Redis pub/sub.
- Postgres 18 with TimescaleDB through PgBouncer and Kysely; BullMQ on Redis with dead-letter queues and a transactional outbox.
- viem reads EVM chains and @solana/kit reads Solana; protocols plug in by math family (reserve, tick and bin pools).
- One image built in stages and deployed by digest with GitHub Actions and Docker Compose, behind Cloudflare and Caddy; telemetry in Grafana Cloud and Sentry.
- Quality gates: strict TypeScript, Vitest with real database containers, Biome, Knip and import boundaries between packages.
What I would watch next
The single server is a deliberate bet, not an accident. Read replicas, sharding and a second region each have a stated trigger, so the system grows when measurements say so, not before.
Screens

Pool 
Mobile
What I took from it
Measure, then decide
The database, the server size and the refresh rules all came from measured data, which made each trade-off easy to explain and easy to revisit.
Know when not to refactor
When the shape of a system is the problem, a clean rebuild with the old one as a reference can be cheaper than changing it piece by piece.
Design for failure first
Chains stop, queues get lost and providers fall behind. Deciding what the system does in each case, before it happens, is most of the backend work.