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
- Open a TCP connection to
bslTcp.host:bslTcp.port. - Start TLS with SNI
bslTcp.tlsSni. - Set
TCP_NODELAY. - Send a 48-byte
HandshakeHellooffering session version2. Bytes36..40carry your requested heartbeat cadence (ms, little-endian;0= let the gateway choose). - Read the 48-byte
HandshakeAck. It carries the negotiated session version (bytes8..10), a 16-byte CSPRNG session token (bytes20..36, used for resume), and the granted heartbeat cadence (bytes36..40). A client that offers only the retired v1 receives aHandshakeReject. - 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
AuthSidecarfor each singleCompactActionFrame, or one sidecar per leg inside aCompactFrameGroup. 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:
- Canonical
SignedActionwith v2 chain binding. - 192-byte
CompactActionFramefor the hot path. - Cold-encoded
AuthSidecarwhosepayload_bytescontains the signed action. - 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:
- Fetch
/api/v1/bsl/connectivity. - Derive compact routing with
bslCompactRoutingFromConnectivityBundle. - Register an Ed25519 session key with a tight policy.
- Keep one persistent TCP/TLS session per quote-engine shard.
- Encode place, quote-replace, cancel, or amend with
encodeBslSessionOrder. - Write
encoded.tcpMessagesdirectly to the socket. - 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
| Kind | Value | Direction | Use |
|---|---|---|---|
CompactActionFrame | 1 | Client -> gateway | One 192-byte action frame. |
CoreAck | 2 | Gateway -> client | Action/batch acknowledgement (sequenced). |
GatewayReject | 3 | Gateway -> client | Fast ingress rejection (sequenced). |
AuthSidecar | 4 | Client -> gateway | Cold signature and signed-action material for the next frame. |
CompactFrameGroup | 10 | Client -> gateway | Atomic group; every item carries its own sidecar. |
CoreAckBatch | 11 | Gateway -> client | Batch acknowledgement (sequenced). |
ClientHeartbeat | 12 | Client -> gateway | Liveness (8-byte u64 send timestamp). |
ServerHeartbeat | 13 | Gateway -> client | Liveness on the granted cadence. |
ResendRequest | 14 | Client -> gateway | Replay sequenced messages from a sequence. |
SessionResume | 15 | Client -> gateway | Resume a prior session after reconnect. |
SessionResumeResult | 16 | Gateway -> client | Resume outcome + continue sequence. |
SequencedData | 17 | Gateway -> client | Envelope wrapping a per-session sequenced message. |
EndOfSession | 18 | Gateway -> client | Graceful 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 aResendRequestwithfrom_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
SessionResumewith the previous session token and the highestsession_seqyou processed. The gateway replies withSessionResumeResult(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 family | Usual cause | Fix |
|---|---|---|
missing_sidecar / AuthFailed | Sent CompactActionFrame without kind 4 immediately before it. | Send AuthSidecar then the matching frame, or use CompactFrameGroup with embedded sidecars. |
sidecar_binding_mismatch | frame.auth_binding does not equal sidecar.auth_binding. | Build frame and sidecar from the same signed action. |
signed_action_binding_mismatch | Sidecar 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_failed | payload_bytes is missing or not cold-encoded SignedAction. | Include the signed action bytes; do not JSON-stringify the sidecar. |
signer_id_mismatch | signer_id is not the recovered 20-byte EVM signer. | Use the authorized wallet/agent address as raw 20-byte bytes. |
stale compact mapping version | Client encoded against an old compact market mapping. | Refresh /api/v1/bsl/connectivity and market metadata before reconnecting. |
MarketUnknown on standalone Cancel/AmendOrder | Direct 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. |
SessionViolation | A 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:
SpotPlaceOrderandOutcomePlaceOrder.SpotQuoteReplaceandQuoteReplaceatomic 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
CancelandAmendOrderwhen the client passesroutingMarket, 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/SpotQuoteReplacefor 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:
| Code | Meaning |
|---|---|
SHARD_BUSY / BackpressureFull | Target market shard or queue is saturated. |
RateLimitExceeded | Session/account/api-key rate limit hit. |
IdempotencyReplay | Duplicate idempotency key within the replay window. |
AuthFailed | Session is missing required institutional credential state. |
SessionViolation | Session-level protocol violation (identity/sequence/mapping). |
NonceReuse | Windowed-nonce replay rejected by the engine. |
MarketUnknown | Market 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 canonicalSignedActionplus 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.