> 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/maker-guide.md).

# Market Maker Guide

Become a maker on the Parlay RFQ relay: receive prediction-market parlay RFQs, price them on your own key, sign EIP-712 quotes, and win the trade by offering the taker the best payout. The relay is a neutral broadcaster — it never signs a quote and never holds your key.

{% hint style="info" %}
**Audience:** EVM engineers who have integrated with RFQ systems before (0x, Hashflow, Paradex, Polymarket CLOB/combos). If you've signed an EIP-712 maker order and posted it to a relayer, you already know 80% of this. The wire protocol here is deliberately Polymarket-combos-parity (message names, RFQ state machine, error codes) adapted to HyperLiquid outcome IDs.

**Trustless-first principle:** the relay tells you things (advisory leg probabilities, fee regime, "you won"). *Verify everything before you sign or settle.* Every section below tells you what to re-derive yourself.
{% endhint %}

***

## 1. Overview

The Parlay RFQ system lets a taker request a quote for a **multi-leg parlay** over HIP-4 prediction-market outcomes (e.g. "Brazil to win AND over 2.5 goals AND Mbappé to score"). Each leg is a real Hyperliquid HIP-4 market that settles on-chain via a Chainlink CRE DON reading FIFA/sports settlement.

As a maker, you are the counterparty. You post **`makerCollateral`** into escrow against the taker's stake. If the parlay loses, you keep the taker's stake. If it wins, the taker takes the whole pot (their net stake + your collateral) and you get nothing.

**Why be a maker?** You earn the gap between the *true* combined probability of the legs and the *odds you quote*. Quote a tighter edge than your competitors and you win the RFQ; quote too tight and you lose money on expectation. Your `edgeBps` is your house margin. The relay runs a sealed competitive auction: lowest effective edge (highest taker payout) wins.

**What the relay is and isn't:**

| The relay DOES                                                           | The relay does NOT          |
| ------------------------------------------------------------------------ | --------------------------- |
| Push every taker RFQ to all connected, authenticated makers              | Sign quotes (it has no key) |
| Verify your EIP-712 signature recovers to your address                   | Custody your funds          |
| Pick the best quote by taker payout                                      | Set your prices             |
| Submit the accepted order on-chain as relayer                            | Decide odds for you         |
| Enforce a server-authoritative fee/spread regime + combo conflict checks | See your private key        |

The escrow contract verifies your signature on-chain (per-owner nonce bitmap). Quote signing happens entirely client-side on your own key, so the relay adds **no new trust assumption** over a direct on-chain interaction.

***

## 2. Architecture

![Maker architecture and data flow](/files/qMOwuFb83BrnnQcgPVZZ)

Key invariant: **prices flow to you directly from Hyperliquid, not through the relay.** The `prob` field on each RFQ leg is advisory display-only. You derive your own probability from `allMids` keyed on `outcomeId`, which is *the same market that settles the leg.*

***

## 3. Quick Start

{% hint style="info" %}
There is a maker SDK — a real, tested TypeScript client for the `/ws/rfq` channel that handles auth, reconnect-with-backoff, heartbeat, and ACK correlation for you. It leaves pricing strategy to you (or you can use its pure pricing helpers). You are also free to speak the raw WebSocket protocol directly — §4–§18 below document every message shape so you can implement your own client in any language.
{% endhint %}

The relay never receives your private key; it only ever sees your address and your signatures.

```ts
const mm = createParlayMakerClient({
  url: "wss://parlay.mute.sh/v2/ws/rfq",        // or your local/staging relay
  apiKey: "your-api-key",                        // issued out-of-band by relay operator
  signer: accountSigner(privateKey),              // your maker key — NEVER leaves this process
  lastLook: false,                                // opt into last-look? OFF by default (see §9)
  onEvent: (ev, data) => console.log(ev, data),
});

await mm.connect(); // opens the socket, sends auth, resolves once the relay confirms

mm.on("rfqRequest", async (rfq) => {
  await mm.submitQuote(rfq, domain, {
    edgeBps: 500,                                 // 5% house edge on every quote
    jitterBps: 40,                                 // ±0.4% so same-edge MMs differ
    priceForLeg: (i, leg) => defaultPriceSource(leg.outcomeId), // HL allMids, trustless
  });
});
```

`connect()` opens the socket (with exponential-backoff auto-reconnect) and authenticates. `submitQuote()` recomputes and verifies `condition_id` against the RFQ's legs before pricing, prices against the net stake, applies your edge, signs the EIP-712 `ParlayQuote`, and sends it — refusing to quote on a hash mismatch (§6).

