Janus: The High-Frequency Arbitrage Engine

Inspiration

This project started from a realization that the "house" always wins because they control the spreads. I was tired of gambling on outcomes; I wanted to gamble on inefficiency. In prediction markets, Polymarket (crypto/blockchain) and Kalshi (regulated/fiat) often trade essentially the same events, but they live in isolated liquidity pools.

Manual trading was impossible. By the time I noticed a price gap, bots had already closed it. Janus became an "anti-gambling" machine: software designed to remove emotion and rely on the math of atomic arbitrage.

What It Does

Janus is a high-frequency system that executes neutral arbitrage between decentralized and centralized prediction markets. It does not predict who wins an election or sports game; it predicts that Price A must converge to Price B.

+-------------------------------------------------------------------------+
|                          EXTERNAL EXCHANGES                             |
|                                                                         |
|    [ POLYMARKET ] <---------------------------.   [ KALSHI ] <-------.  |
|  (Polygon Blockchain)                         | (Regulation / API)   |  |
+----------+------------------------------------|----------+-----------|--+
           | (WSS Data)                         |          | (WSS Data)|
           |                                    |          |           |
           v                                    |          v           |
+-----------------------------------------------|----------------------|--+
|  LOCAL EXECUTION ENVIRONMENT (Mac/Linux)      |                      |  |
|                                               |                      |  |
|  .--------------------------------------------|----------------------|--.
|  | C++ CORE ENGINE (Binary)                   |                      |  |
|  |                                            |                      |  |
|  |   [Poly Feed Thread]           [Kalshi Feed Thread]               |  |
|  |            |                            |                         |  |
|  |            +----------->    <-----------+                         |  |
|  |                     Atomic Store                                  |  |
|  |                          |                                        |  |
|  |            [ SHARED MEMORY (Zero-Lock) ]                          |  |
|  |            (std::atomic MarketSlot Array)                         |  |
|  |                          |                                        |  |
|  |                     Atomic Load                                   |  |
|  |                          v                                        |  |
|  |   .------------------------------------------------------------.  |  |
|  |   | MAIN THREAD (HOT PATH)                                     |  |  |
|  |   |                                                            |  |  |
|  |   |  1. [TouchTracker] (Price Normalization)                   |  |  |
|  |   |         |                                                  |  |  |
|  |   |  2. [ArbLogic] (Spread Calculation)                        |  |  |
|  |   |         |                                                  |  |  |
|  |   |  3. [Risk Controls] --> (If Rejected: Drop)                |  |  |
|  |   |         |                                                  |  |  |
|  |   |  4. [Executor] -----------------> (Execute Limit/Hedge) --/  |  |
|  |   '---------+--------------------------------------------------'  |  |
|  |             |                                                     |  |
|  |             | (JSON Event Stream)                                 |  |
|  '-------------|-----------------------------------------------------'  |
|                | Pipe (STDOUT)                                          |
|                v                                                        |
|  .-------------------------------------------------------------------.  |
|  | NODE.JS BRIDGE LAYER                                              |  |
|  |                                                                   |  |
|  |  [Child Process Mgr] --> [Log Parser] --> [WebSocket Server :3001]|  |
|  '-------------------------------------------------------------------'  |
|                                ^                                        |
+--------------------------------|----------------------------------------+
                                 | WS Connection
                                 v
+-------------------------------------------------------------------------+
|  USER INTERFACE (React)                                                 |
|                                                                         |
|  [ Browser ] <--> [ Recharts Visualization ]                            |
|        |                                                                |
|  [ Control Panel ] --(Command)--> [SocketServer] --(Stdin)--> [C++]     |
+-------------------------------------------------------------------------+

Core Strategy: Maker-Taker

"Taker-Taker" arbitrage is easy to understand but often unprofitable due to double fees (spread + fees on both sides). Janus uses a Maker-Taker strategy:

  • Maker leg (Polymarket): place a post-only limit order on-chain to provide liquidity and improve price positioning.
  • The trap: wait until a counterparty fills the limit order.
  • Taker leg (Kalshi): the moment Polymarket fills, trigger an immediate market order on Kalshi to hedge.

Strategy Evolution (Why We Switched)

We started with a simple taker-taker approach and moved to maker-taker after we measured the real costs:

  • Latency kills edges: even small network delays can erase a 1-2 cent edge before both legs execute.
  • Double fees: paying taker fees twice made "good" arbs unprofitable at scale.
  • Hanging-leg risk: sequential taker orders left us unhedged too often when one leg rejected or slipped.
  • Queue position: making on Polymarket lets us capture edge by waiting for fills instead of racing every tick.

That switch forced a stricter execution state machine: wait for maker fills, hedge only matched size, enforce timeouts, and block re-trades on unhedged slots.

Low-Level Speed: Zero-Copy Architecture and Atomic Packing

Janus uses a bit-packed atomic state. We compress the entire state of a market (Best Bid, Best Ask, Bid Size, Ask Size) into a single unsigned 64-bit integer (uint64_t).

