This recipe walks the COMPLETE integration for a host app that already has its own market cards, a right-side trade drawer, a shared positions table used on both the profile page and market pages, and Privy for wallets - i.e. the shape of the Outcome web app, which this recipe mirrors. If your app uses wagmi, follow the individual guides instead; every step here has a wagmi twin. What you ship, taker-side only:
  • A Combine action on market cards and in the trade panel
  • A parlay drawer sliding in from the right with the slip
  • Parlay rows in the existing positions table (profile + market pages, one component)
  • Cash out on live parlay rows
  • Analytics through your existing pipeline
1

Stand up the environment (dev prices HL testnet HIP-4)

The SDK is config-injected, so the app code is identical across environments. What differs is the deployment behind it:
PieceDev envNotes
HyperEVM chain998 (testnet)contracts live here
HIP-4 pricing sourceHyperliquid TESTNETrelayer + keeper flag HL_TESTNET=true
Relayeryour own worker deployrelayer/ from the parlays.live repo, wrangler deploy
Keeperyour own worker deploysettles from the same HL environment it priced from
Escrow / vault / USDCyour own testnet deployone-time forge deploy, addresses into config
Price and settle from the SAME Hyperliquid environment. A relayer pricing HL testnet with a keeper settling HL mainnet (or vice versa) will strand parlays.
config/parlay.ts
import {createTakerClient} from "@parlays-live/taker";

export const parlayClient = createTakerClient({
  relayerUrl: process.env.NEXT_PUBLIC_PARLAY_RELAYER_URL!, // your HL-testnet relayer
  escrow: process.env.NEXT_PUBLIC_PARLAY_ESCROW! as `0x${string}`,
  collateralToken: process.env.NEXT_PUBLIC_PARLAY_USDC! as `0x${string}`,
  chainId: 998,
});
2

Vendor the packages

