Skip to main content
@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:
HookDefault 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.
id
bigint | number
required
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.
filter
UseMarketsFilter
An optional filter object. All fields are optional and combined with AND logic.
filter.creator
PublicKey
Limit results to markets created by this wallet.
filter.state
number
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.
user
PublicKey
required
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.
market
PublicKey
required
The market PDA.
user
PublicKey
required
The user’s wallet address.
betIndex
bigint
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>
  );
}
returns
CypherEvent[]
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) });
FactorySignatureInvalidates
globalStateKeys.all()() => QueryKeyAll global state queries
marketKeys.all()() => QueryKeyAll market list queries
marketKeys.detail(id)(bigint | number) => QueryKeySingle market query
positionKeys.byUser(pubkey)(PublicKey) => QueryKeyAll positions for a user
positionKeys.forMarket(pubkey)(PublicKey) => QueryKeyAll positions on a market
positionKeys.forPair(market, user, betIndex?)(PublicKey, PublicKey, bigint?) => QueryKeySpecific 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 revealedPool0revealedPool3. 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>;
}