Prerequisites

Before you begin, make sure you have the following installed:

ToolVersionPurpose
Node.js20+Runtime for backend and build tooling
pnpm9+Package manager (monorepo workspaces)
Docker20+Runs ClickHouse and Redis containers
Docker Composev2+Orchestrates infrastructure services
Install pnpm globally with npm install -g pnpm if you do not already have it.

Installation

Clone and install

git clone <your-repo-url>
cd resolvedmarkets
pnpm install

This installs dependencies for all three packages (backend, frontend, mcp-server) via pnpm workspaces.

Start infrastructure

The backend requires ClickHouse (time-series storage) and Redis (caching):

pnpm db:up    # docker-compose up -d
pnpm db:down  # stop infrastructure

This starts ClickHouse on ports 8123/9000 and Redis on port 6379.

Configure environment

cp packages/backend/.env.example packages/backend/.env

For local development the defaults work out of the box. You will need Clerk keys for authentication -- see the Configuration section below.

Quick Start

Run everything

pnpm dev   # starts infra + backend + frontend

Verify the setup

curl http://localhost:3001/health   # should return OK

Open http://localhost:5173 to see the frontend.

Make your first API call

Generate an API key from the /api-keys page, then:

curl -H "X-API-Key: rm_your_key_here" \
  http://localhost:3001/v1/markets/live

This returns a JSON array of active Polymarket crypto prediction markets.

Store your API key securely. It cannot be retrieved after initial generation -- only revoked and replaced.

Data Pipeline

The backend captures orderbook data from Polymarket via an event-driven pipeline:

Polymarket WS          Binance WS
     |                      |
     v                      v
CLOBCollector      CryptoPriceCollector
     |                      |
     v                      |
OrderbookManager  <---------+
     |
     v (event callback)
 SnapshotCapturer
     |
     v (batch)
 ClickHouseBatchWriter --> ClickHouse

Key components

  • MultiMarketFetcher -- Queries Polymarket Gamma API every 30s for active BTC/ETH/SOL/XRP markets across 4 timeframes (5m, 15m, 1h, 1d).
  • CLOBCollector -- Single WebSocket to Polymarket receiving book (full snapshot) and price_change (delta) events.
  • OrderbookManager -- In-memory orderbook state per token. Produces sorted snapshots with best bid/ask, mid price, spread, depth, and sequence numbers.
  • SnapshotCapturer -- Event-driven capture throttled to 50ms min interval (~20Hz/token). Deduplicates via top-5 bid/ask fingerprinting.
  • CryptoPriceCollector -- Streams BTC/ETH/SOL/XRP trades from Binance WebSocket with staleness tracking.
  • ClickHouseBatchWriter -- Batch size 800 snapshots / 500 trades, 2s flush interval, 3 retries.

Storage

ClickHouse

Primary storage for all time-series data in the polymarket database:

TablePurposeEngine
snapshots_hfOrderbook snapshots with bid/ask arrays, timestamps, sequence numbersMergeTree, partitioned by day
tradesIndividual trade eventsMergeTree, partitioned by day
market_statsPre-aggregated market statisticsMaterialized View
api_keysHashed API keys with usage trackingReplacingMergeTree
api_request_logPer-request analyticsMergeTree, 90-day TTL
user_tiersUser tier and credit trackingReplacingMergeTree
paymentsPayment transaction logMergeTree

Redis

Response caching with per-endpoint TTLs and pipeline health statistics. Redis is not used as a message queue -- data flows directly from capturers to the batch writer.

API Layer

Express server on port 3001 serving REST and WebSocket endpoints.

WebSocket authentication

Clients authenticate via message (not query string):

1. Connect to /ws/orderbook
2. Send: { "type": "auth", "apiKey": "rm_..." }
3. Receive: { "type": "auth", "status": "ok" }
4. Send: { "type": "subscribe", "crypto": "BTC" }

A 5-second timeout is enforced. Unauthenticated connections close with code 4001.

See the API Reference for full endpoint documentation with runnable examples.

Markets

Polymarket runs recurring binary prediction markets: will a cryptocurrency be at or above its opening price at the end of a time window?

Each market has two tokens -- UP and DOWN -- each with its own orderbook. Prices are inversely correlated and should sum to ~1.00.

