Skip to main content
Combos are multi-leg positions that combine multiple underlying market outcomes into one YES or NO position. Each Combo is defined by its legs and identified by derived YES and NO position IDs. The request for quote (RFQ) system enables quote-based Combo execution between two participants: Polymarket users (requesters) and market makers (quoters). A user creates a Request, which starts an auction among connected market makers. Market makers compete by submitting Quotes: executable prices they are willing to fill.
  1. User creates an unsigned Request for a Combo price.
  2. RFQ system sends the Request to connected market makers.
  3. Market makers submit signed Quotes within the 400 ms submission window.
  4. RFQ system returns the best Quote to the user.
  5. User accepts the Quote by signing the trade within the 5-second acceptance window.
  6. RFQ system requests Last Look confirmation when Last Look is enabled.
  7. Market maker confirms or declines within the 1-second confirmation window.
  8. RFQ system executes the accepted Combo.
  9. User receives execution updates.
  10. Market maker receives execution updates.
Combo position IDs are complementary to CLOB token IDs. A user can trade the market on the CLOB or can include the market as a leg of a Combo.
This guide shows market makers how to handle Combo RFQs. You will open a quoting session, respond to incoming requests, cancel submitted quotes when needed, confirm fills through Last Look, and monitor execution updates.

Start Quoting

Start by preparing an authenticated quoting session with the RFQ system.
1

Install the Package

Install the Unified TypeScript SDK with the package manager of your choice.
pnpm add @polymarket/client@beta viem
This page uses Viem for wallet signing. See the TypeScript tooling guide for other wallet library integrations.
2

Create a Secure Client

Create an instance of SecureClient with a wallet that has funds for fulfilling user requests and its signer details.
import { createSecureClient, relayerApiKey } from "@polymarket/client";
import { privateKey } from "@polymarket/client/viem";

const client = await createSecureClient({
  wallet: process.env.POLYMARKET_WALLET_ADDRESS!,
  signer: privateKey(process.env.PRIVATE_KEY!),
  apiKey: relayerApiKey({
    key: process.env.RELAYER_API_KEY!,
    address: process.env.RELAYER_API_KEY_ADDRESS!,
  }),
});
The Relayer API key is necessary for setting up trading approvals in the next step. Create a Relayer API key from Settings > API Keys.
3

Set Up Trading Approvals

Set up the approvals required to fill user requests.
await client.setupTradingApprovals();
4

Open an RFQ Session

Open the RFQ session.
const session = await client.openRfqSession();

for await (const event of session) {
  // event: RfqEvent
}
5

Close the Session

You can close the session at any time by calling session.close().
for await (const event of session) {
  if (shouldCloseSession) {
    await session.close();
    break;
  }

  // …
}

Handle Quote Requests

Quote requests describe a user’s intent to buy or sell shares in a Combo defined by a given set of legs. A quote request can currently only buy or sell the YES side of a Combo. The following cases show how a market maker can satisfy a user’s buy or sell request using collateral or inventory.
Quote RequestUsing CollateralFrom Inventory
Buy YESBuy NO at 1 - priceSell YES at price
Sell YESBuy YES at priceSell NO at 1 - price
See Combinatorial Positions for more detail on the YES/NO position model. Quoters should respond within the 400 ms submission window.

Authorize the Quote

1

Switch on Event Type

First, switch on event.type to handle quote requests from the session stream.
switch (event.type) {
  case "quote_request":
    // event: RfqQuoteRequestEvent
    void handleQuoteRequest(event);
    break;

  // …
}
2

Evaluate Request

Then, inspect the RfqQuoteRequestEvent before pricing it.
FieldTypeDescription
rfqIdRfqIdRFQ identifier used to correlate responses
requestorPublicIdRfqRequestorPublicIdPublic identifier for the user request
conditionIdComboConditionIdDerived Combo condition ID
directionRfqDirectionWhether the user wants to buy or sell
sideRfqSide.YesCurrently always RfqSide.Yes
requestedSizeRfqRequestedSizeUser-requested notional or share size
yesPositionIdPositionIdDerived YES Combo position ID
noPositionIdPositionIdDerived NO Combo position ID
legPositionIdsPositionId[]Underlying leg position IDs
submissionDeadlineEpochMillisecondsUnix-millisecond quote submission deadline
requestedSize is an RfqRequestedSize value that describes how the user sized the request.
type RfqRequestedSize =
  | {
      unit: RfqRequestedSizeUnit.Notional;
      value: DecimalString;
    }
  | {
      unit: RfqRequestedSizeUnit.Shares;
      value: DecimalString;
    };
