Skip to main content

Rate Limits

All Senticore surfaces enforce rate limits. Limits depend on the surface and the client tier.

Trading-action limits (POST /api/v1/trading/actions)​

The retail submit path is guarded by two independent limiters — you can trip either one:

LimiterScopeDefaultOn trip
API IP throttle (write routes)per source IP10 req/s, burst 20 (code default; deployments may raise it)429 JSON error envelope + Retry-After; legacy x-ratelimit-after remains readable
Per-account limiterper account50 actions / 1000 ms429 JSON SubmitResponse, code: "rate_limited"

The API normalizes its own IP limiter into the common JSON error envelope. Per-account submit errors keep their flat response by default during migration; send X-SC-Error-Format: envelope-v1 for a common shape. Older builds returned the limiter's plain text Too Many Requests! Wait for {n}s with x-ratelimit-after, so SDK retry parsing accepts that legacy signal too. Two layers in front of the API never return JSON; see Non-JSON responses.

Use the applicable limiter's budget headers to pace requests. These example values are illustrative; limits depend on the account, route and deployment:

X-RateLimit-Limit: 50
X-RateLimit-Remaining: 37
X-RateLimit-Reset-Ms: 1000 # ms UNTIL reset, not an epoch timestamp
Retry-After: 1 # whole seconds, only on 429 / 503

X-RateLimit-Reset-Ms is a countdown in milliseconds, not an absolute time. A value of 1000 means the window resets in one second.

When the server has an authoritative delay hint, a 429/503 response provides Retry-After; honor it before retrying. A 429/503 without Retry-After means the server has no safe delay to advertise. In that case use capped exponential backoff with jitter and continue reconciliation rather than assuming a one-second delay. Legacy x-ratelimit-after is in seconds; body retryAfterMs is in milliseconds (including nested error details). Capacity/backpressure budget metadata describes its own scope and is not an account's trading quota. Do not infer capacity from an absent header, and do not treat a legacy zero limit as a measured account quota. Current code intentionally avoids inventing Retry-After, X-RateLimit-Limit, or X-RateLimit-Source when no real hint is available. For order entry, retain the exact idempotency key and signed payload while reconciling an uncertain result; a timeout does not mean the nonce is free.

Backoff and reconciliation​

For ordinary reads, retry 429/503 responses after the advertised delay, or use exponential backoff with jitter when no delay is present. For mutating order requests, first determine whether the outcome is known:

  • A transport timeout or 503 can occur before or after admission. Keep the same Idempotency-Key and exact signed payload, then query receipts, order state, and private execution events before resending.
  • An HTTP 200 admission acknowledgement is not terminal execution. Continue until a receipt, fill, cancel, or terminal rejection resolves the action.
  • A terminal business rejection is not repaired by backoff alone. Follow its code-specific action, such as selecting a fresh nonce, reducing size, or waiting for trading readiness.

See Troubleshooting HTTP integrations for the diagnostic bundle to retain during an incident.

Public reads and HMAC routes​

