Agara

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.

FieldMeans
price_microMillionths of one collateral unit per share. "600000" = 0.60 collateral units
shares_microMillionths of a share. "1000000" = 1 share
collateral_amount_microMillionths of the configured collateral. "1000000" = 1 collateral unit
fee_microMillionths 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:

# 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:

FieldWhat it isExample
token_idThe market outcome you're trading. Discover via Markets."21742633143463906290569050155826241533067272736897614950488156847949938836455"
order_idYour order's UUID — use this to look up or cancel."9c4a3e7d-12b4-4f8e-9a3c-d2c7f0a45e1b"
condition_idThe 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-After when 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 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 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.