Skip to main content

BSL Direct TCP

BSL Direct TCP is the native low-latency order-entry path for market makers and professional quote engines. It is a persistent TCP/TLS session to the Senticore gateway on Hetzner or a dedicated direct host. It is not an HTTPS endpoint and does not run through Cloudflare.

The wire is a recoverable session (protocol v2): server acknowledgements are sequenced, both sides heartbeat, and a dropped connection can be resumed without losing acks. Use it when HTTP request/response overhead, Cloudflare edge variance, or per-request connection setup is visible in your quoting loop.

Discover the endpoint

Start with the connectivity bundle:

GET /api/v1/bsl/connectivity

Use the returned bslTcp object:

{
"bslTcp": {
"enabled": true,
"transport": "tcp_tls",
"host": "bsl.sentico-labs.xyz",
"port": 9001,
"tlsSni": "bsl.sentico-labs.xyz",
"protocol": "senticore-bsl-tcp-v2",
"gatewayId": 1,
"handshakeBytes": 48,
"messageHeaderBytes": 8,
"compactActionFrameBytes": 192,
"sessionVersion": 2,
"orderEncoding": {
"sdkFunction": "encodeBslOrderFromSignedAction",
"coldPayloadCodec": "serde_json",
"frameCodec": "senticore_compact_action_frame_v1",
"frameVersion": 1,
"mappingVersion": 1,
"gatewayId": 1,
"account": {
"account": "0x...",
"compactAccountIdx": 55,
"ready": true
},
"markets": [
{
"marketId": 7,
"compactMarketIdx": 3,
"symbol": "SPOT:ETH/USDC",
"displaySymbol": "ETH/USDC",
"instrumentType": "spot"
}
]
}
}
}

bsl.sentico-labs.xyz is a DNS-only record pointing directly at the Senticore gateway. Do not substitute https://api.sentico-labs.xyz for this field; HTTPS cannot carry the native TCP frames.

Session flow

  1. Open a TCP connection to bslTcp.host:bslTcp.port.
  2. Start TLS with SNI bslTcp.tlsSni.
  3. Set TCP_NODELAY.
  4. Send a 48-byte HandshakeHello offering session version 2. Bytes 36..40 carry your requested heartbeat cadence (ms, little-endian; 0 = let the gateway choose).
  5. Read the 48-byte HandshakeAck. It carries the negotiated session version (bytes 8..10), a 16-byte CSPRNG session token (bytes 20..36, used for resume), and the granted heartbeat cadence (bytes 36..40). A client that offers only the retired v1 receives a HandshakeReject.
  6. Send framed messages with an 8-byte little-endian message header:
u32_le kind
u32_le payload_len
payload bytes

The first production message is usually an AuthSidecar followed by a compact action frame. The compact single-order frame size is 192 bytes.

Authentication and order submission

BSL Direct TCP has two separate layers:

  • Transport/session layer: TCP/TLS, handshake, heartbeat, session token, sequence tracking, resend, and resume.
  • Order-auth layer: every external compact order is still authorized. The gateway requires an AuthSidecar for each single CompactActionFrame, or one sidecar per leg inside a CompactFrameGroup. Public BSL supports the legacy wallet-signed sidecar and the Ed25519 session-key sidecar when the venue enables session-key auth.

Do not treat AuthSidecar as a once-per-connection login. For a single action, send exactly this order:

HandshakeHello # 48 bytes
HandshakeAck # 48 bytes
AuthSidecar # kind=4, payload=cold-encoded AuthSidecar
CompactActionFrame # kind=1, payload=192-byte hot frame
SequencedData(CoreAck|GatewayReject) # kind=17 from gateway

The gateway stores only one pending sidecar on the session. If you send two AuthSidecar messages before a frame, the second one replaces the first. For an atomic cancel/place or multi-leg group, do not send loose sidecar messages: the CompactFrameGroup payload must contain each frame_bytes plus its matching sidecar.

The legacy wallet-signed AuthSidecarV1 object has this logical shape:

auth_binding 32 bytes blake3(signature || nonce_le_u64 || action_hash_v2)
signature bytes secp256k1 signature from the SignedAction
signer_id 20 bytes EVM address of the authorized account signer/agent
payload_bytes bytes cold-encoded SignedAction; mandatory for V1

action_hash_v2 is the same chain-bound action-signing hash used by HTTP order-entry: domain SENTICORE/ACTION_PAYLOAD/v2, the Arbitrum chainId, the configured verifying contract from the connectivity bundle, and the canonical action payload. The compact frame copies the same auth_binding into its auth_binding field. The sequencer re-decodes payload_bytes, verifies the secp256k1 signature, confirms the signer is authorized for the account, checks that the signed action matches the compact frame, and rejects stale mapping versions, identity changes, replayed nonces, and rate-limit violations.