There is no route-specific budget for public GETs (/api/v1/public/*, /api/v2/public/oracle/feeds, /api/v1/status, /api/v1/health) or for the HMAC-authenticated BSL helpers (GET /api/v1/bsl/receipts, POST /api/v1/bsl/orders/cancel-all, GET /api/v1/accounts/{account}/bootstrap). They are covered by general limits:

LayerApplies toScopeBehavior on trip
Edge proxy request limiterAll routes, with separate zones for public reads, account/trading routes and /api/v1/bsl/*per client IP429 with an HTML body
API IP limiterAll HTTP routes (a stricter write limiter on POST /api/v1/trading/actions)per client IP429 JSON envelope, Retry-After, X-RateLimit-Source
Agent policy limitEvery HMAC-signed request, including receipts, cancel-all and bootstrapper API agent, fixed 60 s window, from the agent policy's per-minute request limit (no limit when unset; builder plans set one)429, agent policy rate limit exceeded
MM/BSL sliding windowOrder entry only (batch, compact, replace); not receipts, cancel-all or bootstrapsee below429 JSON with rateLimit view

The per-IP values of the first two layers are deployment configuration and are not published as a contract. Successful public GETs carry no X-RateLimit-* headers, so there is no budget to read in advance. Pace polling (public market reads are cached for about 2 s: Cache-Control: public, max-age=2), prefer the WebSocket and feed streams for market data, and back off on any 429. Use waitMs (at most 5000 ms) on the receipts route instead of tight polling.

Non-JSON responses: edge proxy and Cloudflare​

api.sentico-labs.xyz is served through Cloudflare. Two responses do not come from the API and are not JSON:

  • 403 text/plain error code: 1010: Cloudflare's bot protection refused the client signature. Observed on 2026-09-23 for a request with Python urllib's default User-Agent; the same request with an explicit User-Agent succeeded.
  • 429 HTML page: the edge proxy's per-IP request limiter.

Always send an explicit, descriptive User-Agent (for example acme-mm/1.4 ([email protected])), check Content-Type before parsing, and treat a non-JSON 4xx/5xx as a transport failure: back off with jitter, do not allocate a new nonce or signature for it, and reconcile mutating requests by receipt or idempotency key before resending. See Error Model.

Market-maker / BSL limits​

The MM/BSL order-entry lanes run a sliding-window limiter with per-scope budgets (defaults; read your live values from GET /api/v1/mm/limits / GET /api/v1/bsl/limits):

ScopeLive budget per 1000 ms window (2026-09-04)
per account4,000 actions
cancels (one bucket shared by all accounts on the lane)64,000 (separate cancel scope)
per API key16,000 actions
per IP16,000 actions
per market64,000 actions
per shard128,000 actions

Two properties that make quote churn cheap:

  • Cancels cost 0 against the action budget (separate cancel scope).
  • Quote-replace charges only net-new legs — replacing a resting quote isn't double-counted.

Every accepted BSL response also carries the budget view (usedActions, remainingActions, resetInMs), so a quote engine can pace itself without waiting for a 429. A 429 on this lane returns a JSON body with the same view:

{ "ok": false, "error": "mm rate limit exceeded scope=account max_actions_per_window=4000",
"rateLimit": { "account": "0x1111…", "windowMs": 1000, "maxActionsPerWindow": 4000,
"usedActions": 4000, "remainingActions": 0, "resetInMs": 1000 } }

Batch and book limits from the same endpoint at review time: maxPerBatch 1,000 actions, lowLatencyMaxActions 32, maxActionTsSkewMs 15,000, binary body cap 8 MiB, 50,000 open orders per account, 10,000 per account and market, 1,000 price levels per side and book (overflow prunes the worst level instead of rejecting the request).

WebSocket limits​

Every connection's limits are advertised in its session frame; the values below are the gateway defaults (environment-overridable on the server) and match what the public endpoint advertised on 2026-09-14.

SubjectDefault
Server heartbeat interval (heartbeatMs)20 s
Idle timeout (idleTimeoutMs)90 s
Private auth deadline (authDeadlineMs)5 s
Subscriptions per connection (maxSubscriptionsPerConnection)32
Markets per public connection (publicMaxMarketsPerConnection)16
Control messages per window (maxControlMessagesPerWindow / controlMessageWindowMs)60 per 60 s
Inbound message size64 KiB
Private resend range10,000 frames per request
Market-feed replay on resume10,000 events per channel

Exceeding a subscription or control-message limit closes the socket with code 4409; a slow consumer gets a warning and then 4408. A stale resume cursor returns resume_required / gap_fill rather than a silent gap — see WebSocket → Reconnect.

FIX rate limits​

SubjectLimit
Logon attempts per minute5
Order entry messages per secondProvisioned by account/tier
Resend messages per requestCapped by FIX_RESEND_MAX_MESSAGES

BSL order-entry limits​

Read the live limits endpoint before starting a quote engine:

GET /api/v1/bsl/limits

The response includes:

  • actions.maxPerBatch
  • actions.lowLatencyMaxActions
  • actions.maxActionTsSkewMs
  • rateLimits.windowMs
  • rateLimits.maxActionsPerWindow
  • rateLimits.maxActionsPerApiKeyWindow
  • rateLimits.maxActionsPerIpWindow
  • ingress.binaryMaxBodyBytes
  • backlog.prebatchedMaxBatches

Per-account throughput can also be restricted by API-agent policy and, for registered apps, by the app's limit profile (see Apps & Brokers).

Existing clients may still read GET /api/v1/mm/limits; it is a compatibility route for the same BSL limit envelope.

Cross-shard credit admission (separate reject reason)​

Distinct from rate limits, an order can also be rejected by the cross-shard credit broker - a per-(account, asset) admission control. This is a separate admission-reject reason, not a throughput limit, so it surfaces as a reject on the action rather than an HTTP 429. The broker is default off and is only active when the engine runs more than one market shard (engine_market_shard_count > 1) and a credit policy is enabled; it is a no-op in the single-shard production default. When enabled it can reject for haircut (MM_CROSS_SHARD_CREDIT_HAIRCUT_PPM), per-account cap (MM_CROSS_SHARD_CREDIT_MAX_ACCOUNT_LOCKED), per-market cap (MM_CROSS_SHARD_CREDIT_MAX_MARKET_LOCKED), or the fail-closed kill-switch (MM_CROSS_SHARD_CREDIT_KILL_SWITCH). The environment variable prefix is legacy; the policy now belongs to the BSL/risk admission model. Rejections are exposed in metrics by reason.

Burst behavior​

All surfaces use a token-bucket model with sustained rate plus a small burst allowance.