# Conventions

> Shared concepts that come up on every endpoint. Worth a skim before your first request — most pitfalls people hit are right here.

## Amounts are in micro units [#amounts-are-in-micro-units]

Every amount is in **millionths of the natural unit** — no floats, no
decimals, no rounding ambiguity.

| Field                     | Means                                                                           |
| ------------------------- | ------------------------------------------------------------------------------- |
| `price_micro`             | Millionths of one collateral unit per share. `"600000"` = 0.60 collateral units |
| `shares_micro`            | Millionths of a share. `"1000000"` = 1 share                                    |
| `collateral_amount_micro` | Millionths of the configured collateral. `"1000000"` = 1 collateral unit        |
| `fee_micro`               | Millionths of the configured collateral                                         |

**On the wire, micro amounts are JSON strings, not numbers.** They
hold 64-bit integers that don't fit in JavaScript's safe-integer
range (2^53), and silently lose precision if encoded as JSON numbers.

* **Responses** always return strings: `"price_micro": "600000"`.
* **Requests** accept both: `"price_micro": "600000"` and
  `"price_micro": 600000` are both valid. String is recommended; pass
  number only if you're sure your client doesn't lose precision.

To convert between micro and human units in Python:

```python
# parsing a response
price_in_collateral = int(resp["price_micro"]) / 1_000_000

# building a request
price_micro_str = str(int(0.60 * 1_000_000))   # "600000"
```

One REST exception: the **orderbook endpoint** returns prices and sizes as
floats in human units (`0.60`, `1.0`) because its main consumer is a
chart renderer. WebSocket streams instead use integer engine units and carry
`price_scale` / `size_scale` on each data frame.