The latency rule is: fetch actionSigning.chainId and actionSigning.verifyingContract once from the connectivity bundle, then compute action_hash_v2 locally for every quote. Do not call POST /api/v1/trading/actions/hash in the BSL hot path. That endpoint is useful for integration debugging and vector comparison, but using it per order adds a serial HTTP round trip before the TCP submit. Direct TCP order entry stays one-way in the steady state:

startup:
GET /api/v1/bsl/connectivity
cache actionSigning chain binding

per order:
local actionSigningHashHex(payload, { chainBinding })
local signAction(payload, key, { chainBinding })
local encodeBslOrderFromSignedAction(signedAction, routing)
TCP write encoded.tcpMessages

This is not a trust shortcut. The sequencer recomputes the same chain-bound hash from payload_bytes, recovers the signer, and rejects the order if local and server interpretation drift.

For a production quote engine, build the action once and derive these artifacts from the same source object:

  1. Canonical SignedAction with v2 chain binding.
  2. 192-byte CompactActionFrame for the hot path.
  3. Cold-encoded AuthSidecar whose payload_bytes contains the signed action.
  4. BSL TCP messages via encodeBslOrderFromSignedAction(signedAction, routing).

Session-key hot path

For lower local signing latency, use BSL Session Keys. The owner wallet signs one short-lived EIP-712 delegation, then the quote engine signs each generated compact frame with an Ed25519 session key:

startup:
GET /api/v1/bsl/connectivity
create Ed25519 session key
wallet-sign SessionDelegationV2 with actionSigning chainId/verifyingContract
POST /api/v1/bsl/session-keys/register
open persistent BSL TCP session

per order:
local encodeBslSessionOrder(payload, routing + session key)
TCP write encoded.tcpMessages

Session-key rollout has two forms. During Phase 3 shadow testing, a legacy wallet-signed AuthSidecarV1 may include optional sessionKeyShadow fields. Those fields are verified and logged only; the wallet signature remains the apply authority and session-key sequence state is not mutated.

The real session-key hot path uses AuthSidecarV2:

auth_scheme "senticore_session_key_v1"
algorithm "ed25519"
session_key_id 32 bytes
session_seq u64, monotone per session key
policy_hash 32 bytes from registered policy JSON
order_hash 32 bytes, chain-bound and policy-bound compact frame hash
signature 64 bytes Ed25519 over order_hash
auth_binding 32 bytes copied into frame.auth_binding
payload_bytes null

The compact frame keeps its existing payload_hash/order-id semantics. Session auth adds a separate order_hash; it does not overwrite the order id. The sequencer resolves session_key_id, checks the owner account, expiry, revocation, policy hash, allowed market/action, qty/notional limits, gateway id, monotone session_seq, and Ed25519 signature before the frame can apply. If the registered policy sets cancelOnDisconnect=true, every admitted place leg must carry a derived order id. The gateway reports TCP teardown with a SessionDisconnect control notice, and the sequencer cancels only still-live orders tracked for that session key.

Use encodeBslSessionOrder from @sentico-labs/sdk@0.1.4 or newer. The server side is controlled by:

BSL_SESSION_KEY_AUTH_SHADOW_ENABLED=true
BSL_SESSION_KEY_AUTH_ENABLED=true
BSL_SESSION_KEY_CANARY_ACCOUNTS=

BSL_SESSION_KEY_AUTH_ENABLED=true enables real AuthSidecarV2 verification. BSL_SESSION_KEY_CANARY_ACCOUNTS is optional. Leave it empty for general access by all active registered session-key accounts; set it to a comma-separated list only when operations wants a temporary account allowlist. The old encodeBslOrderFromSignedAction wallet-signed path remains the stable fallback.

Client production flow:

  1. Fetch /api/v1/bsl/connectivity.
  2. Derive compact routing with bslCompactRoutingFromConnectivityBundle.
  3. Register an Ed25519 session key with a tight policy.
  4. Keep one persistent TCP/TLS session per quote-engine shard.
  5. Encode place, quote-replace, cancel, or amend with encodeBslSessionOrder.
  6. Write encoded.tcpMessages directly to the socket.
  7. Reconcile fills and cancels through private streams, gap-fill, drop-copy, or account/order reads.

Use shadow mode before first account rollout: send wallet-signed orders with sessionKeyShadow while BSL_SESSION_KEY_AUTH_SHADOW_ENABLED=true, then switch that account to real session-key frames once shadow rejects stay flat.

