This is the flagship integration: your app already renders markets (a trading panel, market cards, an order form) and you want a Combine action that collects outcomes into a parlay slip in a right-side drawer. Everything below is headless SDK plus plain React; swap the example styling for your design system freely. What you will wire up:
  1. Outcome buttons that toggle legs into the slip (useParlaySlip)
  2. A right-side drawer opened by the Combine button
  3. Slip rows with remove, a stake input, and a live payout preview
  4. A correlation transparency row
  5. A submit button driving the useSubmitParlay status machine, with analytics

1. Wire outcome buttons into the slip

useParlaySlip is a map of marketId -> backed outcome (0 NO, 1 YES). Tapping the same outcome again removes the leg; tapping the other side flips it. sideOf(marketId) drives the selected state on your existing buttons:
import {useParlaySlip} from "@parlays-live/taker/react";

const slip = useParlaySlip();

// inside your market card / trading panel:
<button
  data-selected={slip.sideOf(market.id) === 1}
  onClick={() => slip.toggle(market.id, 1)}
>
  Yes
</button>
<button
  data-selected={slip.sideOf(market.id) === 0}
  onClick={() => slip.toggle(market.id, 0)}
>
  No
</button>
Hoist the slip above both surfaces (context or a state library) so market cards and the drawer share it. The Combine button is then trivial:
<button onClick={() => setDrawerOpen(true)}>
  Combine {slip.count > 0 && <span>({slip.count})</span>}
</button>

2. Drawer skeleton

Any drawer primitive works (Radix Dialog, Vaul, your own). The minimal fixed-right pattern:
.parlay-drawer {
  position: fixed;
  top: 0; right: 0; bottom: 0;
  width: min(400px, 100vw);
  transform: translateX(100%);
  transition: transform 200ms ease;
  z-index: 50;
  display: flex;
  flex-direction: column;
}
.parlay-drawer[data-open="true"] { transform: translateX(0); }

3. Live payout preview

useSubmitParlay only quotes as part of the submit flow, so the drawer fetches its own preview quote with the raw client (useTakerClient), debounced against slip and stake changes:
import {useEffect, useState} from "react";
import {useAccount} from "wagmi";
import {useTakerClient} from "@parlays-live/taker/react";
import type {PickLeg, QuoteResponse} from "@parlays-live/taker";

function usePreviewQuote(legs: PickLeg[], stake: bigint) {
  const client = useTakerClient();
  const {address} = useAccount();
  const [preview, setPreview] = useState<QuoteResponse | null>(null);
  const [previewError, setPreviewError] = useState<string | null>(null);

  useEffect(() => {
    setPreview(null);
    setPreviewError(null);
    if (!address || legs.length === 0 || stake <= 0n) return;
    const t = setTimeout(() => {
      client
        .quote(legs, stake, address)
        .then(setPreview)
        .catch((e: unknown) => setPreviewError(e instanceof Error ? e.message : "quote failed"));
    }, 400);
    return () => clearTimeout(t);
  }, [client, address, JSON.stringify(legs), stake.toString()]);

  return {preview, previewError};
}
Preview quotes are also how refusals surface before submit: a mutually exclusive combo (two outcomes of the same HL question) comes back as an error like “legs 101 and 102 are mutually exclusive”. Render previewError prominently; the user must change the slip.

4. Correlation transparency

When legs co-move, the quote’s correlation block shows what the guard removed. Render a row whenever reductionBps > 0 so shortened odds never look arbitrary:
const corr = preview?.correlation;
const reduction = corr ? BigInt(corr.reductionBps) : 0n;

{corr && reduction > 0n && (
  <div className="slip-corr">
    Correlation adjustment: -{(Number(corr.reductionBps) / 10000).toFixed(2)}x
    <span>
      ({corr.clusters.filter((c) => c.marketIds.length > 1).map((c) => c.driver).join(", ")} legs move together)
    </span>
  </div>
)}

5. The complete drawer component

Copy-paste and restyle. Uses only real SDK exports; stakes are bigint end to end.
ParlayDrawer.tsx
import {useEffect, useState} from "react";
import {useAccount} from "wagmi";
import {parseUsdc, formatUsdc, type PickLeg, type QuoteResponse} from "@parlays-live/taker";
import {
  useTakerClient,
  useSubmitParlay,
  type useParlaySlip,
  type ParlayEvent,
} from "@parlays-live/taker/react";

type Slip = ReturnType<typeof useParlaySlip>;

type Props = {
  open: boolean;
  onClose: () => void;
  slip: Slip;
  /// Host-supplied labels, e.g. from loadCards() info map:
  /// (marketId, outcome) => "France champion: YES"
  labelFor: (marketId: number, outcome: number) => string;
  /// Optional analytics tap; see the Analytics guide.
  onEvent?: (e: ParlayEvent) => void;
};

