npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sentico-labs/sdk

v0.3.0

Published

Institutional TypeScript SDK for Senticore HTTP, WebSocket, and Order Entry.

Readme

Senticore TypeScript SDK

API migration support: client.public.getExchangeInfo() returns current instruments and trading rules without sending private credentials. HTTP errors accept nested and legacy flat code/requestId/retriable fields and retain nonce recovery details. Configure headers: { "X-SC-Error-Format": "envelope-v1" } to opt into the server's canonical envelope and status mapping; the SDK preserves the current default representation unless explicitly configured.

Retain every acquired nonce lease alongside its command/sequence ID. Reconcile the original receipt with slot.observeTerminal(result). A stale callback or receipt cannot release a newer allocation of the same nonce. The legacy manager.observeTerminal(nonce, result) method remains for unambiguous first allocations, emits one deprecation warning, and on ambiguous reuse returns false and leaves the slot uncertain (it never throws inside a reconciliation loop); its earliest retirement review is 2027-03-07. Unknown admission outcomes still retain nonce ownership until a terminal result is reconciled.

Retries honor Retry-After seconds or HTTP dates, legacy x-ratelimit-after seconds, and body retryAfterMs (including error details), with validated nonnegative timer-safe delays. Existing idempotency/retry gates remain in effect.

The TypeScript SDK for Senticore — HTTP, market data, private streams, and institutional order entry. This is the reference SDK for the platform: the Sentico terminal shares its market-feed implementation, and the examples run as conformance tests against the live protocol.

Language support. TypeScript is the reference SDK and is kept in lockstep with the platform. A Rust SDK exists for latency-sensitive order entry; the Python surface is HTTP-convenience only. When a contract changes, TypeScript moves first.

Package name: @sentico-labs/sdk Current version: 0.3.0

Install

npm install @sentico-labs/[email protected]

0.3.0 adds signed DMS controls and public V2 oracle reads, alongside the latest discovery, retry and nonce-reconciliation fixes. DMS requires explicit order enrollment and signed heartbeats. The current deployment supports one shard and restart recovery; there is no fixed cancellation-latency SLA.

Use client.trading.controlDms / controlBslDms with buildAgentDmsMessage / signBslDmsControl. Include dmsSigningSuffix in delegated order signatures and set dmsEpoch before signing BSL SessionKey orders. Reconcile the completed phase; a control ACK is not proof of cancellation. See https://docs.sentico-labs.xyz/integration/dead-man-switch/.

client.public.getMarketOracleV2(marketId) reads durable policy/progress; client.public.getOracleFeedV2(policyHash) reads a durable feed report. getMarketOracle retains V1 behavior. These methods preserve server errors and never auto-convert a V1 market or manufacture a missing V2 view. Responses are generic records with optional caller-supplied types. See https://docs.sentico-labs.xyz/trading/oracle/.

0.2.0 added the public market-feed v2 client (integrity-checked local order book with checksum verification, gap recovery and the session status frame), typed delegated order-entry batches, typed platform status/readiness including the projection self-heal fields, signed private read tokens, and order-receipt confirmation. BSL Direct TCP order encoding (encodeBslOrderFromSignedAction) has been available since 0.1.1.

For local development from the repository:

npm ci
npm run verify

The SDK targets Node.js 20+ and modern browser runtimes with fetch. HTTP helpers expose default response types and still allow per-call overrides when you have a narrower generated schema:

import type { SenticoreClient } from "@sentico-labs/sdk";

declare const client: SenticoreClient;

const markets = await client.public.listMarkets();
const custom = await client.public.listMarkets<{ items: Array<{ id: number }> }>();

Quick Start

import { SenticoreClient } from "@sentico-labs/sdk";

