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.
Source
The package, its tests and the contract snapshots it generates from.
API reference
Per-operation request and response shapes, which the client mirrors exactly.
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 packThat writes agara-sdk-<version>.tgz. Add it to your own project:
npm install /path/to/agara-sdk-<version>.tgzOnce 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 unitPrices 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 viemimport { 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
Pre-signed orders
The one-time setup for signing locally, and why it beats server-side signing.
Account batches
Sign your own split, merge or withdrawal, one per batch.
Streaming
Channels, sequence numbers and what a gap obliges you to do.
Errors
Problem codes, recovery guidance and the failures you must reconcile.