CompactActionFrame.session_id is a nonzero client/session identifier used by the gateway and sequencer for replay protection and rate scopes. It is separate from the 16-byte resume token returned by the handshake. Use a fresh random or time-derived u64 for each new TCP order-entry session and keep gateway_seq strictly increasing inside that session. QuoteReplace groups consume one gateway_seq per generated frame.

Keep one account/signer/session identity per TCP session. If you need to quote for multiple accounts or signer identities, open separate sessions so session pinning, rate limits, and replay windows stay clean.

Message kinds

KindValueDirectionUse
CompactActionFrame1Client -> gatewayOne 192-byte action frame.
CoreAck2Gateway -> clientAction/batch acknowledgement (sequenced).
GatewayReject3Gateway -> clientFast ingress rejection (sequenced).
AuthSidecar4Client -> gatewayCold signature and signed-action material for the next frame.
CompactFrameGroup10Client -> gatewayAtomic group; every item carries its own sidecar.
CoreAckBatch11Gateway -> clientBatch acknowledgement (sequenced).
ClientHeartbeat12Client -> gatewayLiveness (8-byte u64 send timestamp).
ServerHeartbeat13Gateway -> clientLiveness on the granted cadence.
ResendRequest14Client -> gatewayReplay sequenced messages from a sequence.
SessionResume15Client -> gatewayResume a prior session after reconnect.
SessionResumeResult16Gateway -> clientResume outcome + continue sequence.
SequencedData17Gateway -> clientEnvelope wrapping a per-session sequenced message.
EndOfSession18Gateway -> clientGraceful end-of-session marker.

Sequenced responses, gap-fill, and recovery

Every server-to-client acknowledgement is delivered inside a SequencedData envelope. The envelope payload is:

u64_le session_seq # per-session, monotone, starts at 1
u32_le inner_kind # e.g. CoreAck=2, GatewayReject=3, CoreAckBatch=11
inner payload bytes # Senticore cold codec (JSON in beta)
  • Track session_seq. If it skips (got > expected), you have a gap: send a ResendRequest with from_session_seq = expected. The gateway replays the retained tail verbatim.
  • Heartbeat on the granted cadence. If the peer goes silent for more than three intervals, treat the session as dead and reconnect.
  • After reconnecting and completing a fresh handshake, send SessionResume with the previous session token and the highest session_seq you processed. The gateway replies with SessionResumeResult (code = 0 = accepted) and replays everything after that sequence. If the sequence has aged out of the retransmit window (code = 2), re-snapshot your open orders via REST/FIX order status instead of gap-filling.

The SDKs implement this state machine for you (see below).

Common rejects and fixes

Reject familyUsual causeFix
missing_sidecar / AuthFailedSent CompactActionFrame without kind 4 immediately before it.Send AuthSidecar then the matching frame, or use CompactFrameGroup with embedded sidecars.
sidecar_binding_mismatchframe.auth_binding does not equal sidecar.auth_binding.Build frame and sidecar from the same signed action.
signed_action_binding_mismatchSidecar binding was computed from the wrong action hash, chain id, verifying contract, nonce, or signature.Read actionSigning from the connectivity bundle and use SDK v2 signing helpers.
payload_missing / payload_decode_failedpayload_bytes is missing or not cold-encoded SignedAction.Include the signed action bytes; do not JSON-stringify the sidecar.
signer_id_mismatchsigner_id is not the recovered 20-byte EVM signer.Use the authorized wallet/agent address as raw 20-byte bytes.
stale compact mapping versionClient encoded against an old compact market mapping.Refresh /api/v1/bsl/connectivity and market metadata before reconnecting.
MarketUnknown on standalone Cancel/AmendOrderDirect TCP compact routing needs a market index, while the signed cancel/amend action only carries order_id.Pass routingMarket to the SDK from your local order store/drop-copy. If local order tracking is missing, use HTTP/FIX/FIXP or cancel-all recovery.
SessionViolationA session switched account, signer, or compact session identity.Use one TCP session per account/signer identity.

Current live order-entry support

The high-level SDK encoder builds real Direct TCP order messages. The Direct TCP surface now supports the core institutional order lifecycle; actions that do not carry a market id need explicit routing context.

Supported for live Direct TCP order entry:

  • SpotPlaceOrder and OutcomePlaceOrder.
  • SpotQuoteReplace and QuoteReplace atomic groups. These are the preferred market-maker cancel/replace path because the action carries the market id and every generated compact frame carries the compact market index.
  • Standalone Cancel and AmendOrder when the client passes routingMarket, the market id of the referenced order.