const client = new SenticoreClient({
  publicHttpBaseUrl: "https://api.sentico-labs.xyz",
  tradingHttpBaseUrl: "https://api.sentico-labs.xyz",
  publicWsUrl: "wss://api.sentico-labs.xyz/api/v1/ws/public",
  privateWsUrl: "wss://api.sentico-labs.xyz/api/v1/ws/private/{account}",
  orderEntryHttpBaseUrl: "https://api.sentico-labs.xyz",
  orderEntryBinaryPath: "/api/order-entry/binary",
  bearerToken: process.env.SENTICORE_BEARER_TOKEN,
  orderEntryApiKey: process.env.SENTICORE_ORDER_ENTRY_API_KEY,
  machineAuth: process.env.SENTICORE_API_KEY_ID
    ? {
        apiKeyId: process.env.SENTICORE_API_KEY_ID,
        apiSecret: process.env.SENTICORE_API_SECRET!,
        apiPassphrase: process.env.SENTICORE_API_PASSPHRASE!
      }
    : undefined
});

const markets = await client.public.listMarkets();
console.log(markets.data, markets.requestId, markets.rateLimit);

Transport Reuse

Create one SenticoreClient per process or strategy shard and reuse it for the whole session. Do not construct a new client or HTTP transport for every quote. Node's built-in fetch keeps an internal connection pool; if you pass config.fetch, use a fetch implementation backed by a persistent agent or dispatcher.

Client Planes

  • client.public: anonymous public HTTP reads.
  • client.trading: authenticated private reads and trading writes.
  • client.orderEntry: Institutional Order Entry.
  • client.mm: deprecated alias for client.orderEntry.
  • client.raw: escape hatch for endpoints not promoted to stable helpers yet.
  • client.ws: public and private WebSocket helpers.
  • Direct BSL TCP helpers: BslTcpSession, bslCompactRoutingFromConnectivityBundle, encodeBslOrderFromSignedAction, encodeBslTcpHandshakeHello, decodeBslTcpHandshakeAck, plus low-level frame wrappers.
  • FIX helpers: deriveFixSenderCompId, buildFixLogonFields, encodeFixFrame.
  • FIXP/SBE helpers: FixpSession, encodeFixpNegotiate, encodeFixpEstablish, encodeFixpNewOrderSingle, decodeFixpFrame, FIXP_SBE_SCHEMA_URL.

Legacy SenticoreHttpClient and SenticoreWsClient exports are still present for existing integrations. New integrations should use SenticoreClient.

Public HTTP calls are credential-isolated: they do not inherit bearerToken, machineAuth, or global headers. Those settings remain available to authenticated trading and low-level requests.

Public Market Data

const markets = await client.public.listMarkets();
const book = await client.public.getMarketOrderbook(1, { book: "YES", depth: 20 });
const trades = await client.public.listMarketTrades(1, { limit: 50 });
// Persist the last processed tradeId and catch up without guessing a depth:
const catchUp = await client.public.listMarketTrades(1, {
  sinceId: lastProcessedTradeId,
  order: "asc",
  limit: 1000
});

Trade pages are newest-first by default. cursorTradeId pages backward; sinceId is a strict lower bound and provides deterministic ascending catch-up.

Market Feed (real-time order book)

REST snapshots are for bootstrap and catch-up. For live price discovery use the public feed (v2): an integrity-checked local order book maintained from a snapshot plus sparse deltas, with keccak top-of-book and BLAKE3 full-book checksums verified on every applicable frame. MarketFeedBookMirror handles reconnects, gap recovery, snapshot deadlines and integrity restarts, and never serves a book it could not verify.

import { MarketFeedBookMirror } from "@sentico-labs/sdk";

