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.

Taker fees

When you take liquidity (your order crosses the spread), you pay a small fee in collateral. The fee is automatically reserved when you place a BUY, so your balance has to cover the order's notional plus fee headroom. If your balance is exactly the notional with nothing extra, the order rejects with insufficient_balance.

Maker orders (resting on the book) pay no fee.

Fee amounts are returned in fee_micro on fills and trades.

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.

Trades report unavailable_exchanges when AGARA rows could not be loaded for that page. Retry later to fill the gap.

Positions are the exception: they return your complete current holdings in one response, with no paging to follow. This is intentional — you always get the whole set, never a page of it.

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 400 — drop the cursor and start again from the first page.

Error envelope

Trading API errors return JSON with one field. The HTTP status tells you the class; the message is for you to read. Public /api/v1 validation errors may also include structured details.

{ "error": "limit orders require price_micro" }
StatusWhat it meansRetry?
400Your request is malformed, has a bad value, or failed validation — e.g. an unknown order token, an amount outside its allowed range, a bad time-in-force / post-only combination, or an order that isn't yoursFix the request
401Token missing, invalid, expired, or revokedUse a fresh token
403Your token doesn't have the required scopeCreate a token with the right scopes
404A resource on that endpoint was not found — for example an order hash or orderbook tokenCheck the id
409A pre-signed order with the same hash already existsDon't resubmit
422A well-formed request was rejected against live state — on POST /trade/v1/orders (and the signed variants), your order can't be placed right now: not enough balance or shares, a post-only order that would cross, or a FOK that can't fully fill. Also covers an invalid withdrawal amount/destination. The error field carries the specific reasonFix and retry
424Your wallet setup or signer authorization is incompleteFinish setup, then retry
502, 503We're having a problem — try againYes, with backoff

Backoff for 5xx: start at 50 ms, double up to ~2 s, give up after ~4 retries. 4xx errors won't get better by retrying.

Retries on POST /trade/v1/orders. There is currently no server-side deduplication — a request that fails mid-flight (network timeout, TLS reset, etc.) and is retried will land twice and create two orders. Until idempotency keys ship, the safe pattern is:

  1. After a network failure on a write, don't retry blindly.
  2. Call POST /trade/v1/orders/list to see whether your order made it through (filter by recent timestamp + side + price).
  3. Only resubmit if you can't find a match.

Order status

Your order moves through these states. The terminal ones are bold — once your order reaches one, it won't change again.

PENDING            we received it, not yet submitted
SUBMITTING         submitting it to AGARA
OPEN               resting on the orderbook
PARTIALLY_FILLED   some shares matched, more remaining
MATCHED            ✓ terminal — fully matched
CANCELLED          ✓ terminal — cancelled by you, by us, or by expiry
EXPIRED            ✓ terminal — GTD order passed its expiration
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 recent fills from GET /trade/v1/portfolio/trades to watch progress.

A given order ends in exactly one of the five terminal states. Stop polling at that point — once it's MATCHED, the on-chain settlement of the resulting trades shows up under GET /trade/v1/portfolio/trades, not on the order.

On this page