PolyMatrix is a no-auth, no-mock crypto prediction dashboard focused on real live Polymarket up/down markets and real live Coinbase spot data.
This repo is the source of truth for:
- what the app does,
- how the live data is pulled,
- why certain tradeoffs were made,
- and how the current sniper-style view logic works.
The app tracks live crypto up/down markets for:
- BTC
- ETH
- SOL
- XRP
Across these Polymarket timeframes:
- 5 minute
- 15 minute
- 1 hour
It gives you:
- a matrix-first dashboard,
- a dedicated chart lab view,
- and a sniper view that ranks current setups using price context + momentum.
There is no auth, no fake data, no simulation, and no database dependency for runtime market data.
Primary scan view. This is the centerpiece of the product.
Includes:
- compact header
- live status pills
- full up/down market matrix
- color logic toggle:
Classic,Sniper,Mono - selected market detail panel
- live spot chart panel
The chart becomes the hero.
Includes:
- live Coinbase chart
- support / resistance context
- contract start price reference
- distance-to-support / distance-to-resistance
- distance-to-flip / distance-to-win context
- RSI + EMA trend state
This is now a column-based tactical view tied directly to the matrix.
Includes:
- cheapest Up contract in each timeframe column
- cheapest Down contract in each timeframe column
- dominant direction emphasis per timeframe column
- quick click-through into the exact asset/contract that is being highlighted
- React
- Tailwind CSS
- shadcn/ui primitives
- Recharts for chart rendering
- FastAPI
- Polymarket Gamma API for market metadata validation
- Polymarket public crypto pages as fallback discovery hints
- Polymarket CLOB book endpoint for initial top-of-book seeding
- Polymarket public market WebSocket for live contract updates
- Coinbase Advanced Trade public WebSocket for live spot prices
- Coinbase public candles REST for seeded chart history
This is the most important section.
The app does not rely on weak snapshot-only fields like the raw Gamma outcomePrices as the main source of truth for live contract prices.
Instead, the pipeline is split into discovery, initial seeding, and live updates.
We only care about:
- BTC / ETH / SOL / XRP
- 5m / 15m / 1h up-down markets
The backend route /api/markets/updown resolves these contracts using a two-layer strategy.
For 5m and 15m markets, the backend generates candidate slugs from the current UTC bucket:
btc-updown-5m-<bucket_timestamp>eth-updown-15m-<bucket_timestamp>- etc.
It checks:
- current bucket
- previous bucket
- next bucket
This handles rollover timing and small creation delays.
For 1h markets, the backend generates the current ET hourly slug, for example:
bitcoin-up-or-down-march-31-2026-9pm-etethereum-up-or-down-march-31-2026-9pm-et
It also checks previous and next hour candidates.
If deterministic slug generation alone is not enough, the backend scrapes:
https://polymarket.com/crypto/5Mhttps://polymarket.com/crypto/15Mhttps://polymarket.com/crypto/hourly
It extracts matching candidate slugs from the page HTML and uses them as fallback candidates.
Every candidate slug is validated via:
GET https://gamma-api.polymarket.com/events/slug/<slug>
The backend only accepts markets that are:
- active
- not closed
- have valid
clobTokenIds
That gives us a clean final set of 12 live target markets.
This is where the quality difference really matters.
The app does not use Gamma outcomePrices as the primary visible contract price.
Why?
Because for these short-dated crypto up/down markets, raw snapshot fields can look stale, misleading, or glitchy.
Instead, once the live market slugs are resolved, the backend fetches the CLOB order book for each contract token via:
GET https://clob.polymarket.com/book?token_id=<token_id>
For each token book:
best_bid = max(bids[].price)best_ask = min(asks[].price)
This is important because the raw arrays are not guaranteed to be top-first in the intuitive way. If you just grab the first bid/ask entry, you can get nonsense-looking values like the wrong 1-cent level.
That normalization is the key reason this version behaves much better.
So the initial displayed contract prices are seeded from:
- top-of-book bid
- top-of-book ask
- not from weak display-only snapshot fields
After initial seed, the frontend subscribes directly to:
wss://ws-subscriptions-clob.polymarket.com/ws/market
Using all current live token IDs.
The app sends:
type: "market"assets_ids: [...]custom_feature_enabled: true
And a PING heartbeat every 10 seconds.
The frontend consumes two event types for contract price quality:
Used as a reliable full-book snapshot.
Again, we normalize it with:
best_bid = max(bids)best_ask = min(asks)
Used whenever Polymarket emits direct top-of-book changes.
This becomes the cleanest live update source when available.
Because the pipeline is:
- discover the exact live contracts,
- seed real top-of-book from the CLOB,
- stream live changes from the Polymarket WebSocket,
- avoid relying on glitch-prone display snapshot fields.
That is the core pricing logic.
If you keep only one mental model from this README, keep that one.
Frontend subscribes to Coinbase Advanced Trade public WebSocket:
- ticker channel
- heartbeats channel
For:
- BTC-USD
- ETH-USD
- SOL-USD
- XRP-USD
Backend seeds the chart from Coinbase public candles REST.
This means the chart is useful immediately instead of starting from a blank line.
The chart is no longer just a side decoration.
It is tied to:
- the selected asset
- the selected contract timeframe
- support / resistance range logic
- contract start price
- distance-to-flip context
Current implementation uses recent swing highs/lows from live price history.
Window mapping:
- 5m market -> use the 15 minute running range
- 15m market -> use the 1 hour running range
- 1h market -> use the 4 hour running range
For each selected market:
support = min(low)in the mapped range windowresistance = max(high)in the mapped range window
These are shown in the chart lab and fed into sniper scoring.
Current momentum layer uses:
- RSI
- EMA(5)
- EMA(20)
Trend state:
- bullish if
price > EMA5 > EMA20 - bearish if
price < EMA5 < EMA20 - otherwise neutral
This is intentionally simple and readable for the first sniper pass.
Future candidates:
- CVD
- MACD
- range expansion
- TradingView-derived custom logic
The sniper layer is now centered on timeframe columns, not a generic global ranking.
For each timeframe column (5m, 15m, 1h), the app computes:
- Cheapest Up contract in that column
- Cheapest Down contract in that column
- Dominant side for that column using the aggregated momentum/value model
Visual behavior:
- both cheapest contracts can be surfaced
- the dominant side gets the stronger solid emphasis
- the non-dominant cheap side stays outlined / secondary
This makes sniper mode much more actionable for quick scanning because it answers:
- what is the cheapest Up here?
- what is the cheapest Down here?
- which side currently has the stronger momentum/value bias?
The underlying directional bias still uses the momentum/range model:
- bullish score = bullish setup quality ×
(1 - up_bid) - bearish score = bearish setup quality ×
(1 - down_bid)
But the UI no longer presents this as a broad generic ranking first. It presents it in a column-native PolyMatrix format.
Chart Lab now plots the selected asset with explicit target-to-beat lines for:
- 5m market
- 15m market
- 1h market
These target prices come from each active contract’s start-price threshold, so you can immediately see:
- where price currently is,
- what each timeframe needs to beat,
- and what the corresponding Up / Down contract prices are right now.
Green/red directional read.
Highlights the strongest current bullish and bearish opportunities.
Neutral terminal look with understated emphasis and less visual noise.
The app is responsive-first.
Desktop:
- dense matrix table
- wider chart layouts
Mobile:
- matrix collapses into stacked asset cards
- controls stay reachable
- views remain tab-based on a single page
The goal is to preserve utility rather than blindly shrinking desktop layouts.
This keeps the system light and fast, but it also means the app is fully dependent on public endpoint availability.
That keeps the architecture simpler now. If part 2 becomes a serious trading engine, a more stateful backend process may be worth adding.
It is intentionally interpretable first.
4. Support / resistance is based on recent swing ranges, not a complex institutional indicator stack yet
This makes the first pass easier to reason about and debug.
backend/server.py— API routesbackend/market_data.py— Polymarket discovery, CLOB seeding, Coinbase history fetch
frontend/src/components/dashboard/TerminalDashboard.jsx— page shell + view switchingfrontend/src/components/dashboard/MarketMatrix.jsx— comparison matrix + color logic modesfrontend/src/components/dashboard/SpotChartPanel.jsx— chart lab UIfrontend/src/components/dashboard/SniperBoard.jsx— sniper ranking viewfrontend/src/hooks/useMarketDiscovery.js— fetches normalized live contract listfrontend/src/hooks/usePolymarketFeed.js— live Polymarket WS updatesfrontend/src/hooks/useSpotFeed.js— live Coinbase WS quotesfrontend/src/hooks/useSpotAnalytics.js— seeded chart history for all tracked assetsfrontend/src/lib/dashboard-utils.js— shared formatting + UI helpersfrontend/src/lib/market-analytics.js— support/resistance, RSI, EMA, sniper scoring
- Add a more advanced signal engine layer
- Introduce CVD / volume-based logic
- Add market rollover awareness to smooth transitions even more
- Build part 2 bot logic on top of this signal and pricing foundation
The pricing quality of this app comes from:
- correct market discovery,
- correct CLOB book normalization,
- correct real-time websocket handling,
- and avoiding misleading shortcut fields.
That is the core edge in the current implementation.