@sentico-labs/sdk
v0.1.4
Published
Institutional TypeScript SDK for Senticore HTTP, WebSocket, and Order Entry.
Readme
Senticore TypeScript SDK
Official TypeScript SDK for Senticore HTTP, WebSocket, and Institutional Order Entry.
Package name: @sentico-labs/sdk
Current version: 0.1.1
Install
npm install @sentico-labs/[email protected]Use 0.1.1 or newer for BSL Direct TCP order encoding. 0.1.0 includes the
first high-level encoder; preview packages such as 0.1.0-preview.4 include
the BSL TCP handshake/session helpers but not encodeBslOrderFromSignedAction.
For local development from the repository:
npm ci
npm run verifyQuick 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 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 });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 payload: LocalActionPayload = {
account,
nonce: 4810,
ts: Date.now(),
action: {
kind: "SpotPlaceOrder",
market: 7,
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: 7,
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 index, compact market mapping, and BSL gateway id:
import {
bslCompactRoutingFromConnectivityBundle,
encodeBslOrderFromSignedAction,
signAction,
type LocalActionPayload
} from "@sentico-labs/sdk";
const bundle = await client.orderEntry.getConnectivityBundle();
const routing = bslCompactRoutingFromConnectivityBundle(bundle.data);
if (routing.accountIdx === undefined) {
throw new Error("compactAccountIdx is not ready for this account");
}
let gatewaySeq = 1n;
const sessionId = BigInt(Date.now()) * 1_000_000n;
const payload: LocalActionPayload = {
account,
nonce,
ts: Date.now(),
action: {
kind: "SpotPlaceOrder",
market: 7,
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.
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);
});Replay gaps raise ReplayGapError during frame parsing. Production clients
should resync from an HTTP snapshot and resubscribe from a known sequence.
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.1.1 is the recommended public npm release for BSL Direct TCP order encoding.
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.
