Skip to main content

API Agents

API agents let a wallet owner create durable bot credentials without sending the wallet private key to Senticore. The owner signs an authorization message with the wallet, and the API returns an agent plus optional machine credentials.

Use this flow for server-side bots, BSL quote engines, FIX sessions, market maker tooling, and integration tests that should not ask MetaMask to sign every order.

Choose the agent type deliberately:

Agent typeUse
api_agentStandard server-side API bots, private reads, and delegated HTTP workflows
institutional_agentBSL/FIX business-line access, quote engines, market-maker tooling, drop-copy, and FIX Logon

Runtime-session agents and durable machine agents have different lifetime ceilings. Keep frontend sessions below their caps and use durable agents for server processes:

Agent typeMaximum lifetimeCredential support
browser_session30 daysNo HMAC/Ed25519 credential; wallet-created runtime signer
mobile_session24 hoursNo HMAC/Ed25519 credential; mobile handoff signer
api_agent180 daysHMAC or Ed25519 HTTP machine auth
institutional_agent90 daysHMAC for BSL/FIX/FIXP and protected institutional reads

The browser trading UI intentionally requests slightly less than 30 days to leave clock-skew margin. Do not request exactly the cap from clients whose clock may be ahead of the server.

In the current private beta, FIX order-entry Logon and BSL order-entry machine auth require an HMAC credential on an institutional_agent. A normal api_agent HMAC credential is useful for HTTP trading bots and private reads, but it is rejected for BSL/FIX order entry. Create institutional_agent credentials for every BSL or FIX integration.

After creating the institutional_agent, read the self-service connectivity bundle:

GET /api/v1/bsl/connectivity
GET /api/v1/fix/connectivity

With institutional_agent HMAC headers, the bundle returns the recommended FIX SenderCompID(49), TargetCompID(56), direct FIX host/port, and direct BSL HTTP base URL. Market makers should not need a manual operations ticket just to choose FIX CompIDs.

Credential types​

Credential typeReturned secretsUse
HMACapiKeyId, apiSecret, apiPassphraseFIX Logon, SC-Auth-Version: 2, durable bot/API access
Ed25519apiKeyId, public key onlyHTTP machine auth where Ed25519 is enabled

For FIX, request HMAC credentials. Ed25519 credentials are HTTP-only and are not accepted by the current FIX Logon path.

One-call HMAC agent creation​

POST /api/agents/create can create the agent and issue the HMAC credential in the same response. Set issueInitialCredential to true.

POST /api/agents/create
Content-Type: application/json
{
"authorization": {
"domain": "app.sentico-labs.xyz",
"appName": "sentico",
"scheme": "senticore_typed_v2",
"environment": "Arbitrum One",
"chainId": 42161,
"userWalletAddress": "0xOwnerWallet",
"accountIdHex": "0xOptional32ByteTradingAccountId",
"agentPublicKey": "0xBusinessLineAgentAddress",
"label": "BSL quote engine",
"agentType": "institutional_agent",
"scopes": ["read", "trade", "cancel", "quote", "drop_copy"],
"policy": {},
"expiresAt": 1790000000000,
"authorizationNonce": "7c4a0e4a5d43c4d8c9e2a55b",
"issuedAt": 1760000000000,
"signature": "0x..."
},
"issueInitialCredential": true
}
Scope semantics

quote is limited to quote-native RFQ and institutional MM surfaces. Ordinary REST batch place/amend items require trade; batch cancels require cancel. A quote-only credential therefore cannot place, amend, or cancel an ordinary order through /api/v1/trading/orders/batch.

Successful responses include the agent, the credential id, and the HMAC secret material. Store apiSecret and apiPassphrase immediately; they are shown only once.

{
"ok": true,
"agent": {
"id": "agt_...",
"agentType": "institutional_agent",
"scopes": ["read", "trade", "cancel", "quote", "drop_copy"]
},
"credential": {
"apiKeyId": "spk_...",
"authScheme": "hmac_v2"
},
"apiSecret": "sps_...",
"apiPassphrase": "spp_...",
"warning": "Legacy HMAC secret and passphrase are shown only once..."
}

Authorization message​

The wallet signs the exact text below with the owner wallet. Browser wallets use the normal message signing flow, for example MetaMask personal_sign.

SentiCore Typed Agent Authorization
type:senticore.agent_authorization
version:2
app:sentico
domain:app.sentico-labs.xyz
environment:Arbitrum One
chainId:42161
wallet:0xownerwallet
agentPublicKey:0xbusinesslineagentaddress
label:BSL quote engine
agentType:institutional_agent
scopes:read,trade,cancel,quote,drop_copy
policy:{}
nonce:7c4a0e4a5d43c4d8c9e2a55b
issuedAt:1760000000000
expiresAt:1790000000000

If the authorization request is bound to a specific trading account, send authorization.accountIdHex in the JSON request. It is the internal 32-byte trading-account id returned by the trading-account control plane. The signed message line remains accountId: for compatibility with the typed v2 message:

accountId:0x...

