This page documents a TypeScript client pattern for server-side bots and market makers. It is meant to make the API flow concrete: authenticate, fetch market data, build orders, sign orders, save orders, subscribe to streams, and cancel orders.
This pattern is for backend services and dedicated bot accounts. Do not ship private-key signing code in a browser app, and do not use a primary wallet key for automated trading.
Dependencies
npm install ethers@5 ws @hiero-ledger/sdk
npm install --save-dev @types/ws
The example client uses:
| Package | Purpose |
|---|
ethers@5 | EIP-712 signing and EVM account utilities |
ws | Node.js WebSocket support |
@hiero-ledger/sdk | Hedera account key handling for ED25519 accounts |
The example pins ethers@5 intentionally for CommonJS-friendly bot projects. If your service already runs as ESM, you can adapt the signing helpers to ethers@6+, but do not upgrade the sample dependency without also reviewing the module format and API changes.
Recommended Client Surface
If you build a small wrapper around the API, use a surface like this:
| Method | Purpose |
|---|
authenticate(accountId, privateKey) | Run challenge and verify flow; store the JWT |
getOrderbooks(query) | Discover orderbook token IDs, EVM addresses, decimals, status, and routing flags |
getDepth(orderbookId) | Fetch a depth snapshot |
getDomain() | Fetch and cache the EIP-712 signing domain |
buildOrders(requests) | Request nonce-assigned serialized orders |
signOrder(order, privateKey, domain) | Produce a prefixed signature for a built order |
saveOrders(items) | Submit signed orders |
cancel(orderIds) | Request cancellation for specific order IDs |
cancelAll(nonceFloor) | Emergency cancel by nonce floor |
connectDepthWs(books, handlers) | Subscribe to live depth updates |
connectUserEventsWs(books, handlers) | Subscribe to authenticated order events |
The examples below assume a wrapper with this shape. You can also call the REST and WebSocket endpoints directly.
Minimal Limit Order Flow
const client = new OrderbookApiClient({ environment: 'testnet' })
async function placeLimitOrder() {
await client.authenticate('0.0.123456', process.env.PRIVATE_KEY!)
const domain = await client.getDomain()
const requests = [
{
orderbookId: '3',
type: 'LIMIT' as const,
deadline: String(Math.floor(Date.now() / 1000) + 3600),
inputToken: '0xbase-token-evm-address',
inputAmount: '1000000',
outputToken: '0xquote-token-evm-address',
outputAmount: '950000',
isAMMEnabled: true,
},
]
const builtOrders = await client.buildOrders(requests)
const items = []
for (let i = 0; i < builtOrders.length; i++) {
const signature = await client.signOrder(
builtOrders[i],
process.env.PRIVATE_KEY!,
domain,
)
items.push({
order: builtOrders[i],
signature,
orderbookId: requests[i].orderbookId,
type: requests[i].type,
})
}
const { orders: saved } = await client.saveOrders(items)
for (const order of saved) {
console.log(
`order ${order.meta?.id}: status=${order.meta?.status} nonce=${order.info.nonce}`,
)
}
}
placeLimitOrder().catch(console.error)
Signing Behavior
The bot client signs the serialized order returned by buildOrders().
For ECDSA_SECP256K1 accounts:
- Build the EIP-712 order payload.
- Sign with
wallet._signTypedData(domain, types, order).
- Prefix the signature with
0x00.
For ED25519 Hedera accounts:
- Build the same EIP-712 order payload.
- Hash it with
ethers.utils._TypedDataEncoder.hash.
- Sign the raw hash bytes with the Hiero SDK private key.
- Prefix the signature bytes with
0x00.
Wallet integrations that use Hedera personal sign should instead use the 0x01 signing mode and submit a HIP-632 SignatureMap around the raw wallet signature.
Implementation Notes
- Cache the value returned by
getDomain() for the current session.
- Reuse the same authenticated account for build, sign, save, and cancellation.
- Sign the order returned by
buildOrders(), not the original request body.
- Preserve all integer fields as strings.
- Use
baseTokenDecimals and quoteTokenDecimals from getOrderbooks() when converting between display amounts and raw token units.
- Keep the signature mode byte in the submitted signature.
- Re-authenticate before reconnecting WebSocket streams.
WebSocket Example
await client.authenticate('0.0.123456', process.env.PRIVATE_KEY!)
const ws = client.connectDepthWs(['3'], {
onOpen: () => console.log('depth stream connected'),
onMessage: (message) => {
const event = JSON.parse(message)
console.log(event)
},
onClose: (code, reason) => {
console.log(`depth stream closed: ${code} ${reason}`)
},
onError: (error) => {
console.error(error)
},
})
Production clients should re-authenticate before reconnecting and should not log full WebSocket URLs because the JWT is passed in the query string.
Cancellation Example
await client.cancel([1234, 1235])
Cancellation is asynchronous. Wait for the user-event stream or fetch order history before treating an order as cancelled.
await client.cancelAll('100000000000000000000')
Use nonce-floor cancellation only for emergency recovery or deliberate session shutdown flows.