Open source / MIT licensed

Prediction market
components for React.

Drop-in shadcn components and hooks for building on Hyperliquid HIP-4. Wallet agnostic. Builder fee ready. Install individually.

Install a component

$ npx shadcn@latest add https://ui.purrdict.xyz/r/market-card.json
11 components 11 hooks wagmi + shadcn/ui

Components

UI primitives for prediction market interfaces. Each installs independently with its dependencies.

Market Card

component

Display card for HIP-4 prediction markets with 4 variants: "event" (Yes/No), "recurring" (Up/Down with asset logo), "named-binary" (custom side names), "question" (multi-outcome). Pass market object from useMarkets() and yesMid from mids map. Key props: market (Market), yesMid (number), volume? (number), variant? (MarketVariant), onClick? (callback).

BTC

Bitcoin 15 Min Up or Down?

Target: $87,450

65%
Live
2h 15m 30s
Usage
import { MarketCard } from "@/components/hip4/market-card"
import { useMarkets } from "@/hooks/hip4/use-markets"

function MarketList() {
  // Variant auto-detected from market.description
  // Prices auto-resolved from mids map
  const { markets, mids } = useMarkets()

  return (
    <div className="grid grid-cols-3 gap-4">
      {markets.map((m) => (
        <MarketCard
          key={m.yesCoin}
          market={m}
          mids={mids}
          onSideClick={(side) =>
            router.push(`/market/${m.yesCoin}?side=${side}`)
          }
        />
      ))}
    </div>
  )
}
Props
Prop Type Description
variant MarketVariant "event" | "recurring" | "named-binary" | "question". Default: "event"
market Market Market object — required for event/recurring variants
yesMid number | string Yes mid price (0-1)
noMid number | string No mid price. Defaults to 1 - yesMid
volume number 24h volume in USDH
underlying string Asset symbol for recurring variant (e.g. "BTC")
title string Card title for named-binary variant
sides NamedSide[] Named sides for named-binary variant ({name, avatarUrl?})
questionName string Question title for question variant
outcomes Outcome[] Outcomes for question variant ({name, yesPx})
onClick () => void Click handler
$ npx shadcn@latest add https://ui.purrdict.xyz/r/market-card.json
Dependencies: @purrdict/hip4

Live Price Chart

component

Canvas-based real-time chart for the underlying asset's perp price (what the prediction market resolves against). Feed with useUnderlyingPrice(symbol) which returns { prices, currentPrice }. Key props: symbol (string), prices (PricePoint[]), currentPrice (number), targetPrice? (number, the strike price), color? (string, defaults to asset brand color).

BTCBTC
Usage
import { LivePriceChart } from "@/components/hip4/live-price-chart"
import { useMarkets } from "@/hooks/hip4/use-markets"