If you'd rather price without the convenience wrapper, `priceQuote()` and `combinedProbability()` are exported as pure functions you can call directly with your own leg-price resolver.

***

## 4. Registration

Authentication is a two-factor handshake at the WebSocket layer: an **API key** (issued out-of-band) gates connection, and your **maker address** (recovered from every quote signature) is your real identity.

### API key

The demo/local-dev key is `mm-demo-key`. For production, request a key from the relay operator. The key is checked once in the `auth` message; a bad key gets an `auth` failure response and the socket is unusable until a fresh, valid `auth` is sent.

### The `auth` message

On socket open, send:

```jsonc
{
  "type": "auth",
  "auth": { "apiKey": "mm-demo-key" },          // must match the relay's configured MM API key
  "identity": {
    "maker_address": "0x1234…",                  // your EVM address, recovered from every quote signature
    "signer_address": "0x1234…",                  // usually == maker_address; distinct only for delegated signing setups
    "signature_type": 0                            // 0 = EOA (only type implemented today)
  },
  "approval": {                                    // OPTIONAL: standing EIP-2612 permit (see §8)
    "value": "1606938044258990275541962092341162602522202993782792835301376",
    "deadline": "1750000000",
    "v": 28, "r": "0x…", "s": "0x…"
  },
  "last_look": false                               // opt into last-look? OFF by default (see §9)
}
```

The relay replies:

```jsonc
{ "type": "auth", "success": true, "address": "0x1234…", "role": "maker" }
```

or, on failure:

```jsonc
{ "type": "auth", "success": false, "error": "invalid apiKey" }
```

There is **no backfill of in-flight RFQs on connect.** Unlike a book snapshot, the relay only ever pushes `RFQ_REQUEST` for RFQs created *after* you're authenticated — if you connect mid-auction you simply see the next one. Every message you send before a successful `auth` response is rejected with `RFQ_ERROR{code:"UNAUTHENTICATED"}`.

{% hint style="info" %}
**Identity is forced server-side.** The relay authenticates the session to your `maker_address`/`signer_address`. You cannot quote as another maker, and you cannot censor a rival — the competition key is the address your signature recovers to.
{% endhint %}

***

## 5. Pricing

### Trustless vs advisory

Each RFQ leg carries two probability signals:

* **`outcomeId`** (encoded in `leg_position_ids` as `"#${outcomeId*10+sideIndex}"`) — the *canonical* leg identity. It maps to a real HIP-4 coin you can price yourself: `coinForOutcome(id) = "#${id * 10}"`. This is the same market that settles the leg.
* **`prob`** / **`label`** on the taker-facing leg view — **advisory only**. A trustless maker never prices off these; price your own leg probability independently.

### `defaultPriceSource` (HL `allMids`)

The default price source reads the public Hyperliquid info API. A HIP-4 outcome coin's spot mid ∈ (0,1) *is* its market-implied probability:

```ts
export async function defaultPriceSource(outcomeId: number): Promise<number> {
  const mids = await fetchAllMids();                 // POST /info {type:"allMids"}, cached briefly
  const p = parseFloat(mids[coinForOutcome(outcomeId)]); // coin "#${id*10}"
  if (!Number.isFinite(p)) throw new Error(`no live mid for outcome ${outcomeId}`);
  return Math.min(0.98, Math.max(0.02, p));          // clamp away from 0/1
}
```

Supply your own leg-price resolver to inject a proprietary model, an order-book microprice, or a hedged book view. If your independent read fails for a leg, treat that as a degraded mode — do not fall back to silently quoting on the taker-facing advisory `prob`.

### Edge, jitter, and how `makerCollateral` is computed

Price against the **net stake** (taker stake after deposit fees — see §7), apply your edge, and derive the collateral you must post:

```ts
// combined = ∏ over legs of (sideIndex===0 ? p : 1 - p), clamped to [0.0001, 0.98]
const jitter = jitterBps ? (Math.random() * 2 - 1) * jitterBps : 0;
const edge   = 1 - Math.max(0, edgeBps + jitter) / 10_000;   // e.g. 500bps → 0.95
const payout         = (netStakeUsd * edge) / combined;       // gross pot the taker wins
const makerCollateral = Math.max(0.01, payout - netStakeUsd); // YOUR collateral into escrow
```

* **Higher `edgeBps` → lower payout → lower `makerCollateral` → you rarely win.**
* **Lower `edgeBps` → you win more but bank less margin per trade.**
* **`jitterBps`** randomizes ± so two makers on the same edge don't deadlock the tie-break.

The pot the taker can win is `netStake + makerCollateral`. You post only `makerCollateral`. You pay **zero fees** (§7).