Standalone Cancel and AmendOrder signed actions contain only order_id; Direct TCP routes before apply, so the client must provide the referenced order's market as routingMarket. The sequencer treats that value as a routing hint, not as authority: if the order is already known on another shard or the engine snapshot shows a different market, the frame is rejected before persist. For session-key Direct TCP, market-scoped cancel and amend policies add one more check: the referenced order must be live, belong to the compact account, and match the routed market before the frame is admitted.

Use one of these when you cannot supply a reliable routing market:

  • QuoteReplace/SpotQuoteReplace for quote refreshes where the market is in the action.
  • HTTP BSL, FIX 4.4, or FIXP/SBE when your local order store/drop-copy cannot provide the referenced order's market.

@sentico-labs/sdk@0.1.4 and newer guard standalone Direct TCP cancel/amend by default unless routingMarket is provided. The older allowStandaloneCancelAmend option remains diagnostic-only and leaves the compact market route unset.

Backpressure

The gateway uses bounded internal queues into the sequencing core. When a market shard or IPC lane cannot accept more work, the correct behavior is a fast GatewayReject rather than silently aging quotes in a hidden queue.

Important reject families:

CodeMeaning
SHARD_BUSY / BackpressureFullTarget market shard or queue is saturated.
RateLimitExceededSession/account/api-key rate limit hit.
IdempotencyReplayDuplicate idempotency key within the replay window.
AuthFailedSession is missing required institutional credential state.
SessionViolationSession-level protocol violation (identity/sequence/mapping).
NonceReuseWindowed-nonce replay rejected by the engine.
MarketUnknownMarket routing failed.

For market makers, a fast reject is usable signal. Treat it as a reason to widen, pause, or reconnect instead of retrying blindly. The session stays open across rejects.

Example live backpressure reject:

{"type":"reject","reason":202,"detail":"backpressure"}

This means the message decoded, authenticated far enough to be rejected by the lane, and received a sequenced gateway response, but the order was not accepted or applied. Pause or reduce pressure, refresh the connectivity bundle and open orders if needed, then submit a newly signed action if the strategy still wants the quote.

SDK session clients

The TypeScript SDK ships two pieces you should use together:

  • BslTcpSession: transport-free state machine for handshake, sequence tracking, gap detection, heartbeat cadence, and resume. You own the socket.
  • encodeBslOrderFromSignedAction: high-level order encoder that turns a canonical SignedAction plus compact routing context into executable BSL TCP messages.

Use @sentico-labs/sdk@0.1.4 or newer, or the current repository build. Older preview packages such as 0.1.0-preview.4 only prove handshake/session connectivity; they do not build live order frames.

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

Minimal order-encoding flow:

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

if (routing.accountIdx === undefined) {
throw new Error("account has no compactAccountIdx yet; submit through HTTP BSL first");
}

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;

For a single place/cancel/amend action, encoded.tcpMessages contains AuthSidecar followed by CompactActionFrame. For QuoteReplace/SpotQuoteReplace, it contains one CompactFrameGroup.

Standalone cancel/amend example:

const encodedCancel = encodeBslOrderFromSignedAction(signedCancelAction, {
...routing,
accountIdx: routing.accountIdx!,
sessionId,
gatewaySeq,
routingMarket: 7
});

The low-level helpers remain available for diagnostics and custom encoders:

import {
encodeBslTcpAuthSidecar,
encodeBslTcpCompactActionFrame,
encodeBslTcpCompactFrameGroup
} from "@sentico-labs/sdk";

Those functions only wrap already prepared bytes with the BSL TCP kind,len header. They do not construct AuthSidecar, SignedAction.payload_bytes, or the 192-byte CompactActionFrame.

TypeScript:

cd sdks/typescript
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

Python:

cd sdks/python
python -m pip install -e .
SENTICORE_BSL_TCP_HOST=bsl.sentico-labs.xyz \
SENTICORE_BSL_TCP_PORT=9001 \
SENTICORE_BSL_TCP_TLS_SNI=bsl.sentico-labs.xyz \
python examples/bsl_tcp_handshake.py

TLS posture

The gateway terminates TLS with a public CA certificate for bsl.sentico-labs.xyz; verify the certificate chain against your system trust store and SNI bslTcp.tlsSni. For dedicated clients, mutual TLS (a provisioned client CA) and IP allowlisting are available. Always keep certificate verification on in production.

HTTP compatibility

Compact HTTP order entry remains available:

POST /api/order-entry/binary
POST /api/v1/bsl/orders/compact

Use HTTP for onboarding, admin tools, backfill-friendly bots, and compatibility. Use Direct TCP for latency-sensitive market making.