# Pairs and trading rules

Read each Pair's identifiers, scales, order limits, and trading status from GetSpotConfig, and keep a cached copy current.

[`MarketDataService.GetSpotConfig`](https://testnet.polyester.com/docs/api-docs/connect/marketdata.v1.MarketDataService/GetSpotConfig) (REST [`GET /v1/spot/config`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/spot/config)) returns every spot Asset and Pair with the rules an order must meet: price and quantity increments, minimum and maximum sizes, scales for decoding market data, and the Pair's current trading status. It takes no parameters and needs no authentication.

Read it before you place orders or decode market data, and keep a cached copy current as described in [Cache and refresh](#cache-and-refresh).

## Request

## REST JSON

```http
GET https://api.testnet.polyester.com/v1/spot/config
```

## ConnectRPC ProtoJSON

```http
POST https://api.testnet.polyester.com/marketdata.v1.MarketDataService/GetSpotConfig
Content-Type: application/json

{}
```

See [Environments and hosts](https://testnet.polyester.com/docs/developer-docs/getting-started/environments-and-hosts) for the host of each environment.

## Response

The response holds `assets`, ordered by Asset code, `pairs`, ordered by symbol, the snapshot time `tsSec`, and `maxOpenOrdersTotal`. This is a `BTC-USDT` entry captured from REST on 5 November 2026:

```json
{
	"symbolId": 1,
	"symbol": "BTC-USDT",
	"baseAsset": "BTC",
	"quoteAsset": "USDT",
	"status": "ENABLED",
	"tickSize": "0.01",
	"stepSize": "0.00000001",
	"minQtyBase": "0.00001",
	"minNotionalQuote": "5",
	"maxOrderNotionalQuote": "5000000",
	"maxMarketNotionalQuote": "1000000",
	"maxOpenOrdersPerPair": 200,
	"baseQuantityScale": 8,
	"quoteQuantityScale": 6,
	"referencePriceScale": 9,
	"allowBuyFeeFromBase": true,
	"defaultMarketSlippageBpsBuy": 200,
	"defaultMarketSlippageBpsSell": 200,
	"maxClientRefDriftBps": 100,
	"listingAt": "2026-11-05T12:13:33.827104Z",
	"marketdata": { "orderbookPriceBuckets": [0.01, 0.1, 1, 10, 100] }
}
```

The values are environment configuration, not fixed limits. Read them from the response of the environment you trade on. Every field is described in the [API reference](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/spot/config); the sections below cover how to use them.

REST and ConnectRPC return the same fields. They differ in two places: REST writes the status without its prefix (`ENABLED`), while ConnectRPC uses the enum name (`PAIR_STATUS_ENABLED`); and ConnectRPC JSON omits fields at their default value, such as a `false` boolean.

## Identify a Pair

| Field                     | Meaning                                                                              |
| ------------------------- | ------------------------------------------------------------------------------------ |
| `symbol`                  | The Pair's symbol, such as `BTC-USDT`. REST paths and REST request bodies use it     |
| `symbolId`                | Stable numeric Pair ID. ConnectRPC requests and every realtime channel name use it   |
| `baseAsset`, `quoteAsset` | Asset codes of the Pair. Look up each Asset's scales in `assets` by its `asset` code |

Keep a map from `symbolId` to `symbol` and back. Realtime channel names such as `public:spot:market:trades:1` and ConnectRPC payloads carry only the `symbolId`. Symbols are case-sensitive: `btc-usdt` is an unknown symbol.

## Order rules

All decimal rules are strings, so parse them as exact decimals, never as floating-point numbers.

| Field                    | Rule                                                                                                                                                                                                      |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tickSize`               | Price increment in quote units. A limit price that is not a multiple is rejected with `PRICE_TICK_SIZE`                                                                                                   |
| `stepSize`               | Quantity increment in base units. Other quantities are rejected with `QTY_STEP_SIZE`                                                                                                                      |
| `minQtyBase`             | Smallest order quantity in base units, otherwise `MIN_QTY`                                                                                                                                                |
| `minNotionalQuote`       | Smallest order value in quote units, otherwise `MIN_NOTIONAL`                                                                                                                                             |
| `maxLimitQtyBase`        | Largest quantity of one limit order. A replace is checked on its new total less the quantity already filled. Above it: `MAX_QTY`                                                                          |
| `maxMarketQtyBase`       | Largest quantity of one market order. A market order sized by quote amount is checked once converted to base. Above it: `MAX_QTY`                                                                         |
| `maxOrderNotionalQuote`  | Largest value of one order: quantity times the limit price, or times the protection price for a market order. A buy sized by a quote amount must fit its whole amount. Above it: `MAX_NOTIONAL`           |
| `maxMarketNotionalQuote` | Largest value of one market order: a buy at its protection price, a sell at its reference price before slippage. A market buy sized by a quote amount must fit its whole amount. Above it: `MAX_NOTIONAL` |
| `maxOpenOrdersPerPair`   | Default limit on resting orders one account may hold on the Pair. Past it: `MAX_OPEN_ORDERS_PER_PAIR`                                                                                                     |
| `maxOpenOrdersTotal`     | Top-level field. Default limit on resting orders one account may hold across all Pairs. Past it: `MAX_OPEN_ORDERS_TOTAL`                                                                                  |

The four maximum size fields are absent when the Pair has no such maximum. In the example above, for instance, `BTC-USDT` has no `maxLimitQtyBase` or `maxMarketQtyBase`. Maximums are compared exactly, with no rounding.

Only good-till-cancelled and good-till-date orders that have not ended count toward the open-order limits. Immediate-or-cancel, fill-or-kill, and market orders never count, and a replaced order keeps its place. An account can have a lower limit of its own; `GetSpotConfig` returns only the venue default.

For how quantities, quote budgets, and minimums interact at order entry, see [Order sizing](https://testnet.polyester.com/docs/developer-docs/shared-concepts/order-sizing). The market order fields `defaultMarketSlippageBpsBuy`, `defaultMarketSlippageBpsSell`, and `maxClientRefDriftBps` are described in [Market order price protection](https://testnet.polyester.com/docs/developer-docs/shared-concepts/market-order-price-protection), and `allowBuyFeeFromBase` in [Fee Assets](https://testnet.polyester.com/docs/developer-docs/shared-concepts/fee-assets).

## Scales

ConnectRPC and Protobuf payloads carry prices and quantities as scaled integers. REST and WebSocket JSON carry decimal strings and need no scales.

| Field                   | Where | Decodes                                                                                                                |
| ----------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------- |
| `baseQuantityScale`     | Pair  | Base quantities: order book levels, trade quantities, order quantities                                                 |
| `quoteQuantityScale`    | Pair  | Quote amounts, such as 24h quote volume                                                                                |
| `referencePriceScale`   | Pair  | Prices of [reference candles](https://testnet.polyester.com/docs/developer-docs/market-data/candles#reference-candles) |
| `quantityScale`         | Asset | Asset fields that name this scale, such as supply. Account balances always use a fixed 18 decimals                     |
| `marketDataVolumeScale` | Asset | Base volume of candles and the market overview                                                                         |

Prices of trades, order books, and primary candles always use a fixed scale of 9, whatever the quote Asset. `quantityDisplayDecimals` on an Asset is display guidance only; never use it to decode. See [Scaled integers](https://testnet.polyester.com/docs/developer-docs/connectrpc/scaled-integers) for the conversion rules.

`marketdata.orderbookPriceBuckets` suggests price steps for grouping order book levels in a display. They are display hints in quote units, not trading rules, and order book responses always carry unaggregated levels.

## Trading status

`status` says which order requests the Pair accepts right now.

| Status        | New orders                                                                         |
| ------------- | ---------------------------------------------------------------------------------- |
| `ENABLED`     | Accepted, subject to the listing times below                                       |
| `DISABLED`    | Temporarily rejected with `PAIR_DISABLED`                                          |
| `CANCEL_ONLY` | Rejected with `PAIR_CANCEL_ONLY`. Cancellations are accepted                       |
| `POST_ONLY`   | Accepted only as post-only limit orders. Others are rejected with `PAIR_POST_ONLY` |
| `REDUCE_ONLY` | Rejected with `PAIR_REDUCE_ONLY`                                                   |
| `HALTED`      | Rejected with `MARKET_HALTED`. Cancellations are accepted                          |

When a Pair is halted, Polyester cancels its open orders with the cancel reason `MARKET_HALTED`. Fills before the cancel stand. Cancel-all accepts every status, so you can clear your orders on any Pair; see [Cancel all orders](https://testnet.polyester.com/docs/developer-docs/shared-concepts/cancel-all-orders).

### Listing and delisting times

`listingAt` and `delistingAt` are optional RFC 3339 times. They are independent of `status`: a Pair can be `ENABLED` with a `listingAt` in the future. Before `listingAt`, new orders are rejected with `PAIR_NOT_LISTED_YET`. From `delistingAt` on, new orders are rejected with `PAIR_DELISTED`. A Pair without these fields has no scheduled listing or delisting.

To decide whether you can trade a Pair now, check both: `status` is `ENABLED` (or `POST_ONLY` for post-only orders), `listingAt` is absent or in the past, and `delistingAt` is absent or in the future.

## Cache and refresh

Polyester pushes no realtime updates for Pair configuration, so poll it.

1. Load at startup

   Call `GetSpotConfig` before placing orders or decoding market data. Index `pairs` by `symbolId` and by `symbol`, and `assets` by `asset`.

2. Refresh on a bounded interval

   Re-read the configuration periodically and replace your cached copy as a whole. A response can be served from a cache for up to 10 seconds, so polling more often than that can return the same snapshot. A `tsSec` newer than your copy's means a newer snapshot.

3. Refresh on a rule rejection

   Re-read the configuration when an order is rejected with a Pair rule or status code, such as `PRICE_TICK_SIZE`, `MIN_NOTIONAL`, `MAX_QTY`, `PAIR_CANCEL_ONLY`, or `MARKET_HALTED`, and when a `symbolId` you do not know appears in a stream.

> **Status changes can arrive before your next refresh**
>
> A Pair's status can change between two reads. Treat a status rejection on an order as current and authoritative, even when your cached copy still says `ENABLED`.

## Errors

`GetSpotConfig` takes no input, so it fails only when the service is unavailable. Retry with backoff. Read quotas are not published yet; poll on a bounded interval as described in [Cache and refresh](#cache-and-refresh).
