# Common API reference
`Client<A>` exposes one common contract over an `Adapter`. Provider-specific
methods remain on `A` and are available through `Client::adapter()`.
`Client<Box<dyn Adapter>>` supports runtime provider selection.
## API surface
Configure credentials on the adapter before `Client::new(adapter)`.
| Client | `exchange`, `supports`, `adapter`, `into_adapter` |
| Public REST | `markets`, `trades`, `order_book`, `ticker`, `candles`, `funding_rates` |
| Public stream | `subscribe`, `subscribe_with` |
| Private reads | `balances`, `order_rules`, `asset_networks`, `deposit_addresses`, `deposit_address`, `deposit`, `withdrawal`, `deposits`, `withdrawals`, `open_orders`, `open_orders_on`, `order`, `order_by_client_id`, `orders_by_ids`, `order_history`, `positions`, `positions_on`, `margin_summary`, `funding_payments` |
| Private stream | `subscribe_account`, `subscribe_account_with` |
| Private writes | `create_deposit_address`, `withdraw`, `cancel_withdrawal`, `place_order`, `cancel_order`, `cancel_order_by_client_id`, `cancel_orders`, `set_margin` |
Public REST and market streams require no credentials. Provider and
`MarketKind` support is listed in [provider support](providers.md).
## Data contracts
| `Market` | `{ exchange, kind, base, quote }`; `base` and `quote` are uppercase |
| `Trade` | REST results are newest-first; `taker_side` is the aggressor side |
| `Candle` | `open_time ASC`; `open_time` is the interval start |
| `OrderBook::bids` | `price DESC` |
| `OrderBook::asks` | `price ASC` |
| `Ticker` | Provider summary; field source and aggregation window are provider-specific |
| `Decimal` | 96-bit coefficient and scale `0..=28`; prices, quantities, rates, and amounts never use `f64` |
| `Option<T>` | Provider omission maps to `None`; no inference or zero fill |
| `Timestamp` | UTC nanoseconds since Unix epoch; `Display` is millisecond RFC 3339; exact value via `as_nanos()` |
A `timestamp` documented as local read time records when `maxt` read the
response, not provider event time.
`parse_decimal_exact()` accepts plain or scientific notation only when the
value fits `Decimal` exactly. It returns an error instead of rounding or
truncating.
## Public REST
| `markets(kind)` | Listed `MarketInfo`; a valid `kind` with no listings returns `[]` |
| `trades(market, limit)` | Recent trades, newest-first |
| `order_book(market, depth)` | One snapshot; if `depth` is set, `bids.len() <= depth` and `asks.len() <= depth` |
| `ticker(market)` | One provider summary |
| `candles(request)` | Historical candles, `open_time ASC` |
| `funding_rates(request)` | Public perpetual funding-rate `Page<FundingRate>` |
Limits, book depths, and timestamp sources are provider-specific.
## Candles
### Intervals
`supports(Feature::Candles) == true` guarantees:
`Min1`, `Min3`, `Min5`, `Min15`, `Min30`, `Hour1`, `Hour4`, `Day1`, `Week1`,
`Month1`
Other intervals are provider-specific. An interval without a provider mapping
returns `Error::Unsupported`; it is never rounded to another interval.
### `CandleRequest`
Results are always sorted by `open_time ASC`.
| `from`, `to`, `limit` | `from <= open_time < to`; first `limit` |
| `from`, `to` | `from <= open_time < to` |
| `from`, `limit` | `from <= open_time`; first `limit` |
| `to`, `limit` | `open_time < to`; last `limit` |
| `limit` | Last `limit` |
| `from` | `from <= open_time` |
| `to` | `open_time < to`; one provider page |
| None | One latest provider page |
| `limit == 0` | `Error::InvalidRequest` |
| `from >= to` | `Error::InvalidRequest` |
| Provider calls | At most 100 per request |
| Estimated candles `> 100 * provider_page_cap` | Reject before network I/O |
| REST | `interval_end <= local_read_time` |
| Stream | Provider-specific; see the provider reference |
## Streams
### `Subscription`
| Subscription set | `markets × feeds` |
| Duplicate market or feed | Remove; preserve first insertion order |
| `markets.is_empty() || feeds.is_empty()` | `Error::InvalidRequest` |
| Logical stream | One or more WebSocket connections |
### `StreamConfig`
| `max_reconnect_attempts` | `None` | No finite reconnect limit |
| `initial_reconnect_delay_ms` | `1_000` | First reconnect delay |
| `max_reconnect_delay_ms` | `30_000` | Backoff ceiling |
| `idle_timeout_ms` | `30_000` | `max(config, provider_minimum)` |
| `buffer_size` | `4_096` | `0 -> 1` |
| `overflow` | `Backpressure` | Block the producer while full |
| `Backpressure` | Pause socket reads; no intentional event loss |
| `DropNewest` | Drop new data and errors; preserve reconnect notices |
`DropNewest` is valid only for replaceable snapshots. Trades and closed-candle
events are not replaceable.
### State
| `Some(Ok(event))` | Event |
| `Some(Err(error))` | Non-terminal error |
| `None` | Stream terminated |
| `MarketEvent::Reconnected` | Market events were lost during the disconnect |
| `AccountEvent::Reconnected` | Account events were lost during the disconnect |
| Built-in stream `Drop` | Drop the source and signal all built-in connection tasks to stop |
| Custom stream `Drop` | Drop the source; the producer owns cleanup |
| `close().await` | Await adapter-provided async cleanup, then drop the source |
Reconnect limits apply per underlying connection and do not reset after
healthy traffic. After `AccountEvent::Reconnected`, reload state with
`balances()` and `open_orders()`.
## Capability checks
`Client::supports(feature)` performs no network I/O.
| Operation mapped; required credentials present | `supports(feature) == true`; arguments and provider permissions still apply |
| Private operation mapped; credentials absent | `supports(feature) == false`; call returns `Error::Auth` |
| Structurally unsupported | `supports(feature) == false`; call returns `Error::Unsupported` |
## Errors and retries
| `InvalidRequest` | Local validation before network I/O | `false` |
| `Unsupported` | No mapped operation | `false` |
| `Adapter` | Adapter or foreign-dispatcher contract failure | `false` |
| `Auth` | Credentialed request cannot be built locally | `false` |
| `Exchange` | Provider error response | `kind.is_retryable()` |
| `Transport` | DNS, TLS, socket, timeout | `true` |
| `Decode` | Response schema mismatch | `false` |
`maxt` does not retry or throttle REST calls. Apply provider limits and
backoff in the application. `place_order` or `cancel_order` followed by
`Error::Transport` has an unknown outcome; query account or order state before
retrying.
## Private accounts and orders
| `open_orders*` | Point-in-time snapshot; full provider pagination is not guaranteed |
| `order_rules(market)` | Current fees, order limits, supported order combinations, balances, and average buy prices on Upbit and Bithumb; Bithumb also reports buy/sell price units |
| `order(market, order_id)` | One order selected by the exchange identifier |
| `order_by_client_id(market, client_id)` | One order selected by the identifier supplied at placement |
| `orders_by_ids(request)` | Up to 100 exchange IDs or client IDs; one identifier namespace per request; unresolved IDs may be omitted |
| `order_history(request)` | Newest-first `Page<Order>` containing completed or cancelled orders |
| `cancel_orders(request)` | Non-atomic batch cancellation; returns separate `cancelled` and `failed` lists; provider limits differ |
| `OrderOption::provider_id` | Exact exchange value; a future value has `order_type == None` until maxt adds its normalized meaning |
| `OrderRequest::size` | `Size::Base` or `Size::Quote` |
| Order precision | `MarketInfo` has no common tick size, lot size, or minimum notional; Bithumb's active buy/sell price units are available on `OrderRules`, while Upbit's deprecated `price_unit` is omitted |
| `cancel_order`, `cancel_order_by_client_id` | Return `()` after a valid provider acknowledgement; query the order to resolve races with fills |
| `positions*` | Remove rows where `position.quantity == 0` |
| `MarginSummary` | Provider omission maps to `None` |
| `FundingPayment::amount < 0` | Account paid funding |
Construct order values with `Decimal`. Supported order shapes and validation
rules are provider-specific.
## Asset transfers
| `asset_networks(asset)` | Current deposit and withdrawal availability, provider network ID, fees, and limits for one asset |
| `deposit_addresses()` | Lists all account deposit-address records returned by the provider. A provider can omit `network` and `provider_network`; `address` can be absent while issuance is pending, so a list entry is not necessarily transfer-ready |
| `deposit_address(request)` | Reads an existing address for one asset and network; `address == None` means the provider has not issued it yet |
| `create_deposit_address(request)` | Requests address creation on Upbit and Bithumb; it can return `address == None` while Upbit generates the address asynchronously |
| `deposit(request)`, `withdrawal(request)` | Read one Upbit or Bithumb transfer by its asset and exactly one exchange ID or transaction ID; an omitted reference never means “latest” |
| `deposits(request)`, `withdrawals(request)` | Newest-first transfer-history pages; provider identifiers and raw statuses are retained |
| `withdraw(request)` | Submits one withdrawal without automatic retry; a success means the provider accepted the request, not that the destination credited it |
| `cancel_withdrawal(withdrawal_id)` | Requests cancellation once on Upbit or Bithumb; `()` only means the provider accepted the cancellation request, so query `withdrawal(request)` to observe the final state |
`create_deposit_address()` never polls or retries. Use `deposit_address()` for a
specific asset and network before preparing a transfer; it observes an asynchronously
issued address. Upbit and Bithumb accept only an
asset and network for these calls; their adapters reject `DepositAddressRequest::amount`
before network I/O. See the generated [coverage reference](../bindings/common/generated/api.md)
for endpoint-level support and validation state.
`TransferLookupRequest` requires an asset and exactly one of `id` or `tx_id`.
The lookup and cancellation methods never retry automatically because a transport
failure leaves the provider outcome unknown.
### `OrderHistoryRequest`
| `market` | Optional market filter |
| `statuses` | `Filled` and/or `Cancelled`; empty includes both; any other status is rejected before network I/O |
| `from` | Inclusive creation-time lower bound |
| `to` | Exclusive creation-time upper bound; must be later than `from` |
| Time range | At most seven days when both bounds are set |
| `cursor` | Opaque provider cursor; pass it back unchanged to the same adapter |
| `limit` | `1..=1_000`; default `100` |
| Ordering | Newest first |
A provider without a continuation cursor returns `Page::next == None` and
rejects an input cursor.
### `HistoryRequest`
| `from` | `from <= item.timestamp` |
| `to` | `item.timestamp < to` |
| `cursor` | Opaque resume point from `Page::next`; overrides `from` and must be passed back unchanged to the same adapter |
| `limit` | Page-size target, not a hard maximum |
| `limit == 0` | `Error::InvalidRequest` before network I/O |
| Same-timestamp group crosses `limit` | The page may stop below `limit` and defer the group, or exceed `limit` when the first group alone is larger |
| Next request | Set `request.cursor = page.next` |
| Continue | `page.next.is_some()`, even when `items.is_empty()` |
| Stop | `page.next == None` |
### `MarginRequest`
| Local validation | `leverage.is_some() || margin_mode.is_some()` |
| Provider validation | May require one field or both |
| `set_margin()` | No atomicity or rollback guarantee across both changes |
## Provider-specific APIs
`Client::adapter(&self) -> &A` exposes non-portable batching, native context,
alert, and ledger methods. See each provider reference.
## External adapters
External types may implement `Adapter` with `exchange()` and `supports()`.
Override supported methods; all other methods return `Error::Unsupported`.
Preserve the common contracts above. For a new exchange, implement the Rust
adapter, add its `Exchange` variant and binding schema entries, then generate
the public API for each language being released. Do not manually copy generated
methods, models, or structural wire conversions between languages.
[Adapter checklist](../CONTRIBUTING.md#adapter-checklist)