Cash-out is a two-step flow on a live position row: fetch a quoter-signed buy-back price, show it to the user, then execute it on-chain from the taker’s wallet. useCashOut owns the state machine; you render it.

The status machine

idle -> quoting -> quoted -> cashing -> done
          |                     |
          +------> error <------+
StatusMeaningUI
idleNothing in flight”Cash out” button
quotingPOST /cashout in flightSpinner on the button
quotedSigned quote in hand (quote is set)Confirmation with the price and a countdown
cashingWallet tx sent, awaiting receipt”Confirming…”
donePaid outSuccess state; the row settles on next refetch
errorQuote or tx failed (error is set)Message + retry via reset()
The signed quote expires 120 seconds after issuance (quote.deadline, unix seconds). Disable the confirm button when Date.now() / 1000 > Number(quote.deadline) and offer a re-quote instead; the contract rejects expired signatures anyway, this just saves the user a failed transaction.

Complete example

CashOutButton.tsx
import {useEffect, useState} from "react";
import {useAccount} from "wagmi";
import {formatUsdc, type ParlayRow} from "@parlays-live/taker";
import {useCashOut} from "@parlays-live/taker/react";

export function CashOutButton({row}: {row: ParlayRow}) {
  const {address} = useAccount();
  const {status, quote, error, getQuote, execute, reset} = useCashOut();

  // live countdown while a quote is showing
  const [now, setNow] = useState(() => Math.floor(Date.now() / 1000));
  useEffect(() => {
    if (status !== "quoted") return;
    const t = setInterval(() => setNow(Math.floor(Date.now() / 1000)), 1000);
    return () => clearInterval(t);
  }, [status]);

  // only the taker of a live parlay can cash out
  if (row.settled || !address || row.taker.toLowerCase() !== address.toLowerCase()) return null;

  if (status === "idle" || status === "quoting") {
    return (
      <button disabled={status === "quoting"} onClick={() => getQuote(row.id, address)}>
        {status === "quoting" ? "Pricing..." : "Cash out"}
      </button>
    );
  }

  if (status === "quoted" && quote) {
    const secondsLeft = Math.max(0, Number(quote.deadline) - now);
    const expired = secondsLeft === 0;
    const value = BigInt(quote.cashValue);
    const winProb = (Number(quote.jointProbBps) / 100).toFixed(1);
    return (
      <span className="cashout-confirm">
        <strong>${formatUsdc(value)}</strong>
        <small>
          {winProb}% to win ${formatUsdc(BigInt(quote.pot))} at settlement
          {expired ? " (quote expired)" : ` (${secondsLeft}s)`}
        </small>
        {expired ? (
          <button onClick={() => getQuote(row.id, address)}>Re-quote</button>
        ) : (
          <button onClick={() => execute(row.id, quote)}>Confirm</button>
        )}
        <button onClick={reset}>Cancel</button>
      </span>
    );
  }

  if (status === "cashing") return <span>Confirming...</span>;

  if (status === "done") return <span className="cashout-done">Cashed out</span>;

  // error
  return (
    <span className="cashout-error">
      {error}
      <button onClick={reset}>Retry</button>
    </span>
  );
}
Drop <CashOutButton row={row} /> into the last cell of your positions table row.

Error handling

Two distinct failure surfaces, both landing in status === "error" with a message in error:
  • Quote errors (relayer): "not your parlay" (403), "already settled" (409), "unknown parlay" (404), "could not fetch HL prices" (502). These need a slip-level message, not a retry loop.
  • Execution errors (wallet or chain): user rejection, an expired deadline, or a revert. The hook truncates the reason to the first line, 140 chars, so it is safe to render directly.
After any error, reset() returns to idle (it also clears quote and error).

Analytics

Pass callbacks the same way as submit; see Analytics for the mapping:
const cashout = useCashOut({
  onEvent: (e) => {
    // e.type: "cashout_quoted" | "cashout_confirmed" | "cashout_failed"
  },
  ensureChain: myNetworkGuard, // optional; defaults to wagmi switchChain
});

What happens on-chain

execute calls ParlayEscrow.cashOut(parlayId, cashValue, quoterNonce, deadline, sig) from the user’s wallet (this transaction is the taker’s, not the relayer’s, so the user pays gas here) and waits for the receipt. The escrow pays cashValue to the taker, marks the parlay settled, and releases the remaining escrowed funds to the vault. Details in Cash-out concepts.