const mirror = new MarketFeedBookMirror({
  baseUrl: "https://api.sentico-labs.xyz",
  marketId: 1,
  // Node < 21: WebSocketCtor: (await import("ws")).default,
  onUpdate: ({ levels, seq }) => {
    const book = levels.spot ?? levels.yes; // spot vs binary market
    console.log(seq, "best bid", book.bids[0], "best ask", book.asks[0]);
  },
  // Halt / cancel_only arrives here at push latency (the 5s status poll is a
  // fallback, not the primary signal).
  onSessionStatus: (status) => {
    if (!status.placementReady) console.warn("placement closed:", status.sessionState);
  },
  onIntegrityRestart: (reason) => console.warn("book rebuild:", reason),
});
mirror.start();
// ... mirror.book() returns the verified levels or null while (re)building.
// mirror.stop() tears everything down.

Two operational rules the client enforces for you, and the reasons they exist:

  • encoding=json is sent explicitly. The server default is binary, which is the same JSON delivered in a binary WebSocket frame; a client that omits encoding receives frames it may not parse. parseMarketFeedFrame decodes both encodings if you build a channel yourself.
  • depth=100 is sent explicitly. Omitting depth yields a 20-level book while the session frame advertises 100 — a silent checksum desync.

For a lower-level single channel (no book maintenance) use subscribeMarketFeedChannel({ baseUrl, marketId, channel, onFrame }).

Platform Status & Readiness

const status = (await client.public.getStatus()).data;
if (!isPlacementOpen(status)) {
  console.warn("not accepting orders:", status.sessionState, status.placementReason);
}
// projection self-heal visibility (rolling watermark):
console.log(status.projectionOldestDebtBatchId, status.projectionBackfillActive);

// /readiness answers 200 when ready and 503 (same body) otherwise — this
// normalizes that so a 503 is data, not an exception:
const { ready, readiness } = await client.public.getReadiness();

Collateral Deposit Discovery

Never copy token/vault addresses from a deployment note or derive assetId from an assumed chain ID. Fetch the runtime configuration immediately before building a deposit:

const catalog = await client.public.getAssets();
const usdc = catalog.data.assets.find((asset) => asset.ledgerAsset === "USDC");
if (!usdc) throw new Error("USDC collateral is not enabled");

// Approve `usdc.tokenAddress` to `usdc.vaultAddress`, then call the reported
// deposit function with `usdc.assetId` and an amount in `usdc.decimals`.

The response is authoritative for the running venue and includes chainId, token/vault addresses, decimals, lifecycle flags, the precomputed on-chain assetId, and its derivation. A missing asset means it is not accepted by that runtime. getCollateralAssets() remains as a compatibility alias.

Trading HTTP

import {
  actionSigningHashHex,
  deriveOrderIdHex,
  signAction,
  type LocalActionPayload
} from "@sentico-labs/sdk";

const account = "0x00000000000000000000000000000000000000d3" as const;
const privateKey = process.env.SENTICORE_PRIVATE_KEY!;
const marketId = Number(process.env.SENTICORE_MARKET_ID);
if (!Number.isSafeInteger(marketId) || marketId <= 0) {
  throw new Error(
    "Set SENTICORE_MARKET_ID to a live spot market from /api/v1/public/exchange-info"
  );
}

const payload: LocalActionPayload = {
  account,
  nonce: 4810,
  ts: Date.now(),
  action: {
    kind: "SpotPlaceOrder",
    market: marketId,
    side: "Bid",
    price: 998400,
    qty: 1000,
    timeInForce: "post_only"
  }
};

// Fetch once at startup and cache. Do not call /actions/hash per order.
const chainBinding = (await client.orderEntry.getActionChainBinding()).data;
const signedAction = signAction(payload, privateKey, { chainBinding });

const accepted = await client.trading.submitSignedAction(signedAction, {
  idempotencyKey: "client-order-1"
});

console.log({
  signingHash: actionSigningHashHex(payload, { chainBinding }),
  derivedOrderId: deriveOrderIdHex(payload),
  accepted: accepted.data
});

If you use nonce reservations, include the returned token in the payload before signing:

const reserved = await client.trading.reserveNonce(account, {
  count: 1,
  ttlMs: 30_000,
  idempotencyKey: "nonce-1"
});

