Skip to content

Combos

Describes @stxapp/stx-typescript 0.9.0, the current release.

A combination is one market over two or more legs. As a player you ask for quotes on a set of legs and a stake; makers quote what you win; you accept, and the exchange places one ordinary order for each side on the combination’s market. Combos explains the product and Combo negotiation documents every command, push and refusal at the wire level. This page shows the SDK calls and exactly what each one hands you.

Both sides run on the socket, with an API key that has write access.

A complete maker that quotes every request until Ctrl-C and reports how each one ends is examples/combo_maker.ts; Quote combos as a maker walks through it.

Every reply, event and error below was captured from @stxapp/stx-typescript 0.8.3 on US demo on 21 October 2026 (UTC), and is shown as the SDK returned it: ids, amounts and timestamps are real and nothing was shortened. Three accounts took part, each with its own STX client and socket (await client.websocket().connect()):

Socket Session externalId
Player playerWs player = await playerWs.comboPlayer() 9c87ec1c-588e-4512-9b91-d71653e41458
Maker A makerWs maker = await makerWs.comboMaker() 3914a397-b553-4510-8b54-d6a6c0a096b0
Maker B makerBWs makerB = await makerBWs.comboMaker()

The legs are two pre-match soccer markets, each to win: 29396ce5-a4f3-4d81-b980-2f637ab3a660 (GAL @ LILL, GAL in regulation) and 663ebfe6-26d5-45e0-a0ac-6859f7087832 (MAN @ COMO, MAN in regulation).

const player = await playerWs.comboPlayer();
player.channel.reply;
{ "external_id": "9c87ec1c-588e-4512-9b91-d71653e41458" }

player.externalId is the same external_id: the player’s pseudonym, which makers see on requests and quotes. Nobody sees a maker’s.

const legs = [
{ marketId: "29396ce5-a4f3-4d81-b980-2f637ab3a660", outcome: "won" }, // GAL @ LILL: GAL
{ marketId: "663ebfe6-26d5-45e0-a0ac-6859f7087832", outcome: "won" }, // MAN @ COMO: MAN
] as const;
const { rfq_id } = await player.requestQuote(legs, "5.00");
{ "rfq_id": "190058b7-ae66-4f6f-a30b-dc8951960d98" }

amount is a decimal string in whole cents. A number is never sent: requestQuote(legs, 5) throws TypeError: amount must be a decimal string such as "250.00", got number.

Every maker session gets rfq_opened, often before the player has the requestQuote() reply:

const opened = await maker.waitFor("rfq_opened", { rfqId: rfq_id, timeoutMs: 30_000 });
opened;
{
"event": "rfq_opened",
"payload": {
"amount": "5.0000",
"external_id": "9c87ec1c-588e-4512-9b91-d71653e41458",
"expires_at": 1792558280540276,
"market_id": null,
"quantity": null,
"max_price": "1.0000",
"side": "buy",
"tick_size": "0.0001000",
"rfq_id": "190058b7-ae66-4f6f-a30b-dc8951960d98",
"legs": [
{ "short_name": "GAL @ LILL", "market_id": "29396ce5-a4f3-4d81-b980-2f637ab3a660", "outcome": "won" },
{ "short_name": "MAN @ COMO", "market_id": "663ebfe6-26d5-45e0-a0ac-6859f7087832", "outcome": "won" }
]
}
}

On a buy amount is the stake, and market_id and quantity are null because no combination market exists yet. expires_at is microseconds since the Unix epoch, 5 minutes after the request opened. short_name is the leg market’s short title, which names the game rather than the outcome, so read the outcome from market_id; it is null for a market that has none. Price against max_price and tick_size, never a constant.

await player.snapshot(rfq_id);
{
"status": "open",
"amount": "5.0000",
"expires_at": 1792558280540276,
"market_id": null,
"quantity": null,
"side": "buy",
"rfq_id": "190058b7-ae66-4f6f-a30b-dc8951960d98",
"best_quote": { "amount": null, "expires_at": null, "quote_id": null }
}

Nothing is quoted yet, so every best_quote field is null. snapshot() reads the live state at any moment; it is not held back like best_quote events.

