# Swap

Convert Trading balances through spot order books with quotes, minimum output protection, and idempotent execution.

[`swap.v1.SwapService`](https://testnet.polyester.com/docs/api-docs/connect/swap.v1.SwapService/QuoteSwap) converts one Asset in an account's Trading balance into another through Polyester's spot order books. Use it to build balance conversion into an account dashboard or trading application.

Swap is available through ConnectRPC on Testnet at `api.testnet.polyester.com`. Mainnet is coming soon. This service has no REST resource endpoints.

The source and destination stay in the same Trading Account. There is no deposit address, external payout, or bridge. For deposit-funded, cross-chain swaps, use the separate [QuickSwap documentation](https://testnet.swap.polyester.com/docs/developer-docs/quickswap/overview) for `swap.quickswap.v1`.

## Authentication and account selection

Every method accepts a session token or a signed Ed25519 API key. Follow [ConnectRPC authentication](https://testnet.polyester.com/docs/developer-docs/connectrpc/getting-started) and [Ed25519 API keys](https://testnet.polyester.com/docs/developer-docs/authentication-security/ed25519-api-keys).

Omit `terms.subaccount_id` to use the session owner's Main Account, or the Subaccount an API key is bound to. A session can select a Subaccount by its public ID when the caller has the required role. An API key can act only on its bound account; a Main Account key cannot select a Subaccount.

The caller needs `SWAP` permission. API key policies and Subaccount policies must allow the `SWAP` action; a Subaccount session also needs a trading role that grants it. The resulting orders remain subject to the Subaccount's spot trading policy. 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).

## Quote, create, and follow

1. Request a quote

   Call [`QuoteSwap`](https://testnet.polyester.com/docs/api-docs/connect/swap.v1.SwapService/QuoteSwap) with the source and destination unified Asset IDs, a positive source `amount_e18`, and `max_slippage_bps` from 1 to 1000. One basis point is 0.01%.

   All Swap amounts use 18-decimal integers, including amounts for Assets whose spot order quantities use another scale. Use the `U128` representation described in [Scaled integers](https://testnet.polyester.com/docs/developer-docs/connectrpc/scaled-integers), without floating-point conversion.

   The quote returns estimated output after fees, a minimum output at the requested slippage, per-leg price and fee information, and timestamps. It expires 30 seconds after `quoted_at`. A quote reserves neither funds nor liquidity.

2. Accept the terms

   Call [`CreateSwap`](https://testnet.polyester.com/docs/api-docs/connect/swap.v1.SwapService/CreateSwap) with the same terms, a client-chosen `idempotency_key`, the accepted `min_amount_out_e18`, and the quote's `expires_at` as `quote_expires_at`.

   Creation prices the terms again against current depth. An expired quote or an unachievable minimum is rejected before a new Swap is created. Execution begins when the Swap is accepted; a successful create response does not mean it has finished.

3. Read the outcome

   Call [`GetSwap`](https://testnet.polyester.com/docs/api-docs/connect/swap.v1.SwapService/GetSwap) with either `swap_id` or `idempotency_key` until `is_terminal` is true. The response includes the orders, settled amounts, fees, status, and reason for any incomplete execution. Reading a Swap still requires permission on its funding account.

## Execution and minimum output

The current route uses one direct spot Pair. It can buy or sell depending on the Pair's orientation; it does not find a route through an intermediate Asset. A buy is supported only when the market can charge its fee in the Asset received.

Execution uses immediate-or-cancel limit orders at a protection price derived from the accepted minimum after fees. The minimum defines a rate for the requested amount, rather than a promise that the whole amount will trade. Any portion that cannot trade within protection stays in the account. An amount smaller than an order step can remain even after completion.

Use `executed_amount_e18`, `unspent_amount_e18`, `received_amount_e18`, and `fee_e18` to reconcile the outcome. These count settled fills; unspent amounts become final when `is_terminal` is true.

- **`EXECUTING`**: orders are being placed, filled, or settled.
- **`RECONCILING`**: an order's outcome is still being established. Settled fills already affect balances; keep reading the same Swap.
- **`COMPLETED`**: the amount traded and settled, allowing a remainder smaller than an order step.
- **`PARTIALLY_COMPLETED`**: some of the amount traded and settled. Read `reason` for why execution stopped.
- **`FAILED`**: usually nothing traded. Inspect the reason and amounts before deciding what to do next.

> **A failed Swap can contain fills**
>
> `FAILED` with `MINIMUM_NOT_MET` means settled fills netted less than the accepted minimum rate. The Swap is reported as failed, but those fills are reflected in the returned amounts and balances. Reconcile them before submitting another conversion.

## Limits and fees

Source and destination Assets must differ. Sizing depends on the Pair's order steps, minimum and maximum sizes, and current depth. Read Pair rules from [`GetSpotConfig`](https://testnet.polyester.com/docs/api-docs/connect/marketdata.v1.MarketDataService/GetSpotConfig), as described in [Pairs and trading rules](https://testnet.polyester.com/docs/developer-docs/market-data/pairs-and-trading-rules).

An `AMOUNT_OUT_OF_RANGE` error includes `min_amount_e18` and `max_amount_e18` when known, in source Asset units with 18 decimals. These are current guidance, not reserved capacity. A quote will not accept a buy that exceeds the market's maximum order quantity. If depth or limits change afterward, execution can stop partially with `MAX_ORDER_QUANTITY`.

Swap charges the account's spot taker fee and no additional Swap fee. Quote legs report `taker_fee_rate_percent`, estimated `fee_e18`, and `fee_asset_id`. Fees are charged in the received Asset, and quoted output is already net of fees. `price_impact_bps` describes the average fill price's distance from the best book price; it is not an extra charge.

Execution can also stop for insufficient available balance, market restrictions, order-size rules, trading policy, rate limits, or expiry of the execution window. Read the returned reason and settled amounts. See [Fees and VIP](https://testnet.polyester.com/docs/developer-docs/account/fees-and-vip) and [Rate limits](https://testnet.polyester.com/docs/developer-docs/shared-concepts/rate-limits).

## Retries and errors

Choose an idempotency key for each intended conversion, unique within the caller's account scope. Keys contain 1 to 128 characters from letters, digits, `.`, `_`, `:`, and `-`. Repeating a create with the same key and economic terms returns the existing Swap, including after the quote expires. Reusing it for different terms returns `IDEMPOTENCY_CONFLICT` with the original `swap_id`.

After a timeout, `UNAVAILABLE`, or `DEADLINE_EXCEEDED`, repeat the same create request with the same key and terms. For signed API keys, sign each attempt again with a fresh timestamp. A new key could create a second conversion.

Handle the stable code in `swap.v1.ErrorDetail`, rather than parsing message text:

- **Refresh the quote** for `QUOTE_EXPIRED` or `PROTECTION_UNACHIEVABLE`. Correct the amount for `AMOUNT_OUT_OF_RANGE`, using returned limits when present.
- **Choose a supported conversion** for `ROUTE_UNSUPPORTED` or `DIRECTION_UNSUPPORTED`. A direct market or supported fee direction is missing.
- **Check account access** for `INVALID_SUBACCOUNT_ID`, `SUBACCOUNT_NOT_FOUND`, `SUBACCOUNT_INACTIVE`, or `SWAP_NOT_PERMITTED`.
- **Back off and retry** for `QUOTE_UNAVAILABLE` or `SERVICE_UNAVAILABLE`. Preserve the create request when its outcome is unknown.
- **Resolve the existing Swap** for `IDEMPOTENCY_CONFLICT`. For `SWAP_NOT_FOUND`, check the selector and authenticated account.

Request errors are distinct from a Swap's execution status and reason. The complete codes and ConnectRPC mappings are in [Swap errors](https://testnet.polyester.com/docs/developer-docs/error-handling/error-codes#swap); see [Error responses](https://testnet.polyester.com/docs/developer-docs/error-handling/error-responses) for decoding details.