const reservedPayload: LocalActionPayload = {
  account,
  nonce: Number((reserved.data as { startNonce: number }).startNonce),
  nonceReservationId: (reserved.data as { reservationId: string }).reservationId,
  ts: Date.now(),
  action: {
    kind: "SpotPlaceOrder",
    market: marketId,
    side: "Bid",
    price: 998400,
    qty: 1000,
    timeInForce: "post_only"
  }
};

Institutional Order Entry

The native BSL path for latency-sensitive market makers is Direct TCP/TLS. Read GET /api/v1/bsl/connectivity first and use its bslTcp.host, bslTcp.port, bslTcp.tlsSni, frame version, and message-kind values. This is not an HTTPS URL and should not be sent through Cloudflare.

The SDK includes a session state machine plus a high-level order encoder. Use the connectivity bundle once at startup to extract the chain-binding domain, compact account locality hint, compact market mapping, and BSL gateway id. Account identity is always the verified SignedAction.account or registered session-key account; the target shard normalizes its local dense index before apply:

Heartbeat is mandatory. After the handshake, send session.encodeClientHeartbeat(process.hrtime.bigint()) at least once per negotiated session.heartbeatMs. The gateway closes a silent connection after session.heartbeatTimeoutMs (three missed intervals). Run this timer independently from the quote-refresh loop so an idle strategy still keeps its session alive.

import {
  bslCompactRoutingFromConnectivityBundle,
  encodeBslOrderFromSignedAction,
  signAction,
  type LocalActionPayload
} from "@sentico-labs/sdk";

const bundle = await client.orderEntry.getConnectivityBundle();
const routing = bslCompactRoutingFromConnectivityBundle(bundle.data);

let gatewaySeq = 1n;
const sessionId = BigInt(Date.now()) * 1_000_000n;
const payload: LocalActionPayload = {
  account,
  nonce,
  ts: Date.now(),
  action: {
    kind: "SpotPlaceOrder",
    market: marketId,
    side: "Bid",
    price: 998400,
    qty: 1000,
    timeInForce: "post_only"
  }
};

const signedAction = signAction(payload, privateKey, {
  chainBinding: routing.chainBinding
});

const encoded = encodeBslOrderFromSignedAction(signedAction, {
  ...routing,
  accountIdx: routing.accountIdx,
  sessionId,
  gatewaySeq
});

for (const message of encoded.tcpMessages) {
  socket.write(message);
}
gatewaySeq = encoded.nextGatewaySeq;

encoded.tcpMessages is ready to write to the TLS socket. Single place actions produce AuthSidecar then CompactActionFrame; QuoteReplace and SpotQuoteReplace actions produce one CompactFrameGroup.

Use QuoteReplace/SpotQuoteReplace for production cancel/replace loops over Direct TCP. Standalone Cancel and AmendOrder actions only carry order_id, while the live Direct TCP gateway routes compact frames by market index. The SDK therefore rejects standalone cancel/amend encoding by default; pass allowStandaloneCancelAmend: true only for diagnostics against a gateway that explicitly supports order-id based routing. For production standalone cancel/amend, use HTTP, FIX 4.4, or FIXP/SBE.

If the gateway returns a sequenced reject like {"type":"reject","reason":202,"detail":"backpressure"}, the frame decoded and reached the Direct TCP lane, but that submit attempt was not accepted/applied. Back off, widen, refresh state, and reconcile open orders before resubmitting; do not blindly retry the same signed action.

A CoreAck is admission, not book truth. Persist its sequencer sequence, then consume the private execution stream or query /api/v1/bsl/receipts?account={account}&seqs={sequence} until the action is terminal. A dropped terminal receipt carries the engine reason; a successful atomic QuoteReplace is all-or-nothing, so a ladder cannot be left half-replaced. Do not infer resting state from the public orderbook projection.