function PriceChart({ symbol, targetPrice }: {
  symbol: string
  targetPrice: number
}) {
  // Hook auto-populates prices via WebSocket
  const { mids } = useMarkets()
  const [prices, setPrices] = useState<PricePoint[]>([])

  // Append new price points on every mid update
  useEffect(() => {
    const coin = `#${symbol}`
    const mid = mids[coin]
    if (mid) {
      setPrices((prev) => [
        ...prev.slice(-3600),
        { time: Math.floor(Date.now() / 1000), value: mid },
      ])
    }
  }, [mids, symbol])

  return (
    <LivePriceChart
      symbol={symbol}
      prices={prices}
      targetPrice={targetPrice}
      height={200}
    />
  )
}
Props
Prop Type Description
symbol string Underlying asset symbol (BTC, ETH, SOL, HYPE)
prices PricePoint[] Array of {time, value} price history
currentPrice number Latest live price for the dot indicator
targetPrice number Reference target price dashed line
color string Line color override. Defaults to asset brand (BTC #f7931a, ETH #627eea, SOL #9945ff, HYPE #50e3c2)
height number Chart height in px. Default: 200
theme "light" | "dark" Color scheme. Default: "dark"
$ npx shadcn@latest add https://ui.purrdict.xyz/r/live-price-chart.json

Probability Chart

component

Polymarket-style multi-line step-function probability chart for multi-outcome question markets (3+ outcomes). NOT for binary markets — use ProbabilityBar instead. Feed with useProbabilityHistory(questionMarkets) which returns { series }. Key props: series (OutcomeSeries[]), height? (default 220), theme? ('dark'|'light'). Each outcome auto-assigned a color from OUTCOME_COLORS palette, overridable per series.

Akami42%
Canned Tuna28%
Otoro18%
Other12%
Usage
import { ProbabilityChart } from "@/components/hip4/probability-chart"
import { useProbabilityHistory } from "@/hooks/hip4/use-probability-history"

// Multi-outcome question market (3+ outcomes)
function QuestionChart({ markets }: { markets: Market[] }) {
  const { series, isLoading } = useProbabilityHistory(markets)

  if (isLoading) return <Skeleton />

  return (
    <ProbabilityChart
      series={series}
      height={220}
    />
  )
}
Props
Prop Type Description
series OutcomeSeries[] Array of { id, label, color?, data, currentValue }
height number Chart height in px. Default: 220
theme "light" | "dark" Color scheme. Default: "dark"
$ npx shadcn@latest add https://ui.purrdict.xyz/r/probability-chart.json

Orderbook

component

Dual-pane L2 orderbook with color-coded depth bars, spread display, and click-to-set-price. Feed with useOrderbook(coin) which returns { bids, asks }. Key props: coin (string), bids (BookLevel[]), asks (BookLevel[]), depth? (number), onPriceClick? (price => void). Component calculates spread/mid internally.

BTC Up/Down — OrderbookYes side
PriceSizeTotal
0.68005034.0
0.675011074.3
0.67007550.3
0.660014092.4
0.65509562.2
Spread0.0050(0.76%)
0.650012078.0
0.6450200129.0
0.64008554.4
0.635015095.3
0.63006541.0
Usage
import { Orderbook } from "@/components/hip4/orderbook"
import { useOrderbook } from "@/hooks/hip4/use-orderbook"

function MarketOrderbook({ coin }: { coin: string }) {
  const { bids, asks } = useOrderbook(undefined, coin)

  return (
    <Orderbook
      coin={coin}
      bids={bids}
      asks={asks}
      depth={10}
      onPriceClick={(px) => setLimitPrice(px)}
    />
  )
}
Props
Prop Type Description
coin string Coin identifier for the outcome
bids BookLevel[] Bid levels sorted by price desc
asks BookLevel[] Ask levels sorted by price asc
depth number Levels to display. Default: 10
onPriceClick (price: number) => void Click handler for price level
$ npx shadcn@latest add https://ui.purrdict.xyz/r/orderbook.json
Dependencies: @purrdict/hip4

Recent Trades

component

Scrollable trade feed with color-coded buy/sell sides and monospace prices. Feed with useRecentTrades(coin) which returns { trades }. Key props: trades (Trade[] with side, price, size, time), maxRows? (number, default 50). Pure presentation — no data fetching.

Recent TradesBTC Up/Down
SidePriceSizeTime
Yes65.2¢4514:32
No34.8¢12014:31
Yes65.0¢8014:30
Yes64.8¢2514:29
Yes65.5¢20014:28
No35.0¢6014:27
Yes64.5¢3514:26
Yes64.9¢15014:25
Usage
import { RecentTrades } from "@/components/hip4/recent-trades"
import type { Trade } from "@/components/hip4/recent-trades"

<RecentTrades
  trades={recentTrades}
  maxRows={15}
/>
Props
Prop Type Description
trades Trade[] Array of { side, label, price, size, time }
maxRows number Max visible rows. Default: 20
$ npx shadcn@latest add https://ui.purrdict.xyz/r/recent-trades.json

Trade Form

component

Complete prediction market trade form. Pass bookData={{ bids, asks }} from useOrderbook to auto-resolve mid/bid/ask. Requires sides array from market.sides (e.g. [{name: 'Yes', coin: '#9860'}]). Connect useTrade(exchange) for order submission via onSubmit. Key props: sides, bookData?, minShares? (from useMinShares), usdhBalance? (from usePortfolio), shareBalance?, isConnected?, builder? ({address, fee}), onSubmit? (TradeSubmitParams => Promise).

Bid 64.5¢Mid 65.0¢Ask 65.5¢
AmountBalance: $1,000.00
Usage
import { TradeForm } from "@/components/hip4/trade-form"
import { useOrderbook } from "@/hooks/hip4/use-orderbook"
import { usePortfolio } from "@/hooks/hip4/use-portfolio"
import { useMinShares } from "@/hooks/hip4/use-min-shares"

function TradingPanel({ market }: { market: Market }) {
  // Hooks auto-resolve client from HIP4Provider
  const book = useOrderbook(undefined, market.sides[0].coin)
  const { balance } = usePortfolio(undefined, address)
  const { minShares } = useMinShares(undefined, market.sides[0].coin)

  return (
    <TradeForm
      sides={market.sides}
      bookData={book}
      minShares={minShares}
      usdhBalance={balance}
      isConnected={!!signer}
      onSubmit={async (params) => {
        await placeOrder(params)
      }}
    />
  )
}
Props
Prop Type Description
market Market Market object
side TradeSide "Yes" | "No" — active trade side
onSideChange (side: TradeSide) => void Called when user switches side
currentPrice number Current mid price for selected side (0-1)
minShares number Minimum order size
builder BuilderConfig Builder fee config: { address: string, fee: number }
usdhBalance number User's free USDH balance for validation
isConnected boolean Whether the wallet is connected/signed in
onTrade (params: TradeParams) => Promise<void> Trade execution callback
$ npx shadcn@latest add https://ui.purrdict.xyz/r/trade-form.json
Dependencies: @purrdict/hip4

Market Stats

component

Compact inline stats bar showing volume, trade count, and unique trader count with icons. Pure presentation — pass data directly, no hook needed. Key props: volume (number, 24h USDH), trades (number, total count), traders (number, unique count).

$42.5Kvolume
186trades
24traders
Usage
import { MarketStats } from "@/components/hip4/market-stats"

<MarketStats volume={42500} trades={186} traders={24} />
Props
Prop Type Description
volume number 24h volume in USDH
trades number Total trade count
traders number Unique trader count
$ npx shadcn@latest add https://ui.purrdict.xyz/r/market-stats.json

Probability Bar

component

Two-tone horizontal probability bar. Pure presentation — pass probabilities directly, no hook needed. Key props: yesPx (number 0-1), noPx (number 0-1), yesLabel? (string, default 'Yes'), noLabel? (string, default 'No'). Uses --success and --destructive CSS variables.

Binary market

Yes 72.0¢No 28.0¢

Recurring market

Up 58.0¢Down 42.0¢

Named sides

Hypurr 35.0¢Jeff 65.0¢
Usage
import { ProbabilityBar } from "@/components/hip4/probability-bar"

{/* Default Yes/No */}
<ProbabilityBar yesPx={0.72} noPx={0.28} />

{/* Recurring markets — Up/Down */}
<ProbabilityBar
  yesPx={0.58}
  noPx={0.42}
  yesLabel="Up"
  noLabel="Down"
/>
Props
Prop Type Description
yesPx number Yes probability (0-1)
noPx number No probability (0-1)
yesLabel string Yes side label. Default: "Yes"
noLabel string No side label. Default: "No"
$ npx shadcn@latest add https://ui.purrdict.xyz/r/probability-bar.json

Countdown

component

Segmented countdown timer with urgency colors: red (<1h), amber (<6h), normal. Two variants: 'segments' (DAYS/HRS/MINS/SECS boxes) and 'text' (inline). Key props: expiry (Date), variant? ('segments' | 'text'). Pure presentation — no hook needed.

Active market

02hrs
:
15min
:
30sec

Urgent (<5 min)

04min
:
05sec

After expiry

Settled
Usage
// Segmented countdown (DAYS / HRS / MINS / SECS with urgency colors)
import { Countdown } from "@/components/hip4/countdown"

<Countdown expiry={market.expiry} />

// Simple text countdown (legacy — "2h 15m 30s")
import { CountdownTimer } from "@/components/hip4/countdown"

<CountdownTimer
  expiry={market.expiry}
  onExpire={() => refetchMarket()}
/>
Props
Prop Type Description
expiry Date UTC expiry date
$ npx shadcn@latest add https://ui.purrdict.xyz/r/countdown.json

Position Card

component

Displays a user's position with shares, current value, average entry, and unrealized P&L. Feed with usePortfolio(address) for positions array, and mids from useMarkets for currentPrice. Key props: coin (string), shares (number), currentPrice (number), avgEntry? (number), onSell? (coin => void).

BTC UpYes
100 shares

Avg Entry

45¢

Current Price

65¢

Value

$65.00

P&L

+$20.00 (+44.4%)

entry: 45¢100¢
Usage
import { PositionCard } from "@/components/hip4/position-card"

<PositionCard
  coin={market.yesCoin}
  shares={100}
  avgEntry={0.45}
  currentPrice={0.65}
  onSell={(coin) => openSellDialog(coin)}
/>
Props
Prop Type Description
coin string Coin identifier for the position
shares number Number of shares held
avgEntry number Average entry price. Optional — hides if unknown
currentPrice number Current mid/mark price
onSell (coin: string) => void Sell button click handler
$ npx shadcn@latest add https://ui.purrdict.xyz/r/position-card.json

Rounds Timeline

component

Polymarket-style bottom bar for navigating recurring market rounds. Shows inline tabs with result indicators (▲ above / ▼ below), a "Past" popover dropdown grouped by date, and a streak strip. Pure presentation — pass rounds data directly. Key props: rounds (Round[]), activeRoundId (number), onRoundSelect (callback), period ("15m"|"1d"), underlying? (string), visibleCount? (number, default 5).

BTC — Daily rounds

BTC

Selected: #2631live

HYPE — 15-minute rounds

HYPE

Selected: #2664live

Usage
import { RoundsTimeline } from "@/components/hip4/rounds-timeline"
import type { Round } from "@/components/hip4/rounds-timeline"

const rounds: Round[] = [
  { id: 1, expiry: "2026-03-30T09:30:00Z", result: "above", targetPrice: 67000, settlePrice: 67500 },
  { id: 2, expiry: "2026-03-29T09:30:00Z", result: "below", targetPrice: 68000, settlePrice: 66000 },
]

<RoundsTimeline
  rounds={rounds}
  activeRoundId={1}
  onRoundSelect={(round) => router.push(`/market/${round.id}`)}
  period="1d"
  underlying="BTC"
/>
Props
Prop Type Description
rounds Round[] All rounds sorted by expiry desc. Each: { id, expiry, result, settlePrice?, targetPrice }
activeRoundId number ID of the currently viewed round
onRoundSelect (round: Round) => void Called when a round tab or dropdown item is clicked
period string "15m" = time labels, "1d" = date labels
underlying string Optional asset symbol for icon (e.g. "BTC", "HYPE")
visibleCount number Inline tabs before overflow into Past dropdown. Default: 5
className string Additional Tailwind classes (e.g. sticky positioning)
$ npx shadcn@latest add https://ui.purrdict.xyz/r/rounds-timeline.json

Hooks

React hooks for market data, trading, and wallet interaction. All hooks depend on the HIP-4 SDK.

use-hip4-client

hook

Creates and caches InfoClient + SubscriptionClient from @nktkas/hyperliquid. Returns { info, sub, close }. Cached in ref — stable across rerenders. Pass { testnet: true } for testnet. Usually consumed via HIP4Provider rather than directly.

useHIP4Client(opts?: { testnet?: boolean }) HIP4Client
Usage
const client = useHIP4Client({ testnet: true })

// Or use HIP4Provider for zero prop-drilling:
// <HIP4Provider testnet={true}><App /></HIP4Provider>
// Then all hooks read from context automatically.
$ npx shadcn@latest add https://ui.purrdict.xyz/r/use-hip4-client.json

hip4-provider

hook

React context provider — wrap your app in <HIP4Provider testnet={false}> and all hooks (useMarkets, useOrderbook, etc.) auto-resolve the client. No prop-drilling needed. Key props: testnet? (boolean, default false), children.

<HIP4Provider testnet={boolean}> React.ReactElement
Usage
// Wrap your app once — all hooks auto-resolve client
import { HIP4Provider } from "@/hooks/hip4/hip4-provider"

<HIP4Provider testnet={true}>
  <App />
</HIP4Provider>

// Then in any component:
const { markets } = useMarkets()  // no client arg needed
const { bids } = useOrderbook(market.yesCoin)
$ npx shadcn@latest add https://ui.purrdict.xyz/r/hip4-provider.json

use-hip4-signer

hook

Adapts any wagmi wallet to { address, walletClient } for use with ExchangeClient. Returns null when disconnected. Works with MetaMask, WalletConnect, Privy, Coinbase Wallet. Pass walletClient to new ExchangeClient({ transport, wallet: signer.walletClient }).

useHIP4Signer() HIP4Signer | null
Usage
const signer = useHIP4Signer()
// null when wallet disconnected
$ npx shadcn@latest add https://ui.purrdict.xyz/r/use-hip4-signer.json

use-markets

hook

Discovers all active HIP-4 markets and subscribes to live mid prices via WebSocket. Returns { markets: Market[], mids: Record<string, string>, isLoading, error }. Feed markets to MarketCard and mids[coin] for yesMid. Stable markets array (useMemo) prevents downstream rerenders.

useMarkets(client?: HIP4Client) { markets, mids, isLoading, error }
Usage
// Explicit client
const { markets, mids } = useMarkets(client)

// Or with HIP4Provider (no client needed)
const { markets, mids } = useMarkets()
$ npx shadcn@latest add https://ui.purrdict.xyz/r/use-markets.json

use-orderbook

hook

Subscribes to the L2 orderbook for a prediction market coin via WebSocket. Returns { bids: BookLevel[], asks: BookLevel[], spread: number|null, midPrice: number|null, isLoading, error }. Pass { bids, asks } as bookData to TradeForm. Call as useOrderbook(coin) with HIP4Provider or useOrderbook(client, coin).

useOrderbook(client?: HIP4Client, coin: string) { bids, asks, spread, midPrice }
Usage
// Explicit client
const { bids, asks } = useOrderbook(client, market.yesCoin)

// Or with HIP4Provider
const { bids, asks } = useOrderbook(undefined, market.yesCoin)
$ npx shadcn@latest add https://ui.purrdict.xyz/r/use-orderbook.json

use-portfolio

hook

Fetches USDH balance, outcome token positions, and open orders for a wallet address. Returns { usdh: TokenBalance|null, positions: Position[], openOrders: OpenOrder[], isLoading, error, refresh() }. Pass usdh.total to TradeForm as usdhBalance. Call refresh() after placing an order.

usePortfolio(client?: HIP4Client, address: string) { balance, positions, orders, refresh }
Usage
const { balance, positions } = usePortfolio(
  client,
  address
)
$ npx shadcn@latest add https://ui.purrdict.xyz/r/use-portfolio.json

use-trade

hook

Place and cancel HIP-4 prediction market orders. Takes ExchangeClient (NOT HIP4Client) — create with new ExchangeClient({ transport, wallet }). Returns { buy(params), sell(params), cancel(asset, oid), isSubmitting, lastResult, error }. TradeParams: { coin, asset, shares, price, tif?, markPx?, builder? }.

useTrade(client?: HIP4Client, signer: HIP4Signer) { buy, sell, cancel, isSubmitting, error }
Usage
const { buy, sell, cancel } = useTrade(client, signer)

await buy({
  coin: market.yesCoin,
  shares: 20,
  price: 0.55,
})
$ npx shadcn@latest add https://ui.purrdict.xyz/r/use-trade.json

use-min-shares

hook

Computes the minimum order size for a prediction market coin. Fetches mark price from spotMetaAndAssetCtxs, applies formula: ceil(10/max(min(markPx,1-markPx),0.01)). Returns { minShares: number, markPx: number|null, isLoading, error }. Pass minShares to TradeForm.

useMinShares(client?: HIP4Client, coin: string) { minShares, markPrice, isLoading }
Usage
const { minShares } = useMinShares(
  client,
  market.yesCoin
)
$ npx shadcn@latest add https://ui.purrdict.xyz/r/use-min-shares.json

use-recent-trades

hook

Subscribes to recent trades for a prediction market coin. Returns { trades: RecentTrade[], isLoading, error }. Each trade has { side: 'B'|'S', price: number, size: number, time: number }. Fetches initial history via HTTP then appends live trades via WebSocket. Pass trades to RecentTrades component.

useRecentTrades(coin: string) { trades, isLoading, error }
Usage
import { useRecentTrades } from "@/hooks/hip4/use-recent-trades"

const { trades } = useRecentTrades(market.yesCoin)
<RecentTrades trades={trades} />
$ npx shadcn@latest add https://ui.purrdict.xyz/r/use-recent-trades.json

use-underlying-price

hook

Subscribes to the underlying asset's perp price with candleSnapshot history. Returns { prices, currentPrice } ready for LivePriceChart. Handles history prefetch + live WS updates + deduplication.

useUnderlyingPrice(symbol: string, opts?) { prices, currentPrice, isLoading }
Usage
import { useUnderlyingPrice } from "@/hooks/hip4/use-underlying-price"

// Feeds LivePriceChart with underlying asset perp price
const { prices, currentPrice } = useUnderlyingPrice("BTC", {
  historyMinutes: 60,
})

<LivePriceChart
  symbol="BTC"
  prices={prices}
  currentPrice={currentPrice}
  targetPrice={market.targetPrice}
/>
$ npx shadcn@latest add https://ui.purrdict.xyz/r/use-underlying-price.json

use-probability-history

hook

Subscribes to probability history for all outcomes in a multi-outcome question market. Fetches candleSnapshot history + streams live allMids updates. Returns { series: OutcomeSeries[], isLoading, error }. Pass series directly to <ProbabilityChart series={...} />. Accepts markets array from useMarkets().

useProbabilityHistory(markets: Market[]) { series, isLoading, error }
Usage
import { useProbabilityHistory } from "@/hooks/hip4/use-probability-history"

// Multi-outcome question markets (3+ outcomes)
const { series, isLoading } = useProbabilityHistory(questionMarkets)

<ProbabilityChart series={series} height={220} />
$ npx shadcn@latest add https://ui.purrdict.xyz/r/use-probability-history.json

Utilities

Shared formatting and helper functions used by components.

hip4-format

lib

Price, USDH, countdown, and period formatters for HIP-4 prediction market data. Exports formatMidPrice(px, style?), formatUsdh(amount), formatTargetPrice(price), formatPeriod(market), parseMid(midStr). Used internally by MarketCard, Orderbook, PositionCard, and MarketStats.

$ npx shadcn@latest add https://ui.purrdict.xyz/r/hip4-format.json

Quick Start

Three steps to add prediction market components to your app.

1

Install the SDK

bun add @purrdict/hip4 viem wagmi
2

Add a component

npx shadcn@latest add https://ui.purrdict.xyz/r/market-card.json

This installs the component, its dependencies (countdown, format utils), and adds the npm package.

3

Use it

import { MarketCard } from "@/components/hip4/market-card"
import { useMarkets, useMarket } from "@purrdict/hip4-ui"

function MarketList() {
  const { markets } = useMarkets()

  return (
    <div className="grid grid-cols-3 gap-4">
      {markets.map((m) => (
        <MarketRow key={m.yesCoin} market={m} />
      ))}
    </div>
  )
}

// Each row only rerenders when ITS market price changes
function MarketRow({ market }: { market: Market }) {
  const { yesMid } = useMarket(market)

  return (
    <MarketCard
      market={market}
      yesMid={yesMid}
      onSideClick={(side) =>
        router.push(`/market/${market.yesCoin}?side=${side}`)
      }
    />
  )
}

Or with zero prop-drilling

// Wrap once in layout — all hooks auto-resolve the client
import { HIP4Provider } from "@purrdict/hip4-ui"

export default function Layout({ children }) {
  return (
    <HIP4Provider testnet={true}>
      {children}
    </HIP4Provider>
  )
}

// In any component — no client, no prop drilling:
import { useMarket } from "@purrdict/hip4-ui"

function MarketDetail({ market }: { market: Market }) {
  // Only rerenders for THIS market
  const { yesMid, noMid, minShares } = useMarket(market)
  // ...
}

Named Registry (optional)

Configure a named registry for shorter install commands: npx shadcn@latest add hip4:market-card

// components.json — add the hip4 registry
{
  "registries": {
    "hip4": {
      "url": "https://ui.purrdict.xyz/r"
    }
  }
}

// Then install components by name:
// npx shadcn@latest add hip4:market-card