# Fees and VIP tiers

Read effective maker and taker rates with GetSpotFeeRates, decode the fee charged on each fill, and check VIP tier qualification with VIPService.

Polyester charges a spot trading fee on each fill. A fill that adds liquidity pays the **maker** rate, and a fill that takes liquidity pays the **taker** rate. Rates depend on the VIP tier of the Main Account and can vary by Pair. A maker rate can be negative, which means the fill earns a rebate.

> **Read rates, do not hard-code them**
>
> Tier thresholds and rates can change with a new VIP policy version. Read them from the API before pricing a strategy, and treat the values on this page as field descriptions, not a fee schedule.

## Effective rates for your account

[`FeeService.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 rates that apply to the selected account right now, one row per Pair. Each row carries the Pair (`symbolId` on ConnectRPC, a symbol such as `BTC-USDT` on REST), `makerFeeRatePercent`, `takerFeeRatePercent`, and the `vipTier` used to resolve them. Fills are charged these effective rates. Each row also carries the rates an order reserves its fee at, a `breakdown` of how the effective rates are made up, and `nextChangeAt`; see [Size a buy](#size-a-buy-that-pays-its-fee-in-quote) and [How your effective rates are made up](#how-your-effective-rates-are-made-up).

Rates are percentage strings without `%`, of the fill's notional, so divide by 100 to get a fraction. A negative maker rate is a rebate; the taker rate is never negative. Parse them as exact decimals, not floating-point numbers.

- Omit the Pair filter to get every listed, enabled Pair. Request up to 100 Pairs with repeated `symbol` (REST) or `symbolId` (ConnectRPC) values.
- Pass `subaccountId` to read a Subaccount. Without it, a Subaccount-scoped API key reads its bound Subaccount, and every other caller reads its Main Account. Rates always follow the VIP tier of the Main Account that owns the target account.
- Session callers and API keys can call it. An API key needs `READ_SPOT`.

## Fees on each fill

The fee Asset is chosen per order with `feeAsset`. SELL fees are always in quote; a BUY can pay in quote or have the fee deducted from the base it receives. See [Fee Assets](https://testnet.polyester.com/docs/developer-docs/shared-concepts/fee-assets).

When a BUY pays its fee in quote, the order reserves its quote cost plus a fee allowance. Polyester charges the actual fee per fill and returns any unused reserve to `available` as the order fills or ends. See [Balances and holds](https://testnet.polyester.com/docs/developer-docs/account/balances-and-holds#available-and-reserved).

Each fill records its fee once it settles:

| Where                                                                                                                                  | Fields                                                                                  |
| -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| Your trades ([`GetUserTrades`](https://testnet.polyester.com/docs/api-docs/connect/orders.v1.OrdersReadService/GetUserTrades))         | `feeAmountE18`, `feeAsset`, and `feeIsRebate` for the individual fill.                  |
| Order reads                                                                                                                            | `feeTotal`, the net fee of the order's fills, with `isRebate` when rebates exceed fees. |
| Ledger history ([`ListTransfers`](https://testnet.polyester.com/docs/api-docs/connect/ledger.read.v1.LedgerReadService/ListTransfers)) | Fee and rebate legs with `transferCode` `MAKER_FEE`, `TAKER_FEE`, or `REBATE`.          |

A fill's fee can appear shortly after the fill itself. A tier change applies to later fills; it does not recalculate fees on fills that already settled. A tier earned under a new VIP policy version takes effect only once that version is in force; until then, the account keeps its current tier and rates.

## Size a buy that pays its fee in quote

Each `GetSpotFeeRates` row also carries the rates a new order reserves its fee at when the fee is paid in quote:

- `reserveFeeRatePercent` (ConnectRPC `reserve_fee_rate_percent`): the highest maker or taker rate of every VIP tier under the Pair's fee schedule. It is at least your effective rates, and discounts never lower it.
- `postOnlyReserveFeeRatePercent`: the same for a post-only limit order, from maker rates only.

A buy sized by a quote amount (`maxQuoteDebit`) reserves at the lower of `reserveFeeRatePercent` and `takerFeeRatePercent`.

Size the largest buy limit order your balance allows from the reserve rate `r`, not from your effective maker or taker rate. Work in units of 10⁻¹⁸ of the quote Asset, whatever its own decimals:

```text
N = ceil(price × qty)       notional, rounded up to 10⁻¹⁸ quote
F = ceil(N × r / 100)       fee reserve, rounded up to 10⁻¹⁸ quote
the order has sufficient quote balance when N + F ≤ available
```

Do not round `N` or `F` to the quote Asset's own decimals: that asks for more than the order reserves, so the largest order that fits looks refused. The largest order is the largest quantity on the Pair's step size that fits the balance and still passes the Pair's other limits, such as minimum and maximum quantity, tick size, and maximum notional.

### Example: the largest buy for a balance of 1,000,000

At a reserve rate of `0.15` and a price of `1`, a buy of `998502.24663005` base reserves `N + F = 999999.999999995075` quote and fits an available balance of `1000000`. One more step does not fit.

## How your effective rates are made up

`breakdown` lists the components of the row's effective maker and taker rates, each with the time it took effect and, when scheduled, the time it ends:

- `tierMakerFeeRatePercent` and `tierTakerFeeRatePercent`: your VIP tier's rates under the fee policy in force.
- `marketMultiplier`, when set: the Pair's fee schedule, as `makerMultiplierPercent` and `takerMultiplierPercent`, with `effectiveFrom` and an optional `effectiveTo`.
- `accountDiscounts`: the account discounts in force, each with `makerDiscountPercent`, `takerDiscountPercent`, `allMarkets`, `effectiveFrom`, and an optional `effectiveTo`. The highest maker discount and the highest taker discount apply, possibly from different discounts.
- `referralDiscount`, when set: the referral discount in force, in the same shape.

The effective rate is the tier rate times the multiplier, times one minus the highest account discount, times one minus the referral discount: a 10% account discount and a 20% referral discount on a 0.1% rate give 0.1% × 0.9 × 0.8 = 0.072%. Discounts apply only to a positive rate; a negative maker rate, a rebate, is scaled by the multiplier alone. The [`SpotFeeRateBreakdown`](https://testnet.polyester.com/docs/api-docs/connect/fees.v1.FeeService/GetSpotFeeRates) reference gives the integer steps that reproduce the served rates exactly.

`nextChangeAt`, when present, is the earliest time a component starts or ends. Fetch the rates again then. REST times are RFC 3339 UTC strings. REST always sends `accountDiscounts`, as `[]` when there are none; ConnectRPC JSON omits an empty list and fields at their default value.

## VIP tiers

The VIP tier is set for the Main Account, and every Subaccount inherits it. A tier is earned separately from two rolling 30-day metrics, and the higher result applies:

- **Settled spot volume** in USD across the Main Account and its Subaccounts
- **Average portfolio value** in USD across the same accounts

The qualification rules are explained in [VIP tiers](https://testnet.polyester.com/docs/user-docs/fees-and-vip/vip-tiers).

### Tier catalog

[`VIPService.ListVIPTiers`](https://testnet.polyester.com/docs/api-docs/connect/vip.v1.VIPService/ListVIPTiers) ([`GET /v1/vip/tiers`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/vip/tiers)) is public and cacheable. It returns the active `policyVersion`, its `effectiveFrom` time, the `retentionThresholdBp` used to keep an average portfolio value tier near its boundary, and all eleven tiers. Each tier carries:

- `volumeThresholdUsd` and `aopThresholdUsd`, the entry thresholds (VIP0 has no portfolio threshold)
- `makerFeeRatePercent` and `takerFeeRatePercent`, the tier's base rates before Pair-specific adjustments

Use `GetSpotFeeRates` for the rate a Pair actually charges.

### Your status

[`VIPService.GetVIPStatus`](https://testnet.polyester.com/docs/api-docs/connect/vip.v1.VIPService/GetVIPStatus) ([`GET /v1/vip/status`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/vip/status)) returns the caller's Main Account tier. It accepts interactive sessions only and has no Subaccount selector. API keys are refused with `AUTH_SESSION_KIND_NOT_ALLOWED`.

The response gives the effective `tier` and the tier each metric earns on its own (`volumeTier`, `aopTier`). Each rolling metric has a quality status: `COMPLETE`, `PRICE_UNAVAILABLE`, or `INCOMPLETE_HISTORY`, with the count of incomplete hours.

- `settledVolume30dUsd` is a lower bound while its status is `PRICE_UNAVAILABLE`, and is omitted while it is `INCOMPLETE_HISTORY`.
- `averageAop30dUsd` is sent only while its status is `COMPLETE`.
- `nextTierThresholds` is omitted at VIP10.

An incomplete volume metric can raise the tier when its proven lower bound reaches a higher threshold, but never lowers it. An incomplete portfolio metric keeps its previous tier. A new account uses VIP0 until its first assignment, so the evaluation fields are omitted.

Network and route fees for deposits and withdrawals are separate from trading fees. See [Deposit and withdrawal fees](https://testnet.polyester.com/docs/user-docs/fees-and-vip/deposit-and-withdrawal-fees). Field schemas are in the [API reference](https://testnet.polyester.com/docs/api-docs).
