# Testnet 0.92.5 → 0.92.9

What market makers, trading bots, and funding and account tools must change when Testnet moves from release 0.92.5 to 0.92.9.

This guide is for market makers and trading bots that use REST or the trading WebSocket, and for the funding and account tools around them. It covers the one-time move of Testnet from release 0.92.5 to 0.92.9.

It lists only the changes that need action from you. Every other change is in the [changelog](https://testnet.polyester.com/docs/changelog).

> **Endpoints stay the same**
>
> Continue using your existing Testnet API and WebSocket endpoints. Every route on this page is a path relative to them. REST examples use REST field names and human-unit decimal strings. WebSocket examples labelled "decoded Protobuf" illustrate the contents of a binary message: send binary Protobuf, not JSON text.

## Who needs to act

- **Everyone signing requests:** a 5-second freshness window, an optional receive-window header, no nonce on HTTP, and a fresh signed nonce on every WebSocket login. Typed REST errors and structured rate-limit refusals also change. See [Authentication and request freshness](#authentication-and-request-freshness), [REST validation and error handling](#rest-validation-and-error-handling), and [Rate-limit responses](#rate-limit-responses).
- **WebSocket traders:** optional per-command receive windows and the dead-man switch on the trading socket. See [Trading WebSocket additions](#trading-websocket-additions).
- **SBE codec users:** semantic version 1.2.0 of schema 7, with timed command templates and the `CancelAllAfter` templates. See [SBE timed command templates](#sbe-timed-command-templates) and [Dead-man switch on the trading socket](#dead-man-switch-on-the-trading-socket).
- **Batch-replace users:** admission is answered before completion, pending items are retried in a fresh batch, and batch status is kept for 60 seconds. See [Batch replacements and order reconciliation](#batch-replacements-and-order-reconciliation).
- **Trigger users:** pausing or canceling a trigger needs `TRADE_SPOT`, path and body IDs must agree, and cancel-all does not stop triggers. See [Trigger controls and shutdown safety](#trigger-controls-and-shutdown-safety).
- **Funding and account tools:** explicit API-key permissions, restarted lifecycle pagination, a renamed transfer label, and auto-withdraw pause instead of disable. See [Account permissions and policy tools](#account-permissions-and-policy-tools) and [Funding, transfers and withdrawals](#funding-transfers-and-withdrawals).
- **QuickSwap brokers:** specific outcomes for small amounts and returns, referral codes, and broker vault balances. See [QuickSwap integrations](#quickswap-integrations).

## Migration checklist

- Update request freshness checks and separate HTTP signing from WebSocket login signing.
- Generate a fresh signed nonce for every WebSocket connection attempt.
- Handle receive-window expiry, typed validation errors and rate-limit refusals.
- Reconcile batch replacements within their 60-second retention window.
- Check the API-key permissions used by funding and account tools.
- Restart lifecycle pagination without saved pre-upgrade tokens.
- Refresh public contract bindings, including optional SBE additions if used.

## Authentication and request freshness

These changes apply to every API-key signed request. See [Ed25519 API keys](https://testnet.polyester.com/docs/developer-docs/authentication-security/ed25519-api-keys) and the [API key replay policy](https://testnet.polyester.com/docs/developer-docs/authentication-security/api-key-replay-policy).

### HTTP freshness window is 5 seconds

**Changed:** the default acceptance window for a signed HTTP request changes from 10 seconds to **5 seconds**. A timestamp may be at most **1 second ahead** of server time, previously 10 seconds. Send `X-API-KEY-ID`, `X-API-TIMESTAMP` (Unix milliseconds) and `X-API-SIGNATURE`.

**What to do:** synchronize clocks and sign immediately before sending. Sign the body bytes actually sent, including any decimal padding.

### HTTP requests drop the nonce header

**Removed:** the `X-API-NONCE` header on HTTP requests. The nonce header now belongs only to signed WebSocket logins; see [Fresh signed nonce for WebSocket logins](#fresh-signed-nonce-for-websocket-logins).

**What to do:** remove `X-API-NONCE` from HTTP requests.

### Optional receive-window header

**Added:** the optional `X-API-RECV-WINDOW` header accepts one whole number of milliseconds from `1` to `60000`. When you send it, sign its exact header value as an additional line immediately after the timestamp. When you omit it, keep the existing canonical payload.

Join lines with LF, without a trailing LF. Keep the existing query canonicalization and Ed25519 signature encoding. HTTP signing keeps the percent-decoded public path; WebSocket signing uses the escaped upgrade path.

## Before (0.92.5)

```text
TIMESTAMP_MS
METHOD
PATH
CANONICAL_QUERY
SHA256_BODY_HEX
```

## After (0.92.9)

```text
TIMESTAMP_MS
RECV_WINDOW_MS
METHOD
PATH
CANONICAL_QUERY
SHA256_BODY_HEX
```

The `RECV_WINDOW_MS` line appears only when you send `X-API-RECV-WINDOW`. Without the header, the 0.92.9 payload is the same as in 0.92.5.

**What to do:** choose a receive window appropriate for the request. A longer window does not increase the 1-second future-clock allowance. Session-token requests must not send `X-API-RECV-WINDOW`; that header belongs to API-key signing.

### Requests expire before submission

**Changed:** the default window now also bounds waiting **before submission**. Previously a request accepted on arrival could be submitted after it became stale. Expiry does not cancel an accepted order and does not replace GTD expiry.

**What to do:** if a request may already have been submitted, keep your existing outcome-unknown reconciliation rules. Handle these refusals:

- **`TIMESTAMP_SKEW`** · HTTP 401\
  The timestamp is too old for the window or more than 1 second ahead; nothing was processed. Correct the clock if needed and re-sign with a fresh timestamp.
- **`RECV_WINDOW_INVALID`** · HTTP 401\
  The window header is empty, repeated, malformed or out of range. Correct the header before retrying.
- **`REQUEST_DEADLINE_EXCEEDED`** · HTTP 400\
  An authenticated order or trigger request expired before submission; nothing was submitted. Re-sign with fresh timing if the request is still wanted.
- **`REPLAY_CAPACITY_EXHAUSTED`** · HTTP 429\
  Capacity for remembering signed requests is exhausted; nothing was processed. Back off and re-sign; a shorter window releases capacity sooner.

### Fresh signed nonce for WebSocket logins

**Changed:** signed WebSocket upgrades require a cryptographically random nonce that is included in the signature. A previously reusable or unsigned nonce is no longer enough.

**What to do:** for every connection attempt to `/ws/trade.proto` or `/ws/trade.sbe`:

1. Generate 16 cryptographically random bytes, encoded as exactly 32 lowercase hex characters.
2. Build the signed `GET` payload using the escaped upgrade path, the canonical query and the SHA-256 of an empty body. Insert the receive-window line after the timestamp if you send its header.
3. Append LF followed by `ws-nonce:<nonce>`, with no trailing LF.
4. Send that nonce in `X-API-NONCE`, alongside the key ID, timestamp and signature.

## Before (0.92.5)

The nonce header was not part of the signed payload.

```text
TIMESTAMP_MS
GET
/ws/trade.proto

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
```

## After (0.92.9)

The signed payload ends with the nonce line. This example sends no explicit window.

```text
TIMESTAMP_MS
GET
/ws/trade.proto

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
ws-nonce:32_LOWERCASE_HEX_CHARACTERS
```

The final line is a placeholder: replace it with a newly generated nonce. Do not use the timestamp as the nonce. Never reuse a login nonce after a refused upgrade, disconnect or reconnect. The same signing change applies to signed market-data WebSocket upgrades.

A refused upgrade is an HTTP response before the socket opens. Its body contains a code as text, not an order acknowledgement or a REST problem:

- **`MISSING_HEADERS`** · HTTP 401\
  Supply a correctly formed nonce and the required signing headers.
- **`SIGNATURE_INVALID`** · HTTP 401\
  Check the path, query, nonce suffix and signature.
- **`REPLAY_DETECTED`** · HTTP 401\
  Generate a fresh nonce, timestamp and signature.
- **`REPLAY_CAPACITY_EXHAUSTED`** · HTTP 429\
  Wait briefly, then create a fresh signed login.
- **`AUTH_TEMPORARILY_UNAVAILABLE`** · HTTP 503\
  Back off, then create a fresh signed login.

Trading login reuse is refused even when you reconnect through another endpoint. Login freshness authenticates the upgrade; it does not give every later order a deadline. See [Connection and authentication](https://testnet.polyester.com/docs/developer-docs/sbe-websocket/connection-and-authentication) and the [Protobuf trade WebSocket](https://testnet.polyester.com/docs/developer-docs/connectrpc/protobuf-trade-websocket).

## Trading WebSocket additions

### Protobuf command receive windows

**Changed:** Protobuf client frames add `timestamp_ms` (field 3, int64) and `recv_window_ms` (field 4, uint32). How a command is timed depends on what you set:

- **Timestamp unset or zero, window zero**\
  No per-command deadline.
- **Timestamp set, window zero**\
  5000 ms window.
- **Timestamp set, window 1 to 60000**\
  Explicit window in milliseconds.
- **Window without timestamp, invalid timing, or timestamp over 1000 ms ahead**\
  `BAD_REQUEST`; the sequence is not consumed.
- **Command expires on arrival or before submission**\
  `REQUEST_DEADLINE_EXCEEDED` (308); the sequence is consumed and nothing is submitted.

Ping ignores these fields.

**What to do:** after an expiry at sequence 7, send the next command at sequence 8. After malformed timing at sequence 7, correct it and reuse sequence 7. The timing fields are optional: an existing untimed session remains untimed.

Decoded Protobuf example:

```json
{
	"seq": "7",
	"correlationId": "quote-7",
	"timestampMs": "CURRENT_UNIX_MILLISECONDS",
	"recvWindowMs": 2000,
	"createOrder": {
		"order": {
			"symbolId": 1,
			"side": "BUY",
			"baseQtyScaled": "10000",
			"limitGtc": { "priceTicks": "70000000000000" }
		}
	}
}
```

Use the Pair's actual quantity scale and prices. See the [Protobuf trade WebSocket](https://testnet.polyester.com/docs/developer-docs/connectrpc/protobuf-trade-websocket).

### SBE timed command templates

**Changed:** schema ID 7 remains at version 36; its semantic version advances from 1.0.0 to 1.2.0. Existing untimed messages keep their layouts. New templates add `timestampMs` and `recvWindowMs` to six commands:

| Template                  | ID |
| ------------------------- | -- |
| `CreateOrderTimed`        | 11 |
| `CancelOrderTimed`        | 12 |
| `ModifyOrderTimed`        | 13 |
| `BatchCreateOrdersTimed`  | 14 |
| `BatchReplaceOrdersTimed` | 15 |
| `BatchCancelOrdersTimed`  | 16 |

Their timing rules and sequence handling match [Protobuf command receive windows](#protobuf-command-receive-windows). The suffix adds 12 bytes to the corresponding fixed block, before variable data or groups. One timing pair covers an entire batch. Acknowledgement templates stay unchanged.

**What to do:** refresh your codecs to use these additions. Timing is excluded from batch retry identity, so refresh the timing while preserving the command's retry identity and payload. See [Order entry](https://testnet.polyester.com/docs/developer-docs/sbe-websocket/order-entry) and the [Message reference](https://testnet.polyester.com/docs/developer-docs/sbe-websocket/message-reference).

### Dead-man switch on the trading socket

**Added:** arm, refresh or disarm the same account timer that [`POST /v1/orders/cancel-all-after`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/orders/cancel-all-after) and [`CancelAllAfter`](https://testnet.polyester.com/docs/api-docs/connect/orders.v1.OrdersService/CancelAllAfter) set, directly on the trading socket.

Protobuf adds `ClientFrame.cancel_all_after` (field 18) and `AckResponse.cancel_all_after` (field 8). Decoded Protobuf example:

```json
{
	"seq": "42",
	"correlationId": "heartbeat-42",
	"cancelAllAfter": {
		"requestId": "heartbeat-42",
		"timeoutSec": 60,
		"symbolId": 1,
		"side": "SELL"
	}
}
```

`requestId` is required. Omit `subaccountId`: the socket trades for its key's account. Use `timeoutSec: 0` to disarm; `10` through `120` arms or refreshes. Other values return `CANCEL_ALL_AFTER_TIMEOUT_INVALID` (222).

SBE adds `CancelAllAfter` template 10, `CancelAllAfterAck` template 30, `CommandType.CANCEL_ALL_AFTER = 10`, `RateLimitOperation.CANCEL_ALL_AFTER = 8`, and statuses `ARMED = 1` and `DISABLED = 2`. A zero `symbolId` means every Pair; a null or zero side means both sides.

**What to do:** refresh well inside the timeout, for example every 15 to 30 seconds for a 60-second timer. The timer belongs to the account and is shared with HTTP: closing the socket neither fires nor disarms it. Every arm restarts it from application time, and an arm also replaces its scope. Arms, refreshes and disarms spend cancellation quota.

> **Use a fresh requestId for HTTP heartbeats**
>
> Resending on the WebSocket is specific to the socket. For REST or ConnectRPC heartbeats, use a fresh `requestId` each time: an identical HTTP retry can replay an earlier answer without refreshing the timer.

See [Dead-man switch](https://testnet.polyester.com/docs/developer-docs/shared-concepts/dead-man-switch).

## Batch replacements and order reconciliation

These changes concern [`POST /v1/orders/replace-batch`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/orders/replace-batch) and [`BatchReplaceOrders`](https://testnet.polyester.com/docs/api-docs/connect/orders.v1.OrdersService/BatchReplaceOrders). See [Batch orders](https://testnet.polyester.com/docs/developer-docs/shared-concepts/batch-orders).

### Admission is answered before completion

**Clarified, with improved immediate status reads:** a batch replace answers admission before each accepted item's amend or replace completes. A refresh that follows soon after can therefore be refused while the earlier change is pending. The code depends on how the refresh identifies the order:

- **By `clientOrderId`**\
  `ORDER_MODIFY_IN_PROGRESS` (226).
- **By the returned `replacementOrderId`**\
  `ORDER_PLACEMENT_PENDING` (227), or 226.

227 means placement is not yet confirmed; the order may already be on the book.

**What to do:** inspect each item's admission result. Retry only explicitly rejected items with 226 or 227, after a short, capped backoff with jitter, in a new batch with a **fresh request ID**. Reusing the rejected batch's ID replays its rejection. Never resend admitted items as a fresh batch.

REST retry example:

```http
POST /v1/orders/replace-batch
Content-Type: application/json

{
  "requestId": "refresh-2-retry-1",
  "symbol": "BTC-USDT",
  "items": [
    {"clientOrderId": "quote-bid-1", "newPrice": "100.10"}
  ]
}
```

Keep REST `symbol` and `newPrice`; the binary and Protobuf contracts use `symbolId` and `newPriceTicks`. These are existing interface differences, not renames in this release.

### Batch status is readable at once and kept 60 seconds

**Changed:** an accepted batch can be read as soon as its admission response returns. Previously an immediate read could report not found despite acceptance.

Read status with [`GetBatchReplaceStatus`](https://testnet.polyester.com/docs/api-docs/connect/orders.v1.OrdersReadService/GetBatchReplaceStatus) (`POST /orders.v1.OrdersReadService/GetBatchReplaceStatus`), passing the returned server-issued `batchRequestId` as an exact decimal fixed64 string in Protobuf JSON. This is a public RPC read, not a new REST alias. The execution phase then advances asynchronously.

**What to do:**

- Retention and identical-request admission replay last **60 seconds from acceptance**.
- `UNAVAILABLE` means status cannot currently be established: retry the same status read within that window.
- `NOT_FOUND` means no retained batch for that account and identity. It does **not** establish rejection.
- Once retention ends, reconcile the individual old and replacement orders. Never resubmit an accepted batch because its status is no longer found.
- A refused item's `updatedTsNs` can reflect a later observation time. Do not use it as the refusal's identity.

If the original response was lost, send the identical request with the same request ID within the replay window, or read the batch status when its identity is known. Never use a fresh request ID to resolve an unknown outcome.

## Trigger controls and shutdown safety

See [Triggers](https://testnet.polyester.com/docs/developer-docs/shared-concepts/triggers/overview).

### Pausing or canceling a trigger requires TRADE_SPOT

**Changed:** pausing or canceling a trigger now requires `TRADE_SPOT` on the API key's policy. A read-only key can no longer stop a trigger. These risk-reducing operations do not require the trigger's Pair to remain in the key's market allowlist. This covers [`POST /v1/triggers/{trigger_id}/pause`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/triggers/trigger_id/pause), [`DELETE /v1/triggers/{trigger_id}`](https://testnet.polyester.com/docs/api-docs/rest/DELETE/v1/triggers/trigger_id), [`PauseTrigger`](https://testnet.polyester.com/docs/api-docs/connect/triggers.v1.TriggersService/PauseTrigger) and [`CancelTrigger`](https://testnet.polyester.com/docs/api-docs/connect/triggers.v1.TriggersService/CancelTrigger).

**What to do:** grant `TRADE_SPOT` to the key responsible for trigger shutdown. A key without `TRADE_SPOT`, including a key with no policy, gets HTTP 403 `POLICY_SPOT_TRADE_DENY`; correct the policy rather than retrying. See [API key policies](https://testnet.polyester.com/docs/developer-docs/account/api-key-policies).

### Path and body trigger IDs must agree

**Changed:** modifying, pausing or resuming a REST trigger rejects a non-empty body `triggerId` that differs from the path ID. Previously the path silently won. HTTP 400 `PATH_BODY_ID_MISMATCH` (95) changes neither trigger. The rule applies to [`PUT /v1/triggers/{trigger_id}`](https://testnet.polyester.com/docs/api-docs/rest/PUT/v1/triggers/trigger_id), [`POST /v1/triggers/{trigger_id}/pause`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/triggers/trigger_id/pause) and [`POST /v1/triggers/{trigger_id}/resume`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/triggers/trigger_id/resume).

**What to do:** omit the redundant body ID or send the same ID.

## Before (0.92.5)

A different body `triggerId` was ignored and the path ID won.

```http
PUT /v1/triggers/<trigger-id>
Content-Type: application/json

{"triggerId":"<other-trigger-id>","maxSlippageBps":50}
```

## After (0.92.9)

Send no body ID, or the same ID as the path.

```http
PUT /v1/triggers/<trigger-id>
Content-Type: application/json

{"maxSlippageBps":50}
```

This example tightens an existing wider market-child cap to 0.5%; it is not a universal patch for every trigger type. Changing a child limit price remains unsupported: cancel and recreate the trigger.

For Subaccount triggers, a session-token caller must name `subaccountId`. A Subaccount-bound API key may omit it or name its own Subaccount; a root-scoped trading key must omit it.

### Cancel-all does not stop triggers

**Clarified, not a new behavior:** [`POST /v1/orders/cancel-all`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/orders/cancel-all) and dead-man-switch expiry cancel open orders, including trigger-generated orders, but do not cancel triggers.

- Standalone stop-loss, take-profit, trailing triggers and TWAPs can place new orders afterward.
- A ladder stops once its levels are canceled.
- An attached exit leg ends when its working exit order is canceled; with `oco`, its sibling legs end too. That position loses their protection.
- Other attached legs keep protecting the parent's filled quantity. A parent's legs end if it is canceled before any fill.

**What to do:** to stop automated trading, list and cancel live triggers first, then cancel open orders and reconcile. Do not treat a dead-man-switch heartbeat as protection against all future trigger activity. See [Cancel all orders](https://testnet.polyester.com/docs/developer-docs/shared-concepts/cancel-all-orders).

Successful REST trigger cancellation still returns `status: "CANCELED"`; Protobuf uses `STATUS_CANCELED`. REST trigger-event spellings such as `FIRE_REFUSED` are unchanged; corrected descriptions are not enum migrations.

### Parent-order trigger lists include moved legs

**Changed:** trigger lists filtered by a parent order ([`GET /v1/triggers`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/triggers), [`ListTriggers`](https://testnet.polyester.com/docs/api-docs/connect/triggers.v1.TriggersService/ListTriggers)) now include attached legs that moved to that parent through replacement and then ended.

**What to do:** reconcile against the latest returned parent relationship rather than assuming an absent leg never existed.

## REST validation and error handling

See [Error codes](https://testnet.polyester.com/docs/developer-docs/error-handling/error-codes) and [Error responses](https://testnet.polyester.com/docs/developer-docs/error-handling/error-responses).

### More specific order and trigger error codes

**Changed:** invalid order fields now keep specific public codes instead of being collapsed into generic validation failures. HTTP status remains 400.

- **`INVALID_SIDE`**\
  Invalid side.
- **`INVALID_STP_MODE`**\
  Invalid self-trade prevention mode.
- **`FEE_ASSET_INVALID`**\
  Invalid fee asset.
- **`INVALID_CLIENT_ORDER_ID`**\
  Invalid client order ID, including `newClientOrderId`.
- **`INVALID_MODIFY_BEHAVIOR`**\
  Invalid modify behavior.
- **`TRIGGER_PRICE_SOURCE_UNSUPPORTED`**\
  Invalid trigger price source.
- **`CANCEL_ALL_AFTER_TIMEOUT_INVALID`**\
  Invalid dead-man-switch timeout.

Malformed REST trigger decimals now use `INVALID_ARGUMENT`, previously `BAD_REQUEST`. Malformed order decimals keep `INVALID_ARGUMENT`. Field violations now identify the REST field, including nested batch indexes, for example `items[0].limitGtc.price` or `trigger.stopLoss.triggerPrice`.

**What to do:** keep `violations[].fieldPath` and the specific code. Branch on codes, not diagnostic messages. Orders still express execution through objects such as `limitGtc` and `marketIoc`; do not introduce separate REST order-type or time-in-force fields.

### A zero Subaccount selector depends on the API

**Changed:** order and trigger requests now refuse an explicitly supplied zero Subaccount selector instead of treating it as omitted. On REST, IDs are base58-encoded, so zero is written `"1"` and a literal `"0"` is a malformed value. In Protobuf, zero is an explicitly present fixed64 zero.

**Clarified:** what a zero selector does on each API:

- **Orders and triggers, reads and writes**\
  Refused: REST returns 400 `INVALID_SUBACCOUNT_ID`, ConnectRPC returns `invalid_argument`. Leaving the field out selects the Main Account, or the Subaccount the API key is bound to.
- **Balance and transfer reads**\
  Refused with 400 `INVALID_SUBACCOUNT_ID`.
- **Fee rate and policy selectors**\
  Refused with 400 `AUTH_INVALID_REQUEST`.
- **Address book, deposit addresses, transfer source, withdrawals and auto-withdraw**\
  Zero means "not set": the default account applies, subject to permissions.
- **Transfer destination**\
  Zero is refused.

**What to do:** omit `subaccountId` when selecting the Main Account or a key's bound account. Never serialize a default zero as a substitute for omission.

## Before (0.92.5)

On an order request, an encoded zero was treated as omitted.

```json
{
	"subaccountId": "1",
	"requestId": "quote-7"
}
```

## After (0.92.9)

Leave the field out. On an order request, sending `"1"` returns 400 `INVALID_SUBACCOUNT_ID`.

```json
{
	"requestId": "quote-7"
}
```

### Trailing zeros beyond a field's precision

**Changed:** transfer and withdrawal amounts and policy notional caps now accept trailing zeros beyond their precision, as order prices and quantities already did. For example, `"1.500000000000000000000"` is accepted as `"1.5"`.

**Clarified:** for every field, trailing zeros beyond the precision are accepted and never rounded, and any nonzero digit beyond it is refused. Precision depends on the field:

- **Transfer and withdrawal amounts**\
  18 decimal places.
- **Order and trigger prices**\
  9 decimal places.
- **Quantities**\
  The Pair's base Asset precision. Amounts in the quote Asset follow the quote Asset's precision.
- **External withdrawals**\
  Also limited to the destination token's own precision.
- **Policy notional caps**\
  6 decimal places.

The "18" in the API reference describes numeric precision, not a text length limit.

**What to do:** send values within each field's precision. Do not rely on the server to round.

### Oversized bodies return REQUEST_TOO_LARGE

**Changed:**

- **Auth request bodies over 128 KiB** · HTTP 413\
  `AUTH_REQUEST_TOO_LARGE` becomes `REQUEST_TOO_LARGE`; signed requests also receive a structured problem. Parse the problem and reduce the body.
- **Balance, history, equity and transfer read bodies over 1 MiB** · HTTP 413\
  `PAYLOAD_TOO_LARGE` becomes `REQUEST_TOO_LARGE`. Update your code matching and keep HTTP 413 handling.

### Auth path and body IDs must agree

**Changed:** conflicting resource IDs on affected auth operations now return HTTP 400 `AUTH_PATH_BODY_ID_MISMATCH` (58). This applies to MFA, member and invite, and policy-assignment operations, and to address-book copies.

**What to do:** omit redundant body IDs or match the path.

### Wallet login with an already linked signer

**Changed:** a wallet login ([`POST /v1/auth/login/wallet`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/auth/login/wallet)) with a signer already linked to another account now returns HTTP 409 `AUTH_WALLET_ALREADY_LINKED` (55), instead of an internal failure.

**What to do:** request a new challenge for the address that account already uses. See [Session login](https://testnet.polyester.com/docs/developer-docs/authentication-security/session-login).

### Trading-policy denials stay typed

**Changed:** established trading-policy denials remain typed denials when they cannot be rechecked immediately.

**What to do:** correct permission failures; reserve retry loops for explicit transient responses.

## Rate-limit responses

### Structured 429 problems

**Changed:** REST order and trigger writes and public sign-in and catalog calls now receive a structured HTTP 429 problem when the public API rejects them for rate limiting, replacing earlier HTML or generic JSON responses. A refusal returned by the operation itself keeps its own body and headers.

Example fragment:

```json
{
	"status": 429,
	"code": "RATE_LIMIT_EXCEEDED",
	"rate_limit": {
		"reason": "QUOTA_EXCEEDED",
		"scope": "CLIENT_IP"
	}
}
```

**What to do:** handle the HTTP status, the problem `code` and the optional `rate_limit` fields together.

- Honor `Retry-After` when present.
- Request-count refusals can carry `X-RateLimit-*`; operation quota refusals can carry `RateLimit-*` and richer details.
- Concurrency or instantaneous refusals may carry neither retry nor quota headers. Back off without assuming a server-supplied delay.
- `REPLAY_CAPACITY_EXHAUSTED` is a separate retryable 429, without `Retry-After`; it is not a bad-credentials error.
- Capacity refusals such as `AUTH_RESOURCE_EXHAUSTED` are not ordinary quota exhaustion. Inspect the code before retrying.

Read the configuration with [`GET /v1/rate-limits`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/rate-limits) and the authenticated [`GET /v1/trading/rate-limits`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/trading/rate-limits) ([`GetRateLimitConfig`](https://testnet.polyester.com/docs/api-docs/connect/ratelimit.v1.RateLimitService/GetRateLimitConfig) and [`GetTradingRateLimits`](https://testnet.polyester.com/docs/api-docs/connect/ratelimit.v1.RateLimitService/GetTradingRateLimits) in ConnectRPC). REST and WebSocket aliases of the same trading action share quota; adding sockets or keys does not multiply account capacity. This migration implies no new fixed trading quota. See [Rate limits](https://testnet.polyester.com/docs/developer-docs/shared-concepts/rate-limits).

## Market data, shapes and pagination

### Market overview honors symbolId filters

**Changed:** [`GET /v1/spot/markets/overview`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/spot/markets/overview) ([`ListMarketOverview`](https://testnet.polyester.com/docs/api-docs/connect/marketoverview.v1.MarketOverviewService/ListMarketOverview)):

- Repeated `symbolId` filters are now honored instead of ignored. Check the selected market before using a row's prices.
- An invalid REST `symbolId` now returns 400 `BAD_REQUEST`; unknown or disabled selected IDs yield no row. Send positive uint32 IDs and accept an empty `markets` result.
- A Pair repeated through `symbols`, `symbolId`, or both is now returned once; previously a Pair repeated in `symbols` was refused with HTTP 400. Expect one row per Pair.

```http
GET /v1/spot/markets/overview?symbolId=1&symbolId=23&limit=50
```

**Clarified:** an unfiltered overview already includes configured disabled markets and excludes markets past delisting. That is a documentation correction, not a new enabled-market filter. Overview defaults to 50 rows, maximum 2000, ordered by descending 24-hour USD volume unless you request otherwise. Continue with `nextPageToken` as `pageToken`, keeping the filters and ordering; the page size may change.

### Order-book symbol lookup accepts any case

**Changed:** REST order-book symbol lookup ([`GET /v1/orderbook/{symbol}`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/orderbook/symbol)) accepts case variants and returns the canonical symbol.

**What to do:** normalize cache keys using the returned symbol. See [Order book](https://testnet.polyester.com/docs/developer-docs/market-data/order-book).

### Spot-config assets add includedInEquity

**Added:** assets in [`GET /v1/spot/config`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/spot/config) ([`GetSpotConfig`](https://testnet.polyester.com/docs/api-docs/connect/marketdata.v1.MarketDataService/GetSpotConfig)) add the optional `includedInEquity`.

**What to do:** include assets explicitly marked `true` in equity. `false` contributes no value; an absent value means unknown.

### Historical heatmap pages start from a full book

**Changed:** historical heatmap pages ([`GetOrderbookHeatmap`](https://testnet.polyester.com/docs/api-docs/connect/marketdata.v1.HeatmapService/GetOrderbookHeatmap)) now reconstruct their starting book, including quiet windows, and keep raw best prices where available. Unanchored history before the oldest available snapshot is omitted.

**What to do:** apply deltas oldest-first over the returned base keyframe, and stop at an empty continuation token. Accept an empty chain for a window before replayable history.

### No blanket REST format migration

**Clarified:** there is no blanket REST casing, ID or decimal-format migration in this release. Keep REST lower-camel-case fields, opaque public IDs and human-unit decimal strings. Protobuf trading messages keep their fixed64 IDs and scaled integers.

## Account permissions and policy tools

See [API key policies](https://testnet.polyester.com/docs/developer-docs/account/api-key-policies) and [Subaccounts and roles](https://testnet.polyester.com/docs/developer-docs/account/subaccounts-and-roles).

### Funding-tool reads need explicit API-key permissions

**Changed:** API keys now need explicit account permissions for deposit-address, Subaccount and GuardSigner reads, and for Main Account address-book reads. A key with no policy loses those previously available calls.

Existing policies receive the corresponding access grants on upgrade, but new policies and full replacements of `actions` must keep the permissions your client needs.

- **`CREATE_DEPOSIT_ADDRESS`**\
  Create a deposit address: [`POST /v1/chain/deposit-addresses`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/chain/deposit-addresses), [`CreateDepositAddress`](https://testnet.polyester.com/docs/api-docs/connect/chain.deposit.v1.DepositAddressService/CreateDepositAddress).
- **`READ_DEPOSIT_ADDRESSES`**\
  List deposit addresses: [`GET /v1/chain/deposit-addresses`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/chain/deposit-addresses), [`ListDepositAddresses`](https://testnet.polyester.com/docs/api-docs/connect/chain.deposit.v1.DepositAddressService/ListDepositAddresses).
- **`READ_SUBACCOUNTS`**\
  List and get Subaccounts: [`ListSubaccounts`](https://testnet.polyester.com/docs/api-docs/connect/auth.v1.SubaccountService/ListSubaccounts), [`GetSubaccount`](https://testnet.polyester.com/docs/api-docs/connect/auth.v1.SubaccountViewService/GetSubaccount).
- **`READ_SUBACCOUNT_ACTIVITY`**\
  Subaccount activity: [`ListSubaccountActivity`](https://testnet.polyester.com/docs/api-docs/connect/auth.v1.SubaccountViewService/ListSubaccountActivity).
- **`READ_SUBACCOUNT_MEMBERS`**\
  Member expansion on Subaccount detail. Listing members ([`ListSubaccountMembers`](https://testnet.polyester.com/docs/api-docs/connect/auth.v1.SubaccountService/ListSubaccountMembers)) is newly available to API keys.
- **`READ_SUBACCOUNT_INVITES`**\
  Outgoing invites and invite expansion: [`ListSubaccountInvites`](https://testnet.polyester.com/docs/api-docs/connect/auth.v1.SubaccountService/ListSubaccountInvites).
- **`READ_GUARD_SIGNER_STATUS`**\
  GuardSigner status for accounts within ownership and key scope: [`GetGuardSignerStatus`](https://testnet.polyester.com/docs/api-docs/connect/chain.guard.v1.GuardSignerService/GetGuardSignerStatus).
- **`READ_ADDRESS_BOOK`** or **`MANAGE_ADDRESS_BOOK`**\
  Main Account address-book reads: [`ListAddressBookEntries`](https://testnet.polyester.com/docs/api-docs/connect/auth.v1.AddressBookService/ListAddressBookEntries).
- **`READ_WITHDRAW_VALIDATION`**\
  Validate a withdrawal destination, previously allowed by `EXTERNAL_WITHDRAW`: [`POST /v1/chain/withdraws:validate-destination`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/chain/withdraws:validate-destination), [`ValidateWithdrawDestination`](https://testnet.polyester.com/docs/api-docs/connect/chain.withdraw.v1.WithdrawService/ValidateWithdrawDestination).

**What to do:** attach an API-key policy where none exists and include only the permissions you need. Missing grants return `AUTH_API_KEY_ACCESS_DENIED`, with two exceptions:

- **Address-book reads**\
  `AUTH_SUBACCOUNT_ACCESS_DENIED` on both APIs: HTTP 403 on REST, `permission_denied` on ConnectRPC. If the caller is not allowed to know the address book exists, the answer is 404 instead.
- **Destination validation**\
  `PERMISSION_DENIED`.

The Subaccount detail read omits unauthorized member and invite expansions rather than failing the whole read.

### GuardSigner status stays with the owning Main Account

**Changed:** a key can no longer read GuardSigner status for a Subaccount owned by another Main Account merely because the Subaccount is shared with it.

**What to do:** use the owning account's credential or an authorized interactive session. See [GuardSigner](https://testnet.polyester.com/docs/developer-docs/authentication-security/guard-signer).

### Newly available API-key account reads

**Added:** these permissions are optional. Each grants its named calls only, and belongs on an **API-key policy**, not a Subaccount policy.

- **`READ_API_KEYS`**\
  List and get keys within scope: [`ListApiKeys`](https://testnet.polyester.com/docs/api-docs/connect/auth.v1.ApiKeyService/ListApiKeys), [`GetApiKey`](https://testnet.polyester.com/docs/api-docs/connect/auth.v1.ApiKeyService/GetApiKey).
- **`READ_API_POLICIES`**\
  List and get API-key policies within scope: [`ListApiPolicies`](https://testnet.polyester.com/docs/api-docs/connect/auth.v1.PolicyService/ListApiPolicies), [`GetApiPolicy`](https://testnet.polyester.com/docs/api-docs/connect/auth.v1.PolicyService/GetApiPolicy).
- **`READ_SUBACCOUNT_POLICIES`**\
  List and get Subaccount policies within scope: [`ListSubaccountPolicies`](https://testnet.polyester.com/docs/api-docs/connect/auth.v1.PolicyService/ListSubaccountPolicies), [`GetSubaccountPolicy`](https://testnet.polyester.com/docs/api-docs/connect/auth.v1.PolicyService/GetSubaccountPolicy).
- **`READ_SUBACCOUNT_PERMISSIONS`**\
  Read what the key itself may do: [`GetEffectiveSubaccountPermissions`](https://testnet.polyester.com/docs/api-docs/connect/auth.v1.SubaccountRoleService/GetEffectiveSubaccountPermissions).
- **`READ_VIP_STATUS`**\
  Main Account VIP status: [`GetVIPStatus`](https://testnet.polyester.com/docs/api-docs/connect/vip.v1.VIPService/GetVIPStatus).
- **`READ_SOCIAL_VERIFICATION`**\
  Main Account social-link status: [`GetSocialVerification`](https://testnet.polyester.com/docs/api-docs/connect/auth.v1.SocialVerificationService/GetSocialVerification).
- **`READ_PROFILE`**\
  Main Account profile and username history: [`GetProfile`](https://testnet.polyester.com/docs/api-docs/connect/auth.v1.ProfileService/GetProfile), [`GetUsernameHistory`](https://testnet.polyester.com/docs/api-docs/connect/auth.v1.ProfileService/GetUsernameHistory).
- **`READ_ACCOUNT_DIRECTORY`**\
  Account lookup: [`ResolveAccount`](https://testnet.polyester.com/docs/api-docs/connect/auth.v1.ResolveService/ResolveAccount).
- **`READ_DAILY_CLAIMS`**\
  Main Account daily claim status: [`GetDailyClaimStatus`](https://testnet.polyester.com/docs/api-docs/connect/claims.v1.ClaimsService/GetDailyClaimStatus).
- **`CLAIM_DAILY_REWARD`**\
  Claim the Main Account's daily reward: [`ClaimDailyReward`](https://testnet.polyester.com/docs/api-docs/connect/claims.v1.ClaimsService/ClaimDailyReward).
- **`READ_CHECKPOINTS`**\
  Main Account checkpoints: [`ListMyCheckpoints`](https://testnet.polyester.com/docs/api-docs/connect/checkpoints.v1.CheckpointService/ListMyCheckpoints).
- **`READ_REWARDS`**\
  Main Account reward awards: [`ListMyRewardAwards`](https://testnet.polyester.com/docs/api-docs/connect/rewards.v1.RewardCampaignService/ListMyRewardAwards).

Main-Account-only calls still refuse Subaccount-scoped keys, even if the permission is present. Account and policy management remain interactive-session operations. The new reward acknowledgement call and reward destination changes are session-only.

### Destination validation needs READ_WITHDRAW_VALIDATION

**Changed:** destination validation now requires `READ_WITHDRAW_VALIDATION`, rather than `EXTERNAL_WITHDRAW` alone. Existing policies with withdrawal permission receive the read grant on upgrade.

**What to do:** keep `READ_WITHDRAW_VALIDATION` when you replace `actions`.

### Clear a notional cap with null

**Added:** [`PATCH /v1/auth/policies/subaccounts/{policy_id}`](https://testnet.polyester.com/docs/api-docs/rest/PATCH/v1/auth/policies/subaccounts/policy_id) can clear the notional cap with `{"maxOrderNotional":null}`, using the latest revision in `If-Match`. Omitting the field keeps the cap. Zero is not a clear instruction. A cleared policy still reads back `maxOrderNotional: "0"`.

```http
PATCH /v1/auth/policies/subaccounts/<policy-id>
If-Match: "<latest-revision>"
Content-Type: application/json

{"maxOrderNotional":null}
```

## Funding, transfers and withdrawals

### Transfer reason 1074 is now system_adjustment

**Changed:** REST and JSON transfer streams rename code 1074 from `operator_restore` to `system_adjustment`. Numeric code 1074 stays unchanged. This affects [`GET /v1/transfers`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/transfers), [`ListTransfers`](https://testnet.polyester.com/docs/api-docs/connect/ledger.read.v1.LedgerReadService/ListTransfers) and the [ledger transfers stream](https://testnet.polyester.com/docs/api-docs/streams/private-ledger-transfers).

## Before (0.92.5)

```json
{ "transferCode": "operator_restore", "isDebit": false }
```

## After (0.92.9)

```json
{ "transferCode": "system_adjustment", "isDebit": false }
```

**What to do:** update handwritten label and filter mappings, and use `isDebit` for direction; a system adjustment can debit or credit the account.

### New transfer reason codes

**Added:** these published codes.

| Numeric code | REST/JSON label            |
| ------------ | -------------------------- |
| 1066         | `trading_withdraw_fee_leg` |
| 1067         | `quickswap_polyester_fee`  |
| 1068         | `quickswap_broker_fee`     |
| 1069         | `quickswap_affiliate_fee`  |
| 1072         | `fee_sweep`                |
| 1073         | `fee_refill`               |
| 1075         | `withdraw_failed`          |

Protobuf enum names use uppercase. `withdraw_failed` records funds debited for a rejected or execution-failed withdrawal; do not treat it as an automatic Trading or Funding refund.

### Lifecycle history includes received transfers

**Changed:** account-scoped [`GET /v1/chain/flows`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/chain/flows) ([`ListFlows`](https://testnet.polyester.com/docs/api-docs/connect/chain.lifecycle.v1.LifecycleReadService/ListFlows)) includes transfers received from other accounts, in addition to owned flows. Each row has `participation: "OWNER"` or `"RECIPIENT"`. A movement within one account remains an owner row; each flow appears once.

**What to do:** pass `participation=OWNER` to keep the old owner-only result set.

## Before (0.92.5)

Returned owned flows only.

```http
GET /v1/chain/flows?ownerAccountId=<account-id>
```

## After (0.92.9)

The same owner-only result set needs the participation filter.

```http
GET /v1/chain/flows?ownerAccountId=<account-id>&participation=OWNER
```

A non-default participation filter requires an account selector. Pagination tokens now bind the account selector, participation, every filter including scope, the sort direction and the ordering timestamp. **Discard old tokens and restart from the first page.** Keep the query unchanged while continuing; only `limit` may change. A token used with another query is invalid. Continue until `nextPageToken` is empty. See [Track funding flows](https://testnet.polyester.com/docs/developer-docs/funds-transfers/track-funding-flows).

### Internal-transfer receipts

**Added:** internal-transfer creation ([`POST /v1/trading/internal-transfers`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/trading/internal-transfers)) returns `flowId`. The new [`GET /v1/trading/internal-transfers`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/trading/internal-transfers) ([`GetInternalTransfer`](https://testnet.polyester.com/docs/api-docs/connect/transfer.v1.InternalTransferService/GetInternalTransfer)) reads one accepted receipt by exactly one of `requestId` or `transferId`, with an optional source `subaccountId`.

This read requires an interactive session; API keys are not accepted. Unknown, pending or inaccessible receipts return not found. It has no pagination and moves no funds.

**What to do:** persist `requestId`, `transferId` and `flowId`. Reconcile through the receipt and [`GET /v1/chain/flows/{flow_id}`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/chain/flows/flow_id), rather than resubmitting or matching amounts. See [Internal transfers](https://testnet.polyester.com/docs/developer-docs/funds-transfers/internal-transfers).

### Withdrawal assetId is marked required

**Clarified:** the withdrawal request schema now correctly marks `payload.assetId` required. This documents the existing asset requirement.

**What to do:** keep supplying it when signing withdrawals.

### Rejected withdrawals refund to the wallet

**Changed:** a rejected withdrawal stays open while its refund is pending, then ends refunded when net funds reach the sending smart-account wallet as zTokens. Its request fee (the route fee locked when the withdrawal was requested) is kept. The refund is not automatically credited back to Trading or Funding.

A withdrawal whose destination execution failed after submission ends failed, with `ZIPPER_WITHDRAW_EXECUTION_FAILED`, and is handled separately; do not infer a wallet refund.

The request fee can now have REST status `KEPT` (Protobuf `REQUEST_FEE_STATUS_KEPT`), and the refunded step adds `refundAddress`. That step's amount is net of the kept fee.

**What to do:** reconcile the actual refund location and the kept fee. Terminal processing does not necessarily mean successful delivery. See [Withdraw from Trading](https://testnet.polyester.com/docs/developer-docs/funds-transfers/withdraw-from-trading).

### Wallet balances read

**Added:** [`GET /v1/chain/wallet/balances`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/chain/wallet/balances) ([`GetWalletBalances`](https://testnet.polyester.com/docs/api-docs/connect/chain.withdraw.v1.WithdrawService/GetWalletBalances)), optionally with `subaccountId`, returns nonzero wallet holdings ordered by `zippedAssetId`. Entries contain `zippedAssetId`, `unifiedAssetId`, `chainId`, `ztokenAddress` and a whole-unit decimal `amount`. API keys need `READ_BALANCES`.

### Destination memo rules

**Added:** [`GET /v1/chain/deposit-withdraw/config`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/chain/deposit-withdraw/config) ([`GetDepositWithdrawConfig`](https://testnet.polyester.com/docs/api-docs/connect/chain.zipper.v1.ZipperService/GetDepositWithdrawConfig)) includes `memoRequirement`, `memoLabel` and `memoFormatHint`.

**What to do:** recognize `REQUIRED`, `OPTIONAL`, `NOT_SUPPORTED` and the unknown `MEMO_UNSPECIFIED`. These rules apply to payout destinations, including QuickSwap return and refund destinations, not to deposit instructions. Do not infer "optional" from an unknown value or require a tag on every Network. Apply the Network's actual rule.

### Auto-withdraw REST paths

**Added:** REST paths for auto-withdraw, if you use it. See [Auto-withdraw](https://testnet.polyester.com/docs/developer-docs/funds-transfers/auto-withdraw).

- **Read rule**\
  [`GET /v1/auto-withdraw/rule`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/auto-withdraw/rule)
- **Set rule**\
  [`PUT /v1/auto-withdraw/rule`](https://testnet.polyester.com/docs/api-docs/rest/PUT/v1/auto-withdraw/rule)
- **Pause or resume**\
  [`PUT /v1/auto-withdraw/rule/enabled`](https://testnet.polyester.com/docs/api-docs/rest/PUT/v1/auto-withdraw/rule/enabled)
- **Preview the next run**\
  [`GET /v1/auto-withdraw/preview`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/auto-withdraw/preview)
- **Read run history**\
  [`GET /v1/auto-withdraw/runs`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/auto-withdraw/runs)

### Pause replaces DisableAutoWithdrawRule

**Removed:** `DisableAutoWithdrawRule`. Replace it with [`SetAutoWithdrawRuleEnabled`](https://testnet.polyester.com/docs/api-docs/connect/chain.withdraw.v1.AutoWithdrawService/SetAutoWithdrawRuleEnabled) with `enabled: false`, or the REST pause path.

## Before (0.92.5)

```http
POST /chain.withdraw.v1.AutoWithdrawService/DisableAutoWithdrawRule
Content-Type: application/json

{"expectedVersion":3}
```

## After (0.92.9)

```http
PUT /v1/auto-withdraw/rule/enabled
Content-Type: application/json

{"enabled":false,"expectedVersion":3}
```

Pausing keeps the schedule and destinations and needs no fresh step-up. Resuming requires a fresh step-up and revalidates destinations. API keys can read with `READ_AUTO_WITHDRAW`, but cannot write rules.

**What to do:** send the latest `expectedVersion`; a real state change creates a new version. Requesting the state a rule already has leaves it unchanged. A rule runs only when `enabled` is true and `pause` is absent.

### Auto-withdraw refusal codes

**Changed:** rule refusals use new codes.

| Previous code                                      | New code                 |
| -------------------------------------------------- | ------------------------ |
| `CONFLICT`                                         | `VERSION_CONFLICT`       |
| `AMOUNT_BELOW_MINIMUM`                             | `MIN_AMOUNT_BELOW_FLOOR` |
| `INVALID_DESTINATION_ADDRESS`                      | `DESTINATION_INVALID`    |
| `UNSUPPORTED_ASSET` or `INVALID_DESTINATION_CHAIN` | `ROUTE_UNSUPPORTED`      |

Problems identify `field_path`; below-floor refusals also expose a decimal `min_floor`. Rules add `createdByAccountId`; destinations can include `minFloor`. Unknown floors are omitted.

**What to do:** update your code matching to the new codes.

### Auto-withdraw previews and run steps

**Added:** previews add `status` (`WILL_PAY`, `ROLLS_OVER`, `NO_DESTINATION`, `PAUSED`, `NO_RULE`), `nextRunAt` and, per Asset, `payable`, `heldBack`, `holdReason`, and the optional `minimum`, `valueUsd`, `pricedAt` and `networkFee`. A preview is an estimate from current balances and prices. Unknown optional amounts, times and fees are absent, never synthetic zeroes.

Run steps add the actual `networkFee` when it is reported at finality. A failed withdrawal can keep that fee.

**What to do:** do not treat every failed step as funds restored to the account.

### Broker vault destination cooling period

**Added:** broker vault destinations have a 24-hour whitelist cooling period. An early withdrawal returns `EXTERNAL_DESTINATION_COOLING_DOWN`, with `payable_from` when known. Previews hold with `destination_cooling_down` and an optional `payableFrom`; runs skip that payout and keep the funds for a later run. Ordinary accounts are unaffected.

A broker author's role change pauses their rules with `authority_revoked`. An authorized Owner or Finance member must resume them with a fresh step-up.

## QuickSwap integrations

These changes concern QuickSwap funding and conversion tools, if you use them. They do not change ordinary spot order payloads. See [Other APIs](https://testnet.polyester.com/docs/developer-docs/getting-started/other-apis) and [`QuoteQuickSwap`](https://testnet.polyester.com/docs/api-docs/connect/swap.quickswap.v1.QuickSwapService/QuoteQuickSwap), [`CreateQuickSwap`](https://testnet.polyester.com/docs/api-docs/connect/swap.quickswap.v1.QuickSwapService/CreateQuickSwap), [`POST /v1/quickswaps:quote`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/quickswaps:quote) and [`POST /v1/quickswaps`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/quickswaps).

### Small amounts, returns and compliance transfers end with specific outcomes

**Changed:** QuickSwap now answers small-amount, return and compliance cases with specific outcomes. They arrive in two different forms, and your client should keep them apart:

- **Error codes** refuse a request. Real QuickSwap failures use QuickSwap's own error codes.
- **Lifecycle values** are response fields that describe a swap's progress: reasons, statuses, address and withdrawal states, withdrawal roles and broker roles. They are not error codes.

On the order and trading WebSocket APIs, a market maker only ever sees order error codes, including the order errors named `QUICKSWAP_*`, never QuickSwap's lifecycle values.

**QuickSwap error codes:**

- **`AMOUNT_OUT_OF_RANGE`**\
  A market quote whose guarantee is tiny or rounds to zero, which previously failed without a useful answer. Increase the amount; do not retry unchanged.
- **`PROTECTION_UNACHIEVABLE`**\
  The same case for a TWAP quote. Increase the amount or adjust protection or slicing; do not retry unchanged.
- **`REFERRAL_NOT_SUPPORTED`**, **`REFERRAL_CODE_UNKNOWN`**, **`REFERRAL_PARTNER_INACTIVE`**\
  Referral refusals; see [Referral codes on quote and create](#referral-codes-on-quote-and-create).

**QuickSwap lifecycle values:**

- **Intermediate funds too small for the next market leg**\
  Resolve as below minimum, rather than a protection failure or an unnecessary wait. Recognize reason `INTERMEDIATE_BELOW_MINIMUM` when a refund can be requested; otherwise reason `REFUND_BELOW_MINIMUM` with withdrawal state `NOT_SENT`.
- **Dust-only source returns and intermediate refunds**\
  Can now end terminal instead of remaining executing. Accept terminal status `RETURNED` or `REFUNDED` with reason `REFUND_BELOW_MINIMUM`; a rounded `unsentAmount` can be zero.
- **A credited deposit that cannot be traded**\
  Can now return automatically instead of remaining held. Handle status `RETURNING` then `RETURNED`, with reason `MARKET_CLOSED`, `PROTECTION_NOT_MET`, `DEPOSIT_BELOW_MINIMUM` or `DEPOSIT_ABOVE_MAXIMUM` as applicable.
- **A blocked deposit transferred out after compliance review**\
  Ends with terminal status `CLOSED` (17), withdrawal role `COMPLIANCE_TRANSFER` (4) and lifecycle reason `ZIPPER_COMPLIANCE_TRANSFERRED` (105). This is not a return to the user.

A compliance-transfer withdrawal is finalized but omits recipient, memo, amount and network fee. Its transaction identifies the transfer; the deposit's disposition remains `ON_HOLD`. Do not fill absent values with a guessed payout.

**Other changes:**

- **Minimum deposit**\
  Can increase to guarantee at least one destination token base unit. Read fresh quote limits instead of hardcoding old minimums.
- **Market closure**\
  Waits a bounded time from deposit credit, including for TWAP, rather than the TWAP duration. Continue tracking the existing swap; do not submit another to resolve it.

### Quotes are estimates

**Clarified:** quoted output and fee amounts are estimates. Fee rates are fixed, but actual fees and output can be above or below the quote as the deposited amount and execution change.

**What to do:** use protection fields for guarantees, and the actual execution, withdrawal and charged-fee fields for accounting.

### Referral codes on quote and create

**Added:** optional `referralCode` on quote and create, `quote.referral` with `baseBrokerBps`, `discountBps` and `commissionBps`, and fee kind `REFERRAL` (6). Referral commission comes from the broker fee and is separate from affiliate fees.

**What to do:** do not count referral commission as a broker vault credit. Handle the error codes `REFERRAL_NOT_SUPPORTED`, `REFERRAL_CODE_UNKNOWN` and `REFERRAL_PARTNER_INACTIVE` before retrying without a code.

### Broker vault balances

**Added:** [`GET /v1/quickswap/brokers/{broker}/vault-balances`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/quickswap/brokers/broker/vault-balances) ([`ListVaultBalances`](https://testnet.polyester.com/docs/api-docs/connect/swap.quickswap.v1.QuickSwapBrokerService/ListVaultBalances)) returns current broker vault balances, for broker members and broker API keys. Its settled `amount` includes `pendingOutgoing`.

**What to do:** subtract pending outgoing once for the uncommitted balance. This excludes order holds and is not a withdrawability guarantee. Reads are not an atomic snapshot across vaults. Broker withdrawals and payout settings require Owner or Finance authority.

## Other account-tool additions

### Version check

**Added:** the unauthenticated [`GET /v1/system/version`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/system/version) ([`GetVersion`](https://testnet.polyester.com/docs/api-docs/connect/system.v1.SystemService/GetVersion)) returns the string fields `version`, `commit` and `environment`.

**What to do:** use it to identify the build before testing. It does not prove every endpoint is healthy.

### Changelog post revisions

**Added:** post summaries from [`GET /v1/changelog/posts`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/changelog/posts) ([`ListPosts`](https://testnet.polyester.com/docs/api-docs/connect/changelog.v1.ChangelogService/ListPosts)) add `revision` and `updatedAt`.

**What to do:** cache posts by slug and revision; publication time alone no longer detects a corrected post. If a post changes during change-list pagination ([`GET /v1/changelog/changes`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/changelog/changes)), its changes can repeat: deduplicate them by their stable change IDs.

### Daily Testnet claims

**Added:** `CLAIM_OUTCOME_UNKNOWN` from [`POST /v1/trading/daily-claim/claim`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/trading/daily-claim/claim) ([`ClaimDailyReward`](https://testnet.polyester.com/docs/api-docs/connect/claims.v1.ClaimsService/ClaimDailyReward)) means a recorded claim's payout is not confirmed. Reward REST refusals now consistently use problem JSON.

**What to do:** read the status with [`GET /v1/trading/daily-claim`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/trading/daily-claim) or retry the same daily claim; do not create another payment attempt.

### Telegram social linking

**Added:** Telegram adds `TELEGRAM`, `STATUS_AWAITING_CONFIRMATION`, [`POST /v1/auth/social/verification:confirm`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/auth/social/verification:confirm) and [`POST /v1/auth/social/verification:unlink`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/auth/social/verification:unlink), plus display and action fields. Interactive confirmation requires enrolled MFA and a fresh step-up. Existing trading request authentication does not become interactive MFA.

## Final client checks

1. Compare fresh and expired HTTP requests

   Compare a fresh signed HTTP request with a deliberately expired request, and distinguish authentication skew from pre-submission expiry.

2. Test WebSocket logins

   Test a fresh signed WebSocket login and a replayed login. Reconnect with a new nonce.

3. Check command timing on the socket

   Verify that expired commands advance the sequence, malformed timing does not, and the socket remains usable.

4. Reconcile mixed batch results

   Test mixed batch admission results and reconcile without resubmitting accepted items.

5. Rehearse a shutdown

   Confirm the shutdown key can cancel triggers, then cancel open orders.

6. Check funding tools

   Verify funding-tool permissions and restart lifecycle pagination from page one.

7. Decode the new contract

   Decode new fields and enum values without losing unknown-field tolerance or optional-field presence.
