Place Your First Institutional BSL Quote
This is the shortest operational path to one real institutional BSL quote. It assumes you already have a funded beta credential bundle. If your account is not funded, stop after the connectivity checks.
Environment
export API_BASE_URL="https://api.sentico-labs.xyz"
export ENGINE_ACCOUNT="0x2222222222222222222222222222222222222222"
export OWNER_WALLET="0x1111111111111111111111111111111111111111"
export MARKET_ID="7"
export BOOK="YES"
: "${API_KEY_ID:?set API_KEY_ID from your beta credential bundle}"
: "${API_SECRET:?set API_SECRET from your beta credential bundle}"
: "${API_PASSPHRASE:?set API_PASSPHRASE from your beta credential bundle}"
: "${ACTION_PRIVATE_KEY:?set ACTION_PRIVATE_KEY from your beta credential bundle}"
ENGINE_ACCOUNT must be the 20-byte engine account. Do not put a 32-byte
accountIdHex here. Do not set ACTION_NONCE for the first run unless you are
debugging a nonce conflict; the TypeScript example reads nonceFloor from a
fresh bootstrap and signs with that value.
1. HMAC helper for cURL
Save this as tmp/sc-hmac-v2.py if you want to run the cURL examples.
#!/usr/bin/env python3
import hashlib
import hmac
import os
import sys
import time
from urllib.parse import urlsplit
method = sys.argv[1].upper()
url = sys.argv[2]
body_path = sys.argv[3] if len(sys.argv) > 3 else ""
body = open(body_path, "rb").read() if body_path else b""
api_key = os.environ["API_KEY_ID"]
api_secret = os.environ["API_SECRET"].encode()
api_passphrase = os.environ["API_PASSPHRASE"]
nonce = os.environ.get("SC_NONCE", str(int(time.time() * 1000)))
timestamp = os.environ.get("SC_TIMESTAMP", str(int(time.time() * 1000)))
parts = urlsplit(url)
query_items = [part for part in parts.query.split("&") if part]
query = "&".join(sorted(query_items))
body_hash = hashlib.sha256(body).hexdigest()
signing = "\n".join([
"SENTICORE-HMAC-V2",
f"sc-key:{api_key}",
f"sc-nonce:{nonce}",
f"sc-timestamp:{timestamp}",
f"method:{method}",
f"path:{parts.path}",
f"query:{query}",
f"body-sha256:{body_hash}",
])
signature = "0x" + hmac.new(api_secret, signing.encode(), hashlib.sha256).hexdigest()
print(f"-H SC-Auth-Version: 2")
print(f"-H SC-Key: {api_key}")
print(f"-H SC-Nonce: {nonce}")
print(f"-H SC-Timestamp: {timestamp}")
print(f"-H SC-Passphrase: {api_passphrase}")
print(f"-H SC-Signature: {signature}")
The SDK golden vector for this signing string is:
SENTICORE-HMAC-V2
sc-key:spk_test
sc-nonce:7
sc-timestamp:123456
method:POST
path:/api/v1/private/account
query:a=1&b=2
body-sha256:08220911bbc1dfd55d93796c4d425a9582f94221975933bb2d98d269ab3c7b95
signature = 0x54ddc2c6f1eef6b75c317de7908c2e5c16a4e38561d75a147085cee9b0e6fb00
2. Fetch the connectivity bundle
URL="$API_BASE_URL/api/v1/bsl/connectivity"
eval curl -fsS "$(python3 tmp/sc-hmac-v2.py GET "$URL")" "$URL" | jq .
You need:
actionSigning.chainIdactionSigning.verifyingContract- BSL/FIX/FIXP hosts, ports, and SNI values if you use raw TCP lanes
3. Confirm the account is funded
URL="$API_BASE_URL/api/v1/accounts/$ENGINE_ACCOUNT/bootstrap?fresh=true"
eval curl -fsS "$(python3 tmp/sc-hmac-v2.py GET "$URL")" "$URL" \
| jq '{account, balanceSource, syncStatus, balances, risk}'
Required before sending a bid:
jq -r '.balances.freeUsdcMicro'
If freeUsdcMicro is 0, the first bid should fail with risk or insufficient
balance. Fix funding before debugging signing.
4. Submit one QuoteReplace in TypeScript
Install the SDK:
npm install @sentico-labs/sdk@latest
quote-once.ts:
import {
SenticoreClient,
deriveQuoteReplaceOrderIdHex,
signAction,
type LocalActionPayload,
} from "@sentico-labs/sdk";
import { mkdirSync, writeFileSync } from "node:fs";
const API_BASE_URL = process.env.API_BASE_URL ?? "https://api.sentico-labs.xyz";
const ENGINE_ACCOUNT = must("ENGINE_ACCOUNT");
const API_KEY_ID = must("API_KEY_ID");
const API_SECRET = must("API_SECRET");
const API_PASSPHRASE = must("API_PASSPHRASE");
const ACTION_PRIVATE_KEY = must("ACTION_PRIVATE_KEY");
const MARKET_ID = BigInt(process.env.MARKET_ID ?? "7");
const BOOK = (process.env.BOOK ?? "YES") as "YES" | "NO";
const client = new SenticoreClient({
publicHttpBaseUrl: API_BASE_URL,
tradingHttpBaseUrl: API_BASE_URL,
orderEntryHttpBaseUrl: API_BASE_URL,
orderEntryBinaryPath: "/api/order-entry/binary",
publicWsUrl: "wss://api.sentico-labs.xyz/api/v1/ws/public",
privateWsUrl: "wss://api.sentico-labs.xyz/api/v1/ws/private/{account}",
machineAuth: {
apiKeyId: API_KEY_ID,
apiSecret: API_SECRET,
apiPassphrase: API_PASSPHRASE,
nonce: () => Date.now(),
},
});
type Bootstrap = {
nonceFloor?: number | string;
nextNonce?: number | string;
balances?: { freeUsdcMicro?: number | string };
};
const bootstrap = (await client.raw.request<Bootstrap>(
"GET",
`/api/v1/accounts/${ENGINE_ACCOUNT}/bootstrap`,
{
query: { fresh: true },
machineAuth: true,
},
)).data;
const nonce = BigInt(
process.env.ACTION_NONCE ??
bootstrap.nonceFloor ??
bootstrap.nextNonce ??
0,
);
const idempotencyKey = `first-bsl-quote-${ENGINE_ACCOUNT}-${nonce}`;
const chainBinding = (await client.orderEntry.getActionChainBinding()).data;
const payload: LocalActionPayload = {
account: ENGINE_ACCOUNT,
nonce,
clientOrderId: `docs-yes-${nonce}`,
ts: BigInt(Date.now()),
action: {
kind: "QuoteReplace",
market: MARKET_ID,
legs: [
{
book: BOOK,
cancelOrderId: null,
side: "Bid",
price: 100_000n,
qty: 100_000n,
timeInForce: "post_only",
stpMode: "skip_self",
},
],
},
};
const signed = signAction(payload, ACTION_PRIVATE_KEY, { chainBinding });
const expectedOrderId = deriveQuoteReplaceOrderIdHex(payload, 0);
const response = await client.orderEntry.submitActions([signed], {
idempotencyKey,
responseMode: "detailed",
headers: {
"x-bsl-result-mode": "ack",
"x-senticore-response-mode": "detailed",
},
});
console.log(JSON.stringify({
expectedOrderId,
response: response.data,
}, null, 2));
mkdirSync("tmp", { recursive: true });
writeFileSync(
"tmp/known-good-bsl-batch.json",
`${JSON.stringify({ version: 1, actions: [signed], idempotencyKey }, null, 2)}\n`,
);
function must(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`${name} is required`);
return value;
}
Run:
npx tsx quote-once.ts
To verify the BSL compact facade instead of the direct institutional binary
route, set orderEntryBinaryPath: "/api/v1/bsl/orders/compact".
Expected successful shape:
{
"expectedOrderId": "0x...",
"response": {
"ok": true,
"seqs": [123],
"derivedOrderIds": ["0x..."],
"acceptedActions": 1,
"responseMode": "detailed",
"ackMode": "ingress_wal"
}
}
Persist idempotencyKey, nonce, clientOrderId, expectedOrderId, seqs,
market, book, side, price, and qty before the next strategy tick.
The script also writes tmp/known-good-bsl-batch.json. Use that exact signed
batch with the conformance harness when testing idempotency and nonce replay.
Create a bundle manifest from the template:
cp scripts/e2e/institutional-beta-bundle.example.json tmp/institutional-beta-bundle.json
Edit tmp/institutional-beta-bundle.json so it contains your real owner wallet,
accountIdHex, engineAccount, HMAC credential, funding minimum, and
orderEntry.signedBatchFile. The path is resolved relative to the bundle file.
To generate the known-good signed batch from the bundle instead of copying the TypeScript snippet into your own project:
cd sdks/typescript
npm install
npm run build
: "${SENTICORE_ACTION_PRIVATE_KEY:?set SENTICORE_ACTION_PRIVATE_KEY to the same value as ACTION_PRIVATE_KEY}"
node dist/examples/institutional-known-good-batch.js \
--bundle-file ../../tmp/institutional-beta-bundle.json
cd ../..
The generator fetches the live action-signing chain binding unless the bundle
already includes an actionSigning object, signs knownGoodQuote, writes
orderEntry.signedBatchFile, and prints the expected derived order id plus the
batch SHA-256.
5. Run Python and Rust bundle preflight
These examples do not submit orders. They prove the bundle can authenticate,
that the client is using the 20-byte engineAccount, and that fresh bootstrap
reports enough free USDC before a strategy attempts the first bid.
Python:
cd sdks/python
python3 -m pip install -e .
python examples/institutional_bundle_preflight.py \
--bundle-file ../../tmp/institutional-beta-bundle.json
cd ../..
Rust:
cd sdks/rust
cargo run --example institutional_bundle_preflight -- \
--bundle-file ../../tmp/institutional-beta-bundle.json
cd ../..
Both examples print redacted account and endpoint metadata, the action-signing
chain binding from GET /api/v1/bsl/connectivity, and:
{
"bootstrap": {
"freeUsdcMicro": "100000000",
"minimumFreeUsdcMicro": "100000000",
"fundingOk": true
}
}
If fundingOk is false, stop. The next QuoteReplace bid can be correctly
signed and still fail at the risk layer.
After the TypeScript generator has written orderEntry.signedBatchFile, the
same Python and Rust examples can submit those exact signed bytes. This is
opt-in and still stops before submit if the funding threshold is not met.
Python signed-batch submit:
cd sdks/python
python examples/institutional_bundle_preflight.py \
--bundle-file ../../tmp/institutional-beta-bundle.json \
--submit-signed-batch
cd ../..
Rust signed-batch submit:
cd sdks/rust
cargo run --example institutional_bundle_preflight -- \
--bundle-file ../../tmp/institutional-beta-bundle.json \
--submit-signed-batch
cd ../..
Expected submit fields:
{
"submit": {
"ok": true,
"seqs": [123],
"derivedOrderIds": ["0x..."],
"acceptedActions": 1,
"rejectCount": 0
}
}
6. Read open orders and fills
Use the engine account:
URL="$API_BASE_URL/api/v1/accounts/$ENGINE_ACCOUNT/orders"
eval curl -fsS "$(python3 tmp/sc-hmac-v2.py GET "$URL")" "$URL" | jq .
URL="$API_BASE_URL/api/v1/accounts/$ENGINE_ACCOUNT/fills"
eval curl -fsS "$(python3 tmp/sc-hmac-v2.py GET "$URL")" "$URL" | jq .
If the HTTP submit response was an ack, these reads or your private
stream/drop-copy are the source of final truth.
7. Replace or cancel
To refresh the quote, submit another QuoteReplace with:
- a fresh action nonce
- same
marketandbook cancelOrderIdset to the previous derived order id if the order is still open- the new price and quantity
To cancel without a replacement, submit a signed Cancel action for the order
id and reconcile from orders/drop-copy.
8. Debug failures
| Symptom | Check first |
|---|---|
bad account, expected 20 bytes | You used accountIdHex instead of engineAccount. |
401 on BSL connectivity or submit | HMAC headers missing, wrong API key, wrong passphrase, clock skew, or stale SC-Nonce. |
403 on BSL/FIX | Credential is probably api_agent, not institutional_agent, or missing quote/drop_copy scope. |
invalid_signature | Action signer does not recover to the engine account or delegated signer; check chain binding from connectivity bundle. |
nonce_below_floor, nonce_replayed | Re-read bootstrap/account, choose a fresh in-window nonce, and re-sign. |
insufficient_balance | Read bootstrap fresh and compare required notional against balances.freeUsdcMicro. |
actionResults[].status = "rejected" | The request reached sequencing; inspect rejectCode, rejectReason, market, book, and fresh balances. |
| HTTP 200 but no local order state | ack is not terminal state. Read orders/fills/stream/drop-copy. |
Python HMAC-only smoke
Use this when you only want to prove credentials and funding before integrating the SDK signer.
import json
import os
import subprocess
import urllib.request
api = os.environ["API_BASE_URL"]
acct = os.environ["ENGINE_ACCOUNT"]
url = f"{api}/api/v1/accounts/{acct}/bootstrap?fresh=true"
headers = {}
for line in subprocess.check_output(["python3", "tmp/sc-hmac-v2.py", "GET", url], text=True).splitlines():
_, raw = line.split(" ", 1)
name, value = raw.split(": ", 1)
headers[name] = value
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=10) as res:
body = json.load(res)
print(json.dumps({
"account": body.get("account"),
"freeUsdcMicro": body.get("balances", {}).get("freeUsdcMicro"),
"syncStatus": body.get("syncStatus"),
}, indent=2))
Rust HMAC signing sketch
The HMAC preimage is plain UTF-8 bytes. A Rust implementation should reproduce the same signing string and signature vector shown above.
use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256};
type HmacSha256 = Hmac<Sha256>;
fn body_sha256_hex(body: &[u8]) -> String {
hex::encode(Sha256::digest(body))
}
fn hmac_v2(secret: &str, signing_string: &str) -> String {
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(signing_string.as_bytes());
format!("0x{}", hex::encode(mac.finalize().into_bytes()))
}
8. Run the conformance harness
After the first quote path works, run the repository harness with the same credential bundle:
: "${SENTICORE_API_BASE_URL:?set SENTICORE_API_BASE_URL}"
: "${SENTICORE_API_KEY_ID:?set SENTICORE_API_KEY_ID from your beta bundle}"
: "${SENTICORE_API_SECRET:?set SENTICORE_API_SECRET from your beta bundle}"
: "${SENTICORE_API_PASSPHRASE:?set SENTICORE_API_PASSPHRASE from your beta bundle}"
: "${SENTICORE_ENGINE_ACCOUNT:?set SENTICORE_ENGINE_ACCOUNT to your 20-byte engine account}"
export SENTICORE_MIN_FREE_USDC_MICRO=100000000
export SENTICORE_CONFORMANCE_SUBMIT=1
export SENTICORE_CONFORMANCE_IDEMPOTENCY=1
export SENTICORE_CONFORMANCE_REPLAY=1
node scripts/e2e/institutional-conformance-kit.cjs \
--bundle-file tmp/institutional-beta-bundle.json
Do not move to unattended live order submit until the HMAC vector, connectivity call, fresh bootstrap funding check, nonce replay behavior, and first QuoteReplace reconciliation are all green.