@cypher-zk/sdk/react exports a CypherProvider component and fourteen hooks that cover every read and write operation in the protocol. The query hooks are built on TanStack Query v5 and cache results automatically. The mutation hooks wrap the high-level client.actions.* methods and invalidate related caches on success so your UI stays in sync without any manual work.
All hooks must be used inside a <CypherProvider>. Calling a hook outside the provider throws immediately with a descriptive error.
Provider setup
Construct a CypherClient once - typically in a top-level component or module - and pass it to CypherProvider. Wrap CypherProvider inside your TanStack QueryClientProvider:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { CypherClient } from '@cypher-zk/sdk';
import { CypherProvider } from '@cypher-zk/sdk/react';
import { useWallet } from '@solana/wallet-adapter-react';
import { Connection } from '@solana/web3.js';
const queryClient = new QueryClient();
function Providers({ children }: { children: React.ReactNode }) {
const wallet = useWallet();
const client = useMemo(() => {
if (!wallet.publicKey) return null;
return new CypherClient({
connection: new Connection(process.env.NEXT_PUBLIC_RPC_URL!, 'confirmed'),
wallet: {
publicKey: wallet.publicKey,
signTransaction: wallet.signTransaction!,
signAllTransactions: wallet.signAllTransactions!,
},
cluster: 'mainnet',
});
}, [wallet.publicKey]);
if (!client) return <>{children}</>;
return (
<QueryClientProvider client={queryClient}>
<CypherProvider client={client}>
{children}
</CypherProvider>
</QueryClientProvider>
);
}
Accessing the client from a hook
useCypherClient() returns the CypherClient instance from context. Use it when you need direct client access inside a component, for example to build raw instructions.
import { useCypherClient } from '@cypher-zk/sdk/react';
function MarketDebug() {
const client = useCypherClient();
// client.markets.all(), client.actions.placeBet(...), etc.
}
Query hooks
Query hooks fetch on-chain state and cache it via TanStack Query. Each hook accepts an optional second opts argument that maps to any standard UseQueryOptions field - staleTime, refetchInterval, enabled, etc.
Default cache stale times are tuned to Cyphers’ update cadence:
| Hook | Default staleTime |
|---|
useGlobalState() | 30 s - protocol config changes only on admin operations |
useMarket(id) | 10 s - updates on bet, resolve, and claim |
useMarkets() | 10 s |
useUserPositions(user) | 5 s - updates on every new bet |
Override per call:
// Never go stale - invalidate manually when you know something changed
const { data: config } = useGlobalState({ staleTime: Infinity });
// Poll aggressively during a live event
const { data: market } = useMarket(id, { refetchInterval: 1_000 });
useGlobalState
Fetches the protocol GlobalState account - fee rates, accepted mint, market counter, and admin key.
import { useGlobalState } from '@cypher-zk/sdk/react';
function FeeDisplay() {
const { data, isLoading, error } = useGlobalState();
if (isLoading) return <Spinner />;
if (error) return <ErrorBanner error={error} />;
return <p>Protocol fee: {data.protocolFeeRate} bps</p>;
}
returns
UseQueryResult<GlobalStateAccount>
TanStack Query result containing the deserialized GlobalStateAccount.
useMarket
Fetches a single market by its numeric ID.
The market ID. Corresponds to MarketAccount.marketId on-chain.
import { useMarket } from '@cypher-zk/sdk/react';
function MarketDetail({ marketId }: { marketId: bigint }) {
const { data: market } = useMarket(marketId);
return <h1>{market?.inlineQuestion ?? 'Loading…'}</h1>;
}
returns
UseQueryResult<MarketAccount | null>
null when the market PDA does not exist on-chain.
useMarkets
Fetches and filters all markets. Returns the full list when called with an empty object.
An optional filter object. All fields are optional and combined with AND logic.
Limit results to markets created by this wallet.
Limit results to markets in this on-chain state (MarketState.Active = 0, etc.).
import { useMarkets } from '@cypher-zk/sdk/react';
import { MarketState } from '@cypher-zk/sdk';
function ActiveMarketList() {
const { data: markets = [] } = useMarkets({ state: MarketState.Active });
return (
<ul>
{markets.map(({ publicKey, account }) => (
<li key={publicKey.toBase58()}>#{account.marketId.toString()}</li>
))}
</ul>
);
}
returns
UseQueryResult<{ publicKey: PublicKey; account: MarketAccount }[]>
Array of market entries, each with its on-chain address and decoded account data.
useUserPositions
Fetches every position held by a user across all markets.
The wallet address to look up.
import { useUserPositions } from '@cypher-zk/sdk/react';
import { useWallet } from '@solana/wallet-adapter-react';
function MyPositions() {
const { publicKey } = useWallet();
const { data: positions = [] } = useUserPositions(publicKey ?? undefined);
return <p>{positions.length} open position(s)</p>;
}
returns
UseQueryResult<{ publicKey: PublicKey; account: EncryptedPositionAccount }[]>
All positions the user holds. Empty array when the user has no bets.
usePosition
Fetches a single position for a specific (market, user, betIndex) tuple.
The user’s wallet address.
Defaults to 0n. Increment to fetch additional bets the user placed on the same market.
import { usePosition } from '@cypher-zk/sdk/react';
function PositionCard({ market, user }: { market: PublicKey; user: PublicKey }) {
const { data: position } = usePosition(market, user);
if (!position) return <p>No position found.</p>;
return <p>Claimed: {position.claimed ? 'Yes' : 'No'}</p>;
}
returns
UseQueryResult<EncryptedPositionAccount | null>
null when no position PDA exists for the given tuple.
useMarketEvents
Subscribes to the protocol’s real-time event stream over WebSocket and returns an accumulating array of CypherEvent objects.
useMarketEvents() requires an active WebSocket connection. It will not work with HTTP-only RPC providers. Ensure your Connection is initialized with a wss:// endpoint or a provider that supports WebSocket subscriptions.
import { useMarketEvents } from '@cypher-zk/sdk/react';
function EventFeed() {
const events = useMarketEvents();
return (
<ul>
{events.map((ev, i) => (
<li key={i}>{ev.name} - market {ev.data.market?.toBase58()}</li>
))}
</ul>
);
}
Accumulated array of parsed events since the component mounted. Events are prepended (newest first).
Mutation hooks
Mutation hooks wrap client.actions.* and return a standard TanStack Query UseMutationResult. Call .mutate(variables) or .mutateAsync(variables) to trigger the transaction.
Every mutation hook automatically invalidates the relevant query caches on success. You do not need to call queryClient.invalidateQueries manually unless you bypass these hooks and send instructions directly.
The hooks that drive Arcium MPC computations (usePlaceBet, useResolveMarket, useClaimPayout, useClaimRefund) accept an optional onProgress callback that fires at each stage of the transaction lifecycle:
type ActionStage =
| 'validating'
| 'fetching-state'
| 'encrypting' // placeBet only
| 'submitting'
| 'awaiting-callback' // Arcium MPC actions only
| 'refetching'
| 'done';
usePlaceBet
Places a private bet on a market. The bet amount and side are Rescue-encrypted before the transaction is submitted.
Invalidates: marketKeys.detail(market), positionKeys.byUser(user)
import { usePlaceBet } from '@cypher-zk/sdk/react';
function BetButton({ market, side, amount }) {
const placeBet = usePlaceBet();
return (
<button
onClick={() =>
placeBet.mutate({
market,
side, // 0 = No / first outcome, 1 = Yes / second outcome, etc.
amount, // bigint, USDC lamports (min MIN_BET_USDC = 1_000_000n)
onProgress: (ev) => console.log(ev.stage, ev.message),
})
}
disabled={placeBet.isPending}
>
{placeBet.isPending ? 'Placing…' : 'Place Bet'}
</button>
);
}
useCreateMarket
Creates a new YesNo prediction market and locks the creator bond.
Invalidates: marketKeys.all()
import { useCreateMarket } from '@cypher-zk/sdk/react';
function CreateMarketForm() {
const createMarket = useCreateMarket();
const handleSubmit = () => {
createMarket.mutate({
question: 'Will ETH hit $10k in 2025?',
closeTime: BigInt(Math.floor(Date.now() / 1000) + 7 * 24 * 3600),
category: 3, // Tech
bondAmount: 20_000_000n, // $20 USDC (minimum)
challengePeriod: 86_400n, // 24 hours
});
};
return <button onClick={handleSubmit}>Create Market</button>;
}
useResolveMarket
Resolves a market via Arcium MPC. Only the market’s resolver wallet can call this successfully.
Invalidates: marketKeys.detail(market)
import { useResolveMarket } from '@cypher-zk/sdk/react';
function ResolveButton({ marketId, outcome }) {
const resolveMarket = useResolveMarket();
return (
<button onClick={() => resolveMarket.mutate({ marketId, outcomeValue: outcome })}>
Resolve Market
</button>
);
}
useClaimPayout
Claims the winning payout for a resolved market. Any user with a winning position can call this.
Invalidates: positionKeys.byUser(user), marketKeys.detail(market)
import { useClaimPayout } from '@cypher-zk/sdk/react';
function ClaimButton({ marketId }) {
const claimPayout = useClaimPayout();
return (
<button onClick={() => claimPayout.mutate({ marketId })}>
Claim Winnings
</button>
);
}
useClaimRefund
Claims a refund when a market has passed its resolution deadline without resolving. Any user with an open position can call this.
Invalidates: positionKeys.byUser(user), marketKeys.detail(market)
import { useClaimRefund } from '@cypher-zk/sdk/react';
function RefundButton({ marketId }) {
const claimRefund = useClaimRefund();
return (
<button onClick={() => claimRefund.mutate({ marketId })}>
Claim Refund
</button>
);
}
useCancelMarket
Cancels an Active market that has zero bets. Returns the creator bond.
Invalidates: marketKeys.all()
import { useCancelMarket } from '@cypher-zk/sdk/react';
import { cancelEligibility } from '@cypher-zk/sdk';
function CancelButton({ market }) {
const cancelMarket = useCancelMarket();
const { ok, reason } = cancelEligibility(market);
return (
<button
disabled={!ok}
title={reason ?? undefined}
onClick={() => cancelMarket.mutate({ marketId: market.marketId })}
>
Cancel Market
</button>
);
}
useFlagResolution v0.2+
Flags a pending resolution as disputed during the challenge window. Anyone can call this.
Invalidates: marketKeys.detail(market)
import { useFlagResolution } from '@cypher-zk/sdk/react';
function FlagButton({ marketId }) {
const flagResolution = useFlagResolution();
return (
<button onClick={() => flagResolution.mutate({ marketId })}>
Flag Resolution
</button>
);
}
useFinalizeResolution v0.2+
Finalizes a resolution once the challenge window has elapsed without a dispute. Anyone can call this.
Invalidates: marketKeys.detail(market)
import { useFinalizeResolution } from '@cypher-zk/sdk/react';
function FinalizeButton({ marketId }) {
const finalizeResolution = useFinalizeResolution();
return (
<button onClick={() => finalizeResolution.mutate({ marketId })}>
Finalize Resolution
</button>
);
}
useAdminOverrideResolution v0.2+
Allows the Cyphers admin to override the outcome of a disputed market. Requires the admin wallet.
Invalidates: marketKeys.detail(market)
import { useAdminOverrideResolution } from '@cypher-zk/sdk/react';
function AdminOverridePanel({ marketId }) {
const adminOverride = useAdminOverrideResolution();
return (
<button onClick={() => adminOverride.mutate({ marketId, outcomeValue: 1 })}>
Override to YES
</button>
);
}
Query key factories
Use query key factories when you need manual cache control - for example after sending a raw instruction without going through a mutation hook, or when implementing optimistic updates.
Import them from @cypher-zk/sdk/react:
import {
globalStateKeys,
marketKeys,
positionKeys,
} from '@cypher-zk/sdk/react';
import { useQueryClient } from '@tanstack/react-query';
const qc = useQueryClient();
// After manually sending an instruction that affects a market:
await qc.invalidateQueries({ queryKey: marketKeys.detail(marketId) });
await qc.invalidateQueries({ queryKey: positionKeys.byUser(userPublicKey) });
| Factory | Signature | Invalidates |
|---|
globalStateKeys.all() | () => QueryKey | All global state queries |
marketKeys.all() | () => QueryKey | All market list queries |
marketKeys.detail(id) | (bigint | number) => QueryKey | Single market query |
positionKeys.byUser(pubkey) | (PublicKey) => QueryKey | All positions for a user |
positionKeys.forMarket(pubkey) | (PublicKey) => QueryKey | All positions on a market |
positionKeys.forPair(market, user, betIndex?) | (PublicKey, PublicKey, bigint?) => QueryKey | Specific position tuple |
Optimistic updates
For perceived snappiness, update the market bet count optimistically before the transaction confirms. Always roll back on error:
import { usePlaceBet, marketKeys } from '@cypher-zk/sdk/react';
import { useQueryClient } from '@tanstack/react-query';
function BetButton({ marketId, ...betParams }) {
const qc = useQueryClient();
const placeBet = usePlaceBet({
onMutate: async ({ market }) => {
await qc.cancelQueries({ queryKey: marketKeys.detail(market) });
const previous = qc.getQueryData(marketKeys.detail(market));
qc.setQueryData(marketKeys.detail(market), (m) =>
m ? { ...m, totalBetsCount: m.totalBetsCount + 1n } : m
);
return { previous };
},
onError: (_err, { market }, ctx) => {
if (ctx) qc.setQueryData(marketKeys.detail(market), ctx.previous);
},
});
return <button onClick={() => placeBet.mutate(betParams)}>Bet</button>;
}
Do not optimistically update revealedPool0–revealedPool3. These fields hold MPC-computed encrypted totals that only become correct after the Arcium callback finalizes. Any value you set will be overwritten - or worse, cause a stale-data bug if the callback is delayed.
Suspense mode
The SDK hooks do not ship a built-in suspense variant. Use useSuspenseQuery from TanStack Query directly with the key factories:
import { useSuspenseQuery } from '@tanstack/react-query';
import { useCypherClient, globalStateKeys } from '@cypher-zk/sdk/react';
function ProtocolStats() {
const client = useCypherClient();
const { data } = useSuspenseQuery({
queryKey: globalStateKeys.all(),
queryFn: () => client.globalState.fetch(),
});
// data is non-nullable - the Suspense boundary handles the loading state
return <p>Markets created: {data.marketCounter.toString()}</p>;
}