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.
About the output on this page
Section titled “About the output on this page”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).
1. Join
Section titled “1. Join”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.
2. Player requests a quote
Section titled “2. Player requests a quote”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.
3. Makers receive the request
Section titled “3. Makers receive the request”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.
4. Player reads the request
Section titled “4. Player reads the request”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.
5. Makers quote what the player wins
Section titled “5. Makers quote what the player wins”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.
6. Player sees the best quote
Section titled “6. Player sees the best quote”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.
7. A maker withdraws a quote
Section titled “7. A maker withdraws a 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.
8. Accepting above the best
Section titled “8. Accepting above the best”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.
9. Player accepts
Section titled “9. Player accepts”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.
10. Both sides are told
Section titled “10. Both sides are told”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.
A cash out
Section titled “A cash out”1. Player opens the cash out
Section titled “1. Player opens the cash out”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.
2. Makers receive it
Section titled “2. Makers receive 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.
3. The only quote is withdrawn
Section titled “3. The only quote is withdrawn”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 }4. New quotes, and an accept at a floor
Section titled “4. New quotes, and an accept at a floor”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.
How a request ends
Section titled “How a request ends”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.
Cancelled
Section titled “Cancelled”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.
The requesting channel leaves
Section titled “The requesting channel leaves”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" } }Expired
Section titled “Expired”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’s order is refused
Section titled “The player’s order is refused”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.
A leg closes while the request is open
Section titled “A leg closes while the request is open”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.
As a player
Section titled “As a player”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.
As a maker
Section titled “As a maker”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.
Events
Section titled “Events”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.
Errors
Section titled “Errors”| 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.
Connections
Section titled “Connections”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.