Tracked cryptos and timeframes

Crypto5m15m1h1d
BTCYesYesYesYes
ETHYesYesYesYes
SOLYesYesYesYes
XRPYesYesYesYes

Identifiers

  • conditionId (market_id) -- Primary identifier, a hex string for each market instance.
  • tokenId -- Two per market (UP and DOWN). Used for WebSocket subscriptions.
  • slug -- Human-readable identifier, e.g. btc-updown-5m-1772448000.
SOL uses sol- for short timeframes but solana- for hourly/daily series. This inconsistency comes from Polymarket's naming.

Market discovery

The backend queries the Polymarket Gamma API:

GET https://gamma-api.polymarket.com/events?tag_id=102127&active=true&closed=false&limit=100

Markets with enableOrderBook: false are skipped (typically near resolution time).

Data Fidelity

The system captures snapshots in response to every orderbook event -- not by polling at fixed intervals. This event-driven approach means no changes are missed (subject to WebSocket delivery guarantees).

Throttling and deduplication

A 50ms minimum interval per token caps capture at ~20Hz. Unchanged states are detected via top-5 bid/ask fingerprinting and skipped.

Dual timestamps

FieldSourceMeaning
eventTimestampPolymarket WSWhen the exchange emitted the event
captureTimestampLocalWhen we processed and stored it

The delta measures full pipeline latency. Both stored as DateTime64(3, 'UTC') in ClickHouse.

Sequence numbers

Each token's orderbook maintains a monotonically increasing sequence number. Gaps indicate missed events -- check captureThrottled and captureDeduplicated counters to distinguish intentional skips from data loss.

A gap in sequence numbers does not always mean data was lost. Throttled and deduplicated captures intentionally skip numbers.

Tiers & Payments

FeatureFreePro ($29/mo)Enterprise ($99/mo)
Rate limit60/min500/min3000/min
History1 hour30 daysFull archive
WebSocketNo2 connections10 connections
MCP accessNoRead-onlyFull
API keys1520

Payments are processed via NOWPayments (cryptocurrency). Prepaid credit packs are also available from the Pricing page.

Configuration

Backend uses dotenv. Copy packages/backend/.env.example to .env.

Required variables

VariableDescription
CLICKHOUSE_HOSTClickHouse hostname (default: localhost)
CLICKHOUSE_PORTClickHouse HTTP port (default: 8123)
CLICKHOUSE_USERClickHouse username
CLICKHOUSE_PASSWORDClickHouse password
REDIS_HOSTRedis hostname (default: localhost)
REDIS_PORTRedis port (default: 6379)
PORTBackend server port (default: 3001)
CLERK_PUBLISHABLE_KEYClerk frontend key
CLERK_SECRET_KEYClerk backend secret

Optional variables

VariableDescription
NOWPAYMENTS_API_KEYNOWPayments API key for crypto payments
NOWPAYMENTS_WEBHOOK_SECRETHMAC secret for payment webhooks
HF_API_URLMCP server target API URL
HF_API_KEYMCP server API key
MCP_TRANSPORTMCP transport: stdio (default) or http
MCP_PORTMCP HTTP transport port

ClickHouse Schema

All tables live in the polymarket database. The primary table for orderbook data:

CREATE TABLE polymarket.snapshots_hf (
  market_id      String,
  token_id       String,
  token_side     Enum8('UP'=1, 'DOWN'=2),
  crypto         LowCardinality(String),
  timeframe      LowCardinality(String),
  best_bid       Float64,
  best_ask       Float64,
  mid_price      Float64,
  spread         Float64,
  bid_depth      Float64,
  ask_depth      Float64,
  bids           Array(Tuple(Float64, Float64)),
  asks           Array(Tuple(Float64, Float64)),
  crypto_price       Float64,
  crypto_price_age_ms Int32,
  event_timestamp    DateTime64(3, 'UTC'),
  capture_timestamp  DateTime64(3, 'UTC'),
  sequence_number    UInt64
) ENGINE = MergeTree()
PARTITION BY toDate(capture_timestamp)
ORDER BY (crypto, timeframe, market_id, token_side,
          capture_timestamp)