accountIdHex is not the 20-byte owner wallet address and it is not the 20-byte engine account used in signed order payloads. Most external integrations should omit accountIdHex on first setup; the API derives the wallet's main trading account. Putting the wallet address into accountIdHex returns bad accountId: expected 32 bytes.

For the default policy, sign exactly policy:{} as shown above. For a custom policy, do not hand-roll JSON key order. Use the SDK helper or mirror the server canonical policy object exactly (allowedMarkets, allowedAccounts, ipAllowlist, maxOrderNotionalMicro, rateLimitPerMinute, institutionalProfile) before signing.

appName and domain must match the registered application, and environment and chainId must match the deployed runtime. The Sentico first-party example uses sentico with app.sentico-labs.xyz; Sentirlo (app ID sentipredict) is a separate app for its own registered domains. A broker uses its own assigned app identity, not a first-party identity copied from this example. Changing an identity field requires a new owner signature. Generate fresh nonce and timestamps rather than submitting the illustrative values above.

Environment binding​

The examples on this page are the production values. The sandbox uses a different signing domain, and a signature made for one environment is rejected by the other.

Environmentdomain (example)environmentchainId
Productionapp.sentico-labs.xyzArbitrum One42161
Sandbox (launching)app.sentico-sandbox.xyzsandbox421614

Substitute both lines in the JSON request and in the signed text; everything else is unchanged. For the sandbox the signed block reads:

domain:app.sentico-sandbox.xyz
environment:sandbox
chainId:421614

chainId is the value the server actually enforces: it is compared against the deployed runtime's chain id on every wallet-admin, agent-authorization and session-delegation message. environment is a label, not a guard - the server does not validate that string. Do not rely on it for isolation.

Take chainId from actionSigning.chainId in GET /api/v1/bsl/connectivity on the host you are talking to, rather than hardcoding either number. Your app's domain must be a registered origin host of your app in that environment's registry; sandbox origins are registered against a sandbox app id. See Sandbox (launching — see Status) and Apps & Brokers.

TypeScript example​

This example uses an owner wallet to authorize a newly generated BSL/FIX agent address. It requests HMAC credentials by setting issueInitialCredential: true.

import { BrowserProvider, Wallet, hexlify, randomBytes } from "ethers";

const API_BASE_URL = "https://api.sentico-labs.xyz";
const owner = "0xOwnerWallet";
const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();

const now = Date.now();
const institutionalLifetimeMs = 90 * 24 * 60 * 60 * 1000 - 5 * 60 * 1000;
const agent = Wallet.createRandom();
const authorization = {
domain: "app.sentico-labs.xyz",
appName: "sentico",
scheme: "senticore_typed_v2",
environment: "Arbitrum One",
chainId: 42161,
userWalletAddress: owner.toLowerCase(),
agentPublicKey: agent.address.toLowerCase(),
label: "BSL quote engine",
agentType: "institutional_agent",
scopes: ["read", "trade", "cancel", "quote", "drop_copy"],
policy: {},
expiresAt: now + institutionalLifetimeMs,
authorizationNonce: hexlify(randomBytes(12)).slice(2),
issuedAt: now
};

const message = [
"SentiCore Typed Agent Authorization",
"type:senticore.agent_authorization",
"version:2",
`app:${authorization.appName}`,
`domain:${authorization.domain}`,
`environment:${authorization.environment}`,
`chainId:${authorization.chainId}`,
`wallet:${authorization.userWalletAddress}`,
`agentPublicKey:${authorization.agentPublicKey}`,
`label:${authorization.label}`,
`agentType:${authorization.agentType}`,
`scopes:${authorization.scopes.join(",")}`,
`policy:${JSON.stringify(authorization.policy)}`,
`nonce:${authorization.authorizationNonce}`,
`issuedAt:${authorization.issuedAt}`,
`expiresAt:${authorization.expiresAt}`
].join("\n");

const signature = await signer.signMessage(message);