The `price_e6` you send alongside your quote must equal `round(netStake * 1e6 / (netStake + makerCollateral))` (round-half-up) — the relay recomputes this itself and rejects a mismatch beyond ±1 unit as `QUOTE_MISMATCH`. Likewise `size_e6` must equal `netStake + makerCollateral` exactly.

***

## 6. Quote Signing

You sign an EIP-712 `ParlayQuote` on your own key. The relay re-derives the signer off-chain and the escrow re-derives it on-chain; both must equal your `maker` address.

### EIP-712 domain

```ts
const domain = {
  name: "ParlayEscrow",
  version: "1",
  chainId: 998,                                          // HyperEVM testnet
  verifyingContract: escrowAddress,                       // from the RFQ's escrow_terms, or the Appendix
};
```

{% hint style="warning" %}
Never hardcode `verifyingContract`. Read it from the RFQ's `escrow_terms.escrow` field (or confirm it against the Appendix at connect time) — a quote signed against the wrong escrow address never recovers to your address on the real deployment.
{% endhint %}

### The 10 `ParlayQuote` fields

The v2 escrow has **no `spreadBps` field** in the signed struct — spread is an owner-level escrow config, not a maker-signed term.

```ts
const ParlayQuote = [
  { name: "maker",          type: "address" }, // your address (signature must recover to this)
  { name: "taker",          type: "address" }, // from the RFQ's escrow_terms
  { name: "legsHash",       type: "bytes32" }, // == condition_id — VERIFY IT (see below)
  { name: "takerCollateral",type: "uint256" }, // GROSS taker stake, 6dp USDC — echo from RFQ
  { name: "makerCollateral",type: "uint256" }, // YOUR collateral, 6dp — determines the odds
  { name: "builder",        type: "address" }, // echo from RFQ (zero if no builder)
  { name: "protocolFeeBps", type: "uint16"  }, // echo from RFQ EXACTLY
  { name: "builderFeeBps",  type: "uint16"  }, // echo from RFQ EXACTLY
  { name: "nonce",          type: "uint256" }, // your random 256-bit nonce
  { name: "deadline",       type: "uint256" }, // unix seconds; quote dies after this
] as const;
// spreadBps is NOT signed — it is owner-set escrow config (v2). A v1 ParlayQuote has 11 fields
// (adds spreadBps between builderFeeBps and nonce); if you see 11 fields you're looking at a
// v1 shape that will not verify against the v2 escrow's typehash.
```

### Signing with viem

```ts
import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount(privateKey);

const quote = {
  maker: account.address,
  taker: escrowTerms.taker,
  legsHash: rfq.condition_id,
  takerCollateral: BigInt(escrowTerms.taker_collateral_e6),
  makerCollateral: parseUnits(makerCollateral.toFixed(6), 6),
  builder: escrowTerms.builder,
  protocolFeeBps: escrowTerms.protocol_fee_bps,
  builderFeeBps:  escrowTerms.builder_fee_bps,
  // spreadBps is NOT signed in v2 — owner-set escrow config. Do NOT include here.
  nonce: rand256(),               // crypto.getRandomValues → 32-byte bigint
  deadline: BigInt(Math.floor(Date.now() / 1000) + 60),
};

const quoteSig = await account.signTypedData({
  domain, types: { ParlayQuote }, primaryType: "ParlayQuote", message: quote,
});
```

### Why you MUST verify `condition_id` / `legsHash`

{% hint style="danger" %}
The relay hands you `condition_id` on the RFQ, but **you sign collateral against it.** A malicious or buggy relay could hand you a `condition_id` that doesn't correspond to the legs it showed you. Recompute it yourself and refuse to quote on a mismatch:

```ts
const LEG_TYPEHASH = keccak256(toBytes("ParlayLeg(uint32 outcomeId,uint8 sideIndex)"));
const legsHashOf = (legs: {outcomeId:number; sideIndex:number}[]) => {
  const hashes = legs.map(l => keccak256(
    encodeAbiParameters(parseAbiParameters("bytes32, uint32, uint8"),
      [LEG_TYPEHASH, l.outcomeId, l.sideIndex])));
  return keccak256(encodePacked(hashes.map(() => "bytes32"), hashes)); // legs in canonical (sorted) order
};

if (legsHashOf(legs).toLowerCase() !== rfq.condition_id.toLowerCase()) {
  throw new Error("legsHash mismatch — refuse to quote");
}
```

This is a cheap, mechanical check — do it on every RFQ before you sign, whether you're using the maker SDK (which does it for you) or your own pricer.
{% endhint %}

