Skip to main content

Error Model

Senticore returns errors in two distinct shapes depending on where a request fails. Knowing which one you're looking at is the whole game:

  1. The HTTP error envelope — for request-level failures (bad shape, auth, not found, rate limit). A { "ok": false, "error": { … } } object.
  2. The SubmitResponse — for POST /api/v1/trading/actions (and MM/BSL order entry). Even a rejected order returns a flat SubmitResponse with a top-level error string and a derived code.

1. HTTP error envelope

{
"ok": false,
"error": {
"code": "BAD_REQUEST",
"message": "bad request: unknown field `foo`",
"retriable": false,
"requestId": "01J9Z6X7Q2K8N4M0",
"details": { "cause": "unknown_field" }
}
}
  • The wrapper is { ok, error }. Inner fields are camelCase.
  • retriable (note the spelling — not retryable) is true only for 409, 429, 503, 500.
  • requestId is always present — quote it in support requests.
  • details is omitted when there's nothing structured to add; details.cause carries the machine reason for shape/validation failures.

error.code is the status class, not a business reason

The envelope code is derived from the HTTP status — it is not a per-rule business code:

HTTP statuserror.codeMeaning
400 / 422BAD_REQUESTMalformed request or strict schema/shape rejection
401UNAUTHORIZEDAuthentication required or signature invalid
403FORBIDDENAuthenticated but not authorized for this action
404NOT_FOUNDResource/route unknown
409CONFLICTIdempotency or state conflict (retriable)
429RATE_LIMITEDRate limit exceeded (retriable)
406NOT_ACCEPTABLEContent negotiation failed
503SERVICE_UNAVAILABLETemporarily unavailable (retriable)
500INTERNAL_ERROR / SERVER_ERRORInternal error (retriable)

Business reasons (insufficient balance, post-only-would-cross, nonce rejects, …) are not in the envelope — they come back on the submit path as a SubmitResponse.code (§2).

400 vs 422 on the submit path

Strict schema/shape failures (unknown field, missing field, bad enum) map to BAD_REQUEST and typically surface the cause in details.cause. Business rejections on the submit endpoint (insufficient balance, stale nonce) come back as HTTP 400 with a SubmitResponse, not the envelope — see below.

2. Submit responses

POST /api/v1/trading/actions always returns a SubmitResponse — the same 9 keys every time, with optionals set to null rather than omitted.

Accepted (HTTP 200, not 202):

{
"accepted": true, "ok": true, "seq": 84213377,
"derivedOrderId": "0x7d3a1f90…", "error": null, "code": null,
"nextUsableNonce": null, "nonceFloor": null, "nonceWindow": null
}

ok mirrors accepted. derivedOrderId is set only for order-placing actions, null otherwise.

Rejected (e.g. HTTP 400, stale nonce):

{
"accepted": false, "ok": false, "seq": null, "derivedOrderId": null,
"error": "stale nonce: account=0x1111… nonce=41 next_nonce=42",
"code": "nonce_mismatch",
"nextUsableNonce": 42, "nonceFloor": null, "nonceWindow": null
}

code, nextUsableNonce, nonceFloor, and nonceWindow are derived from the rejection — every nonce reject carries the window fields so you can resynchronize without guessing. See Order Concurrency & Nonces.

Submit code values

These are the actual codes the submit path emits. Treat anything not listed (or null) as non-retriable unless the HTTP status says otherwise, and keep the raw error text in your logs.

CodeRetriableMeaning
nonce_below_floorNo (pick fresh)Below the window floor — permanently consumed/stale
nonce_outside_windowAfter floor advancesToo far ahead; fill lower nonces first
nonce_replayedNo (pick fresh)In-window but already used/in-flight
nonce_mismatchAfter resyncUncategorized nonce reject; use nextUsableNonce
insufficient_balanceNoBalance below order requirement
post_only_would_crossNoPost-only order would have crossed
self_trade_preventedNoSelf-trade-prevention triggered
order_not_foundNoCancel/amend target unknown or already terminal
invalid_signatureNoSignature verification failed
market_not_foundNoMarket id not recognized
expiredNoAction or order expiry passed
unknown_variant · unknown_field · missing_field · schema_violationNoStrict schema/shape rejection
rate_limitedYesBack off per Retry-After
backpressureYesIngress backlog limit hit; back off and retry
runtime_unavailableYesWrite runtime in recovery/unavailable

3. MM/BSL per-action rejections

The order-entry batch surface reports per-action outcomes when you request the full result mode. In private beta, treat full as a provisioned/verified contract rather than the default happy path — the tested beta submit mode is x-bsl-result-mode: ack with x-senticore-response-mode: detailed.

full responses carry an actionResults array of HotpathOrderResult:

{
"ok": true,
"responseMode": "full",
"actionResults": [
{
"seq": 4811, "clientOrderId": "mm-quote-1", "status": "rejected",
"rejectCode": "POST_ONLY_WOULD_CROSS",
"rejectReason": "post-only order would cross the spread",
"filledQty": 0, "leavesQty": 0, "feeMicro": 0, "fills": [],
"engineTsMs": 1781190000123, "serverTsMs": 1781190000125
}
]
}
  • statusfilled, partially_filled, resting, canceled, applied, rejected. Only rejected entries carry rejectCode / rejectReason.
  • rejectCode is a coarse engine label: NONCE_REJECTED, INSUFFICIENT_BALANCE, ORDER_NOT_FOUND, POST_ONLY_WOULD_CROSS, SELF_TRADE_PREVENTED, QUEUE_LIMIT, KILL_SWITCH, ENGINE_REJECTED. rejectReason preserves the raw detail.

Request-level status: a dropped action returns HTTP 409; if apply does not complete within the durable-ack timeout the request returns HTTP 503 with actionResults possibly null or partial.

See Raw Signed Actions for the full HotpathOrderResult schema.

4. Rate-limit headers

Every submit response carries rate-limit headers (see Rate Limits for the actual budgets):

  • X-RateLimit-Limit, X-RateLimit-Remaining
  • X-RateLimit-Reset-Msmilliseconds until the window resets, a small number like 1000. It is not an absolute epoch timestamp.
  • Retry-After — whole seconds, emitted only on 429 / 503.
The edge limiter returns plain text

The per-account/MM limiters return a JSON SubmitResponse on 429. The separate edge IP limiter returns a plain-text 429 body (Too Many Requests! Wait for {n}s) with only an x-ratelimit-after header. Handle a non-JSON 429.

Retry semantics

  • Retry only retriable: true (envelope) / retriable submit codes.
  • Exponential backoff with jitter, starting ~250 ms, capping ~5 s.
  • Always respect Retry-After on 429 / 503.
  • Use idempotency keys so retries are safe (Idempotency).
  • Surface non-retriable errors to the strategy without automatic retry; never retry a nonce_below_floor or nonce_replayed — pick a fresh in-window nonce instead.