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
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": 600000are 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:
# 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.
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
Three identifiers you'll work with:
| Field | What it is | Example |
|---|---|---|
token_id | The market outcome you're trading. Discover via 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. | "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
- 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
List your orders,
open orders,
trades, and 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:
{ "limit": 100 }The response carries a next_cursor inside a pagination object. To
fetch the next page, pass it back as cursor:
{ "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 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)
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
API failures use Problem Details with a stable code and structured
recovery instruction:
{
"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 for the complete HTTP, asynchronous, retry, and rate-limit contract.
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-Afterwhen rate limited and follow any explicit recovery instruction. - Trade service WebSockets. Open a new socket, authenticate again when required, and resend your subscriptions. Follow the market stream and account stream reconnect procedures to refresh state before resuming quoting; events missed during the gap are not replayed.
- API service price streams. Reopen the multi-symbol or single-symbol 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 to reconcile your orders and account state before resuming placements.
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 placedPOST /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 to watch progress. Terminal FAILED and
REJECTED orders include a typed failure; see
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.
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.