`condition_id` is synonymous with the on-chain `legsHash`: it's a `keccak256` over the array of per-leg hashes, computed over the **canonical** leg order (sorted by `(outcomeId, sideIndex)`, deduplicated) — the same canonicalization the relay applies before ever broadcasting the RFQ, so a correctly-recomputed hash always matches. The relay has already run the full combo-identity and conflict-check pipeline (contradictory legs, dead/unknown outcomes, same-market and same-entity collisions, category homogeneity) before an RFQ ever reaches you — 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 complete conflict-check taxonomy and error codes. You're re-deriving the hash for signature-safety, not re-running those business-rule checks yourself.

***

## 7. Fee Regime

Fees are **server-authoritative** and come entirely off the **taker**. You, the maker, pay nothing — you simply post `makerCollateral` and receive it back (plus the taker's net stake) on a loss, or forfeit it on a win.

```
At deposit (escrow, on-chain):
  protocolFee = takerCollateral * protocolFeeBps / 10000   // dynamic, scales with leg count + stake size
  builderFee  = takerCollateral * builderFeeBps  / 10000   // only if builder != 0x0
  netStake    = takerCollateral - protocolFee - builderFee

Maker prices against netStake (NOT the gross takerCollateral).
The signed takerCollateral stays GROSS; the escrow splits fees on-chain.

On a WIN (taker), a win-only spread markdown applies to the payout:
  pot              = netStake + makerCollateral
  effectivePayout  = pot * (1 - spreadBps / 10000)
```

`protocolFeeBps` and `spreadBps` are **both dynamic, computed per RFQ at creation time** — not fixed constants:

* **`protocolFeeBps`** scales with leg count (more legs, more settlement/correlation risk) and tapers down for large stakes. It's clamped so `protocolFeeBps + builderFeeBps` never exceeds the contract's fee cap.
* **`spreadBps`** is solved in closed form so the *total* expected protocol hold (certain deposit fee + expected win-only markdown) hits a target percentage of the gross stake, regardless of the parlay's odds. It can be `0` when the deposit fee alone already meets that target, but is not zero by default in general.

Both values are fixed at RFQ-creation time and handed to you (and every other maker quoting that RFQ) as part of the RFQ's fee/escrow terms — you don't choose or negotiate them, you echo them verbatim into your signed quote.

Key consequences for a maker:

* **You quote on the RFQ's net stake, not the gross stake.** The maker SDK does this automatically if you use `submitQuote`.
* **You pay no protocol or builder fee.** Those are taker-side, deducted at deposit.
* **`spreadBps` is a win-only protocol markdown** on the taker's payout (owner-config in v2, computed dynamically per RFQ). It does not change your collateral, and it is **not a field in your signed quote** — the escrow enforces it independently as owner config.
* **On a loss you get the full `makerCollateral` back plus the net stake; on a win you get 0.** Standard binary-counterparty economics.

***

## 8. Standing Allowance

You don't want to sign a USDC permit on every quote — that's latency and a signing round-trip per RFQ. Instead, send **one standing EIP-2612 permit at auth time**; the relay submits it to the USDC token's own `permit()` function (paying the gas itself), setting your on-chain allowance to the escrow once. After that, every quote you send needs no fresh permit at all.

### One-time permit (in `auth.approval`)

```ts
const PERMIT_TYPES = { Permit: [
  { name: "owner",    type: "address" }, { name: "spender", type: "address" },
  { name: "value",    type: "uint256" }, { name: "nonce",   type: "uint256" },
  { name: "deadline", type: "uint256" },
] };

const value    = 2n ** 200n;                                   // effectively unlimited
const deadline = BigInt(Math.floor(Date.now()/1000) + 365*24*3600); // 1 year
const nonce    = await pub.readContract({ /* USDC.nonces(maker) */ });

const sig = await account.signTypedData({
  domain: { name: "USD Coin", version: "1", chainId: 998, verifyingContract: usdcAddress },
  types: PERMIT_TYPES, primaryType: "Permit",
  message: { owner: maker, spender: escrow, value, nonce, deadline },
});
// → { value, deadline, v, r, s } sent in the auth message's `approval` field; the RELAYER submits it (gasless to you)
```

The `approval` field on `auth` is optional: if you already hold a qualifying on-chain USDC allowance for the escrow (from a prior session, or set up out-of-band), you can omit it entirely and just authenticate.

### Quotes need no per-quote permit

Because the standing allowance already exists on-chain, the relay's execution path uses it directly when it submits your winning quote — you do not attach a fresh permit signature to every `RFQ_QUOTE` you send. There is nothing extra for you to sign or include per quote beyond the `ParlayQuote` itself.

{% hint style="warning" %}
**Gate: a maker with no standing allowance cannot win.** The relay rejects quotes from unapproved makers with `RFQ_ERROR{code:"ALLOWANCE_VALIDATION_FAILED"}` ("maker has no standing allowance") because placing one on-chain would be a guaranteed revert, burning relayer gas on a phantom quote. Always complete `auth` (with `approval`, or a pre-existing allowance) before quoting. The relay also checks your live USDC balance covers `makerCollateral` at quote time (`BALANCE_VALIDATION_FAILED` if not) — a standing allowance alone isn't sufficient if you're temporarily out of funds.
{% endhint %}

***

## 9. Last Look

Last look lets the **winning** maker confirm or reject a trade in the moment before the relayer submits it on-chain. It's **opt-in and OFF by default**, set once at `auth` time (`last_look: true`) — you cannot toggle it per-RFQ.

### Opt in

Set `last_look: true` in your `auth` message (§4). From then on, every RFQ you win as the current-best quote triggers a confirmation round-trip before execution.

### The state machine

After the taker accepts your quote, the RFQ enters **`AWAITING_MAKER_CONFIRMATION`** instead of going straight to execution. The relay sends you:

```
relay ─ RFQ_CONFIRMATION_REQUEST ──▶ winning MM
  { rfq_id, quote_id, signer_address, maker_address, signature_type,
    leg_position_ids, condition_id, yes_position_id, no_position_id,
    direction, side, fill_size_e6, price_e6, confirm_by }
```

You respond:

```
MM ─ RFQ_CONFIRMATION_RESPONSE ──▶ relay
  { rfq_id, quote_id, decision: "CONFIRM" | "DECLINE" }
```

and the relay ACKs with `ACK_RFQ_CONFIRMATION_RESPONSE`.

* **`CONFIRM`** → the RFQ moves to `EXECUTING` and the relayer submits the on-chain transaction.
* **`DECLINE`** → your quote is dropped from consideration (you're marked declined for this RFQ) and the engine falls back to the next-best live quote from a different maker, if one exists.
* **Timeout at `confirm_by`** (configurable, **default 1000ms**, and also enforced if your socket is disconnected or otherwise undeliverable) is treated **exactly like a `DECLINE`** — last look is **fail-closed**, not fail-open. A slow or unresponsive maker loses the trade to the next-best quote; it does not get executed on your behalf by default.
* **Fallback budget:** the engine will retry against the next-best quote up to **`maxFallbacks` times (default 2)** before giving up and rejecting the RFQ entirely (terminal status `REJECTED`) if every attempted maker declines or times out. Each fallback re-enters `AWAITING_REQUESTER_ACCEPTANCE` first — the taker's signature was bound to the declined quote's hash, so the taker must re-accept against the new quote before your (or another maker's) last-look window opens again.
* Only the *currently-winning* maker that opted in is ever asked, and only that maker may `CONFIRM`/`DECLINE` — a confirmation from any other address is rejected as `UNAUTHORIZED_ROLE`.

Use last look sparingly — it adds a real round-trip of latency to every trade you win, and a high decline rate means you lose trades outright (fail-closed) rather than just falling behind in a queue. It exists for genuine stale-quote / inventory-limit protection, not as a routine "look before you leap." If you don't need it, leave `last_look: false` and your fills go straight from acceptance to execution.

***

## 10. WebSocket Events

Connect at **`/ws/rfq`** (production: `wss://parlay.mute.sh/v2/ws/rfq`). All messages are JSON with a `type` discriminator.

### Messages the relay SENDS to you

| `type`                          | Payload (key fields)                                                            | Meaning                                                                                               |
| ------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `auth`                          | `{ success, address?, role?, error? }`                                          | Response to your `auth` — connected + authed, or rejected                                             |
| `RFQ_REQUEST`                   | `RFQRequest` (rfq\_id, leg\_position\_ids, condition\_id, escrow\_terms, …)     | A new taker RFQ. Price + quote it                                                                     |
| `ACK_RFQ_QUOTE`                 | `{ rfq_id, quote_id }`                                                          | Your quote was accepted into the book                                                                 |
| `ACK_RFQ_QUOTE_CANCEL`          | `{ rfq_id, quote_id }`                                                          | Your cancel was processed                                                                             |
| `RFQ_CONFIRMATION_REQUEST`      | `{ rfq_id, quote_id, confirm_by, … }`                                           | You won + opted into last look; confirm/decline (§9)                                                  |
| `ACK_RFQ_CONFIRMATION_RESPONSE` | `{ rfq_id, quote_id, decision }`                                                | Your confirm/decline was recorded                                                                     |
| `RFQ_EXECUTION_UPDATE`          | `{ rfq_id, status, tx_hash? }`                                                  | `MATCHED`/`MINED`/`RETRYING`/`CONFIRMED`/`FAILED` — pushed to the winning maker as its trade executes |
| `RFQ_TRADE`                     | `{ rfq_id, condition_id, leg_position_ids, price_e6, size_e6, executed_at, … }` | Broadcast to **all** makers (winners and losers) once a trade executes                                |
| `RFQ_ERROR`                     | `{ code, error, request_type?, rfq_id?, quote_id? }`                            | One of the 28 typed `RfqErrorCode`s (bad key/identity, mismatch, expired, etc.)                       |
| `pong`                          | `{}`                                                                            | Reply to your `ping`                                                                                  |

### Messages you SEND to the relay

| `type`                      | Payload                                                                                            | When                                             |
| --------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `auth`                      | `{ auth:{apiKey}, identity:{maker_address,signer_address,signature_type}, approval?, last_look? }` | Once, on socket open                             |
| `RFQ_QUOTE`                 | `{ rfq_id, price_e6, size_e6, signed_order }`                                                      | Per RFQ you choose to price                      |
| `RFQ_QUOTE_CANCEL`          | `{ rfq_id, quote_id, signer_address, maker_address }`                                              | Withdraw a live quote                            |
| `RFQ_CONFIRMATION_RESPONSE` | `{ rfq_id, quote_id, decision }`                                                                   | Only when answering a `RFQ_CONFIRMATION_REQUEST` |
| `ping`                      | `{}`                                                                                               | Heartbeat (optional)                             |

There is no `hello`/`welcome` handshake and no `lastlook`/`lastlook:ack`/`quote:reject` message pair — those names belong to an older, unrelated v1 relay protocol. If you're reading code or docs that mention them, you're looking at the wrong protocol version for this endpoint.

***

## 11. Winning Quote Selection

The relay runs a **sealed competitive auction per RFQ**. The winner is the quote with the **lowest `price_e6`** (price is expressed as net-stake-over-payout, so a lower price means a bigger payout for the same stake — equivalently, the taker gets the highest payout / you're offering the tightest effective edge).

* **Ranking key:** lowest `price_e6` wins; since fees and spread are identical across all makers on a given RFQ, this is equivalent to **highest `makerCollateral` wins.**
* **Tie-break:** earliest received. Quote fast.
* **One quote per maker per RFQ:** a new quote from the same address *replaces* your prior one (dedup is on the recovered maker address). Re-quote freely to improve your price — canceled or expired quotes never count against you.
* Quotes are only considered while the RFQ is in `COLLECTING_QUOTES` — once that window closes (a short, fixed duration from RFQ creation), late quotes are rejected as `SUBMISSION_WINDOW_CLOSED`.

To win consistently: tighten `edgeBps`, keep `jitterBps` small, and minimize your quote latency.

***

## 12. Combo Conflict Checks

Every RFQ's leg set has already passed the relay's combo-identity and conflict-check pipeline **before** it is ever broadcast to you — you will never be asked to price a structurally invalid or self-contradictory combo. The checks the relay runs (in order) reject the whole RFQ before creation if they fail:

* **Same-outcome-both-sides** — a combo cannot contain YES and NO of the same outcome.
* **Leg metadata / liveness** — every leg must resolve to a real, still-live market.
* **Mutual exclusion** — at most one leg per underlying market/question (prevents e.g. "Team A wins" AND "Team A wins by 2+" from the same fixture being priced as independent).
* **No same entity across legs** — the same team/entity cannot appear twice across the whole parlay, even in different matches.
* **Category homogeneity** — a combo must stay within one category (e.g. no mixing sports and crypto legs in one parlay).

All of these surface as the `CONTRADICTORY_LEGS` or `LEG_METADATA_UNAVAILABLE` error codes if you ever see a rejected RFQ creation attempt reflected elsewhere in the system — as a maker you never handle them directly, since a non-conforming combo simply never reaches you as an `RFQ_REQUEST`. 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 exhaustive taxonomy and exact error codes.

The combined probability you compute (`∏ p_i`, independence assumption) is a reasonable default given these guards, but they're a floor, not a ceiling — if you believe residual correlation exists between legs the relay didn't catch, widen your `edgeBps` or decline to quote.

***

## 13. Self-Trade Guard

The escrow **reverts on-chain if the taker's accepted quote binds `maker == taker`** ("Self-trade not allowed"). This matters most in **demo / test setups** where one operator runs both a maker bot and a taker UI from related keys — make sure your maker key is distinct from any taker key you test with, or a trade you'd win against your own RFQ will fail on-chain instead of settling.

In production this is a non-issue (makers and takers are different parties), but the guard is unconditional at the contract level, so don't design any "wash" or self-hedging flow that routes through a single address on both sides.

***

## 14. Field-Match Guards

The relay is the source of truth for the **fee/spread/builder regime and the leg set.** Your signed quote must echo these *exactly* or it's rejected with `RFQ_ERROR{code:"QUOTE_MISMATCH"}` before it ever enters the book. The relay reconstructs the canonical quote fields from the RFQ and compares field-by-field:

| Field                  | Rule                                                                                                                     |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `legsHash`             | must equal the RFQ's `condition_id`                                                                                      |
| `taker`                | must equal the RFQ's taker                                                                                               |
| `builder`              | must equal the RFQ's builder                                                                                             |
| `protocolFeeBps`       | must equal the RFQ's `escrow_terms.protocol_fee_bps`                                                                     |
| `builderFeeBps`        | must equal the RFQ's `escrow_terms.builder_fee_bps`                                                                      |
| `takerCollateral`      | must equal the RFQ's gross `escrow_terms.taker_collateral_e6`                                                            |
| `price_e6` / `size_e6` | must be internally consistent with `netStake` + `makerCollateral` (§5) — `price_e6` allows ±1 unit of rounding tolerance |
| `makerCollateral`      | must be `> 0`                                                                                                            |
| `deadline`             | must not already be in the past                                                                                          |
| signature              | must recover to `maker`                                                                                                  |

Separately, `INVALID_IDENTITY` covers malformed/missing addresses or an unsupported `signature_type` (only `0`/EOA is implemented today), `ALLOWANCE_VALIDATION_FAILED` covers a missing standing allowance, and `BALANCE_VALIDATION_FAILED` covers insufficient live USDC balance for `makerCollateral` (§8).

**Practical rule:** copy `builder`, `protocolFeeBps`, `builderFeeBps`, `takerCollateral`, `legsHash`/`condition_id`, `taker`, and `deadline` *verbatim* from the RFQ's `escrow_terms`. The **only** fields you choose are `makerCollateral` (your price) and `nonce` (random). `spreadBps` is **not** a signed field in v2 — it is owner-set escrow config and never appears in your quote at all. The maker SDK's `submitQuote` does exactly this; if you hand-roll a pricer, mirror it.

{% hint style="info" %}
Why so strict? You are *consenting* to the fee regime by signing it. If you could sign different fees than the RFQ advertised, the on-chain split would diverge from what the taker agreed to. The field-match makes your signature a binding agreement to the exact escrowed terms.
{% endhint %}

***

## 15. Nonce Bitmap

The escrow tracks **per-owner nonces** in an on-chain bitmap and verifies each quote's nonce hasn't been consumed for that maker. This gives replay protection without you maintaining sequential nonce state.

* **Use a fresh random 256-bit nonce per quote.** With a 256-bit space, collisions are cryptographically impossible, so random is safe — no counter, no coordination, no per-RFQ bookkeeping.

  ```ts
  const rand256 = () =>
    BigInt("0x" + Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("hex"));
  ```
* Because the bitmap is **per-owner (your address)**, your nonces never collide with another maker's. Two makers can use overlapping nonce values with no interaction.
* A nonce is only consumed when a parlay using that quote is actually placed on-chain. Quotes that lose the auction or expire never touch the bitmap — you don't "waste" nonces by quoting aggressively.

***

## 16. Reference: Maker Client Options

The maker SDK's client constructor accepts, at minimum:

| Option                           | Meaning                                                                                                                           |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `url`                            | WS url of the relay. Default is the production `/v2/ws/rfq` endpoint.                                                             |
| `apiKey`                         | API key issued out-of-band (local-dev default: `mm-demo-key`).                                                                    |
| `signer`                         | Signs `ParlayQuote`/`CashoutOffer` typed data on your key. The relay NEVER receives the key itself.                               |
| `makerAddress`                   | Optional distinct collateral-owner address, if different from the signer. Defaults to the signer's address for both roles.        |
| `lastLook`                       | Opt into last look (§9). Default `false`.                                                                                         |
| `approval`                       | A pre-built standing EIP-2612 approval to send in `auth.approval` (§8). Omit if you already have a qualifying on-chain allowance. |
| `authTimeoutMs` / `ackTimeoutMs` | Deadlines for the initial auth and for per-message ACKs. Default 10s each.                                                        |
| `pingIntervalMs`                 | Client-initiated heartbeat interval. Default 25s; `0` disables.                                                                   |
| `onEvent`                        | Observability hook for connect/reconnect/error lifecycle events.                                                                  |

The convenience `submitQuote(rfq, domain, { edgeBps, jitterBps?, priceForLeg, validUntil?, deadlineSec? })` call handles the whole price → sign → send pipeline for one `RFQ_REQUEST` in one call, including the mandatory `condition_id` re-verification (§6).

***

## 17. Reference: `RFQRequest`

This is what every `RFQ_REQUEST` push hands you.

| Field                                | Meaning                                                                                                                                                                                                                                 |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rfq_id`                             | Unique RFQ identifier                                                                                                                                                                                                                   |
| `requestor_public_id`                | The taker's EOA                                                                                                                                                                                                                         |
| `leg_position_ids`                   | Canonical, sorted leg identifiers, `"#${outcomeId*10+sideIndex}"` per leg                                                                                                                                                               |
| `condition_id`                       | keccak of sorted `ParlayLeg` hashes — **recompute and verify before signing** (§6)                                                                                                                                                      |
| `yes_position_id` / `no_position_id` | Derived position identifiers for the combo's two outcomes                                                                                                                                                                               |
| `direction` / `side`                 | Always `BUY` / `YES` today (taker buys the YES combo)                                                                                                                                                                                   |
| `requested_size`                     | `{ unit: "notional" \| "shares", value_e6 }`                                                                                                                                                                                            |
| `submission_deadline`                | Unix ms — end of the quote-collection window                                                                                                                                                                                            |
| `escrow_terms`                       | `{ chain_id, escrow, taker, builder, taker_collateral_e6 (gross), net_stake_e6 (price your quote on THIS), protocol_fee_bps, builder_fee_bps, spread_bps }` — the fee/escrow regime you consent to by echoing it into your signed quote |

`sideIndex` (encoded in each leg position id): **0 = YES / Long**, **1 = NO / Short**. Combined-probability math uses `p` for side 0 and `1 - p` for side 1.

***

## 18. Reference: `RFQ_QUOTE` message

This is the object you send back as `type: "RFQ_QUOTE"`:

```jsonc
{
  "type": "RFQ_QUOTE",
  "rfq_id": "rfq_…",
  "price_e6": "…",            // round(netStake*1e6 / (netStake+makerCollateral)), round-half-up
  "size_e6": "…",              // netStake + makerCollateral (the pot/payout)
  "signed_order": {
    "maker":           "0x…", // your address
    "taker":           "0x…", // echoed from escrow_terms
    "legsHash":         "0x…", // echoed from condition_id
    "takerCollateral":  "…",   // echoed from escrow_terms (gross, 6dp)
    "makerCollateral":  "…",   // YOUR price (6dp) — the only economic field you choose
    "builder":          "0x…", // echoed from escrow_terms
    "protocolFeeBps":   0,     // echoed from escrow_terms
    "builderFeeBps":    0,     // echoed from escrow_terms
    "nonce":            "…",   // your random 256-bit nonce
    "deadline":         "…",   // unix seconds
    "signature":        "0x…"  // EIP-712 sig over the 10-field ParlayQuote (no spreadBps)
  }
}
```

The relay replies `{ type: "ACK_RFQ_QUOTE", rfq_id, quote_id }` on success, assigning the `quote_id` server-side — you never choose your own quote id on the WS channel.

{% hint style="info" %}
`maker` in the signed order is checked against your authenticated session identity — the competition/dedup key is the address recovered from `signature`. You cannot impersonate or censor another maker.
{% endhint %}

***

## Appendix: Reference deployment

| Parameter             | Value                                                                                                                         |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Chain                 | HyperEVM **testnet**, chainId **998**                                                                                         |
| EIP-712 quote domain  | `ParlayEscrow` / `1` / `998` (verifying contract = the deployed escrow — read it from `escrow_terms.escrow`, do not hardcode) |
| EIP-712 permit domain | `USD Coin` / `1` / `998`                                                                                                      |
| HL info API (prices)  | `https://api.hyperliquid.xyz/info` `{type:"allMids"}`                                                                         |
| MM WebSocket          | `/ws/rfq` (production: `wss://parlay.mute.sh/v2/ws/rfq`)                                                                      |
| USDC decimals         | 6 (all collateral is 6dp fixed-point bigint strings)                                                                          |
| Combo limits          | 2–6 legs per parlay, stake $1–$1000                                                                                           |

{% hint style="warning" %}
This is a **testnet-only** deployment. Contract addresses and demo API keys rotate between deployments — always confirm the live escrow address via the relay's own `escrow_terms` on a real RFQ (or your onboarding contact) rather than hardcoding one from documentation or example code.
{% endhint %}


---

# 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/maker-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.
