Skip to main content
placeBet is the primary way users interact with Cyphers markets. Before the transaction is submitted, the SDK generates a fresh x25519 keypair, derives a shared secret with the Arcium MXE, and encrypts both the outcome side and stake amount using RescueCipher. Only the encrypted ciphertexts land on-chain - your chosen side remains private even after settlement.
Persist userKeypair.privateKey immediately after a successful bet. This is the only key that can decrypt your position to reveal which side you bet on. There is no recovery path - the key is generated client-side and never transmitted to any server. If you lose it, your funds are still claimable, but you will not be able to pre-verify whether you are a winner before paying for the MPC computation.

placeBet

Places a single encrypted bet on a YesNo or MultiOutcome market.
const { sig, userKeypair, betIndex } = await client.actions.placeBet({
  market: marketId,
  side: 1,             // 1 = YES for YesNo; outcome index for MultiOutcome
  amount: 5_000_000,   // $5.00 USDC (6 decimals)
  onProgress: (event) => console.log(event.stage, event.message),
});

// ⚠️ Persist the private key before the function returns
await saveSecret(marketId, betIndex, userKeypair.privateKey);

Parameters

market
PublicKey
required
The market PDA to bet on. Must be in the "betting" phase (active and before lockTimestamp).
side
number
required
The outcome index to bet on. For YesNo markets: 0 = NO, 1 = YES. For MultiOutcome markets: 0 through outcomeCount - 1, matching the labels returned by getMarketOptionLabels.
amount
number
required
Stake in USDC lamports (6 decimal places). The minimum is market.minBet (typically 1_000_000 = $1). Protocol and LP fees are deducted from this amount; the remainder is your netAmount tracked on-chain.
onProgress
(event: ActionProgressEvent) => void
Optional callback invoked at each stage of the bet flow. Use this to drive progress UI - see the stage reference below.

Return value

sig
string
Solana transaction signature for the placePrivateBet instruction.
userKeypair
UserCryptoKeypair
The freshly generated x25519 keypair used to encrypt this bet. You must persist userKeypair.privateKey. Key by (market, betIndex) so you can handle multiple bets on the same market.
betIndex
number
Zero-based index of this bet within the (market, user) pair. Increments by one per bet. Needed when fetching the position PDA and when claiming.

Progress stages

onProgress is called once for each stage in order. The stage field is one of:
StageTypical durationWhat’s happening
"validating"~10 msChecks market state, amount ≥ minBet, side in range
"fetching-state"~200 ms (cached)Loads GlobalState, MXE public key, address lookup table
"encrypting"~50 msGenerates x25519 keypair, derives shared secret, RescueCipher encrypts [amount, side]
"submitting"~1–2 sSends the transaction and awaits slot confirmation
"awaiting-callback"8–30 sArcium MPC nodes run the circuit and submit the callback
"refetching"~300 msRe-fetches the position account to confirm on-chain state
"done"-Flow complete; return value is ready
The "awaiting-callback" stage involves a round-trip through the Arcium MPC cluster and typically takes 8–30 seconds on devnet. Display a spinner or streaming label during this stage so users know the network is working, not stalled.

Placing bets with progress UI

import {
  marketPhase,
  getMarketOptionLabels,
  parseCypherError,
  type ActionProgressEvent,
} from "@cypher-zk/sdk";

async function bet(
  client: CypherClient,
  marketId: PublicKey,
  question: string,
  side: number,
  amountUsdc: number,
  onStage: (e: ActionProgressEvent) => void,
) {
  const market = await client.markets.fetchByPda(marketId);

  if (marketPhase(market) !== "betting") {
    throw new Error("Market is not accepting bets");
  }

  const labels = getMarketOptionLabels(market, question);
  console.log(`Betting on "${labels[side]}" for $${amountUsdc / 1_000_000}`);

  try {
    const { sig, userKeypair, betIndex } = await client.actions.placeBet({
      market: marketId,
      side,
      amount: amountUsdc,
      onProgress: onStage,
    });

    // Immediately persist the private key
    await saveSecret(marketId, betIndex, userKeypair.privateKey);

    return { sig, betIndex };
  } catch (err) {
    const parsed = parseCypherError(err);
    throw new Error(parsed ? `${parsed.name}: ${parsed.msg}` : String(err));
  }
}

Multi-bet example (v0.8.0+)

A single user can place multiple bets on the same market. Each bet gets its own betIndex starting from 0. Key your saved private key by both market and bet index:
// First bet on this market
const result1 = await client.actions.placeBet({ market, side: 1, amount: 2_000_000 });
await saveSecret(market, result1.betIndex, result1.userKeypair.privateKey);
// betIndex = 0

// Second bet on the same market
const result2 = await client.actions.placeBet({ market, side: 1, amount: 3_000_000 });
await saveSecret(market, result2.betIndex, result2.userKeypair.privateKey);
// betIndex = 1

// Later, when claiming, you need each betIndex individually
await client.actions.claimPayout({ market });
Use the usePosition(market, user, betIndex) React hook to fetch each individual position. Without a betIndex it defaults to 0n, so you won’t see subsequent bets unless you explicitly pass the index.