Private execution reports are capability-gated and disabled by default. New clients should construct new BslTcpSession(gatewayId, { privateExecutionReportVersion: 2 }). V2 negotiates the highest common version and falls back to V1 when necessary. Each V2 trade is one atomic executionReportV2 event with fill_lifecycle; persist its cursor.sourceSequence only after processing the whole event. orderQty and cumQty are absent together for legacy orders, while leavesQty and ordStatus remain authoritative. The legacy privateExecutionReports: true option explicitly requests wire-compatible V1.

GET /api/v1/readiness exposes sessionState and cancelOnlyAvailable. In cancel_only, new quantity is rejected while standalone cancels and zero-quantity QuoteReplace cleanup remain available. Refetch /api/v1/bsl/connectivity after every reconnect for current routing metadata. compactAccountIdx is explicitly non-authoritative: a stale hint cannot select another account or block an otherwise valid signed order. Use client.orderEntry.getCompactAccountMapping() (the authenticated GET /api/v1/bsl/account-mapping endpoint) to inspect primary/shard mappings; mappingConsistent=false is an operational repair signal, while admissionSafeWhenMappingDiverges=true confirms that the signed identity is normalized before apply.

The low-level wrappers encodeBslTcpAuthSidecar, encodeBslTcpCompactActionFrame, and encodeBslTcpCompactFrameGroup only wrap already prepared bytes with the BSL TCP kind,len header. They do not build the 192-byte compact frame or the cold AuthSidecar.

Session-Key Fast Lane (Ed25519)

Per-order wallet ECDSA signing costs ~1.8 ms client-side. The session-key lane replaces it with a one-time EIP-712 wallet delegation to a locally generated Ed25519 key, then ~0.05 ms Ed25519 signs per order:

import {
  SESSION_DELEGATION_SCHEME_EIP712_V2,
  WALLET_ADMIN_ACTION_REGISTER_SESSION_KEY,
  createSessionKey,
  encodeBslSessionOrder,
  signSessionDelegationEip712,
  signWalletAdminAuth
} from "@sentico-labs/sdk";

// 1) Local Ed25519 session key. sessionKeyId is blake3-derived and must match
//    the venue's registry id (asserted by the SDK tests).
const sessionKey = createSessionKey();

// 2) One-time registration: EIP-712 SessionDelegationV2 signed by the owner
//    wallet, wrapped in a short-lived typed wallet-admin auth.
const delegationSignature = signSessionDelegationEip712(delegationInput, walletKey);
const walletAuth = signWalletAdminAuth(adminInput, WALLET_ADMIN_ACTION_REGISTER_SESSION_KEY, walletKey);
const registered = await client.orderEntry.registerSessionKey({
  walletAuth,
  accountIdHex, // 32-byte trading account id
  sessionPublicKey: sessionKey.publicKey,
  policy: { allowedMarkets: [7], allowedActions: ["spot_place", "spot_quote_replace"] },
  validFrom: Date.now(),
  expiresAt: Date.now() + 12 * 60 * 60 * 1000,
  delegationNonce: "unique-nonce-1",
  delegationScheme: SESSION_DELEGATION_SCHEME_EIP712_V2,
  delegationSignature
});

// 3) Per order: no wallet key involved. sessionSeq is strictly increasing per
//    session key (persist it across restarts like a FIX MsgSeqNum).
const encoded = encodeBslSessionOrder(payload, {
  ...routing,
  accountIdx: routing.accountIdx,
  sessionId,
  gatewaySeq,
  sessionPrivateKey: sessionKey.privateKey,
  sessionKeyId: sessionKey.sessionKeyId,
  sessionSeq,
  policyHash: (registered.data as { policyHash: `0x${string}` }).policyHash
});
for (const message of encoded.tcpMessages) socket.write(message);

