Agara
SDKs

TypeScript

Install the TypeScript client, read a market without a token, place and settle an order, and handle the failures that matter.

@agara/sdk wraps every documented operation: trading, portfolio, market and event discovery, order and account-batch signing, both router WebSocket streams, and both price feeds. It ships as ESM and needs Node 22 or newer. Its only dependency is a lossless JSON parser, so large integers survive a round trip instead of silently becoming floats.

Quickstart

Install the SDK

Not on npm yet, so build it from the public repository once:

git clone https://github.com/Mirai-Tech-Inc/agara-sdks.git
cd agara-sdks/typescript && npm ci && npm run build && npm pack

That writes agara-sdk-<version>.tgz. Add it to your own project:

npm install /path/to/agara-sdk-<version>.tgz

Once the package is published, the clone and pack step disappears and the tarball path becomes @agara/sdk. Nothing else on this page changes.

Create a public client

Discovery needs no credentials, so start without a token:

import { PublicClient } from '@agara/sdk'

const client = new PublicClient({ baseUrl: process.env.AGARA_BASE_URL })

Read a market

const { markets } = await client.listMarkets({ source: 'agara', state: 'ACTIVE', limit: 5 })
for (const market of markets) {
  console.log(market.slug, market.condition_id)
}

const book = await client.getOrderbook(markets[0].tokens[0].token_id)
console.log('best bid', book.bids.at(0), 'best ask', book.asks.at(0))

source is required and lowercase. Passing AGARA is a validation failure before the request leaves your process.

Place an order and wait for the outcome

AgaraClient adds the authenticated surface plus polling helpers. Create a token first, following Authentication:

export AGARA_TOKEN="agt_..."
import { AgaraClient } from '@agara/sdk'

const client = new AgaraClient({
  baseUrl: process.env.AGARA_BASE_URL,
  token: process.env.AGARA_TOKEN,
})

const accepted = await client.placeOrder({
  token_id: tokenId,
  type: 'LIMIT',
  side: 'BUY',
  time_in_force: 'GTC',
  price_micro: '600000',
  shares_micro: '1000000',
})

const settled = await client.waitForOrder(accepted.order_id, { pollIntervalMs: 700 })
console.log(settled.status, settled.size_matched_micro)

waitForOrder returns when the server's own is_terminal flag is set, not when the status string looks final. A partially filled order that will never fill again is terminal, and it comes back for you to inspect rather than being treated as a failure. Acceptance is not settlement: read status and failure on the result.

Amounts

Every money, price and share field is an integer string in millionths. There is no float convenience layer, and a value that cannot be represented exactly is rejected locally rather than rounded:

import { parseUnits, formatUnits } from '@agara/sdk'

parseUnits('0.60')       // 600000n
formatUnits(600000n)     // "0.60"
parseUnits('0.0000005')  // throws: finer than one micro unit

Prices run from 1 to 999999, one tick below a whole dollar. See Conventions for ticks and minimum order value.

Pagination

List endpoints are cursor-paged. The page iterators preserve the envelope, so you keep each page's metadata rather than a flattened array:

for await (const page of client.orderPages({ limit: 50 })) {
  for (const order of page.orders) console.log(order.internal_id, order.status)
}

To resume a scan later, keep the cursor the server hands back and pass it as the starting point:

const first = await client.listOrders({ limit: 50 })
const resumeFrom = first.pagination.next_cursor

const next = await client.listOrders({ limit: 50, cursor: resumeFrom })

openOrderPages and tradePages behave the same way. A server that repeats a cursor raises rather than looping forever.

Types

Concrete request and response types come from the contract snapshots, so they track the API rather than being written by hand:

import type { Order, Fill, BatchSubmission } from '@agara/sdk/types'

Any schema the API documents is reachable through the two service namespaces:

import type { TradingSchemas, CatalogueSchemas } from '@agara/sdk/types'

type Position = TradingSchemas['PortfolioPosition']
type Market = CatalogueSchemas['MarketsListItem']

Error handling

import { AgaraError, TransportError } from '@agara/sdk'

try {
  await client.cancelAllOrders()
} catch (error) {
  if (error instanceof AgaraError) {
    console.error(error.problem?.code, error.status)
    if (error.isRetryable) {
      // the server's own recovery guidance permits another attempt
    }
  } else if (error instanceof TransportError && error.outcomeUnknown) {
    // the request left but no response arrived: it may already have taken effect
    const open = await client.listOpenOrders({ limit: 50 })
  }
  throw error
}

isRetryable reflects the recovery guidance the server sent, checked against the registry of known problem codes, so an unrecognised code never authorises an automatic retry.

outcomeUnknown is the one worth designing for. It means the request was sent and the result is genuinely unknown, so the safe response is to read the state back rather than assume failure. An aborted cancel can still take effect. See Errors for the full code list.

Signing

Server-side signing adds latency to order ingestion. To sign in your own process you need the orders:place_signed scope and a signer.

npm install viem
import { signOrder } from '@agara/sdk/signing'
import { privateKeyToAccount } from 'viem/accounts'

const owner = privateKeyToAccount(process.env.AGARA_OWNER_KEY)
const signed = await signOrder(
  {
    domain: { chainId: Number(process.env.AGARA_CHAIN_ID), exchangeContract },
    maker: depositWalletAddress,
    tokenId,
    side: 'BUY',
    priceMicro: 600000n,
    sharesMicro: 1000000n,
  },
  owner,
)
await client.placeSignedOrder(signed)

maker is the account's deposit wallet, not the owner address that signs. The same module composes presigned split, merge and withdraw account batches through signBatch. Full walkthrough in Pre-signed orders.

Realtime subscriptions

import { marketStream } from '@agara/sdk/streaming'

const stream = marketStream({
  baseUrl: process.env.AGARA_BASE_URL,
  channels: [{ name: 'orderbook', token_id: tokenId }],
})

for await (const event of stream) {
  if (event.type === 'frame') console.log(event.frame)
  if (event.type === 'gap') console.log('missed messages, recover with', event.recovery)
}

A gap event means your client missed messages and tells you how to recover: discard local state and take a fresh snapshot, or reconcile over REST. Unread events are bounded, so a consumer that stops reading is disconnected rather than allowed to grow without limit. Streaming covers the channels and Account stream the private one.

Next steps