export function ParlayDrawer({open, onClose, slip, labelFor, onEvent}: Props) {
  const client = useTakerClient();
  const {address} = useAccount();
  const {submit, status, error, result, reset} = useSubmitParlay({onEvent});

  // stake as a string for the input; parsed to raw 6dp bigint for everything else
  const [stakeInput, setStakeInput] = useState("25");
  let stake = 0n;
  try {
    stake = stakeInput ? parseUsdc(stakeInput) : 0n;
  } catch {
    stake = 0n;
  }

  // debounced preview quote
  const [preview, setPreview] = useState<QuoteResponse | null>(null);
  const [previewError, setPreviewError] = useState<string | null>(null);
  const legsKey = slip.legs.map((l) => `${l.marketId}:${l.outcome}`).join(",");
  useEffect(() => {
    setPreview(null);
    setPreviewError(null);
    if (!address || slip.legs.length === 0 || stake <= 0n) return;
    const legs: PickLeg[] = slip.legs;
    const t = setTimeout(() => {
      client
        .quote(legs, stake, address)
        .then(setPreview)
        .catch((e: unknown) => setPreviewError(e instanceof Error ? e.message : "quote failed"));
    }, 400);
    return () => clearTimeout(t);
  }, [client, address, legsKey, stake.toString()]);

  const oddsX = preview ? (Number(preview.quote.combinedOddsBps) / 10000).toFixed(2) : null;
  const corr = preview?.correlation;
  const reduction = corr ? BigInt(corr.reductionBps) : 0n;
  const busy = status !== "idle" && status !== "done" && status !== "error";

  const statusLabel: Record<typeof status, string> = {
    idle: `Place parlay`,
    approving: "Approving USDC...",
    quoting: "Locking odds...",
    signing: "Sign in wallet...",
    submitting: "Submitting...",
    done: "Parlay placed",
    error: "Try again",
  };

  return (
    <aside className="parlay-drawer" data-open={open} aria-label="Parlay slip">
      <header className="drawer-head">
        <h2>Parlay slip ({slip.count})</h2>
        <button onClick={onClose} aria-label="Close">x</button>
      </header>

      {/* legs */}
      <ul className="slip-legs">
        {slip.legs.map((leg) => (
          <li key={leg.marketId} className="slip-leg">
            <span>{labelFor(leg.marketId, leg.outcome)}</span>
            <button onClick={() => slip.remove(leg.marketId)} aria-label="Remove leg">
              remove
            </button>
          </li>
        ))}
        {slip.count === 0 && <li className="slip-empty">Tap outcomes to add legs</li>}
      </ul>

      {/* stake */}
      <label className="slip-stake">
        Stake (USDC)
        <input
          inputMode="decimal"
          value={stakeInput}
          onChange={(e) => setStakeInput(e.target.value)}
          disabled={busy}
        />
      </label>

      {/* payout preview */}
      {preview && (
        <div className="slip-summary">
          <div>
            <span>Combined odds</span>
            <strong>{oddsX}x</strong>
          </div>
          <div>
            <span>Payout if all hit</span>
            <strong>${formatUsdc(BigInt(preview.quote.payout))}</strong>
          </div>
          {corr && reduction > 0n && (
            <div className="slip-corr">
              <span>Correlation adjustment</span>
              <strong>-{(Number(corr.reductionBps) / 10000).toFixed(2)}x</strong>
            </div>
          )}
        </div>
      )}
      {previewError && <p className="slip-error">{previewError}</p>}

      {/* submit */}
      <button
        className="slip-submit"
        disabled={!address || slip.count < 1 || stake <= 0n || busy || !!previewError}
        onClick={() => {
          if (status === "done" || status === "error") reset();
          submit(slip.legs, stake).then(() => slip.clear()).catch(() => undefined);
        }}
      >
        {statusLabel[status]}
      </button>

      {status === "done" && result && (
        <p className="slip-done">Parlay #{result.parlayId} is live. Tx: {result.txHash}</p>
      )}
      {status === "error" && error && <p className="slip-error">{error}</p>}
    </aside>
  );
}

Notes on the flow

  • The submit quote is fresh. submit re-quotes internally right before signing, so the preview never goes stale into the order. The user signs exactly what the maker signed; the payout they confirm in the wallet is the payout they get.
  • Status machine. approving -> quoting -> signing -> submitting -> done, with error from any stage. The first-ever submit includes a USDC approve transaction (maxUint256, so it never recurs); every later submit is fully gasless.
  • ensureChain. By default the hook calls wagmi switchChain to the client’s chainId before writing. Hosts with their own network toggle pass ensureChain in the callbacks instead.
  • Clearing. The example clears the slip on success. Keeping it and showing a “view position” link into your positions table also works well.

Where labels come from

If your app already knows its market names, pass your own labelFor. If not, the SDK re-exports Hyperliquid market loading:
import {loadCards, type MarketInfo} from "@parlays-live/taker";

const {cards, info, graph} = await loadCards(false); // false = HL mainnet (where HIP-4 lives)
const labelFor = (marketId: number, outcome: number) => {
  const m: MarketInfo | undefined = info.get(marketId);
  if (!m) return `Market ${marketId}`;
  return `${m.context}: ${m.sideLabels[outcome === 1 ? 0 : 1]}`;
};