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:
| Limiter | Scope | Default | On trip |
|---|---|---|---|
| API IP throttle (write routes) | per source IP | 10 req/s, burst 20 (code default; deployments may raise it) | 429 JSON error envelope + Retry-After; legacy x-ratelimit-after remains readable |
| Per-account limiter | per account | 50 actions / 1000 ms | 429 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-Keyand 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:
| Layer | Applies to | Scope | Behavior on trip |
|---|---|---|---|
| Edge proxy request limiter | All routes, with separate zones for public reads, account/trading routes and /api/v1/bsl/* | per client IP | 429 with an HTML body |
| API IP limiter | All HTTP routes (a stricter write limiter on POST /api/v1/trading/actions) | per client IP | 429 JSON envelope, Retry-After, X-RateLimit-Source |
| Agent policy limit | Every HMAC-signed request, including receipts, cancel-all and bootstrap | per 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 window | Order entry only (batch, compact, replace); not receipts, cancel-all or bootstrap | see below | 429 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:
403text/plainerror code: 1010: Cloudflare's bot protection refused the client signature. Observed on 2026-09-23 for a request with Pythonurllib's default User-Agent; the same request with an explicit User-Agent succeeded.429HTML 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):
| Scope | Live budget per 1000 ms window (2026-09-04) |
|---|---|
| per account | 4,000 actions |
| cancels (one bucket shared by all accounts on the lane) | 64,000 (separate cancel scope) |
| per API key | 16,000 actions |
| per IP | 16,000 actions |
| per market | 64,000 actions |
| per shard | 128,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.
| Subject | Default |
|---|---|
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 size | 64 KiB |
| Private resend range | 10,000 frames per request |
| Market-feed replay on resume | 10,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
| Subject | Limit |
|---|---|
| Logon attempts per minute | 5 |
| Order entry messages per second | Provisioned by account/tier |
| Resend messages per request | Capped 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.maxPerBatchactions.lowLatencyMaxActionsactions.maxActionTsSkewMsrateLimits.windowMsrateLimits.maxActionsPerWindowrateLimits.maxActionsPerApiKeyWindowrateLimits.maxActionsPerIpWindowingress.binaryMaxBodyBytesbacklog.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.