@sentico-labs/sdk
v0.3.0
Published
Institutional TypeScript SDK for Senticore HTTP, WebSocket, and Order Entry.
Maintainers
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 verifyThe 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 forclient.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=jsonis sent explicitly. The server default isbinary, which is the same JSON delivered in a binary WebSocket frame; a client that omitsencodingreceives frames it may not parse.parseMarketFeedFramedecodes both encodings if you build a channel yourself.depth=100is sent explicitly. Omittingdepthyields 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.jsPhase-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.jsThe 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.jsFIXP / 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.