No npm publish needed. Copy packages/taker and packages/sdk from the parlays.live repo into your workspace’s packages/ (both are source-shipped TypeScript, no build step; a packages/* pnpm workspace glob picks them up). Peer ranges are react >=18 and viem >=2 - both satisfied by a catalog setup. wagmi stays optional and unused in this recipe.
3

Bind the wallet: the Privy adapter

Privy exposes an EIP-1193 provider per connected wallet. The adapter turns that plus the client’s call builders into the full money flows - no wagmi:
lib/parlay/use-parlay-adapter.ts
import {useMemo} from "react";
import {useWallets} from "@privy-io/react-auth";
import {createWalletAdapter, type ParlayEvent} from "@parlays-live/taker";
import {hyperEvmTestnet} from "@parlays-live/sdk";
import {parlayClient} from "@/config/parlay";
import {track} from "@/lib/analytics"; // your PostHog wrapper

export function useParlayAdapter() {
  const {wallets} = useWallets();
  const wallet = wallets[0]; // or your active-wallet selection

  return useMemo(() => {
    if (!wallet) return null;
    return createWalletAdapter(
      parlayClient,
      {
        getProvider: () => wallet.getEthereumProvider(),
        getAddress: () => wallet.address as `0x${string}`,
      },
      {
        chain: hyperEvmTestnet,
        onEvent: (e: ParlayEvent) => track(`parlay.${e.type}`, e),
      },
    );
  }, [wallet]);
}
One adapter per active wallet; rebuilding on wallet change is cheap. The onEvent union is identical to the wagmi hooks’, so analytics mapping is portable (see the analytics guide).
4

Slip state, shared app-wide

Market cards and the drawer must see the same slip, so hold it in a context using the pure helpers (no wagmi hook needed):
lib/parlay/slip-context.tsx
"use client";
import {createContext, useContext, useMemo, useState, type ReactNode} from "react";
import {toggleLeg, removeLeg, toLegs, type Picks} from "@parlays-live/taker";

const SlipCtx = createContext<ReturnType<typeof useSlipState> | null>(null);

function useSlipState() {
  const [picks, setPicks] = useState<Picks>({});
  const [open, setOpen] = useState(false);
  return useMemo(
    () => ({
      picks,
      legs: toLegs(picks),
      count: Object.keys(picks).length,
      open,
      openDrawer: () => setOpen(true),
      closeDrawer: () => setOpen(false),
      toggle: (marketId: number, outcome: number) => {
        setPicks((p) => toggleLeg(p, marketId, outcome));
        setOpen(true); // adding a leg reveals the drawer
      },
      remove: (marketId: number) => setPicks((p) => removeLeg(p, marketId)),
      clear: () => setPicks({}),
    }),
    [picks, open],
  );
}

export function ParlaySlipProvider({children}: {children: ReactNode}) {
  return <SlipCtx.Provider value={useSlipState()}>{children}</SlipCtx.Provider>;
}
export function useSlip() {
  const v = useContext(SlipCtx);
  if (!v) throw new Error("wrap the app in <ParlaySlipProvider>");
  return v;
}
Mount it once in your app providers, next to the Privy and QueryClient providers.
5

The Combine action (market cards + trade panel)

Add a small affordance per outcome row on the market card, and an “Add to combo” secondary action in the trade drawer/panel. Both just call toggle:
components/market-card/combine-action.tsx
import {useSlip} from "@/lib/parlay/slip-context";

export function CombineAction({marketId, outcome}: {marketId: number; outcome: number}) {
  const slip = useSlip();
  const selected = slip.picks[marketId] === outcome;
  return (
    <button
      onClick={() => slip.toggle(marketId, outcome)}
      aria-pressed={selected}
      className={selected ? "combine-btn combine-btn-on" : "combine-btn"}
    >
      {selected ? "In combo" : "Combine"}
    </button>
  );
}
Re-tapping the same outcome removes the leg; tapping the other side flips it - that is the toggleLeg contract, so selected-state stays trivially in sync.
6

The parlay drawer (right side)

Reuse your existing drawer primitives (the same container/animation your trade drawer uses). Inside: slip rows, stake input, a debounced preview quote, and the submit button driven by the adapter through a react-query mutation:
components/parlay-drawer/parlay-drawer.tsx
"use client";
import {useEffect, useState} from "react";
import {useMutation, useQuery} from "@tanstack/react-query";
import {formatUsdc, parseUsdc} from "@parlays-live/taker";
import {parlayClient} from "@/config/parlay";
import {useParlayAdapter} from "@/lib/parlay/use-parlay-adapter";
import {useSlip} from "@/lib/parlay/slip-context";

export function ParlayDrawer() {
  const slip = useSlip();
  const adapter = useParlayAdapter();
  const [stakeText, setStakeText] = useState("10");
  const stake = parseUsdc(stakeText || "0"); // taker address is resolved inside the adapter

  // Debounced PREVIEW quote for payout display. Submit re-quotes fresh, so a
  // stale preview can never leak into a signed order.
  const [debouncedLegs, setDebouncedLegs] = useState(slip.legs);
  useEffect(() => {
    const t = setTimeout(() => setDebouncedLegs(slip.legs), 350);
    return () => clearTimeout(t);
  }, [slip.legs]);

  const preview = useQuery({
    queryKey: ["parlay-preview", debouncedLegs, stake.toString()],
    enabled: debouncedLegs.length >= 2 && stake > 0n && !!adapter,
    queryFn: async () =>
      parlayClient.quote(debouncedLegs, stake, "0x0000000000000000000000000000000000000001"),
    // any address works for a preview; the real quote is bound to the taker at submit
    staleTime: 5_000,
  });

  const submit = useMutation({
    mutationFn: () => adapter!.submitParlay(slip.legs, stake),
    onSuccess: () => slip.clear(),
  });

  if (!slip.open) return null;
  const q = preview.data?.quote;
  const oddsX = q ? (Number(q.combinedOddsBps) / 10_000).toFixed(2) : null;

  return (
    <aside className="parlay-drawer">{/* your right-drawer container */}
      <header>
        <h3>Combo slip ({slip.count})</h3>
        <button onClick={slip.closeDrawer}>Close</button>
      </header>

      {slip.legs.map((l) => (
        <div key={l.marketId} className="slip-row">
          {/* resolve marketId to your market title/side labels */}
          <span>Market {l.marketId} - {l.outcome === 1 ? "YES" : "NO"}</span>
          <button onClick={() => slip.remove(l.marketId)}>x</button>
        </div>
      ))}

      <label>
        Stake (USDC)
        <input inputMode="decimal" value={stakeText} onChange={(e) => setStakeText(e.target.value)} />
      </label>

      {q && (
        <div className="slip-summary">
          <span>{oddsX}x combined</span>
          <span>To win ${formatUsdc(BigInt(q.payout))}</span>
          {preview.data?.correlation && BigInt(preview.data.correlation.reductionBps) > 0n && (
            <small>correlation-adjusted (see pricing docs)</small>
          )}
        </div>
      )}

      <button
        disabled={!adapter || slip.count < 2 || stake <= 0n || submit.isPending}
        onClick={() => submit.mutate()}
      >
        {submit.isPending ? "Placing..." : "Place combo"}
      </button>
      {submit.error && <p className="error">{(submit.error as Error).message}</p>}
    </aside>
  );
}
For fine-grained button copy per step (approving / quoting / signing / submitting), pass onStatus in the adapter options and mirror it into state - the same status machine as the drawer guide.
7

