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

<Cards>
<Card title="Source" href="https://github.com/Mirai-Tech-Inc/agara-sdks/tree/main/typescript">
The package, its tests and the contract snapshots it generates from.
</Card>

<Card title="API reference" href="/docs/api-reference">
Per-operation request and response shapes, which the client mirrors exactly.
</Card>
</Cards>

## Quickstart [#quickstart]

<Steps>
<Step>
### Install the SDK [#install-the-sdk]

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

```bash
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:

<Tabs items={["npm","pnpm","yarn","bun"]}>
<Tab value="npm">
```bash
npm install /path/to/agara-sdk-<version>.tgz
```
</Tab>

<Tab value="pnpm">
```bash
pnpm add /path/to/agara-sdk-<version>.tgz
```
</Tab>

<Tab value="yarn">
```bash
yarn add /path/to/agara-sdk-<version>.tgz
```
</Tab>

<Tab value="bun">
```bash
bun add /path/to/agara-sdk-<version>.tgz
```
</Tab>
</Tabs>

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

<Step>
### Create a public client [#create-a-public-client]

Discovery needs no credentials, so start without a token:

```ts
import { PublicClient } from '@agara/sdk'

const client = new PublicClient({ baseUrl: process.env.AGARA_BASE_URL })
```
</Step>

<Step>
### Read a market [#read-a-market]

```ts
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.
</Step>

<Step>
### Place an order and wait for the outcome [#place-an-order-and-wait-for-the-outcome]

`AgaraClient` adds the authenticated surface plus polling helpers. Create a
token first, following [Authentication](/docs/authentication):

```bash
export AGARA_TOKEN="agt_..."
```

```ts
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.
</Step>
</Steps>

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

```ts
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](/docs/conventions) for ticks and minimum order value.

## Pagination [#pagination]

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

```ts
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:

```ts
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 [#types]

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

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

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

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

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

## Error handling [#error-handling]

```ts
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](/docs/errors) for the full code list.

## Signing [#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.

<Tabs items={["viem account","Your own signer"]}>
<Tab value="viem account">
```bash
npm install viem
```

```ts
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)
```
</Tab>

<Tab value="Your own signer">
The signer is one method, so a hardware wallet or remote signing service works
as long as it produces a viem-compatible EIP-712 signature:

```ts
import type { TypedDataSigner } from '@agara/sdk/signing'

const remote: TypedDataSigner = {
  async signTypedData(parameters) {
    return await myKeyService.signEip712(parameters)
  },
}
```

Keys never reach the SDK either way: it hands you the typed data and takes back
a signature.
</Tab>
</Tabs>

`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](/docs/api-traders/pre-signed-orders).

## Realtime subscriptions [#realtime-subscriptions]

<Tabs items={["Market channels","Your account"]}>
<Tab value="Market channels">
```ts
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)
}
```
</Tab>

<Tab value="Your account">
```ts
import { accountStream } from '@agara/sdk/streaming'

const stream = accountStream({
  baseUrl: process.env.AGARA_BASE_URL,
  token: process.env.AGARA_TOKEN,
})

for await (const event of stream) {
  if (event.type === 'frame') console.log(event.frame)
}
```
</Tab>
</Tabs>

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](/docs/streaming) covers the channels
and [Account stream](/docs/account-stream) the private one.

## Next steps [#next-steps]

<Cards>
<Card title="Pre-signed orders" href="/docs/api-traders/pre-signed-orders">
The one-time setup for signing locally, and why it beats server-side signing.
</Card>

<Card title="Account batches" href="/docs/api-traders/account-batches">
Sign your own split, merge or withdrawal, one per batch.
</Card>

<Card title="Streaming" href="/docs/streaming">
Channels, sequence numbers and what a gap obliges you to do.
</Card>

<Card title="Errors" href="/docs/errors">
Problem codes, recovery guidance and the failures you must reconcile.
</Card>
</Cards>