const response = await fetch(`${API_BASE_URL}/api/agents/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
authorization: { ...authorization, signature },
issueInitialCredential: true
})
});

if (!response.ok) {
throw new Error(await response.text());
}

const created = await response.json();
await secretStore.write(`senticore/${created.agent.id}`, {
apiSecret: created.apiSecret,
apiPassphrase: created.apiPassphrase,
agentPrivateKey: agent.privateKey
});
console.log({ agentId: created.agent.id, apiKeyId: created.credential.apiKeyId });

Here secretStore.write represents your encrypted secrets manager; implement it with your platform's secret store. Do not substitute console.log or write the object to plaintext. The generated agentPrivateKey belongs to the agent, not to the owner wallet. Keep it with the bot if the bot will sign delegated trading actions. Never export or upload the owner wallet private key.

Two-step credential creation​

If the agent already exists, create or rotate a credential separately:

POST /api/agents/{agentId}/credentials/create
Content-Type: application/json
{
"walletAuth": {
"domain": "app.sentico-labs.xyz",
"appName": "sentico",
"scheme": "senticore_typed_v2",
"environment": "Arbitrum One",
"chainId": 42161,
"walletAddress": "0xOwnerWallet",
"nonce": "0f2a9d64a2f40efb6e0f567c",
"issuedAt": 1760000000000,
"expiresAt": 1760000300000,
"signature": "0x..."
},
"credentialKind": "hmac",
"cancelOnDisconnect": true,
"defaultStpMode": "cancel_taker"
}

The wallet signs this admin message:

SentiCore Typed Wallet Admin Authorization
type:senticore.wallet_admin
version:2
app:sentico
domain:app.sentico-labs.xyz
environment:Arbitrum One
chainId:42161
wallet:0xownerwallet
action:create_agent_credential
targetAgentId:agt_...
nonce:0f2a9d64a2f40efb6e0f567c
issuedAt:1760000000000
expiresAt:1760000300000

To create an Ed25519 credential instead, send credentialKind: "ed25519" plus publicKeyAlgorithm: "ed25519" and publicKey. Do not use Ed25519 for FIX.

Using HMAC credentials​

For HTTP machine auth:

SC-Auth-Version: 2
SC-Key: <apiKeyId>
SC-Nonce: <fresh tuple nonce>
SC-Timestamp: <unix ms>
SC-Passphrase: <apiPassphrase>
SC-Signature: <hmac signature>

Always send SC-Auth-Version: 2 for HMAC v2. If the header is omitted, the server treats the request as legacy v1 and signs a different canonical string.

For FIX Logon, use an HMAC credential issued to the business-line institutional_agent:

FIX tagValue
553apiKeyId
554apiSecret:apiPassphrase
49Bundle fix.orderEntry.senderCompIdTag49 for order entry, or fix.dropCopy.senderCompIdTag49 for drop-copy
56Bundle fix.targetCompIdTag56, normally SENTICORE
57order_entry or drop_copy

The private-beta FIX listener is raw TCP/TLS, not an HTTPS route. Current live operations exposes direct TCP/TLS on fix.sentico-labs.xyz:9878 with TLS SNI fix.sentico-labs.xyz. A Cloudflare-proxied api.sentico-labs.xyz:9878 connection will not work unless operations configures a TCP proxy or DNS-only record.

For BSL order-entry over HTTP, institutional_agent HMAC headers can authorize the account-scoped business-line path without a shared lane key. A separate order-entry entitlement may still be required for dedicated capacity or a contractual rate tier:

X-Senticore-Order-Entry-Key: <order-entry-api-key>

That key is provisioned through builder or BSL institutional onboarding and controls the order-entry lane and rate tier. The signed action inside the batch still needs valid account authorization and risk checks.

BSL session keys​

Direct TCP quote loops sign with an Ed25519 session key instead of the agent or owner key per order. Registration (POST /api/v1/bsl/session-keys/register) is a wallet-authorized call, not a machine-auth call — verified 2026-09-13:

  • send Origin: https://app.sentico-labs.xyz and wallet-auth appName "Sentico" (the registry resolves the app by id or display name, case-insensitively, and binds it to that origin; a different app name gets 401);
  • send no SC-* HMAC headers on this request (403 otherwise);
  • include "cancel" in the policy's allowedActions when you will arm the dead-man switch, otherwise POST /api/v1/bsl/dms answers DMS_AUTH_REJECTED.

Policy fields, revocation and the rollout flags are on BSL Session Keys.

Subaccounts​

One owner wallet can hold up to 256 trading accounts through /api/v1/trading/accounts*. Each subaccount is its own 20-byte engine account id with its own nonce window and open-order caps; transfers between them are USDC-only and signed by the owner wallet. Agents are bound per account (ACCOUNT_MISMATCH otherwise), and private WebSocket tokens bind to the main account.

Nonce handling​

Each account uses a windowed nonce with a window of 256. The admission precheck excludes the action's own already-claimed pending nonce, so an HMAC- or agent-self-submitted order is never rejected for its own in-flight claim. Duplicate protection is preserved: a second action that reuses an existing (account, nonce) pair is rejected. Allocate unused nonces per engine account inside the advertised window; multiple actions may be in flight. A monotonic allocator is optional. This action window differs from SC-Nonce tuple replay and from strict delegated-auth nonces.

Client order id on agent orders​

clientOrderId (cloid) is part of the signed action payload for direct-HTTP and FIX orders: it is folded into the signing hash and the derived order id, and it is propagated into the order-entry result and the drop-copy stream.

On older delegated agent batch paths, cloid may not be carried into the engine action payload. For durable client-side deduplication, use the separate Idempotency-Key header; use clientOrderId for strategy reconciliation after the engine has accepted or rejected the order.

Security notes​

  • Show apiSecret and apiPassphrase only once.
  • Store only hashes/encrypted secrets server-side; never log the raw response.
  • Use policy, IP allowlists, expiry, and narrow scopes for production bots.
  • Revoke compromised agents with POST /api/agents/revoke.
  • Rotate credentials before sharing access with a new bot or operator.