Sign the delegation and register with the same policy object; the SDK normalizes it identically on both sides (normalizedSessionKeyPolicy), and the venue recomputes the policy hash from the registered JSON. Always put the venue-returned policyHash into per-order options. The wallet keeps custody: delegation scope/lifetime is policy-bounded (24 h max), keys are revocable via client.orderEntry.revokeSessionKey, and the venue enforces market/action/qty policy plus a per-key replay window before persist.

Runnable end-to-end example (env vars documented in the file header):

npm run build && node dist/examples/bsl-tcp-session-key.js

Phase-3 shadow validation is also supported: keep wallet ECDSA as the apply authority and attach sessionKeyShadow to encodeBslOrderFromSignedAction so the sequencer records whether the same frame would pass session-key auth.

Runnable handshake example:

SENTICORE_BSL_TCP_HOST=bsl.sentico-labs.xyz \
SENTICORE_BSL_TCP_PORT=9001 \
SENTICORE_BSL_TCP_TLS_SNI=bsl.sentico-labs.xyz \
npm run build && node dist/examples/bsl-tcp-handshake.js

The HTTP compact order-entry path remains available for compatibility and backfill-friendly integration. It uses compact JSON bytes on the wire:

{"version":1,"actions":[...],"idempotencyKey":"..."}

The SDK sends this payload with Content-Type: application/x-senticore-order-entry-batch and X-Senticore-Order-Entry-Key.

The canonical private-beta HTTP endpoint is POST /api/order-entry/binary. The live edge maps that alias to the trading plane's /api/v1/mm/orders/batch.bin handler. If you connect directly to a trading-plane service without the edge alias, set orderEntryBinaryPath: "/api/v1/mm/orders/batch.bin".

const response = await client.orderEntry.submitActions([signedAction], {
  idempotencyKey: "order-entry-batch-1",
  responseMode: "summary"
});

console.log(response.data.ok, response.data.seqs, response.data.ackMode);

Low-latency clients that already produce venue payload bytes can call:

await client.orderEntry.submitEncoded(encodedPayload, {
  idempotencyKey: "order-entry-raw-1"
});

Reconcile ack responses through receipts or execution gap-fill instead of polling bootstrap in a hot loop:

const receipts = await client.orderEntry.getActionReceipts(account, response.data.seqs, {
  waitMs: 250
});

const replay = await client.orderEntry.getAccountExecutions(account, {
  fromSeq: response.data.lastSeq ?? response.data.seqs.at(-1)
});

FIX Logon

Create an institutional_agent HMAC credential, then read GET /api/v1/bsl/connectivity for the direct FIX endpoint and CompIDs. The SDK includes helpers for the Logon frame:

import {
  buildFixLogonFields,
  deriveFixSenderCompId,
  encodeFixFrame
} from "@sentico-labs/sdk";

const senderCompId = deriveFixSenderCompId(account, agentId, "order_entry");
const frame = encodeFixFrame(buildFixLogonFields({
  account,
  apiKeyId,
  apiSecret,
  apiPassphrase,
  senderCompId,
  targetCompId: "SENTICORE",
  targetSubId: "order_entry"
}));

Runnable example:

SENTICORE_ACCOUNT=0x... \
SENTICORE_AGENT_ID=agt_... \
SENTICORE_API_KEY_ID=spk_... \
SENTICORE_API_SECRET=... \
SENTICORE_API_PASSPHRASE=... \
npm run build && node dist/examples/fix-logon.js

FIXP / SBE

FIXP is raw TCP/TLS on the direct FIXP host, not HTTPS. Read GET /api/v1/fixp/connectivity; the published SBE schema is available at GET /api/v1/fixp/schema and in the bundle as fixp.wire.schemaUrl.

The SDK exposes the wire codec and a small sans-I/O session helper:

import {
  FixpSession,
  FixpSide,
  decodeFixpFrame,
  encodeFixpNewOrderSingle
} from "@sentico-labs/sdk";

const session = new FixpSession({
  apiKeyId,
  apiSecret,
  apiPassphrase,
  account,
});