Where:
  • notional: the target value of the request in collateral currency. For example, "3" means the user wants roughly 3 pUSD worth of the Combo, with the resulting share size derived from the quote price.
  • shares: the target number of Combo outcome tokens. For example, "10" means the user wants 10 shares, or 10,000,000 base units.
In both cases, value is a normalized decimal string.
3

Submission

Finally, handle pricing, quote submission, and persistence outside the session loop before the event.submissionDeadline deadline. Price the request as pUSD per YES Combo share; for example, 0.45 means 0.45 pUSD per share. If you do not want to quote the request, skip submission.
async function handleQuoteRequest(event: RfqQuoteRequestEvent) {
  const price = priceComboRequest(event);

  if (price === undefined) return;

  const reference = await event.quote({ price });

  storeQuoteReference(reference);
}

Quote Partial Fills

If you only want to fill part of the requested size, pass size with the quote. size is a normalized decimal value: "10" means 10 shares, or 10,000,000 base units. When omitted, the SDK quotes the full requested size.
await event.quote({
  price: "0.45",
  size: "10",
});

Use Inventory

By default, quotes use collateral (pUSD) to buy YES or NO tokens as needed to satisfy the quote request according to the combinatorial position logic. Pass source: "inventory" when you want to quote from existing inventory instead.
await event.quote({
  price: "0.45",
  source: "inventory",
});

Manage Combo Positions

Use Combo position workflows to manage inventory throughout the quote lifecycle.

List Combo Positions

List Combo positions as part of your background inventory sync. Keep this state fresh outside the quote path.
Use client.listComboPositions(...) to page through Combo positions for the authenticated account. Filter by status, Combo condition ID, or Combo position ID when you only need a subset of positions.
import { ComboPositionStatus, type ComboPosition } from "@polymarket/client";

const positions = client.listComboPositions({
  status: ComboPositionStatus.Open,
  pageSize: 50,
});

for await (const page of positions) {
  for (const position of page.items) {
    // position: ComboPosition
  }
}
Each returned item is a ComboPosition.
type ComboPosition = {
  conditionId: ComboConditionId;
  positionId: PositionId;
  moduleId: number;
  userAddress: Address;
  shares: DecimalString;
  entryAvgPriceUsdc?: DecimalString | null;
  entryCostUsdc?: DecimalString | null;
  status: ComboPositionStatus;
  firstEntryAt: IsoDateTimeString;
  resolvedAt?: IsoDateTimeString | null;
  legsTotal: number;
  legsResolved: number;
  legsPending: number;
  legs: ComboPositionLeg[];
};
You can filter positions by the following criteria:
const positions = client.listComboPositions({
  conditionId: "<combo_condition_id>",
});

Inventory Management

If you want to quote from inventory, build the inventory before quote requests arrive. Splitting converts collateral into complementary Combo positions for a set of legs. Merging converts matching complementary Combo positions back into collateral.
Use client.splitPosition(...) with legs to create Combo inventory from collateral. amount is in pUSD base units.
const split = await client.splitPosition({
  amount: 10_000_000n,
  legs: ["<leg_position_id_1>", "<leg_position_id_2>"],
});

const splitOutcome = await split.wait();

// splitOutcome.transactionHash identifies the confirmed split transaction.
Use client.mergePositions(...) with the same legs to merge complementary Combo positions back into collateral. Pass amount: "max" to merge the largest matching amount available.
const merge = await client.mergePositions({
  amount: "max",
  legs: ["<leg_position_id_1>", "<leg_position_id_2>"],
});

const mergeOutcome = await merge.wait();

// mergeOutcome.transactionHash identifies the confirmed merge transaction.

Redeem Resolved Positions

When a Combo position resolves, redeem the winning position to settle it back to collateral.
Use client.redeemPositions(...) with a Combo positionId. The SDK redeems the available balance for that resolved position.
const redeem = await client.redeemPositions({
  positionId: "<yes_position_id|no_position_id>",
});

const redeemOutcome = await redeem.wait();