await makerB.quote(rfq_id, "8.00", 120_000, { clientOrderId: null });
await maker.quote(rfq_id, "10.00", 60_000, { clientOrderId: "tssdk-capture-buy" });
{ "amount": "8.0000", "external_id": "9c87ec1c-588e-4512-9b91-d71653e41458", "quote_id": "f6fdf76e-0c74-44cf-8180-b0f1fb0f09df" }
{ "amount": "10.0000", "external_id": "9c87ec1c-588e-4512-9b91-d71653e41458", "quote_id": "05308411-acfa-4b81-af8c-c390ad41a254" }

The reply carries no price or size: the bind sets those. external_id is the player’s. expiresInMs counts from receipt, and clientOrderId is copied onto the maker’s order if the quote binds.

const best = await player.waitFor("best_quote", { rfqId: rfq_id, timeoutMs: 30_000 });
best;
{
"event": "best_quote",
"payload": {
"amount": "10.0000",
"expires_at": 1792558040771767,
"market_id": null,
"rfq_id": "190058b7-ae66-4f6f-a30b-dc8951960d98",
"quote_id": "05308411-acfa-4b81-af8c-c390ad41a254"
}
}

Both quotes arrived inside the request’s first 500 ms, so one best_quote came, carrying only the better one, 500 ms after requestQuote() returned. The player only ever sees the best amount, never who quoted it. A snapshot() now shows the same quote under best_quote.

const extra = await maker.quote(rfq_id, "9.00", 60_000);
await maker.withdrawQuote(rfq_id, extra.quote_id);
{ "amount": "9.0000", "external_id": "9c87ec1c-588e-4512-9b91-d71653e41458", "quote_id": "4e3056f3-cb9a-4ea0-9866-5b62148c9ea2" }
{ "withdrawn": "4e3056f3-cb9a-4ea0-9866-5b62148c9ea2" }

Withdrawing it again, or withdrawing another maker’s quote, throws STXComboRefusedException with code unknown_quote. A $9 quote below the best $10 changes nothing the player sees, so no best_quote followed either call.

accept() takes the least amount you will take. Asking for more than anything live throws STXComboPriceMovedException, and the request stays open:

try {
await player.accept(rfq_id, best.payload.quote_id!, "25.00");
} catch (err) {
if (err instanceof STXComboPriceMovedException) console.log(err.code, err.best);
}
"price_moved"
{ "quote_id": "05308411-acfa-4b81-af8c-c390ad41a254", "amount": "10.0000", "expires_at": 1792558040771767 }

err.best is the live best quote, ready to accept again.

const placed = await player.accept(rfq_id, best.payload.quote_id!, best.payload.amount!);
{ "market_id": "c5cde34a-d13c-4849-8135-8ed0baf70ccd", "quantity": "14.00", "price": "0.3334", "order_id": "fba8fd62-3389-4d3d-ac09-a0b383f6c11e" }

The bind created the combination market c5cde34a-d13c-4849-8135-8ed0baf70ccd. The price is max_price × stake / (stake + win) = 5 / 15 = 0.33333, rounded up to the tick: 0.3334. The size is whole contracts, floor(5 / 0.3334) = 14. The player pays at most $4.6676 plus fees and collects $14.00 if both legs win. The reply says what was placed, not what filled.

The player gets placed: the accept() reply plus rfq_id. It can arrive before the reply, so waitFor also looks at events already received:

await player.waitFor("placed", { rfqId: rfq_id });
{
"event": "placed",
"payload": {
"market_id": "c5cde34a-d13c-4849-8135-8ed0baf70ccd",
"quantity": "14.00",
"price": "0.3334",
"order_id": "fba8fd62-3389-4d3d-ac09-a0b383f6c11e",
"rfq_id": "190058b7-ae66-4f6f-a30b-dc8951960d98"
}
}

Maker A, whose $10 bound, gets quote_accepted (with market_id still null, since the market did not exist yet), then placed with its own order at the same price and size, then rfq_closed:

{ "event": "quote_accepted", "payload": { "external_id": "9c87ec1c-588e-4512-9b91-d71653e41458", "market_id": null, "rfq_id": "190058b7-ae66-4f6f-a30b-dc8951960d98", "quote_id": "05308411-acfa-4b81-af8c-c390ad41a254" } }
{ "event": "placed", "payload": { "external_id": "9c87ec1c-588e-4512-9b91-d71653e41458", "market_id": "c5cde34a-d13c-4849-8135-8ed0baf70ccd", "quantity": "14.00", "price": "0.3334", "order_id": "b24cabe6-9a53-48e3-b891-2a19245690ec", "rfq_id": "190058b7-ae66-4f6f-a30b-dc8951960d98", "quote_id": "05308411-acfa-4b81-af8c-c390ad41a254" } }
{ "event": "rfq_closed", "payload": { "reason": "placed", "market_id": "c5cde34a-d13c-4849-8135-8ed0baf70ccd", "rfq_id": "190058b7-ae66-4f6f-a30b-dc8951960d98" } }

Maker B, whose $8 did not bind, gets negotiation_closed for its quote and the same rfq_closed:

{ "event": "negotiation_closed", "payload": { "reason": "placed_elsewhere", "external_id": "9c87ec1c-588e-4512-9b91-d71653e41458", "market_id": "c5cde34a-d13c-4849-8135-8ed0baf70ccd", "rfq_id": "190058b7-ae66-4f6f-a30b-dc8951960d98", "quote_id": "f6fdf76e-0c74-44cf-8180-b0f1fb0f09df" } }
{ "event": "rfq_closed", "payload": { "reason": "placed", "market_id": "c5cde34a-d13c-4849-8135-8ed0baf70ccd", "rfq_id": "190058b7-ae66-4f6f-a30b-dc8951960d98" } }

From here the request is gone: snapshot(), accept() and quote() on 190058b7-... throw STXComboRefusedException with code not_found. c5cde34a-... is an ordinary market, and each side follows its fills, positions and settlements on its own socket: playerWs.fills(), playerWs.positions() and playerWs.settlements() for the player, the same on makerWs for the maker.

const out = await player.cashOut("c5cde34a-d13c-4849-8135-8ed0baf70ccd");
{ "market_id": "c5cde34a-d13c-4849-8135-8ed0baf70ccd", "quantity": "14.00", "rfq_id": "7422de30-8cab-478e-b42c-612eeada2170" }

A cash out is a new request with its own rfq_id. The exchange sets the size: what you hold less your resting sells. A fill reaches your position a moment after the trade, so a cashOut() sent straight after buying can throw STXComboRefusedException with code nothing_to_sell; retry it.

{
"event": "rfq_opened",
"payload": {
"amount": null,
"external_id": "9c87ec1c-588e-4512-9b91-d71653e41458",
"expires_at": 1792558283797002,
"market_id": "c5cde34a-d13c-4849-8135-8ed0baf70ccd",
"quantity": "14.00",
"max_price": "1.0000",
"side": "sell",
"tick_size": "0.0001000",
"rfq_id": "7422de30-8cab-478e-b42c-612eeada2170",
"legs": [
{ "short_name": "GAL @ LILL", "market_id": "29396ce5-a4f3-4d81-b980-2f637ab3a660", "outcome": "won" },
{ "short_name": "MAN @ COMO", "market_id": "663ebfe6-26d5-45e0-a0ac-6859f7087832", "outcome": "won" }
]
}
}

On sell the roles of the fields swap: market_id is the combination, quantity the size, and amount is null. Makers quote what they pay for all 14 contracts. player.snapshot(out.rfq_id) shows the same: "side": "sell", "amount": null, "quantity": "14.00", and a best_quote of nulls until someone quotes.

Maker A quotes $2.10 and withdraws it. The player sees the quote, then a best_quote with nulls once nothing is live:

{ "event": "best_quote", "payload": { "amount": "2.1000", "expires_at": 1792558044008239, "market_id": "c5cde34a-d13c-4849-8135-8ed0baf70ccd", "rfq_id": "7422de30-8cab-478e-b42c-612eeada2170", "quote_id": "59157575-1471-4d39-b160-10453c8f72ae" } }
{ "event": "best_quote", "payload": { "amount": null, "expires_at": null, "market_id": "c5cde34a-d13c-4849-8135-8ed0baf70ccd", "rfq_id": "7422de30-8cab-478e-b42c-612eeada2170", "quote_id": null } }

Accepting the withdrawn quote now throws STXComboPriceMovedException with every field of err.best null:

await player.accept(out.rfq_id, "59157575-1471-4d39-b160-10453c8f72ae", "2.10");
{ "quote_id": null, "amount": null, "expires_at": null }

Maker B quotes $3.72 and maker A $4.20. Outside the opening window each change is pushed:

{ "event": "best_quote", "payload": { "amount": "3.7200", "expires_at": 1792558045115565, "market_id": "c5cde34a-d13c-4849-8135-8ed0baf70ccd", "rfq_id": "7422de30-8cab-478e-b42c-612eeada2170", "quote_id": "024f767f-0ec7-4cdc-b270-f2a5b2b3f546" } }
{ "event": "best_quote", "payload": { "amount": "4.2000", "expires_at": 1792558045191937, "market_id": "c5cde34a-d13c-4849-8135-8ed0baf70ccd", "rfq_id": "7422de30-8cab-478e-b42c-612eeada2170", "quote_id": "0132f947-7bee-4cb7-a939-38d3b8e2aaf4" } }

The player accepts naming maker B’s quote at "3.72". accept() binds the best live quote at that amount or more, whoever made it, so maker A’s $4.20 binds:

const sold = await player.accept(out.rfq_id, "024f767f-0ec7-4cdc-b270-f2a5b2b3f546", "3.72");
{ "market_id": "c5cde34a-d13c-4849-8135-8ed0baf70ccd", "quantity": "14.00", "price": "0.3000", "order_id": "4d1bf79e-7b44-45d3-a10d-53f2897f8e9f" }

On a cash out the price is amount / quantity = 4.20 / 14 = 0.3000, rounded down to the tick. The player sold 14 at 0.3000 against 0.3334 paid. Maker A gets quote_accepted (naming the combination this time, since it exists), then placed with its buy of 14 at 0.3000 and rfq_closed placed; maker B gets negotiation_closed placed_elsewhere for its $3.72.

Every ending reaches the player as placed or negotiation_closed, each maker’s quotes as placed or negotiation_closed, and every maker session as rfq_closed.

await player.cancel("7e08eef2-d2ae-4247-860d-b2812bb149e0");
{ "cancelled": "7e08eef2-d2ae-4247-860d-b2812bb149e0" }

Before the reply, the player got negotiation_closed, maker B (holding a $1.10 quote) got negotiation_closed for it, and both makers got rfq_closed:

{ "event": "negotiation_closed", "payload": { "reason": "cancelled", "market_id": null, "rfq_id": "7e08eef2-d2ae-4247-860d-b2812bb149e0" } }
{ "event": "negotiation_closed", "payload": { "reason": "cancelled", "external_id": "9c87ec1c-588e-4512-9b91-d71653e41458", "market_id": null, "rfq_id": "7e08eef2-d2ae-4247-860d-b2812bb149e0", "quote_id": "5cf079f5-d7e0-47bf-a72a-b132407601b9" } }
{ "event": "rfq_closed", "payload": { "reason": "cancelled", "market_id": null, "rfq_id": "7e08eef2-d2ae-4247-860d-b2812bb149e0" } }

A second cancel() throws STXComboRefusedException with code not_found.

A request belongs to the channel that opened it. Here a second player socket opened a166c823-42b0-4438-b987-ba3727ce72a4, maker A quoted $3.00, and that socket’s session called leave(). The player’s main session, which did not open the request, still heard it end, and so did maker A:

{ "event": "negotiation_closed", "payload": { "reason": "cancelled", "market_id": null, "rfq_id": "a166c823-42b0-4438-b987-ba3727ce72a4" } }
{ "event": "negotiation_closed", "payload": { "reason": "cancelled", "external_id": "9c87ec1c-588e-4512-9b91-d71653e41458", "market_id": null, "rfq_id": "a166c823-42b0-4438-b987-ba3727ce72a4", "quote_id": "a9632e81-aa7f-4c88-96b6-fe7d8310f93b" } }

A request lives 5 minutes. This one, a $1.00 stake on SHK @ INT to win and RCL @ BRU to lose, held maker A’s $2.50 and was left to run out:

{ "event": "negotiation_closed", "payload": { "reason": "expired", "market_id": null, "rfq_id": "5326d6fb-066a-44e4-abf7-0cedfdee63b5" } }
{ "event": "negotiation_closed", "payload": { "reason": "expired", "external_id": "9c87ec1c-588e-4512-9b91-d71653e41458", "market_id": null, "rfq_id": "5326d6fb-066a-44e4-abf7-0cedfdee63b5", "quote_id": "90fa5ab4-fb86-4da9-88b2-edfac3be4328" } }
{ "event": "rfq_closed", "payload": { "reason": "expired", "market_id": null, "rfq_id": "5326d6fb-066a-44e4-abf7-0cedfdee63b5" } }

A quote that expires before its request is not pushed when it expires. Its maker hears negotiation_closed expired when the next quote on the request arrives. Here maker A’s $1.00 quote lived 1 second, and maker B’s $1.10 arrived 1.5 seconds later:

{ "event": "negotiation_closed", "payload": { "reason": "expired", "external_id": "9c87ec1c-588e-4512-9b91-d71653e41458", "market_id": null, "rfq_id": "7e08eef2-d2ae-4247-860d-b2812bb149e0", "quote_id": "10bd1e24-a051-44ba-a770-49384c6ee057" } }

The player opened two cash outs for the same 14 contracts, which nothing prevents, and sold them through the first. Accepting maker A’s $3.50 on the second found nothing left to sell:

await player.accept("dc467ba4-9b5b-4fa1-96f5-59ac7abfd1b7", "9c858517-b60f-4598-a202-4730197faaec", "3.50");
{
"name": "STXComboRefusedException",
"message": "accept on rfq:c7a6abb5-5e79-489d-951f-c043d4377fac refused: player_refused:nothing_to_sell",
"code": "player_refused",
"detail": "nothing_to_sell",
"command": "accept"
}

The request ended for everyone. The player got negotiation_closed player_refused; maker A got quote_accepted first, then negotiation_closed player_refused and rfq_closed player_refused:

{ "event": "negotiation_closed", "payload": { "reason": "player_refused", "market_id": "c5cde34a-d13c-4849-8135-8ed0baf70ccd", "rfq_id": "dc467ba4-9b5b-4fa1-96f5-59ac7abfd1b7" } }
{ "event": "quote_accepted", "payload": { "external_id": "9c87ec1c-588e-4512-9b91-d71653e41458", "market_id": "c5cde34a-d13c-4849-8135-8ed0baf70ccd", "rfq_id": "dc467ba4-9b5b-4fa1-96f5-59ac7abfd1b7", "quote_id": "9c858517-b60f-4598-a202-4730197faaec" } }
{ "event": "negotiation_closed", "payload": { "reason": "player_refused", "external_id": "9c87ec1c-588e-4512-9b91-d71653e41458", "market_id": "c5cde34a-d13c-4849-8135-8ed0baf70ccd", "rfq_id": "dc467ba4-9b5b-4fa1-96f5-59ac7abfd1b7", "quote_id": "9c858517-b60f-4598-a202-4730197faaec" } }

err.detail is the order refusal: here nothing_to_sell, elsewhere for example insufficient_assets or loss_limit24_hours. A quote_accepted is not a promise: it can be followed by negotiation_closed instead of placed. A further cashOut() on the combination now throws code nothing_to_sell.

Leg status is checked when a request opens and again at the bind. This $1.00 request on the two 15-minute crypto markets 6e0bc6bd-5fb5-4323-822e-f930b10da543 (BTC 15 min) and 2cab001b-80a8-4274-a85c-de1d90f90289 (ETH 15 min) opened at 04:58:30 UTC, maker A quoted $2.00, and the player accepted at 05:00:20, 20 seconds after both markets stopped trading:

await player.accept("d27b9fa2-ba0c-406b-9d63-501d41222fcb", "4bfc0e96-c989-49ba-8536-331f20bda681", "2.00");
{
"name": "STXComboRefusedException",
"message": "accept on rfq:c7a6abb5-5e79-489d-951f-c043d4377fac refused: leg_not_quotable",
"code": "leg_not_quotable",
"command": "accept"
}

Maker A heard quote_accepted before the bind read the legs again, then the ending; the player got negotiation_closed before the error:

{ "event": "quote_accepted", "payload": { "external_id": "9c87ec1c-588e-4512-9b91-d71653e41458", "market_id": null, "rfq_id": "d27b9fa2-ba0c-406b-9d63-501d41222fcb", "quote_id": "4bfc0e96-c989-49ba-8536-331f20bda681" } }
{ "event": "negotiation_closed", "payload": { "reason": "leg_not_quotable", "external_id": "9c87ec1c-588e-4512-9b91-d71653e41458", "market_id": null, "rfq_id": "d27b9fa2-ba0c-406b-9d63-501d41222fcb", "quote_id": "4bfc0e96-c989-49ba-8536-331f20bda681" } }
{ "event": "rfq_closed", "payload": { "reason": "leg_not_quotable", "market_id": null, "rfq_id": "d27b9fa2-ba0c-406b-9d63-501d41222fcb" } }
{ "event": "negotiation_closed", "payload": { "reason": "leg_not_quotable", "market_id": null, "rfq_id": "d27b9fa2-ba0c-406b-9d63-501d41222fcb" } }

A new requestQuote() on the same legs then throws code leg_not_quotable straight away.

Combo negotiation lists every ending, including maker_refused (a maker whose side of the bind fails) and the endings that need the exchange itself to fail.

ws.comboPlayer() joins your player topic. requestQuote() opens a request; makers’ quotes arrive as best_quote events, and accept() binds the best one at the amount you name or more.

import { STX, STXComboPriceMovedException } from "@stxapp/stx-typescript";
const client = new STX();
const ws = await client.websocket().connect();
const player = await ws.comboPlayer();
const { rfq_id } = await player.requestQuote(
[
{ marketId: "29396ce5-a4f3-4d81-b980-2f637ab3a660", outcome: "won" },
{ marketId: "663ebfe6-26d5-45e0-a0ac-6859f7087832", outcome: "won" },
],
"5.00",
);
const best = await player.waitFor("best_quote", { rfqId: rfq_id, timeoutMs: 30_000 });
try {
const placed = await player.accept(rfq_id, best.payload.quote_id!, best.payload.amount!);
console.log(placed); // { market_id: "c5cde34a-d13c-4849-8135-8ed0baf70ccd", quantity: "14.00", price: "0.3334", order_id: "fba8fd62-3389-4d3d-ac09-a0b383f6c11e" }
} catch (err) {
if (err instanceof STXComboPriceMovedException) console.log("best is now", err.best.amount);
else throw err;
}

Fills, positions and settlements of the combination arrive on ws.fills(), ws.positions() and ws.settlements() like any other market, under the market_id from the reply. cashOut(marketId) asks makers to quote for what you hold of a combination, and the rest works the same way. snapshot(rfqId) reads a request and its best quote now, and cancel(rfqId) ends it.

ws.comboMaker() joins your maker topic and delivers every request opened after the join as rfq_opened, your own account’s included. Quote an amount with an expiry; quote_accepted and placed follow when yours binds.

const maker = await ws.comboMaker({
onEvent: async (e) => {
if (e.event === "rfq_opened" && e.payload.side === "buy") {
// e.payload.amount is the stake, e.g. "5.0000"; price off max_price and tick_size
const { quote_id } = await maker.quote(e.payload.rfq_id, "10.00", 60_000, { clientOrderId: "tssdk-capture-buy" });
console.log("quoted", quote_id); // "05308411-acfa-4b81-af8c-c390ad41a254"
} else if (e.event === "placed") {
console.log(e.payload.order_id, e.payload.quantity, e.payload.price); // "b24cabe6-9a53-48e3-b891-2a19245690ec" "14.00" "0.3334"
} else if (e.event === "negotiation_closed") {
console.log(e.payload.quote_id, e.payload.reason); // "f6fdf76e-0c74-44cf-8180-b0f1fb0f09df" "placed_elsewhere"
}
},
});

withdrawQuote(rfqId, quoteId) removes one of your quotes. You may hold several quotes on one request; the player is shown only the best.

Each session hands you typed events, narrowed on event:

Session Event Payload fields
ComboPlayer best_quote rfq_id, market_id, quote_id, amount, expires_at (the last three null when nothing is live)
ComboPlayer placed rfq_id, market_id, order_id, quantity, price
ComboPlayer negotiation_closed rfq_id, market_id, reason: cancelled, expired, leg_not_quotable, unknown_leg, player_refused, maker_refused
ComboMaker rfq_opened rfq_id, market_id, external_id, side, amount, quantity, max_price, tick_size, expires_at, legs
ComboMaker rfq_closed rfq_id, market_id, reason: placed, cancelled, expired, leg_not_quotable, unknown_leg, player_refused, maker_refused, ended
ComboMaker quote_accepted rfq_id, market_id, external_id, quote_id
ComboMaker placed rfq_id, market_id, external_id, quote_id, order_id, quantity, price
ComboMaker negotiation_closed rfq_id, market_id, external_id, quote_id, reason: placed_elsewhere, maker_refused, cancelled, expired, leg_not_quotable, unknown_leg, player_refused

Take them with onEvent, with for await (const e of player), one at a time with next(timeoutMs), or with waitFor(event, { rfqId, quoteId, timeoutMs }). waitFor also looks at events already received, so a placed that arrives before the accept() reply is not missed. player.events holds the latest 1000. Payloads keep the exchange’s snake_case fields: money is a dollar string, quantities are strings and expires_at is microseconds since the Unix epoch. Every socket of an account receives that account’s player events, whichever socket opened the request.

Exception When
STXCombosUnavailableException Combos are not available on this environment. Thrown by comboPlayer() and comboMaker(); nothing else is affected.
STXComboPriceMovedException accept() found nothing live at your amount or better. err.best is the live best quote, all null when nothing is live; the request stays open.
STXComboRefusedException Any other refusal. err.code is the exchange’s reason, err.command the command, and err.message reads <command> on <topic> refused: <reason>. For player_refused, err.detail holds the order refusal.
TypeError A money argument that is not a string, thrown before anything is sent.

All three combo exceptions extend STXChannelException, so err.topic and err.reply (the exchange’s reply) are there too. Joining on an exchange without combos:

try {
await ws.comboPlayer();
} catch (err) {
if (err instanceof STXCombosUnavailableException) console.log(err.message, err.reason);
else throw err;
}
"Combos are not available on this environment."
"unmatched topic"

Joining with a read-only API key throws STXComboRefusedException with the exchange’s sentence as the code:

{
"name": "STXComboRefusedException",
"message": "phx_join on rfq:bc2d4b34-5d21-4a2a-8af6-d1ad3846ac3f refused: This API key does not have write access",
"code": "This API key does not have write access",
"command": "phx_join"
}

The refusals captured for this page, each an STXComboRefusedException with this code:

Call err.code
requestQuote() with one leg combo_minimum_two_markets_needed
requestQuote() with six legs combo_maximum_legs_exceeded
requestQuote() with a marketId that is not a UUID malformed_id
requestQuote() with an outcome other than won or lost unknown_outcome
requestQuote() with a leg id no market has, or a combination’s id unknown_leg
requestQuote() naming one market twice duplicate_legs
requestQuote() or accept() once a leg has stopped trading leg_not_quotable
requestQuote() with "1e400" malformed_money
requestQuote() with "0" or "-5.00" non_positive_amount
requestQuote() with "5.001" sub_cent_amount
requestQuote() with more than your balance insufficient_assets
cashOut() with an ordinary market or an unknown id unknown_combination
cashOut() with a combination that is no longer open market_not_quotable
cashOut() with nothing left to sell nothing_to_sell
snapshot(), cancel(), accept() or quote() on a request that has ended, or never existed not_found
snapshot(), cancel() or accept() from another account not_the_requester
any call with an id that is not a UUID malformed_id
quote() with expiresInMs of 0 or 1.5 expires_at_required
quote() with a clientOrderId over 255 bytes malformed_client_order_id
quote() with "0" non_positive_amount
quote() with "0.0000001", too small to price price_out_of_range
quote() of "0.01" on a "0.50" stake, which buys no whole contract below_one_contract
quote() on your own account’s request cannot_quote_own_request
withdrawQuote() of a quote you no longer hold, or another maker’s unknown_quote

Combo negotiation lists every reason, including those only a failing exchange sends.

A request belongs to the channel that opened it, and a quote to the channel that placed it. Leaving or a dropped socket ends your open requests (cancelled) and withdraws your quotes. The SDK rejoins after a reconnect, but the requests and quotes do not come back: open them again.