socket.write(Buffer.concat(session.start())); // Negotiate + Establish

socket.write(encodeFixpNewOrderSingle({
  clOrdId: "mm-1",
  account,
  marketId: 1,
  price: 1_000_000,
  orderQty: 1,
  side: FixpSide.Bid,
}));

const message = decodeFixpFrame(frameFromSocket);

WebSocket

const socket = client.ws.connectPublic((frame) => {
  console.log(frame);
});

socket.addEventListener("open", () => {
  client.ws.subscribe(socket, {
    type: "l2Book",
    marketId: 1,
    depth: 20
  });
});

Private user streams:

const token = await client.trading.issuePrivateWsToken(account, {
  ttlMs: 60_000
});

const socket = client.ws.connectPrivate(account, token.data.token, (frame) => {
  console.log(frame);
});

The calls above are intentionally raw and remain suitable for diagnostics. Strategy processes should use the additive managed stream, which authenticates before subscribing, commits a cursor per private channel after successful strategy processing, suppresses duplicates, fills recoverable gaps with resend, follows the server's proactive rotation journal, refreshes tokens on every connection, and reconnects with bounded exponential full jitter:

const cursors = {
  async load(account: string, channel: string) {
    return cursorDatabase.get(`${account}:${channel}`);
  },
  async save(account: string, channel: string, cursor: number) {
    await cursorDatabase.put(`${account}:${channel}`, cursor);
  }
};

const privateStream = client.ws.reliablePrivate({
  account,
  tokenProvider: async () =>
    (await client.trading.issuePrivateWsToken(account, { ttlMs: 60_000 })).data,
  subscriptions: [
    { type: "orderUpdates" },
    { type: "userFills" },
    { type: "dropCopy" }
  ],
  cursorStore: cursors,
  onEvent: async (frame, { channel, seq }) => {
    // The stream commits seq after this succeeds. Make the strategy operation
    // idempotent by `${channel}:${seq}` so a crash before that commit is safe.
    await strategy.applyIdempotent(`${channel}:${seq}`, frame.data);
  },
  onResyncRequired: async ({ channel, oldestAvailableSeq }) => {
    // Stop the strategy and rebuild it from authoritative HTTP snapshots.
    console.error("private stream resync required", channel, oldestAvailableSeq);
  },
  onError: (error) => console.error(error.code, error.message)
});

await privateStream.start();
// During shutdown:
await privateStream.close();

start() resolves only after auth, every subscribe ACK, and every channel's initial snapshot_end; no strategy is declared live while catch-up is pending. resume_required, an unavailable gap_fill, cursor persistence failure, or a strategy callback failure stops the managed stream fail-closed. Construct a new managed stream only after authoritative snapshot reconciliation. Raw parseFrame continues to raise ReplayGapError for replay-gap frames.

Delivery is intentionally at-least-once. Cursor commits happen after the strategy callback so a process loss cannot silently skip a fill. A crash after the callback but before the cursor commit can redeliver the same event; use channel + seq as the idempotency key for all strategy side effects.

Error Handling

import { RateLimitError, SenticoreApiError } from "@sentico-labs/sdk";

try {
  await client.public.listMarkets();
} catch (error) {
  if (error instanceof RateLimitError) {
    console.log("retry after", error.retryAfter);
  } else if (error instanceof SenticoreApiError) {
    console.log(error.status, error.requestId, error.payload);
  }
}

Release Compatibility

0.2.0 is the current release. It is still a pre-1.0 SDK, so minor releases may refine beta-only surfaces before public mainnet. The stable namespace split is public, trading, orderEntry, raw, and ws. mm remains as a deprecated compatibility alias. The market-feed client (MarketFeedBookMirror, subscribeMarketFeedChannel) is exported at the top level.

The version:check script fails the build if the README advertises a version that differs from package.json — version claims here are verified, not maintained by hand.