// redeemOutcome.transactionHash identifies the confirmed redemption transaction.
You can list resolved winning positions first, then redeem each one.
import { ComboPositionStatus } from "@polymarket/client";

for await (const page of client.listComboPositions({
  status: ComboPositionStatus.ResolvedWin,
})) {
  for (const position of page.items) {
    const redeem = await client.redeemPositions({
      positionId: position.positionId,
    });

    await redeem.wait();
  }
}

Get Combo Markets

Use the Combo markets catalog to retrieve active markets that can be used as Combo legs. Markets are ordered by volume descending.
Use client.listComboMarkets(...) to page through markets that can be used as Combo legs.
const paginator = client.listComboMarkets({ pageSize: 50 });

for await (const page of paginator) {
  // page.items: ComboMarket[]
}
Use exclude to omit markets you have already shown or selected.
const paginator = client.listComboMarkets({
  exclude: selectedConditionIds,
  pageSize: 50,
});
The SDK returns structured YES and NO outcomes.
type ComboMarket = {
  id: MarketId;
  conditionId: CtfConditionId;
  slug: string;
  title: string;
  outcomes: {
    yes: {
      label: string;
      positionId: PositionId;
      price: DecimalString;
    };
    no: {
      label: string;
      positionId: PositionId;
      price: DecimalString;
    };
  };
  image: string;
  volume: number;
  tags: string[];
};

Map Legs to Markets

Market makers should build their own view of the markets that support Combos before quote requests arrive. Combo-enabled markets expose a list of position IDs with two entries: the first is the YES position ID and the second is the NO position ID. These IDs identify the outcome positions your pricing system can map back to market data.
Fetch non-closed markets and index them by position ID in your own market data store.
for await (const page of client.listMarkets({ closed: false })) {
  for (const market of page.items) {
    for (const positionId of market.positionIds) {
      marketByPositionId.set(positionId, market);
    }
  }
}
You can also fetch markets by leg position ID on demand, but most market makers will want this context ready before the 400 ms quote window starts.
const page = await client
  .listMarkets({
    positionIds: event.legPositionIds,
  })
  .firstPage();

const markets = page.items;

Listen to Execution Updates

Execution updates tell you what happened after one of your quotes was selected. Use them to reconcile RFQ state, transaction hashes, and terminal execution outcomes in your own systems.
1

Switch on the Event Type

First, switch on event.type to handle execution updates from the same session stream.
switch (event.type) {
  case "execution_update":
    // event: RfqExecutionUpdateEvent
    handleExecutionUpdate(event);
    break;

  // …
}
2

Inspect the Execution Update

Then, inspect the execution update before reconciling the selected RFQ. Execution updates are correlated by rfqId.
type RfqExecutionUpdateEvent = {
  type: "execution_update";
  rfqId: RfqId;
  status: RfqExecutionStatus;
  txHash?: TxHash;
};
where RfqExecutionStatus could be:
StatusMeaning
RfqExecutionStatus.MatchedThe quote was selected and handed off to execute.
RfqExecutionStatus.MinedThe execution transaction was mined.
RfqExecutionStatus.RetryingExecution is being retried.
RfqExecutionStatus.ConfirmedExecution completed successfully.
RfqExecutionStatus.FailedExecution failed.
3

Reconcile Execution State

Finally, persist the update and treat RfqExecutionStatus.Confirmed and RfqExecutionStatus.Failed as terminal states.
function handleExecutionUpdate(event: RfqExecutionUpdateEvent) {
  storeExecutionUpdate(event);

  if (event.status === RfqExecutionStatus.Confirmed) {
    markQuoteConfirmed(event.rfqId);
    return;
  }

  if (event.status === RfqExecutionStatus.Failed) {
    markQuoteFailed(event.rfqId);
  }
}

Cancel Quotes

After you submit a quote, keep the returned quote reference. If your price, inventory, or risk changes before the quote is selected, use that reference to request cancellation.
A cancellation acknowledgement means the RFQ system processed the cancellation request. It does not guarantee the quote was withdrawn from an RFQ that was already selected.
1

Store the Quote Reference

First, keep the quote reference returned by event.quote(…). It contains the rfqId and quoteId needed to cancel the quote.
const reference = await event.quote({ price: 0.45 });

// reference.rfqId: RfqId
// reference.quoteId: RfqQuoteId
2

Cancel the Quote