Positions: one component, two surfaces

Wrap the adapter’s positions() in react-query, then reuse your existing positions-table component by adding a parlay row variant (discriminated union). The profile page filters with positionsOf, a market page with positionsOnMarket - same component, same data hook:
lib/parlay/use-parlay-positions.ts
import {useQuery} from "@tanstack/react-query";
import {positionsOf, positionsOnMarket, potOf, bookedOddsBps} from "@parlays-live/taker";
import {useParlayAdapter} from "./use-parlay-adapter";

export function useParlayPositions() {
  const adapter = useParlayAdapter();
  const q = useQuery({
    queryKey: ["parlay-positions"],
    enabled: !!adapter,
    queryFn: () => adapter!.positions(),
    refetchInterval: 8_000,
  });
  const rows = q.data ?? [];
  return {
    rows,
    isLoading: q.isLoading,
    mine: (addr?: `0x${string}`) => positionsOf(rows, addr),
    onMarket: (marketId: number) => positionsOnMarket(rows, marketId),
  };
}
// profile page:            rows = useParlayPositions().mine(wallet.address)
// market page (id 189):    rows = useParlayPositions().onMarket(189)
// row rendering:           legs, stake, potOf(row), bookedOddsBps(row)/10000 + "x", row.settled
Interleave with your existing single-market position rows via a kind: "single" | "parlay" union - full pattern in the positions guide.
8

Cash out on live rows

const cashout = useMutation({
  mutationFn: async (parlayId: number) => {
    const q = await adapter!.cashoutQuote(parlayId); // show q.cashValue first
    return adapter!.cashOut(parlayId, q);
  },
});
Quotes expire in ~2 minutes; on a deadline revert just re-quote. UX details (countdown, re-quote button) in the cash-out guide.
9

Flag it and QA

Ship behind your feature-flag infra. QA checklist against dev HL-testnet markets:
  • two-leg combo quotes, signs (one wallet prompt for approve when needed, one signature), lands on-chain, appears in the drawer-cleared state
  • correlated legs show shortened odds vs the naive product; mutually exclusive legs are refused with a clear error
  • positions rows appear on profile AND the involved market pages, settle states update, cash-out pays out
  • analytics events flow (parlay.quote_requestedparlay.confirmed)

What stays out of the host app

Pricing, correlation, margin accounting, oracle settlement, the vault, and all admin surfaces remain in the parlays.live stack. The host integrates a taker.