Both write hooks (useSubmitParlay, useCashOut) accept a ParlayCallbacks object with an onEvent tap. The SDK fires a typed event at every funnel step; your app maps them to whatever analytics backend it uses. The SDK itself never talks to analytics.
export type ParlayCallbacks = {
  onEvent?: (e: ParlayEvent) => void;
  ensureChain?: () => Promise<void>; // optional custom wallet-network guard
};

The full ParlayEvent union

export type ParlayEvent =
  | {type: "quote_requested"; legCount: number; stakeRaw: bigint}
  | {type: "quote_received"; legCount: number; combinedOddsBps: string; payoutRaw: bigint}
  | {type: "quote_failed"; legCount: number; reason: string}
  | {type: "submitted"; legCount: number; stakeRaw: bigint; txHash: string}
  | {type: "confirmed"; legCount: number; stakeRaw: bigint; parlayId: string; payoutRaw: bigint}
  | {type: "failed"; stage: SubmitStatus; reason: string; legCount: number}
  | {type: "cashout_quoted"; parlayId: number; cashValueRaw: bigint}
  | {type: "cashout_confirmed"; parlayId: number; cashValueRaw: bigint; txHash: string}
  | {type: "cashout_failed"; parlayId: number; reason: string};
EventFired whenFunnel meaning
quote_requestedSubmit begins pricingIntent: user hit the button
quote_receivedRelayer returned a maker-signed orderPriced
quote_failedPricing rejected (refusal, risk limit, HL down)Priced-out
submittedRelay tx sent (txHash)On-chain
confirmedParlay id known, flow completeConverted
failedAny stage threw; stage says whichDrop-off, with the exact step
cashout_quotedBuy-back price shown to the userCash-out intent
cashout_confirmedcashOut tx confirmedCash-out converted
cashout_failedQuote or execution failedCash-out drop-off
Raw amounts (stakeRaw, payoutRaw, cashValueRaw) are 6-decimal bigint. Convert to display dollars only at the analytics edge with Number(formatUsdc(raw)); never feed raw bigints to JSON-based trackers directly (JSON.stringify throws on bigint).

PostHog mapping

This mirrors the reference app (app/src/hooks/useSubmitParlay.ts) event for event:
analytics.ts
import {formatUsdc} from "@parlays-live/taker";
import type {ParlayEvent} from "@parlays-live/taker/react";
import posthog from "posthog-js";

const usd = (raw: bigint) => Number(formatUsdc(raw));

export function onParlayEvent(e: ParlayEvent): void {
  switch (e.type) {
    case "quote_requested":
      posthog.capture("slip.quote_requested", {category: "trade", leg_count: e.legCount, stake_usd: usd(e.stakeRaw)});
      break;
    case "quote_received":
      posthog.capture("slip.quote_received", {
        category: "trade",
        leg_count: e.legCount,
        combined_odds: Number(e.combinedOddsBps) / 10000,
        payout_usd: usd(e.payoutRaw),
      });
      break;
    case "quote_failed":
      posthog.capture("slip.quote_failed", {category: "error", leg_count: e.legCount, reason: e.reason});
      break;
    case "submitted":
      posthog.capture("parlay.submitted", {category: "trade", leg_count: e.legCount, stake_usd: usd(e.stakeRaw), tx_hash: e.txHash});
      break;
    case "confirmed":
      posthog.capture("parlay.confirmed", {
        category: "trade",
        leg_count: e.legCount,
        stake_usd: usd(e.stakeRaw),
        parlay_id: Number(e.parlayId),
        payout_usd: usd(e.payoutRaw),
      });
      break;
    case "failed":
      posthog.capture("parlay.failed", {category: "error", stage: e.stage, reason: e.reason, leg_count: e.legCount});
      break;
    case "cashout_quoted":
      posthog.capture("cashout.quote_viewed", {category: "trade", parlay_id: e.parlayId, quote_usd: usd(e.cashValueRaw)});
      break;
    case "cashout_confirmed":
      posthog.capture("cashout.confirmed", {category: "trade", parlay_id: e.parlayId, proceeds_usd: usd(e.cashValueRaw), tx_hash: e.txHash});
      break;
    case "cashout_failed":
      posthog.capture("cashout.failed", {category: "error", parlay_id: e.parlayId, reason: e.reason});
      break;
  }
}
Wire it once per hook call site:
import {useSubmitParlay, useCashOut} from "@parlays-live/taker/react";
import {onParlayEvent} from "./analytics";

const submit = useSubmitParlay({onEvent: onParlayEvent});
const cashout = useCashOut({onEvent: onParlayEvent});

The ensureChain callback

The second host concern in ParlayCallbacks. By default, every write hook calls wagmi’s switchChain to client.config.chainId before touching the wallet (failures are swallowed; the write itself will surface a wrong-network error). Hosts with their own network model (a testnet/mainnet toggle, a modal-based switcher) inject theirs instead:
import {useSwitchChain} from "wagmi";

function useNetworkGuard() {
  const {switchChainAsync} = useSwitchChain();
  return {
    ensure: async () => {
      // your own logic: check the toggle, prompt a modal, then switch
      await switchChainAsync({chainId: 998});
    },
  };
}

const {ensure} = useNetworkGuard();
const submit = useSubmitParlay({ensureChain: ensure, onEvent: onParlayEvent});
If ensureChain throws, the flow stops with status: "error" and a failed event whose stage is the step it died in.

Funnel dashboards worth building

  • slip.quote_requested -> slip.quote_received -> parlay.submitted -> parlay.confirmed: the core conversion funnel; the gap between the first two is pricing rejections and HL latency.
  • slip.quote_failed grouped by reason: separates correlation refusals (“mutually exclusive”) from risk limits (“stake above maximum”) from infrastructure (“could not fetch HL prices”).
  • parlay.failed grouped by stage: signing failures are wallet rejections (UX), submitting failures are reverts or relayer issues (ops).