Why this matters:

  • One CPU instruction: read the entire market state in a single atomic load.
  • Zero locking: feed handlers and the execution engine read and write the same market slot without generic mutexes.
  • Bit-masking: extraction is constant-time and cache-friendly.
// From backend/src/core/types.hpp
inline void unpack_book(uint64_t packed, uint16_t& yes, uint16_t& no) {
    yes = (packed >> 48) & 0xFFFF;
    no  = (packed >> 32) & 0xFFFF;
}

The Slab (MarketSlot) and Continuous Refresh

The slab is a flat array of MarketSlot structs, aligned to cache lines and shared across threads:

  • Each slot holds two AtomicOrderbooks: one for Kalshi, one for Polymarket.
  • Feed handlers update the slab on every websocket tick using pack_book(...) and atomic stores.
  • TouchTracker snapshots best bid/ask and sizes with nanosecond timestamps, so the strategy loop only scans "touched" slots instead of 20,000 markets every cycle.
  • Book timestamps are stored out-of-line (separate arrays) to preserve hot cache alignment and detect stale data.

This is how we keep prices continuously refreshed without locking the hot path.

Authentication and Hashing (Exchange-Side)

Both exchanges require custom signing and hashing logic. Getting this right was a major part of the build.

Kalshi WebSocket Auth

  • Uses RSA-SHA256 signatures with a millisecond timestamp.
  • Signs the exact string: timestamp + "GET" + "/trade-api/ws/v2".
  • Signature is Base64-encoded and sent in a JSON connect message with the keyId.

Polymarket CLOB Auth

  • Uses EIP-712 structured data signing with Keccak-256 (not SHA3-256).
  • The signer derives the wallet address and checksums with EIP-55 rules.
  • REST order posting uses HMAC (base64url secret) for L2 API authentication.

Polymarket Order Types We Use

The CLOB client supports multiple order styles:

  • GTC (Good-Til-Cancel) post-only maker orders.
  • GTD (Good-Til-Date) post-only maker orders with expiry.
  • FOK (Fill-Or-Kill) for strict taker execution.
  • FAK (Fill-And-Kill) used for fast arbitrage and partial-fill tolerance.

Maker-taker mode uses post-only orders on Polymarket and hedges fills with taker orders on Kalshi.

Execution and Risk Controls (Hot Path)

We do not fire orders just because there is an apparent edge. Execution requires passing multiple gates:

  • Freshness checks: stale books are rejected using per-slot timestamp ages.
  • Edge checks: required edge is min_profit + buffer before firing.
  • Risk caps: total volume, per-event exposure, and outstanding maker size.
  • Hedge timeouts: max hedge latency and max slippage thresholds.
  • Unhedged exposure: any one-leg fill permanently blocks the slot to prevent compounding risk.

Latency is measured end-to-end (parse, detect, send, ack, fill) and exported over the bridge for live monitoring.

Debugging Notes (Things That Broke)

Some of the most painful bugs were low-level:

  • Keccak-256 vs SHA3-256: using the wrong hash broke EIP-712 signatures.
  • Token ID handling: token IDs are decimal uint256 values, not hashes.
  • Maker/taker amount math: subtle unit conversions caused incorrect sizing.
  • EIP-55 checksum rules: mixed-case address logic had to be exact.
  • Nonce and expiration handling: bad defaults caused rejected orders.

These fixes are now baked into the CLOB client and are required for reliable execution.

Bridge and UI

Janus runs a Node.js bridge that:

  • Spawns the C++ core process.
  • Tails JSONL logs (opportunities, execution, prices).
  • Broadcasts health and telemetry over WebSocket.

The React UI uses Auth0 for user authentication and provides a live dashboard with charts and control toggles.

Challenges: The Price of Profitability

Hanging-Leg Risk (Execution Risk)

When Polymarket fills the maker leg, we are briefly exposed until the hedge completes.

Failure modes:

  • Kalshi API timeout
  • Rate limiting (HTTP 429)
  • Price moves during the hedge window

Mitigation:

  • Optimistic concurrency control in the hedge executor
  • Max unhedged time and max slippage enforcement
  • Emergency liquidation if hedge fails

API Disparities

  • Polymarket: Polygon blockchain (block times + finality)
  • Kalshi: centralized database (fast WebSockets/REST)

Bridging these required a queue-based event loop in a Node.js bridge to buffer Kalshi's fast events so they do not overrun slower on-chain ticks.

Micro Details (That Matter)

  • Atomic state locks: fine-grained atomic packing replaces coarse-grained mutexes.
  • WebSocket heartbeats: raw TCP-level heartbeat using ASIO to prevent silent disconnects.
  • Cache-aligned slabs: each MarketSlot fits one 64-byte cache line.

What I Learned

Latency is not just code. It is network topology. Even with optimized C++, running locally means packets still travel to exchange servers. True HFT is physical distance; the next step is moving the binary to a co-located VPS.

What Is Next

  • Move from a desktop app to a headless, daemonized service.
  • Improve hedge retry logic with exponential backoff to reduce Kalshi execution failures and ensure trades exit with profit locked in.

Built With

Share this project:

Updates