Limit orders and market buys must be between **0.10 and 100,000 collateral units**.
Market sells must be between **1 and 100,000 shares** — see
[Order size limits](/docs/orders#order-size-limits).

### Fees [#fees]

Trading is currently fee-free on every market: maker and taker fills both pay
nothing, and `fee_micro` is `0` on every fill and trade.

Fees are set per market, so budget for them rather than assuming zero. When a
market carries a fee, a BUY reserves the worst-case fee alongside its notional,
and a balance that covers exactly the notional rejects with
`insufficient_balance`. Maker orders (resting on the book) never pay a fee.

## Identifiers [#identifiers]

Three identifiers you'll work with:

| Field          | What it is                                                                                                       | Example                                                                           |
| -------------- | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `token_id`     | The market outcome you're trading. Discover via [Markets](/docs/markets).                                        | `"21742633143463906290569050155826241533067272736897614950488156847949938836455"` |
| `order_id`     | Your order's UUID — use this to look up or cancel.                                                               | `"9c4a3e7d-12b4-4f8e-9a3c-d2c7f0a45e1b"`                                          |
| `condition_id` | The underlying market (one market has two outcomes, both share a condition). Also from [Markets](/docs/markets). | `"0x21742633…"`                                                                   |

The `order_id` is what we return from `POST /trade/v1/orders` and what you
pass to `DELETE /trade/v1/orders/{order_id}` later. Store it on your side.

## Timestamps [#timestamps]

* **In responses:** ISO-8601 UTC strings — `"2026-05-12T10:23:45.678Z"`.
* **In requests** (e.g. `expiration_unix_seconds`): Unix seconds as an integer.

## Pagination [#pagination]

[List your orders](/docs/orders#list-your-orders),
[open orders](/docs/portfolio#open-orders),
[trades](/docs/portfolio#trades), and [activities](/docs/portfolio#activities)
page with opaque keyset cursors. The list endpoints take `limit` and
`cursor` in the request body; trades and activities take them as query
parameters. Leave `cursor` off for the first page:

```json
{ "limit": 100 }
```

The response carries a `next_cursor` inside a `pagination` object. To
fetch the next page, pass it back as `cursor`:

```json
{ "limit": 100, "cursor": "eyJ2IjoxLCJyIjoib3JkZXJzL2xpc3Qi…" }
```

When `next_cursor` comes back `null`, you've reached the end. Treat
the cursor as opaque — don't parse or build one yourself, just echo
it back. A cursor is tied to the request it came from, so keep its
filters the same as you page; change them and start over from
the first page. Default `limit` is 500 (50 for activities), max 500.

Every trades page can be partial when an exchange could not be loaded. Check
`unavailable_exchanges` on every page and do not advance a cursor from a
partial response. For an AGARA-only walk, any page containing `AGARA` means you
must discard the entire accumulated walk and restart without a cursor. For a
complete multi-exchange walk, do the same when the array is nonempty. Continue
only after every page satisfies the condition for your requested exchange set.

[Positions](/docs/portfolio#positions) are unpaginated, but a `200` can still
be partial. An AGARA-only consumer must retry the whole read while
`unavailable_exchanges` includes `AGARA`; a multi-exchange consumer must retry
while the array is nonempty. Trust the response only after the condition for
your requested exchange set is satisfied.

The public markets API (`/api/v1`, see [Markets](/docs/markets#pagination))
pages the same way, with its own `cursor` / `next_cursor`. There too a
cursor is tied to the filters and sort that produced it; reuse it with
different filters or a different sort and the request fails with
`422 validation_failed` — drop the cursor and start again from the first page.

## Errors and recovery [#errors-and-recovery]

API failures use Problem Details with a stable `code` and structured
`recovery` instruction:

```json
{
  "type": "urn:agara:problem:post-only-would-cross",
  "title": "Post-only order would cross",
  "status": 422,
  "code": "post_only_would_cross",
  "request_id": "8051f907-4eeb-4ca9-bd84-b447ea65a68c",
  "recovery": { "strategy": "none" }
}
```

Branch on `code`, not the display text. Follow `recovery` rather than treating
every response with the same HTTP status alike. In particular, do not blindly
repeat a write after a timeout: reconcile its identity or current state first.

See [Errors and recovery](/docs/errors) for the complete HTTP, asynchronous,
retry, and rate-limit contract.

## Reconnecting streams [#reconnecting-streams]

Reconnect automatically when a stream closes unexpectedly. Connections to the
Trade service (market and account WebSockets) and API service (price streams)
may disconnect periodically and without prior notice, even if your client has
not requested a disconnect. Do not wait for an advance warning or an error
message before attempting to reconnect.

* **Retry with backoff.** Use exponential backoff with jitter for reconnect
  attempts. Respect `Retry-After` when rate limited and follow any explicit
  [recovery instruction](/docs/errors).
* **Trade service WebSockets.** Open a new socket, authenticate again when
  required, and resend your subscriptions. Follow the
  [market stream](/docs/streaming#reconnect-strategy) and
  [account stream](/docs/account-stream#reconnect-strategy) reconnect procedures
  to refresh state before resuming quoting; events missed during the gap are
  not replayed.
* **API service price streams.** Reopen the
  [multi-symbol](/docs/api-reference/market-data/prices/get-api-v1-prices-stream)
  or [single-symbol](/docs/api-reference/market-data/prices/get-api-v1-prices-pyth-pro-stream)
  SSE connection with the same requested symbols. Treat prices as stale until
  fresh data arrives.

A disconnect does not cancel resting orders. Use the
[API trading recovery procedure](/docs/api-traders/recovery) to reconcile your
orders and account state before resuming placements.

## Order status [#order-status]

Use `is_terminal` on the order response to check whether matching has finished.
`PARTIALLY_FILLED` can describe either a resting order or a completed FAK order.

```
PENDING            we received it, not yet submitted
SUBMITTING         submitting it to AGARA
OPEN               resting on the orderbook
PARTIALLY_FILLED   some shares matched; check is_terminal for completion
MATCHED            shares matched; check is_terminal for completion
CANCELLED          ✓ terminal — cancelled by you, by us, or by expiry
EXPIRED            ✓ terminal — not used for AGARA orders; an expired AGARA GTD order is CANCELLED
REJECTED           ✓ terminal — we couldn't accept the order
FAILED             ✓ terminal — the order couldn't be placed
```

`POST /trade/v1/orders` returns immediately with `PENDING` — that
just means "we got it." Poll `GET /trade/v1/orders/{order_id}` or read the
[account stream](/docs/account-stream) to watch progress. Terminal `FAILED` and
`REJECTED` orders include a typed `failure`; see
[Accepted work can fail later](/docs/errors#accepted-work-can-fail-later).

Stop waiting for matching when `is_terminal` is `true`. A completed AGARA FAK
partial fill returns `PARTIALLY_FILLED` with `is_terminal: true`: the executed
shares remain filled and the remainder has been cancelled. A resting GTC/GTD
partial fill has `is_terminal: false`. A user cancellation or another
cancellation reason still returns `CANCELLED`. Treat an order's status as
provisional until it reflects every fill and `order_cancelled` you received,
and longer if a late fill is still plausible: an order reported `CANCELLED` can
still change to `MATCHED` if fills that beat the cancel filled it completely,
or to `PARTIALLY_FILLED` if it was a FAK order that filled only in part.

Keep polling if an AGARA market FAK order has `is_terminal: false`, even when
its status is `MATCHED`. It can later finish as `PARTIALLY_FILLED` when the
unfilled remainder is cancelled.

Keep tracking fills and their settlement separately under
`GET /trade/v1/portfolio/trades`. A terminal order response does not replace
the [account-stream reconciliation checks](/docs/account-stream#reconnect-strategy).

Update SDK polling helpers that use a fixed list of terminal status names.
Older Rust and Python `wait_for_terminal` helpers do not recognize completed
partial fills and can wait until their timeout; use `is_terminal` instead.
