> For the complete documentation index, see [llms.txt](https://mute-sh.gitbook.io/mute.sh/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mute-sh.gitbook.io/mute.sh/integration-guides/taker-guide.md).

# Taker Guide

**Audience:** Frontend developers and bot authors building on top of the Parlay RFQ relay — anyone who wants to let users place fixed-odds, multi-leg prediction-market parlays on-chain.

**What you will learn:** How to browse markets, broadcast a request-for-quote (RFQ), receive competing signed quotes from market makers, pick the best price, sign a four-field EIP-712 order plus a USDC permit, and submit everything in one on-chain transaction. Leg resolution is automatic via a Chainlink CRE workflow feeding an on-chain resolution registry — you do not run any oracle logic yourself. But **settlement is not instant and not proactively pushed to you**: your parlay *fills* the moment your transaction confirms, and it *settles* (pays out) later, asynchronously, once every leg resolves. See [§13](#13-settlement) for the difference.

{% hint style="info" %}
**Market-maker integration is separate.** This guide never asks you to run pricing logic — the market makers (MMs) do that. You ask for a price, you get competing quotes, you take the best.
{% endhint %}

***

## 1. Overview

The Parlay relay is a **request-for-quote (RFQ) system for fixed-odds, multi-leg prediction-market parlays** settled on-chain. As a taker you:

1. Pick a set of **legs** — outcomes from World Cup match markets (e.g. "Brazil to win", "France-vs-X is a draw"). A parlay needs **2 to 6 legs** on this deployment (see [§4](#4-step-1--browse-markets)).
2. **Broadcast an RFQ** with your legs and stake. You sign nothing yet — this is just intent.
3. The relay fans your RFQ out to **every connected market maker**. Each MM prices it independently and signs its own quote. Quote collection is a short, fixed window (a few hundred ms), not an open-ended auction — see [§5](#5-step-2--create-the-rfq).
4. You receive **competing quotes**, sorted best-first. Best = highest payout to you.
5. You **pick one**, sign a tiny EIP-712 order + a USDC permit, and the relay's relayer submits it on-chain. Both your stake and the MM's collateral are escrowed in one transaction. This step is asynchronous: acknowledging your `accept` is not the same moment as the transaction landing — see [§10](#10-step-7--accept).
6. When the real-world matches settle, a **Chainlink CRE workflow** detects it from Hyperliquid mainnet data and reports it into an on-chain resolution registry. Once a per-outcome dispute window elapses, anyone can crank `resolveLeg`, and once every leg in your parlay is resolved the escrow **credits** the winner's share — a separate claim pulls it out (gasless: sign an EIP-712 `WithdrawRequest` via the relay's `claim:prepare`/`claim` WS messages and the relayer submits `withdrawWithSig`; or call `withdraw()` directly if you have gas). This is the slow, async part; see [§13](#13-settlement).

### Why this design matters to a taker

* **Best-price guarantee.** Many MMs compete on every RFQ; you always see — and can take — the tightest price in the book. The relay never prices anything itself.
* **Fixed odds.** The moment you accept a quote, your payout is locked. No slippage, no AMM curve.
* **Trustless acceptance.** You sign the *hash of exactly the quote you picked* (`quoteHash`). The relayer physically cannot swap in a worse quote, change a fee, or touch your legs. More on this in [§12](#12-understanding-quotehash).
* **MMs take the risk.** The maker posts collateral to cover your win. If you win, you get the whole pot. If you lose, the maker keeps it. You never owe more than your stake.

### Environment (testnet)

| Thing                          | Value                                                  |
| ------------------------------ | ------------------------------------------------------ |
| Chain                          | HyperEVM **testnet**, chainId `998`                    |
| Escrow contract (v2)           | `0x551D4A7EdB71583C7C12900128727D857e308925`           |
| Mock USDC (EIP-2612, 6dp)      | `0x75b69d0E8fA3CdB644b0f069E4e876B85A76E137`           |
| EIP-712 domain (orders/quotes) | `{ name: "ParlayEscrow", version: "1", chainId: 998 }` |
| EIP-712 domain (USDC permit)   | `{ name: "USD Coin", version: "1", chainId: 998 }`     |

Markets, odds and the leg set are curated from Hyperliquid mainnet `outcomeMeta` (real FIFA World Cup markets). The relay exposes them via `GET /v1/rfq/combo-markets`.

***

## 2. Architecture

![Taker integration architecture](/files/c7NYYhYDUvZ7rqcNaBUt)

There is no push notification for settlement — poll `GET /v1/rfq/:rfqId` (or index the escrow's events yourself) to find out when your parlay has settled and call `withdraw()`. See [§13](#13-settlement) for the full breakdown.

The relay is a **neutral white-label broadcast relay**. It validates legs, computes the fee regime, fans the RFQ to MMs, verifies each MM's signature, and submits the winning combination on-chain. **It never signs a quote and never sets a price.**

{% hint style="info" %}
**v2 is WebSocket-first.** The entire taker flow — RFQ creation, live quote pushes, prepare, and accept — runs over a single WebSocket connection to `/v2/ws/taker`. REST endpoints (`POST /v1/rfq`, `POST /v1/rfq/:id/prepare`, `POST /v1/rfq/:id/accept`) also exist for non-WS integrations, but the WS path is preferred.
{% endhint %}

***

## 3. Quick Start

End-to-end in TypeScript using `ParlayV2Client` — a reference client implementation you copy into your own project (it is **not published to npm**; there is no installable package for it). The sections below (§4–§18) document the raw WebSocket/REST protocol directly, in case you'd rather implement your own client or the reference implementation doesn't fit your stack. The entire flow runs over a single WebSocket — no polling needed.

```ts
import { ParlayV2Client } from "./parlay-v2-client";
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(process.env.PK as `0x${string}`);
const taker = account.address;

// Wallet-agnostic EIP-712 signer — pass viem's signTypedData or a browser wallet adapter.
const signTypedData = (params: any) => account.signTypedData(params);

// Create the client (connects to wss://parlay.mute.sh/v2/ws/taker by default).
const client = new ParlayV2Client({
  url: "wss://parlay.mute.sh/v2/ws/taker",
  taker,
  signTypedData,
});

// One-shot: opens socket → hello → RFQ → waits for best quote →
// prepare → signs ParlayOrder + EIP-2612 permit → accept → waits for FILLED.
const result = await client.placeCombo({
  legs: [
    { outcomeId: 110, sideIndex: 0 }, // e.g. Brazil to win (YES)
    { outcomeId: 230, sideIndex: 0 }, // e.g. France to win in a DIFFERENT match
  ],
  stakeUsd: 25,
});

console.log(result);
// { rfqId, parlayId, txHash, status: "FILLED" }

client.close();
```

{% hint style="success" %}
That is the complete taker lifecycle using the high-level `placeCombo` helper: connect → hello → RFQ → live quote competition → prepare → sign two EIP-712 structs → accept → FILLED. Two signatures, one on-chain transaction. The sections below break down each step.
{% endhint %}

### Low-level WS flow (without the SDK helper)

If you prefer to drive the protocol manually:

```ts
const client = new ParlayV2Client({ url: "wss://parlay.mute.sh/v2/ws/taker" });
await client.connect();

// Step 1 — register taker address for snapshot pushes
await client.hello(taker);

// Step 2 — create RFQ (server fans out to all MMs)
const { rfqId } = await client.createRfq({
  legs: [{ outcomeId: 110, sideIndex: 0 }, { outcomeId: 230, sideIndex: 0 }],
  stakeUsd: 25,
  taker,
});

// Step 3 — wait for AWAITING_REQUESTER_ACCEPTANCE (live quotes pushed automatically)
// The client emits state changes via subscribe(); pick bestQuoteId from state.

// Step 4 — prepare the winning quote
const prepared = await client.prepare(rfqId, bestQuoteId);

// Step 5 — sign ParlayOrder + EIP-2612 permit using prepared.domains.*
// (same signing code as shown in §8 and §9)

// Step 6 — accept (relay submits on-chain)
await client.accept(rfqId, bestQuoteId, { orderSig, permitSig });

client.close();
```

***

## 4. Step 1 — Browse Markets

```http
GET /v1/rfq/combo-markets?limit=20&cursor=<opaque>
```

Returns one entry **per outcome** (Polymarket-combos-parity shape — a 3-way match market like "Brazil vs Argentina" appears as three separate entries, one per team/draw), cursor-paginated.

```jsonc
{
  "markets": [
    {
      "id": "m4210",
      "condition_id": "m4210",
      "position_ids": ["#42100", "#42101"],
      "slug": "m4210",
      "title": "Brazil to win",
      "outcomes": ["Yes", "No"],
      "outcome_prices": ["0.471000", "0.529000"],
      "image": "",
      "volume": 0.029,
      "tags": ["soccer"]
    }
  ],
  "next_cursor": "MjA="
}
```

Key fields for building legs:

* **`position_ids`** — `[yesPositionId, noPositionId]`, each a HIP-4 coin string `"#${outcomeId * 10 + sideIndex}"`. Recover the numeric fields yourself: `outcomeId = Math.floor(n / 10)`, and `sideIndex` is `n % 10` (`0` = **YES**, `1` = **NO**). For the example above, `outcomeId = 4210`; the YES leg is `{ outcomeId: 4210, sideIndex: 0 }`.
* **`outcome_prices`** — `[yesProb, noProb]` as 6dp strings, the implied probability the relay derived from HL mids. Reference only for your UI — the MMs price against their own books.
* **`condition_id`** — this market's identity. It is unrelated to the *combo-level* `condition_id` you'll see later ([§7](#7-step-4--quote-selection), [§12](#12-understanding-quotehash)) — that one hashes your whole leg set, not a single market. See [Maker Protocol Spec §6, Combo identity](/mute.sh/reference/maker-protocol-spec.md#6-combo-identity-condition_id-position-ids-and-conflict-checks) for the full combo-identity mechanics if you're curious why a well-formed-looking RFQ can still get rejected before any MM sees it.

There is currently **no endpoint that returns builder config** (fee bps, spread, or per-request limits) — those are documented as static facts in [§11](#11-fee-breakdown) and [§18](#18-reference--combo-conflict-checks) instead. Pre-validate against the numbers in this guide, not a live config fetch.

> A "match" market always has 3 legs: Team A, Draw, Team B. You can take any one of them YES or NO, but **not two from the same match** (see [§18](#18-reference--combo-conflict-checks)).

***

## 5. Step 2 — Create the RFQ

Over WebSocket (preferred), send a `rfq` message on `/v2/ws/taker`:

```jsonc
{ "type": "rfq", "taker": "0xYourWallet…",
  "legs": [
    { "outcomeId": 4210, "sideIndex": 0 },
    { "outcomeId": 7330, "sideIndex": 1 }
  ],
  "stake_usd": 25, "reqId": "r1" }
```

Or via REST:

```http
POST /v1/rfq
```

```jsonc
{
  "taker": "0xYourWallet…",
  "legs": [
    { "outcomeId": 4210, "sideIndex": 0 },   // Brazil to win
    { "outcomeId": 7330, "sideIndex": 1 }     // NOT a draw in a different match
  ],
  "stake_usd": 25                              // USD, gross (1 ≤ stake ≤ 1000)
}
```

**Response** (an RFQ snapshot — the same shape you get back from `GET /v1/rfq/{rfqId}`, `prepare`, and `accept`; see [§6](#6-step-3--get-quotes)):

```jsonc
{
  "status": "COLLECTING_QUOTES",
  "request": {
    "rfq_id": "rfqv2_3f9a2c1b0e7d6a5c4b",
    "requestor_public_id": "0xYourWallet…",
    "leg_position_ids": ["#42100", "#73301"],
    "condition_id": "0x9c…",       // combo identity — keccak256 over the canonical leg set
    "yes_position_id": "0x1a…",
    "no_position_id": "0x2b…",
    "submission_deadline": 1751049600400   // unix ms — a few hundred ms out, NOT ~1 hour
  }
}
```

There is no `makers`-fanned-out count and no separate top-level `rfqId`/`deadline` fields in the real response — the `rfq_id` and `submission_deadline` live inside `request`. `rfq_id`s are prefixed `rfqv2_`.

On a validation failure you get `400` with `{ "error": "…" }` — e.g. stake out of range, contradictory legs, or an outcome that is not an eligible market. See [§15](#15-error-cases).

{% hint style="info" %}
`submission_deadline` is the **quote-collection window**, and it is short — a few hundred milliseconds, not an hour. MMs must submit their signed quotes before it elapses; you don't act on this value directly. Each returned **quote** also carries its own `expiresAt` (epoch ms, typically 60–90 s out) — that's the number you check before `prepare`/`accept`. Always act on a fresh quote.
{% endhint %}

***

## 6. Step 3 — Get Quotes

You have two options. **For a UI, prefer the WebSocket stream** — the relay *pushes* the quote book as MMs respond, so there is no polling. For a simple bot, polling is fine.

### Option A — Poll (REST)

```http
GET /v1/rfq/{rfqId}
```

This returns the same **RFQ snapshot** shape as `POST /v1/rfq` (see [§5](#5-step-2--create-the-rfq)) — `{ status, request, competition_ends_at, quote_id, ... }` — not a pre-formatted quote list. It tells you the RFQ's lifecycle status (`COLLECTING_QUOTES` → `AWAITING_REQUESTER_ACCEPTANCE` → …) but does not include the friendly `{quoteId, maker, netPayout, odds, ...}` per-quote view. **That richer shape is only pushed over the WebSocket** (`quotes` message, Option B below) — if you're polling instead of subscribing, you will see the RFQ's status change to `AWAITING_REQUESTER_ACCEPTANCE` once competition closes, but you'll need the WS `quotes` push (or the equivalent maker-facing REST, if your integration reads it) to get individual quote pricing. In practice, **use the WebSocket** — there is no REST endpoint that mirrors the `quotes` push.

### Option B — Subscribe (WebSocket, preferred)

Open the taker socket at `/v2/ws/taker` and run the *whole flow* over it — hello, RFQ, pushed quotes, prepare, accept — no HTTP at all. See [§14](#14-websocket-stream) for the full protocol. The relevant part:

```ts
const ws = new WebSocket("wss://parlay.mute.sh/v2/ws/taker");
ws.onopen = () => {
  // 1. Register your address so quote/exec pushes reach you
  ws.send(JSON.stringify({ type: "hello", address: taker }));
};
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  if (msg.type === "ready") { /* markets, escrow, limits bootstrapped */ }
  if (msg.type === "hello:ok") {
    // 2. Create RFQ
    ws.send(JSON.stringify({ type: "rfq", taker, legs, stake_usd: 25, reqId: "r1" }));
  }
  if (msg.type === "rfq:created") rfqId = msg.rfqId;
  if (msg.type === "quotes" && msg.rfqId === rfqId) {
    // 3. Live push — quotes sorted best-first. msg.bestQuoteId is the winner.
    render(msg.quotes);
  }
};
```

The server pushes a fresh `{ type:"quotes", rfqId, count, quotes:[…], bestQuoteId }` message *every time a new quote lands*, so your odds tick up live as MMs compete.

***

## 7. Step 4 — Quote Selection

Each quote carries everything you need to show the bet to a user:

| Field                  | Meaning                                                                                                                                                 |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `quoteId`              | Opaque ID you pass to `prepare`/`accept`.                                                                                                               |
| `maker`                | The market maker's on-chain address. There is no separate MM label/`mmId` field.                                                                        |
| `price_e6` / `size_e6` | The maker's raw quoted price and size, 6dp integer strings. Mostly useful for logging/analytics — prefer `payout`/`netPayout`/`odds` below for display. |
| `makerCollateral`      | USD the MM puts up to cover your win. **Higher = better odds for you.**                                                                                 |
| `payout`               | The full pot on a win (`netStake + makerCollateral`), before any win-only spread.                                                                       |
| `netPayout`            | What you actually receive on a win, in USD (`payout` less any win-only spread).                                                                         |
| `odds`                 | `netPayout / stake` — the effective multiplier on the gross dollars you put in.                                                                         |
| `best`                 | `true` for the single best quote.                                                                                                                       |

{% hint style="warning" %}
Quotes in this push do **not** carry a visible per-quote expiry field today — there is no `expiresAt` in the `quotes` message. A quote can still be rejected as stale at `prepare`/`accept` time (`QUOTE_UNAVAILABLE`/`QUOTE_MISMATCH` if it's been withdrawn or is no longer the winner), so treat any quote as perishable and act promptly — don't build UI logic that depends on a client-visible countdown, since the server doesn't give you one.
{% endhint %}

**The book is sorted by `netPayout` descending**, so `quotes[0]` is always the best price and `bestQuoteId` names it. The relay's tie-break among equal-`netPayout` quotes is earliest-received.

```ts
const best = quotes.quotes[0];
// e.g. stake $25, odds 2.857 → if every leg hits you receive ~$71.42.
console.log(`${best.maker}: ${best.odds.toFixed(2)}× → $${best.netPayout.toFixed(2)} on win`);
```

You are free to take *any* quote, not just the best — pass whichever `quoteId` you want to the next step.

***

## 8. Step 5 — Prepare & Sign the ParlayOrder

Before you sign, call `prepare`. The server builds the **exact** order and permit bound to the quote you chose and hands them back. You never construct `quoteHash` yourself.

Over WS: `{ type: "prepare", rfqId, quoteId, reqId? }` → server replies `{ type: "prepared", ... }`.

Or via REST:

```http
POST /v1/rfq/{rfqId}/prepare      Body: { "quote_id": "q_8a7b6c5d4e3f21" }
```

**Response:**

```jsonc
{
  "quote_id": "q_8a7b6c5d4e3f21",
  "order": {
    "taker": "0xYourWallet…",
    "quoteHash": "0x9c…",          // EIP-712 hash of EXACTLY the chosen ParlayQuote
    "nonce": "48572930…",
    "deadline": "1751049600"
  },
  "permit": {
    "owner": "0xYourWallet…",
    "spender": "0x551D4A7EdB71583C7C12900128727D857e308925",
    "value": "25000000",            // gross stake in 6dp USDC (25 USDC)
    "nonce": "3",                   // your live USDC permit nonce, read on-chain
    "deadline": "1751049600"
  },
  "domains": {
    "parlay": { "name": "ParlayEscrow", "version": "1", "chainId": 998,
                "verifyingContract": "0x2cB2…" },
    "permit": { "name": "USD Coin", "version": "1", "chainId": 998,
                "verifyingContract": "0x75b6…" }
  },
  "pricing": {
    "stake": 25, "protocolFee": 0.25, "builderFee": 0.0625, "netStake": 24.6875,
    "pot": 71.42, "price_e6": "350000", "size_e6": "71420000",
    "spreadBps": 0, "makerCollateral": 46.73
  }
}
```

Field notes:

* The key is `quote_id` (snake\_case) in this response body, not `quoteId`.
* `pricing.pot` is the full escrowed amount on a win (`netStake + makerCollateral`) — there is no separate `payout`/`netPayout`/`quotedMultiplier`/`effectiveMultiplier` field here. With `spreadBps: 0` the taker's actual take-home equals `pot` (see [§11](#11-fee-breakdown)); compute it yourself as `pot * (1 - spreadBps / 10000)` if `spreadBps` is nonzero. The live `quotes` push ([§6](#6-step-3--get-quotes)) *does* include a precomputed `netPayout`/`odds` per quote — use that for display, and treat `prepare`'s `pricing` block as the authoritative numbers for the order you're about to sign.
* There is no `maker` object in the response — you already know which MM you picked from the `quotes` push's `maker` field, and it's implicit in `quote_id`.

### The ParlayOrder struct (4 fields)

EIP-712 is just "sign a structured object instead of raw bytes" — the wallet shows the user human-readable fields, and the contract recovers your address from the signature. The **ParlayOrder** you sign has exactly four fields:

```
ParlayOrder(address taker, bytes32 quoteHash, uint256 nonce, uint256 deadline)
```

Sign it with the `domains.parlay` domain the server returned:

```ts
const orderSig = await walletClient.signTypedData({
  account,
  domain: prep.domains.parlay,
  types: {
    ParlayOrder: [
      { name: "taker",     type: "address" },
      { name: "quoteHash", type: "bytes32" },
      { name: "nonce",     type: "uint256" },
      { name: "deadline",  type: "uint256" },
    ],
  },
  primaryType: "ParlayOrder",
  message: {
    taker:     prep.order.taker,
    quoteHash: prep.order.quoteHash as `0x${string}`,
    nonce:     BigInt(prep.order.nonce),
    deadline:  BigInt(prep.order.deadline),
  },
});
```

{% hint style="danger" %}
**Do NOT compute or verify `quoteHash` yourself.** The server derives it from the 10-field `ParlayQuote` struct the MM signed (no `spreadBps` — that is owner-set escrow config in v2). Any local recomputation risks using the wrong encoding, wrong domain, or stale data. Always use `prep.order.quoteHash` verbatim as returned by `prepare`. Do NOT modify any field of `prep.order` before signing — changing even one byte changes the hash, breaks the maker's quote signature, and causes the on-chain transaction to revert.
{% endhint %}

Notice the order does **not** contain your legs, the collateral, or the fees. It only commits to `quoteHash` — which already fingerprints all of that. That is the whole trust model; see [§12](#12-understanding-quotehash).

***

## 9. Step 6 — Sign the USDC Permit (EIP-2612)

So the relayer can pull your stake into escrow without you having sent a separate `approve` transaction, you sign an **EIP-2612 permit** for the exact stake amount. (If you would rather pre-approve the escrow with a normal ERC-20 `approve`, you still need *a* permit signature here because the escrow's `createParlayWithPermit` path expects one — sign it anyway; the gas cost of an unused permit is negligible on testnet.)

The permit is a signed message authorizing `spender` (the escrow) to spend `value` (your gross stake) of your USDC. Use the `permit` object and the `domains.permit` domain from `prepare` verbatim:

```ts
const permitSig = await walletClient.signTypedData({
  account,
  domain: prep.domains.permit,            // { name:"USD Coin", version:"1", chainId:998, verifyingContract: USDC }
  types: {
    Permit: [
      { name: "owner",    type: "address" },
      { name: "spender",  type: "address" },
      { name: "value",    type: "uint256" },
      { name: "nonce",    type: "uint256" },
      { name: "deadline", type: "uint256" },
    ],
  },
  primaryType: "Permit",
  message: {
    owner:    prep.permit.owner,
    spender:  prep.permit.spender,         // the escrow
    value:    BigInt(prep.permit.value),   // gross stake, 6dp
    nonce:    BigInt(prep.permit.nonce),   // your on-chain USDC nonce (server already read it)
    deadline: BigInt(prep.permit.deadline),
  },
});
```

The relay splits this signature into `{v, r, s}` for the contract's `Permit` tuple — you just hand it the 65-byte hex.

> **Maker side, for reference:** MMs do *not* sign a permit per trade. On connect they sign a single huge "standing allowance" permit once, and every quote then carries a zero-permit using that allowance. You, the taker, sign a fresh per-stake permit each time.

***

## 10. Step 7 — Accept

Over WS (preferred):

```jsonc
{ "type": "accept", "rfqId": "rfq_3f9a2c1b0e7d6a5c4b",
  "quoteId": "q_8a7b6c5d4e3f21",
  "orderSig": "0x…", "permitSig": "0x…", "reqId": "r4" }
```

Or via REST:

```http
POST /v1/rfq/{rfqId}/accept
```

```jsonc
{
  "quote_id": "q_8a7b6c5d4e3f21",
  "orderSig": "0x…",     // your ParlayOrder signature
  "permitSig": "0x…"     // your EIP-2612 permit signature
}
```

The relay re-validates everything (legs still valid, quote not expired, your balance covers the stake, both signatures recover to your address), optionally runs **last look** on the winning MM, then the relayer submits `createParlayWithPermit(...)` on-chain. Both collaterals are escrowed in that one transaction.

**Response — this is only an acknowledgement, not the execution result:**

```jsonc
{
  "status": "EXECUTING",
  "request": { "rfq_id": "rfqv2_3f9a2c1b0e7d6a5c4b", "...": "..." }
}
```

{% hint style="danger" %}
**`accept` does not wait for the on-chain transaction.** It returns as soon as the relay has validated and queued your signatures — before the transaction is even submitted, let alone mined. There is no `parlayId` or `txHash` in this response. Those arrive **asynchronously**, a short time later, via `RFQ_EXECUTION_UPDATE` pushes on your taker WebSocket connection (or the maker's, if you're the maker) — see [§14](#14-websocket-stream):

1. `RFQ_EXECUTION_UPDATE { status: "MATCHED", tx_hash }` — the transaction has been submitted but not yet confirmed.
2. `RFQ_EXECUTION_UPDATE { status: "CONFIRMED", tx_hash, parlay_id }` — the transaction is mined and `ParlayCreated` was emitted. **This is the moment your parlay is filled.** Persist `parlay_id` here — it's your handle for resolution/settlement tracking.
3. `RFQ_EXECUTION_UPDATE { status: "FAILED" }` (plus an `RFQ_ERROR` with `code: "TRADE_SUBMISSION_FAILED"`) if the on-chain call reverts or times out.

If you're driving REST only with no open WebSocket, poll `GET /v1/rfq/{rfqId}` — its `status` field transitions `EXECUTING` → `FILLED` or `FAILED` — since you have no other way to observe the outcome. The high-level `placeCombo()` SDK helper ([§3](#3-quick-start)) hides all of this and just awaits `FILLED`.
{% endhint %}

On failure you get `{ "error": "…" }` synchronously from `accept` for request-level problems (bad signature, expired quote); execution-level failures (the on-chain call itself reverting) only surface via the async `FAILED` push above. Common cases — expired quote, last-look reject, insufficient balance — are in [§15](#15-error-cases).

***

## 11. Fee Breakdown

All fees come off **your stake at deposit**. The maker pays nothing in fees and contributes its full `makerCollateral` to the pot.

The **protocol fee** is not a fixed constant — the relay computes it per-RFQ from a base rate plus a per-leg surcharge (more legs, more settlement/correlation risk) with a taper for large stakes, capped at 10% (`MAX_FEE_BPS`). On this deployment's default configuration the per-leg surcharge and large-stake taper are both disabled, so in practice you will currently see it land on a flat **100 bps (1%)** regardless of leg count or stake — but treat that as this deployment's current settings, not a protocol guarantee. The **builder fee** is 0 unless a builder code is configured for your RFQ (this deployment currently runs with **no builder attached**, so `builderFee` is 0 in practice — the worked example below shows a nonzero builder fee only to illustrate the math). Always read `pricing.protocolFee`/`pricing.builderFee` from `prepare`'s response rather than hardcoding these numbers in your UI.

```
  gross stake                       $25.0000
  − protocol fee  (100 bps = 1%)   −$ 0.2500    →  25 × 100 / 10000
  − builder fee   ( 25 bps = 0.25%)−$ 0.0625    →  25 × 25  / 10000   (only if a builder is set — 0 on this deployment today)
  ───────────────────────────────────────────
  net stake                         $24.6875    →  this is what the MM prices against
  + maker collateral                $46.7300    →  from the winning quote
  ───────────────────────────────────────────
  POT (escrowed)                    $71.4175    →  the full amount the winner takes
```

So the **pot** = `netStake + makerCollateral`, and that is exactly what is locked in escrow.

### The win-only spread

There are actually **two independent spread numbers** in this system — know the difference:

1. **`pricing.spreadBps`** (from `prepare`, and echoed in the `quotes` push as the basis for `netPayout`) — the relay's own display estimate, computed per-RFQ to target a fair overall hold. It is **not maker-signed** and not read from chain; it's what the relay *shows* you.
2. **The escrow's on-chain `protocolSpreadBps`** — a single global value the escrow owner sets directly on the contract, snapshotted per-parlay at creation time, and applied **only at settlement**. This is the number that actually reduces your payout on-chain.

Operationally these two should track each other, but they are computed independently — the relay does not read the on-chain value when pricing a quote. If you need the ground truth for a specific parlay's spread, read the escrow's `parlaySpreadBps(parlayId)` after creation rather than trusting the quoted number retroactively. Either way, the win-only markdown is applied the same way:

```
  netPayout = pot × (1 − spreadBps / 10000)
```

On this deployment `spreadBps` is currently 0 in practice (no builder, default target-hold config resolves to 0 extra spread beyond the protocol fee), so `netPayout == pot`. The `odds` you see in the `quotes` push (`netPayout / grossStake`) already bakes in whatever spread is quoted, so you never have to do the math for display — but it is useful to know the spread is taken from your winnings, not from the maker, and never on a loss.

| Outcome       | You receive                                          | Maker receives                                          |
| ------------- | ---------------------------------------------------- | ------------------------------------------------------- |
| All legs win  | the full `netPayout` (pot, less any win-only spread) | nothing                                                 |
| Any leg loses | nothing                                              | the full pot (your net stake + its own collateral back) |

The fees (`protocolFee`, `builderFee`) are computed integer-exact in 6dp to mirror the escrow's on-chain split to the wei — the `pricing` block from `prepare` is authoritative for display.

***

## 12. Understanding `quoteHash`

This is the heart of the trust model, so it's worth a paragraph.

When an MM quotes, it signs an EIP-712 **ParlayQuote** — a 10-field struct that pins down *everything* about the deal:

```
ParlayQuote(
  address maker, address taker, bytes32 legsHash,
  uint256 takerCollateral, uint256 makerCollateral,
  address builder, uint16 protocolFeeBps, uint16 builderFeeBps,
  uint256 nonce, uint256 deadline
)
```

Note: there is no `spreadBps` in the signed struct — in v2 the spread is an escrow owner-level config, not a maker-signed term. `quoteHash` is the EIP-712 struct hash of that 10-field object. When you accept a quote, **your ParlayOrder commits only to `order.quoteHash`** — and the escrow recomputes that hash from the quote it is handed and checks it matches.

The consequence: the relayer (or anyone) **cannot perform surgery on the deal**. It cannot:

* swap in a different (worse) quote — the hash wouldn't match,
* change your legs — `legsHash` is inside the quote,
* change the collateral or your stake — both are inside the quote,
* bump a fee or the spread — all inside the quote,
* redirect the maker or builder — inside the quote.

Any tampering changes `quoteHash`, which breaks either your order signature or the maker's quote signature, and the on-chain `createParlayWithPermit` reverts. You are bound to **exactly** the terms you saw, and nothing else — that's why your order only needs four fields and no separate slippage floor.

***

## 13. Settlement

**Fill and settlement are two different moments, and only fill is pushed to you.** Your parlay *fills* the instant `createParlayWithPermit` is mined — that's the `RFQ_EXECUTION_UPDATE CONFIRMED` push from [§10](#10-step-7--accept), and it is immediate and proactive. Your parlay *settles* (i.e. actually resolves and pays out) later, asynchronously — anywhere from minutes to hours after fill — gated by real-world match completion plus a dispute window. **There is currently no proactive push for settlement.** You must watch for it yourself:

1. Each leg's `outcomeId` corresponds to a real Hyperliquid mainnet market. When the underlying FIFA match settles, that outcome finalizes on HL.
2. A **Chainlink CRE workflow** watches for resolution requests, reads the settled result from HL mainnet, and delivers a report to an on-chain consensus contract, which finalizes the result in a resolution registry the escrow trusts. The relay best-effort triggers this request (`requestResolution`) right after your parlay fills — it does **not** call `resolveLeg` itself.
3. Once an outcome is finalized **and** its dispute window has elapsed, `resolveLeg(parlayId, legIndex)` becomes callable — by anyone, permissionlessly — for each leg tied to that outcome. It's on-chain and unauthenticated; you can call it yourself if nothing else has yet.
4. When every leg on your parlay is resolved, the escrow **settles**: if every leg won, the taker's share is credited; if any leg lost, the maker's is. This emits **`ParlaySettled(parlayId, winner, payout)`** — or **`ParlayVoided(parlayId, taker, maker, takerRefund, makerRefund)`** if a leg was voided and no leg lost (both sides get refunded).
5. **Settlement credits a balance, it does not push you the money.** The escrow uses a pull-payment model: winnings and refunds are added to your `withdrawable` balance on the contract, and you (or anyone, via `withdrawTo`) must call `withdraw()` to actually receive the USDC. "Settled" and "paid out" are not the same block.

{% hint style="warning" %}
There is no working REST endpoint today that reports a single parlay's leg-by-leg resolution status or aggregate settlement state — `GET /v1/positions/combos` exists but currently always returns an empty list, and there is no `GET /v1/parlay/:id`-style route in this relay. Until one ships, **index the escrow's events directly**: `ParlayCreated` (at fill), `LegResolved(parlayId, legIndex, outcomeId, won)` (per leg, as each resolves), and `ParlaySettled` / `ParlayVoided` (at final settlement) — then call `withdraw()` once you see your `ParlaySettled`.
{% endhint %}

***

## 14. WebSocket Stream

For a live UI, run the entire flow over the taker socket — no HTTP polling at all.

```
WS  /v2/ws/taker   (production: wss://parlay.mute.sh/v2/ws/taker)
```

On connect the server immediately sends a `ready` bootstrap message:

```jsonc
{
  "type": "ready",
  "escrow": "0x551D4A7EdB71583C7C12900128727D857e308925",
  "chainId": 998,
  "limits": { "minLegs": 2, "maxLegs": 6, "minStakeUsd": 1, "maxStakeUsd": 1000 },
  "markets": [ /* same per-outcome shape as GET /v1/rfq/combo-markets, see §4 — up to 100 entries */ ],
  "stack": { "chainId": 998, "escrow": "0x2cB2…", "usdc": "0x75b6…", "registry": "0x…", "signalConsensus": "0x…", "resolutionRequester": "0x…" },
  "makers": { "connected": 5 }
}
```

This is the one place the full deployed contract stack and live MM count are exposed — verify `stack.escrow`/`stack.usdc` match what you sign against before trusting a `prepare` response. `limits` gives you the real `minLegs`/`maxLegs`/`minStakeUsd`/`maxStakeUsd` for this deployment — prefer these over hardcoding numbers from this guide, in case they're tuned later.

### Messages you send

| `type`    | Body                                                          | Effect                                                                                                                                             |
| --------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `hello`   | `{ address, reqId? }`                                         | Sets your session address. Server replies `hello:ok`.                                                                                              |
| `rfq`     | `{ taker, legs:[{outcomeId, sideIndex}], stake_usd, reqId? }` | Creates an RFQ; replies `rfq:created`, then pushes live quotes.                                                                                    |
| `prepare` | `{ rfqId, quoteId, reqId? }`                                  | Builds the order+permit; replies `prepared`.                                                                                                       |
| `accept`  | `{ rfqId, quoteId, orderSig, permitSig, reqId? }`             | Queues execution; replies `accepted` (ack only — see [§10](#10-step-7--accept)), then pushes `RFQ_EXECUTION_UPDATE` as the on-chain tx progresses. |
| `status`  | `{ rfqId, reqId? }`                                           | Request an RFQ snapshot; replies `rfq:state`.                                                                                                      |
| `ping`    | —                                                             | → `pong`.                                                                                                                                          |

### Messages you receive

| `type`                 | Meaning                                                                                                                                                                                                                                                                                                                                                        |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ready`                | Bootstrap on connect, shown above.                                                                                                                                                                                                                                                                                                                             |
| `hello:ok`             | Ack of your `hello`, echoes the registered address.                                                                                                                                                                                                                                                                                                            |
| `rfq:created`          | `{ rfqId, snapshot }` after a `rfq` message.                                                                                                                                                                                                                                                                                                                   |
| `quotes`               | `{ rfqId, count, quotes:[…], bestQuoteId }` — **pushed every time a new quote lands**.                                                                                                                                                                                                                                                                         |
| `prepared`             | The order + permit + domains + pricing to sign (same shape as REST `prepare`, §8).                                                                                                                                                                                                                                                                             |
| `accepted`             | `{ rfqId, snapshot }` — relay has received your signatures and queued execution. Not the fill result.                                                                                                                                                                                                                                                          |
| `RFQ_SNAPSHOT`         | Full RFQ state snapshot pushed on status changes.                                                                                                                                                                                                                                                                                                              |
| `RFQ_EXECUTION_UPDATE` | `{ rfq_id, status, tx_hash?, parlay_id? }` — `status` is `MATCHED` (tx submitted) → `CONFIRMED` (mined, parlay filled) or `FAILED`. There is no literal `"FILLED"` status string here — `FILLED` is an `RFQSnapshot.status` value and the terminal result the `placeCombo()` SDK helper resolves to, but the execution-update payload itself uses `CONFIRMED`. |
| `RFQ_ERROR`            | `{ code: "TRADE_SUBMISSION_FAILED", error, rfq_id }` — sent (in addition to the `FAILED` execution update) when the on-chain submission itself fails. Distinct from `rfq:error` below.                                                                                                                                                                         |
| `rfq:error`            | `{ error, code, reqId? }` — protocol error for a specific client request (bad message, unknown RFQ, etc).                                                                                                                                                                                                                                                      |
| `pong`                 | Heartbeat reply.                                                                                                                                                                                                                                                                                                                                               |

The `quotes` push is the no-poll core: subscribe once, and your odds update live as the MMs compete. Pick a `quoteId`, send `prepare`, sign the `prepared` payload, send `accept`, then watch for `RFQ_EXECUTION_UPDATE`. There is no further push once you reach `CONFIRMED` — settlement is untracked by this channel; see [§13](#13-settlement).

***

## 15. Error Cases

Request-level failures come back as `{ "error": "<message>", "code": "<CODE>" }` (WS: also as an `rfq:error` message with the same fields) with a `4xx`/`5xx` status over REST. The `code` values below come directly from the relay's error type — handle on `code`, not the message string:

| `code`                                                                   | Where                                       | What it means / what to do                                                                                                                                                                                                                                |
| ------------------------------------------------------------------------ | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INVALID_RFQ`                                                            | create                                      | Bad shape (no legs), stake outside `$1–$1000`, leg count outside `[minLegs, maxLegs]` (2–6 by default), or an outcome already finalized. Pre-validate with the `limits` from `ready`/[§4](#4-step-1--browse-markets).                                     |
| `CONTRADICTORY_LEGS`                                                     | create                                      | One of: the same outcome on both YES and NO, two legs from the same underlying match, the same entity (team) in two legs, or legs that mix categories (e.g. sports + crypto) — see [§18](#18-reference--combo-conflict-checks).                           |
| `LEG_METADATA_UNAVAILABLE`                                               | create                                      | An `outcomeId` that isn't a live, known market. Re-check against `GET /v1/rfq/combo-markets`.                                                                                                                                                             |
| `UNKNOWN_RFQ`                                                            | `prepare`/`accept`/`status`                 | The RFQ ID doesn't exist — typo, or it already terminated (expired/filled/rejected). Re-broadcast.                                                                                                                                                        |
| `EXPIRED_RFQ`                                                            | create / `accept`                           | The RFQ's short quote-collection window elapsed with no acceptable quote, or your prepared order/permit ticket aged out (10 min). Re-broadcast or re-`prepare`.                                                                                           |
| `QUOTE_UNAVAILABLE` / `QUOTE_MISMATCH`                                   | `accept`                                    | The quote you're accepting isn't the current winner, or was withdrawn/expired. Re-fetch quotes and pick the current best.                                                                                                                                 |
| `INVALID_ACCEPTANCE`                                                     | `accept`                                    | Missing `orderSig`/`permitSig`, wrong RFQ status, or no `prepare` ticket on file — call `prepare` first.                                                                                                                                                  |
| `PRE_EXECUTION_BALANCE_RESERVATION_FAILED` / `BALANCE_VALIDATION_FAILED` | execution (async, via `RFQ_ERROR`/`FAILED`) | Your USDC balance doesn't cover the stake, or the maker's standing allowance can't cover its collateral. Fund your address and retry with a fresh RFQ.                                                                                                    |
| `TRADE_SUBMISSION_FAILED`                                                | execution (async, via `RFQ_ERROR`)          | The on-chain `createParlayWithPermit` call reverted or timed out after `accept` returned. Comes with the `FAILED` `RFQ_EXECUTION_UPDATE` — see [§10](#10-step-7--accept).                                                                                 |
| Last-look decline                                                        | execution                                   | The winning MM opted into last-look and declined (or didn't answer within \~1s). The engine automatically falls back to the next-best quote (up to 2 fallbacks) before giving up — you don't need to manually re-pick unless all fallbacks are exhausted. |
| Signature mismatch                                                       | `accept`                                    | Order/permit didn't recover to your address — you signed the wrong domain or stale `prepare` data. Re-`prepare` and re-sign.                                                                                                                              |

There is currently **no documented rate limiting** on this relay's endpoints (unlike the older v1 demo, which does rate-limit) — don't assume a `429` means you're being throttled by design; treat it as a signal to back off and retry regardless.

> **Last look** is opt-in per MM and off by default. If the MM you picked enabled it, the relay gives that MM \~1 second to confirm before submitting on-chain; no response is treated as a decline. A decline never strands you — the engine automatically retries the next-best quote.

***

## 16. Getting Test USDC

Testnet only — you need the mock USDC at `0x75b69d0E8fA3CdB644b0f069E4e876B85A76E137` (6dp, EIP-2612) in your wallet to stake anything.

**This v2 relay does not expose a faucet or a balance endpoint.** If you're used to the older demo's `POST /api/faucet` / `GET /api/balance/:address`, those are v1-only — they belong to a separate process and are not part of this relay's API surface. Mint test USDC directly against the mock token contract instead (it's a standard test ERC-20 with an open `mint`, or coordinate with the team for a testnet drop), and check your balance with a normal `balanceOf` read on `0x75b69d0E8fA3CdB644b0f069E4e876B85A76E137` via any RPC client.

***

## 17. Reference — v2 Endpoints

### REST

| Method & Path                 | Purpose                  | Body / Params                       | Returns                                                                                                                                                    |
| ----------------------------- | ------------------------ | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /health`                 | Relay health             | —                                   | `{ ok, redis, makers, openRfqs, markets }`                                                                                                                 |
| `GET /v1/rfq/combo-markets`   | Available combo markets  | `?limit&cursor`                     | `{ markets, next_cursor }` — see [§4](#4-step-1--browse-markets)                                                                                           |
| `GET /v1/positions/combos`    | Taker's open positions   | —                                   | **Stub** — always returns `{ combos: [], pagination: {...} }` today. Don't rely on it yet; see [§13](#13-settlement) for the real way to track a position. |
| `POST /v1/rfq`                | Create an RFQ            | `{ taker, legs, stake_usd }`        | RFQ snapshot — see [§5](#5-step-2--create-the-rfq)                                                                                                         |
| `GET /v1/rfq/:rfqId`          | RFQ snapshot             | path: `rfqId`                       | RFQ snapshot (not a quote list — see [§6](#6-step-3--get-quotes))                                                                                          |
| `POST /v1/rfq/:rfqId/prepare` | Build order+permit       | `{ quote_id }`                      | `{ quote_id, order, permit, domains, pricing }` — see [§8](#8-step-5--prepare--sign-the-parlayorder)                                                       |
| `POST /v1/rfq/:rfqId/accept`  | Queue on-chain execution | `{ quote_id, orderSig, permitSig }` | RFQ snapshot — **an ack, not the fill result**; watch `RFQ_EXECUTION_UPDATE` for that, see [§10](#10-step-7--accept)                                       |

### WebSocket

| Endpoint          | Role                                                             |
| ----------------- | ---------------------------------------------------------------- |
| `WS /v2/ws/taker` | Taker: full bidirectional flow — see [§14](#14-websocket-stream) |
| `WS /v2/ws/rfq`   | Maker: authenticated RFQ channel — see maker guide               |

***

## 18. Reference — Combo Conflict Checks

The relay runs **four** leg-conflict checks server-side, before an RFQ is ever broadcast to a market maker — all four surface as `CONTRADICTORY_LEGS` (or `LEG_METADATA_UNAVAILABLE` for a dead/unknown leg), checked in this order:

### 1. Same outcome, both sides

**A leg can't appear as both YES and NO** in one combo — that's a direct self-contradiction, not just a correlation issue, so it's checked first.

> ❌ Brazil to win (YES) **+** Brazil to win (NO) → rejected: *"outcome 4210 appears on both sides."*

### 2. Mutual exclusion (same market)

**No two legs from the same underlying market.** A 3-way match market has mutually-exclusive outcomes (Team A win / Draw / Team B win) — you cannot combine two of them, because at least one is guaranteed to lose, which would make the "parlay" meaningless or trivially un-winnable.

> ❌ Brazil to win **+** Brazil-vs-Argentina is a Draw → rejected: *"two legs from the same market."*

### 3. No same entity across legs

**No team appears twice across the whole parlay**, even in *different* matches. This blocks correlated bets where the same entity drives multiple legs (and would let you construct a lower-variance position the fixed-odds book isn't priced for).

> ❌ Brazil to win (vs Argentina) **+** Brazil to win (vs Mexico, later round) → rejected: *"entity "Brazil" appears in two legs."*

### 4. Category homogeneity

**A combo must stay within one category** (e.g. all soccer, or all crypto — never mixed). Legs without a known category don't trigger this check.

> ❌ Brazil to win (soccer) **+** BTC above $100k (crypto) → rejected: *"cannot mix categories in one combo."*

Plus the hard structural limits: **2–6 legs** (`minLegs`/`maxLegs`, confirm the live values from the WS `ready` message's `limits` — see [§14](#14-websocket-stream)), and stake **$1–$1000** (`minStakeUsd`/`maxStakeUsd`). There is currently no `maxPayoutUsd` cap on this deployment. All four conflict checks and the structural limits are enforced identically whether you go through `POST /v1/rfq` or the `rfq` WS message — there's no way to sneak a contradictory combo past them by picking one transport over the other.

If you're building a market picker and want to understand *why* a combo is rejected before you even try broadcasting it — e.g. what exactly counts as "the same market," or how the combo's own identity hash (its `condition_id`) is derived from the leg set — the full mechanics live in [Maker Protocol Spec §6, Combo identity](/mute.sh/reference/maker-protocol-spec.md#6-combo-identity-condition_id-position-ids-and-conflict-checks); this guide only covers what a taker needs to build valid legs.

***

### Appendix — Field cheat-sheet

* **`outcomeId`** (uint32): HL outcome you bet on. Derived from a market's `position_ids` in `GET /v1/rfq/combo-markets` — see [§4](#4-step-1--browse-markets).
* **`sideIndex`** (0|1): `0` = YES/outcome happens, `1` = NO/it doesn't. On settlement a leg `won` when `result == (sideIndex === 0 ? 1 : 2)`.
* **`stake`** (number, USD): gross dollars in. Fees come off this.
* **`makerCollateral`** / **`netPayout`** / **`odds`**: higher collateral → higher payout → higher odds. The book is sorted by `netPayout`.
* **`quoteHash`**: the only thing your order signs; binds you to the full quote (§12).
* All on-chain USDC amounts are **6-decimal** fixed-point (e.g. `"25000000"` = 25 USDC).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://mute-sh.gitbook.io/mute.sh/integration-guides/taker-guide.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
