# Market maker guide

An end-to-end checklist for running a quoting system on Polyester, with links to the page that owns each topic.

This guide is a map for building a market-making system on Polyester. Each section names the decision you have to make, states the rule that shapes it, and links to the page with the details. Work through it in order the first time, then use it as a review before you go live.

Build and test on Testnet: the API host is `api.testnet.polyester.com` and the trade host is `trade.api.testnet.polyester.com`. Mainnet is coming soon. If you have not placed an order yet, start with the [Quickstart](https://testnet.polyester.com/docs/developer-docs/getting-started/quickstart).

## Choose an interface

Most market makers combine three paths: one for order entry, one for order state, and one for reads and recovery. Every order path reaches the same order handling and shares one rate-limit budget.

| Need                             | Use                                                                                                                                                                                        |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Lowest-overhead order entry      | [SBE trade WebSocket](https://testnet.polyester.com/docs/developer-docs/sbe-websocket/getting-started): pipelined binary commands, with your order and execution events on the same socket |
| Order entry with Protobuf types  | [Protobuf trade WebSocket](https://testnet.polyester.com/docs/developer-docs/connectrpc/protobuf-trade-websocket): the Orders API messages over one signed session                         |
| Order entry over HTTP            | [REST](https://testnet.polyester.com/docs/developer-docs/rest/getting-started) or [ConnectRPC](https://testnet.polyester.com/docs/developer-docs/connectrpc/getting-started)               |
| Your order state                 | The [private WebSocket](https://testnet.polyester.com/docs/developer-docs/connectrpc/private-websocket)                                                                                    |
| Market data                      | The [realtime channels](https://testnet.polyester.com/docs/developer-docs/market-data/realtime-channels)                                                                                   |
| Your trades and balances         | The realtime connection in the [WebSocket session model](https://testnet.polyester.com/docs/developer-docs/shared-concepts/websocket-session-model)                                        |
| Setup, reads, and reconciliation | REST or ConnectRPC                                                                                                                                                                         |

Both trade WebSockets run on the trade host. The SBE socket is described in [Connection and authentication](https://testnet.polyester.com/docs/developer-docs/sbe-websocket/connection-and-authentication), [Order entry](https://testnet.polyester.com/docs/developer-docs/sbe-websocket/order-entry), and [Responses and events](https://testnet.polyester.com/docs/developer-docs/sbe-websocket/responses-and-events). Compare every interface in the [Connectivity matrix](https://testnet.polyester.com/docs/developer-docs/getting-started/connectivity-matrix).

## Set up keys and Subaccounts per strategy

Give each strategy its own Subaccount. A Subaccount has its own balances, orders, rate-limit pools, and dead-man switch, so one strategy's failure stays contained. See [Subaccounts and roles](https://testnet.polyester.com/docs/developer-docs/account/subaccounts-and-roles).

A new Subaccount is read-only until you attach a Subaccount policy that allows trading. Each API key also needs an API key policy, and the effective access is the intersection of both. Grant a quoting key only `TRADE_SPOT` on the markets it quotes. See [API key policies](https://testnet.polyester.com/docs/developer-docs/account/api-key-policies).

Bind each key to the Subaccount it trades. A trade WebSocket session trades for the key's account only, so open one session per Subaccount. Restrict each key to your servers' addresses with an [IP allowlist](https://testnet.polyester.com/docs/developer-docs/authentication-security/api-key-ip-allowlists), and sign every request as in [Ed25519 API keys](https://testnet.polyester.com/docs/developer-docs/authentication-security/ed25519-api-keys).

## Load Pair rules and scales

[`GetSpotConfig`](https://testnet.polyester.com/docs/api-docs/connect/marketdata.v1.MarketDataService/GetSpotConfig) ([`GET /v1/spot/config`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/spot/config)) returns each Pair's tick size, step size, minimum and maximum sizes, open-order limits, and trading status. Load it at startup, refresh it on an interval, and reload it whenever an order is rejected for a Pair rule or status.

A Pair can be `POST_ONLY`, which accepts only post-only orders, or `HALTED`, which cancels your open orders. ConnectRPC, Protobuf, and SBE carry prices and quantities as scaled integers, and identify a Pair by its numeric `symbolId`. See [Pairs and trading rules](https://testnet.polyester.com/docs/developer-docs/market-data/pairs-and-trading-rules) and [Scaled integers](https://testnet.polyester.com/docs/developer-docs/connectrpc/scaled-integers).

## Build the quoting loop

**Quote post-only.** A post-only limit order is refused with `POST_ONLY_CROSS` instead of taking liquidity. Post-only is set on good-till-cancelled and good-till-date limit orders. See [Order types and lifecycle](https://testnet.polyester.com/docs/developer-docs/shared-concepts/order-types-and-lifecycle).

**Refresh with batch replace.** [`BatchReplaceOrders`](https://testnet.polyester.com/docs/api-docs/connect/orders.v1.OrdersService/BatchReplaceOrders) ([`POST /v1/orders/replace-batch`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/orders/replace-batch)) modifies up to 50 orders of one Pair in one request. An item that keeps its price and does not raise its quantity is amended in place and keeps its queue priority; any other change replaces the order with a new `orderId`. Batch create places up to 20 orders and batch cancel removes up to 50. See [Batch orders](https://testnet.polyester.com/docs/developer-docs/shared-concepts/batch-orders).

**Keep your identifiers stable.** Give every order a `clientOrderId`, up to 36 characters. It stays reserved for at least 60 seconds after the order ends, and a reused ID is rejected, not replayed. Give every batch a new `requestId`; repeating it within the batch's retry window returns the original result. See [Client order IDs](https://testnet.polyester.com/docs/developer-docs/shared-concepts/client-order-ids).

**Read before you resend.** An answer ending in `_OUTCOME_UNKNOWN`, a timeout, or a dropped connection means the command may have applied. Read the order before you send anything new. See [Error responses](https://testnet.polyester.com/docs/developer-docs/error-handling/error-responses#retries-and-idempotency).

## Set up risk controls

| Control                                                                                                                          | What it does                                                                                                                                                                  |
| -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Dead-man switch](https://testnet.polyester.com/docs/developer-docs/shared-concepts/dead-man-switch)                             | Cancels the account's open orders when your heartbeats stop. Arm it with a timeout of 10 to 120 seconds and refresh it before it runs out. Each Subaccount has its own switch |
| [Cancel all orders](https://testnet.polyester.com/docs/developer-docs/shared-concepts/cancel-all-orders)                         | Cancels every matching open order, by Pair and side. It does not cancel standalone triggers, so cancel those first when you stop a strategy                                   |
| [Self-trade prevention](https://testnet.polyester.com/docs/developer-docs/shared-concepts/self-trade-prevention)                 | Stops your orders from matching each other across the Main Account and all its Subaccounts. The default is `EXPIRE_MAKER`; set the mode on each order                         |
| [Market order price protection](https://testnet.polyester.com/docs/developer-docs/shared-concepts/market-order-price-protection) | Bounds the price of market orders, such as hedges, with a slippage tolerance                                                                                                  |

Closing a connection does not trigger the dead-man switch, and an open connection does not keep it alive: only a heartbeat call does. Send each heartbeat with a new `requestId`.

## Stay within rate limits

Placements and cancellations draw from separate pools per trading account. Every key and transport of that account shares them, so extra keys add no capacity. Each batch item costs as much as a single command, and a cancel-all costs more. Read your effective limits with [`GET /v1/trading/rate-limits`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/trading/rate-limits).

On `RATE_LIMIT_EXCEEDED`, wait for `Retry-After` or `retry_after_ms`, then retry with the same identifiers and a fresh signature. Two refusals are not your quota: `SYSTEM_RATE_LIMITED` means the venue-wide rate is exhausted, so back off and retry, and `OVERLOADED` means too many of your order requests are outstanding, so retry once one completes. A per-IP limit also applies to REST and ConnectRPC order writes, and trade WebSocket commands do not count toward it. See [Rate limits](https://testnet.polyester.com/docs/developer-docs/shared-concepts/rate-limits).

Capacity above the published tiers needs an account-specific override; retail VIP status does not grant it.

## Consume your private stream and reconcile

The [private WebSocket](https://testnet.polyester.com/docs/developer-docs/connectrpc/private-websocket) sends a snapshot of an account's orders, then every change as a unit with a cursor. Each order carries a `version`: keep the copy with the higher version when you merge stream updates with reads. Send `sync` after a command to wait until the stream reflects it.

Check `seq` on every frame. On a gap, subscribe again with your last cursor. When the server answers `resyncRequired` instead, install the new snapshot and read fills from your trade history, because the events in between are not delivered. See [Gaps, resume, and resync](https://testnet.polyester.com/docs/developer-docs/connectrpc/private-websocket#gaps-resume-and-resync).

After any reconnect, reconcile from the read APIs before you quote again: open orders with [`GET /v1/orders/open`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/orders/open) and fills with [`GET /v1/spot/trades`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/spot/trades). Rebuild your order book from a snapshot and its `bookSeq`, as in [Order book](https://testnet.polyester.com/docs/developer-docs/market-data/order-book), and backfill missed public trades by trade ID, as in [Trades](https://testnet.polyester.com/docs/developer-docs/market-data/trades). Your trades and balances on the realtime connection follow the delivery and recovery rules of the [WebSocket session model](https://testnet.polyester.com/docs/developer-docs/shared-concepts/websocket-session-model#delivery-and-recovery).

## Know your fees

[`GetSpotFeeRates`](https://testnet.polyester.com/docs/api-docs/connect/fees.v1.FeeService/GetSpotFeeRates) ([`GET /v1/spot/fee-rates`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/spot/fee-rates)) returns the maker and taker rates that apply to an account, per Pair. A negative maker rate is a rebate. Rates follow the VIP tier of the Main Account, which every Subaccount inherits. Read rates from the API instead of hard-coding them. To size the largest buy that pays its fee in quote, use the row's `reserveFeeRatePercent` (`postOnlyReserveFeeRatePercent` for a post-only order), not your effective rate. See [Fees and VIP](https://testnet.polyester.com/docs/developer-docs/account/fees-and-vip#size-a-buy-that-pays-its-fee-in-quote).

## Run it in production

Review checklist

Before you go live

- [Clocks are synchronized; signed timestamps must be within 10 seconds of server time](https://testnet.polyester.com/docs/developer-docs/authentication-security/ed25519-api-keys)
- [Every request attempt is signed again with a fresh timestamp](https://testnet.polyester.com/docs/developer-docs/authentication-security/api-key-replay-policy)
- [Reconnects back off with jitter and fetch new tokens or a fresh signed upgrade](https://testnet.polyester.com/docs/developer-docs/sbe-websocket/connection-and-authentication)
- [The dead-man switch is armed for every quoting account](https://testnet.polyester.com/docs/developer-docs/shared-concepts/dead-man-switch)
- [Alerts branch on error codes, never on message text](https://testnet.polyester.com/docs/developer-docs/error-handling/error-codes)
- Outcome-unknown answers trigger a read before any resend
- Your client ignores unknown fields and enum values

> **Testnet upgrades**
>
> A Testnet upgrade can change contracts and reset trading state without sending order events, so your bot must be able to restart from an empty book. Each upgrade is announced in the [changelog](https://testnet.polyester.com/docs/changelog) with what to change; see [API changes and versioning](https://testnet.polyester.com/docs/developer-docs/getting-started/api-changes-and-versioning). Regenerate SBE codecs and Protobuf types from the contracts of the environment you connect to, as [Environments and hosts](https://testnet.polyester.com/docs/developer-docs/getting-started/environments-and-hosts#contracts-per-environment) describes.
