@openpredict/metapredict-widgets
v0.1.3
Published
Embeddable React widgets for the metapredict prediction market — markets feed, predict / fund / claim lifecycle drop-in.
Maintainers
Readme
@openpredict/metapredict-widgets
Embeddable React widgets for the metapredict prediction market. Drop the components into any React app and get the same live markets metapredict.xyz renders — markets feed, single-market lifecycle widget, price chart, claims list, history.
Pre-mainnet. Defaults connect to Solana devnet and the production Metapredict API. Mount the
<NetworkBadge>to remind users.
Install
pnpm add @openpredict/metapredict-widgets
# or: npm i @openpredict/metapredict-widgets
# or: yarn add @openpredict/metapredict-widgetsPeer deps: react@>=18 <20, react-dom. Everything else (@openpredict/metapredict-sdk, @solana/kit, @solana/react-hooks, @solana-program/token, lightweight-charts, vaul, zustand, clsx, tailwind-merge) is bundled.
Every read + write flows through @openpredict/metapredict-sdk — the public HTTP API at www.metapredict.xyz/api/*. No tanstack-query closure, no Convex client, no on-chain codama deep imports.
Quick start
"use client";
import { MetapredictProvider, MarketsFeed } from "@openpredict/metapredict-widgets";
import "@openpredict/metapredict-widgets/styles.css";
export default function App() {
return (
<MetapredictProvider>
<MarketsFeed onSelect={(slug) => console.log("picked", slug)} />
</MetapredictProvider>
);
}No config needed — MetapredictProvider defaults to the live Metapredict API and Solana devnet RPC.
For a full reference app see apps/sdk-demo/ in the repo (Next.js 16 / React 19, ships zero env config).
Provider
<MetapredictProvider> mounts (outside-in):
ResolvedConfigContext— exposes the resolved API/RPC/cluster config to the widget tree.KitSolanaProvider—@solana/react-hookswith wallet-standard auto-discovery (Phantom, Solflare, Backpack, …) whenwalletModeis"internal".<SDKProvider>— holds theMetapredictClientused for all reads, subscriptions, and tx-builds.<div class="mpx-root">— theme + style scope.<PredictFlowHost />— singleton drawer host shared across allMarketWidgetinstances on the page.
<MetapredictProvider config={{
apiUrl?: "https://metapredict.xyz/api", // optional, prod default
apiKey?: "mp_pk_anon_xxx", // optional bearer for higher rate limits
rpcUrl?: "https://api.devnet.solana.com", // optional; SPL balance reads + wallet signing
cluster?: "devnet" | "mainnet" | "testnet" | "localnet", // inferred if omitted
theme?: { cssVars: { "--mpx-color-accent": "#facc15" } }, // optional
walletMode?: "internal" | "external", // "internal" (default) mounts our wallet provider; "external" assumes the host already mounted KitSolanaProvider above.
}}>
{children}
</MetapredictProvider>The host does not need to mount tanstack-query or Solana providers itself. walletMode: "external" is the one opt-out: skip the widget's KitSolanaProvider and the widget will read the active wallet via useWalletConnection() from the host's tree.
Components
<MarketsFeed>
Lists active markets from the public API. The feed polls every 30s and paginates through the full topic catalog before filtering active topics.
<MarketsFeed
onSelect={(slug) => /* ... */}
renderItem={(market) => /* optional override */}
headerSlot={/* optional */}
footerSlot={/* optional */}
showCountdown={true} // default
/><MarketWidget>
Single-market lifecycle drop-in: question + pools + Predict YES/NO + wallet button. Opens connect/fund/predict drawers via the singleton predict-flow store.
<MarketWidget
topicSlug="btc-15m"
defaultSide?: "yes" | "no"
headerSlot?: ReactNode
showCountdown?: boolean
/><MarketChart>
Live price history rendered with lightweight-charts. Reads topic.streamSlug and subscribes to api.streams.getSamplesInRange. Non-crypto markets render a clean empty state (no fake series).
<MarketChart
topicSlug="btc-15m"
windowMinutes?: 60 // default
height?: 240 // default
/><ClaimsList>
Every prediction the connected wallet can still claim. One-click claim per row via the useClaim hook.
<ClaimsList
walletPubkey?: string // override; defaults to connected wallet
onSelectMarket?: (slug: string) => void
/><PositionsHistory>
Full prediction timeline for the wallet — active, won, lost, tie, claimed, pending, failed. Filter tabs (All / Active / Won / Lost), status pill per row, inline Claim button on winning rows, link to the on-chain tx via Solana Explorer (cluster-aware).
<PositionsHistory
walletPubkey?: string
onSelectMarket?: (slug: string) => void
limit?: 50
/><AddFunds>
Inline funding UI — buys PAPER with devSOL via the hoisted fluid swap_single instruction. Live oracle quote + slippage floor. Mounted automatically inside <MarketWidget>'s fund drawer; exposed standalone for advanced layouts.
<AddFunds onFunded={() => /* called when swap confirms */} /><NetworkBadge>
Tiny pill showing the current cluster (DEVNET / MAINNET / TESTNET / LOCALNET).
<NetworkBadge hideOnMainnet={true} /><WidgetWalletButton>
Connect / disconnect via wallet-standard auto-discovery. Used inside <MarketWidget>; exposed standalone for header bars.
<WidgetWalletButton /><PredictDrawer>
Pure presentational drawer — owns no data hooks. Driven by the singleton predict-flow store. Exposed for advanced layouts that want to reuse the visual but own the data flow.
Hooks
| Hook | Returns | Notes |
|---|---|---|
| useMarketsFeed() | { data, isLoading, error } | Active topics from the public API. |
| useMarket(topicSlug) | { data: { topic, round } \| null } | Single topic + its current round. |
| useUserActivity({ userPubkey, status, limit }) | UserActivityRow[] | Prediction history joined with rounds + topics. status: "active" \| "claimed" \| "all". |
| useStreamHistory({ streamSlug, fromMs?, toMs?, limit? }) | { points: { time, value }[], isLoading } | Deduped, sorted, lightweight-charts-ready. |
| useStreamLatest(streamSlug) | Latest tick | Useful as a chart overlay marker. |
| useTokenBalance({ mint, decimals?, refreshIntervalMs? }) | { balance } | SPL balance via getTokenAccountBalance. null while disconnected, 0 if ATA missing. |
| useSigner() | Stable kit TransactionSigner for the active wallet. | |
| useWidgetWallet() | { publicKey, connected, status, isReady } | |
| usePredictTransaction() | tanstack mutation | Builds predict ix, derives next nonce, sends + confirms. |
| usePaperSwap() + quotePaperOut({ solIn, … }) | devSOL → PAPER swap mutation + pure quote fn | |
| useClaim() | tanstack mutation | Claim a winning prediction. |
| useWidgetConfig() | { rpcUrl, convexUrl, cluster } | Resolved provider config. |
Theming
Override CSS variables on .mpx-root via the theme.cssVars prop. Three brand-sampled themes ship built in (JUPITER_THEME, POLYMARKET_THEME, KALSHI_THEME); custom themes are a flat Record<string, string>.
import {
MetapredictProvider,
JUPITER_THEME,
BUILT_IN_THEMES,
} from "@openpredict/metapredict-widgets";
<MetapredictProvider config={{
theme: {
cssVars: {
"--mpx-color-bg": "#0a0a0a",
"--mpx-color-fg": "#fafafa",
"--mpx-color-accent": "#facc15",
"--mpx-radius": "0px",
},
},
}} />
// or pick a built-in:
<MetapredictProvider config={{ theme: JUPITER_THEME }} />Theme prop changes hot-swap CSS-var values without recreating the SDK/Solana clients — safe to bind to per-user state.
Token vocabulary:
- Colors:
--mpx-color-{bg,card,fg,muted,border,accent,yes,no,tie,error,warn} - Radii:
--mpx-radius,--mpx-radius-sm,--mpx-radius-pill,--mpx-radius-button - Spacing:
--mpx-spacing,--mpx-card-padding - Fonts:
--mpx-font-{mono,sans,display},--mpx-font-size,--mpx-font-size-lg - Buttons:
--mpx-button-{yes,no,primary}-{bg,fg,shadow},--mpx-button-padding-{y,x},--mpx-button-font-{size,weight}
Errors
import { WidgetTxError, isWidgetTxError } from "@openpredict/metapredict-widgets";
try {
await predict.mutateAsync({ ... });
} catch (e) {
if (isWidgetTxError(e) && e.code === "no-signer") {
/* user-facing: not connected */
}
}What's bundled
Bundled: @openpredict/metapredict-sdk (the public HTTP API client), @solana/kit, @solana/react-hooks, @solana-program/token (SPL ATA derivation for useTokenBalance), vaul, lightweight-charts, lucide-react, zustand, clsx, tailwind-merge.
Not bundled: @coral-xyz/anchor, @solana/web3.js, convex, @tanstack/react-query, the codama-rendered on-chain SDK, fluid swap_single. The widget package consumes the HTTP API and signs locally with the user's wallet — none of the on-chain ix builders are pulled into the browser bundle.
Bundle size: ~89KB ESM / ~99KB CJS (down from ~200KB pre-API-pivot — convex + tanstack-query + codama were doing the heavy lifting).
Network
Defaults: API at https://metapredict.xyz/api + Solana devnet RPC for SPL balance reads + wallet signing. Override per-instance via the provider config prop. Real on-chain markets, real PAPER token, real fluid swap pool. Pre-mainnet — use <NetworkBadge> to remind users.
How it works under the hood
- Reads:
mp.listTopics,mp.getTopic,mp.getActiveRound,mp.getChart,mp.getUserActivity,mp.listClaimables— all plain HTTPGETs against/*. - Live updates:
mp.subscribe.activeRound,mp.subscribe.userPredictions,mp.subscribe.streamPrice— SSE streams with auto-reconnect / exponential backoff inside the SDK. - Writes:
mp.buildPredictTx/mp.buildClaimTx/mp.buildSwapTxreturn base64-encoded transactions with blockhash + accounts already wired by the server. The widget decodes the bytes, has the connected wallet add its signature viamodifyAndSignTransactions, re-encodes, and POSTs tomp.submitTx(/tx/submit). No local RPC submit, no nonce probing, no PDA derivation in the browser.
License
Apache-2.0. See LICENSE for the full text.