Then, pass that reference to session.cancelQuote(…) on the same live RFQ session.
if (shouldCancelQuote) {
  const ack = await session.cancelQuote(reference);

  // ack.rfqId: RfqId
  // ack.quoteId: RfqQuoteId
}

Last Look

Last Look is a separate confirmation step for makers that have it enabled. If a selected quote requires confirmation, run a final risk check before the confirmation deadline and either confirm or decline the quote.
1

Switch on the Event Type

First, switch on event.type to handle confirmation requests from the same session stream.
switch (event.type) {
  case "confirmation_request":
    // event: RfqConfirmationRequestEvent
    void handleConfirmationRequest(event);
    break;

  // …
}
2

Inspect the Confirmation Request

Then, inspect the confirmation request before running your final risk check. It includes the selected quote, the final fill size, and the event.confirmBy deadline for your Last Look response.
type RfqConfirmationRequestEvent = {
  type: "confirmation_request";
  rfqId: RfqId;
  quoteId: RfqQuoteId;
  conditionId: ComboConditionId;
  direction: RfqDirection;
  side: RfqSide.Yes;
  price: DecimalString;
  fillSize: DecimalString;
  yesPositionId: PositionId;
  noPositionId: PositionId;
  legPositionIds: PositionId[];
  confirmBy: EpochMilliseconds;
  confirm(): Promise<RfqConfirmationAck>;
  decline(): Promise<RfqConfirmationAck>;
};
3

Confirm or Decline

Finally, run your final risk check outside the session loop and respond before the event.confirmBy deadline.
async function handleConfirmationRequest(event: RfqConfirmationRequestEvent) {
  const canStillFill = runFinalRiskCheck(event);

  if (canStillFill) {
    await event.confirm();
    return;
  }

  await event.decline();
}

Handle Errors

In this section, we will talk you through how to handle errors with the RFQ system.

Open the RFQ Session

Wrap client.openRfqSession() in try/catch and use OpenRfqSessionError.isError(…) to narrow the error type.
try {
  const session = await client.openRfqSession();
} catch (error) {
  if (!OpenRfqSessionError.isError(error)) throw error;

  switch (error.name) {
    case "TransportError":
      // error: TransportError
      break;
  }
}

Submit a Quote

Wrap event.quote(…) in try/catch and use RfqQuoteError.isError(…) to narrow the error type.
try {
  const reference = await event.quote({ price });
  // …
} catch (error) {
  if (!RfqQuoteError.isError(error)) throw error;

  switch (error.name) {
    case "RfqQuoteRejectedError":
      // error: RfqQuoteRejectedError
      // error.rfqId: RfqId
      // error.code: RfqErrorCode | undefined
      break;
    case "SigningError":
      // error: SigningError
      break;
    case "TimeoutError":
      // error: TimeoutError
      break;
    case "TransportError":
      // error: TransportError
      break;
    case "UserInputError":
      // error: UserInputError
      break;
  }
}

Cancel a Quote

Wrap session.cancelQuote(…) in try/catch and use RfqCancelQuoteError.isError(…) to narrow the error type.
try {
  const ack = await session.cancelQuote(reference);
  // …
} catch (error) {
  if (!RfqCancelQuoteError.isError(error)) throw error;

  switch (error.name) {
    case "RfqCancelQuoteRejectedError":
      // error: RfqCancelQuoteRejectedError
      // error.rfqId: RfqId
      // error.quoteId: RfqQuoteId
      // error.code: RfqErrorCode | undefined
      break;
    case "TimeoutError":
      // error: TimeoutError
      break;
    case "TransportError":
      // error: TransportError
      break;
  }
}

Confirm or Decline

Wrap event.confirm() or event.decline() in try/catch and use RfqConfirmationError.isError(…) to narrow the error type.
try {
  if (canStillFill) {
    await event.confirm();
  } else {
    await event.decline();
  }
} catch (error) {
  if (!RfqConfirmationError.isError(error)) throw error;

  switch (error.name) {
    case "RfqConfirmationRejectedError":
      // error: RfqConfirmationRejectedError
      // error.rfqId: RfqId
      // error.quoteId: RfqQuoteId
      // error.code: RfqErrorCode | undefined
      break;
    case "TimeoutError":
      // error: TimeoutError
      break;
    case "TransportError":
      // error: TransportError
      break;
  }
}