The two budgets your calls spend — the gateway’s and the hub’s — what triggers EPX0080–EPX0082 and EIN0080, and how to design a request workflow that rarely hits them.

The gateway’s pools

Every call reaches the gateway first. It limits:
  • secret-key traffic with one pool per organization, shared by all its keys, whatever IP they call from;
  • project-id traffic with a pool per project, shared by every visitor, plus a bucket per visitor IP. Visitors behind one shared IP, such as an office or a mobile carrier, share one visitor bucket;
  • failed credentials per IP: 20 missing, malformed or unknown keys or project ids within a minute lock that IP out of every call for the rest of the minute.
The two pools are separate, so a busy frontend never spends your servers’ budget. A refusal is a 429 with EPX0080 (a pool), EPX0082 (one visitor) or EPX0081 (failed credentials); the rows are on API keys and rate limits.

The hub’s buckets

Behind the gateway, the hub limits per organization: your organization is one tenant, and every call it makes, with a key or with its project id, draws on the same buckets. The three quote routes (POST /quote, /quote/preview, /quote/prepare) share one bucket; POST /order and /order/openfor share another. The rate itself — 5 requests/s, burst 20 — and the idle-sweep rule are the spec row on Limits.

What is charged, and when

The hub charges a request once it has validated, before the quote pipeline or the order service does any work — one that fails validation costs nothing, while one the service refuses afterwards, or answers as an idempotent duplicate, has already been charged. The catalog, status, listing and on-chain routes have no hub-level limit beyond its per-IP limiter.

Reading a 429 from the gateway

EPX0080 and EPX0082 carry the back-off twice: error_params[0] in milliseconds, and a Retry-After header in seconds. EPX0081 carries it in Retry-After only: the seconds left in the lockout. Wait that long, then retry. On EPX0081, first fix the credential and stop whatever repeats it: the failures that count are missing, malformed or unknown keys and project ids, and a valid one never counts, but during the lockout every call from that IP is refused, valid ones included. Every call whose credential is accepted also reports the budget left, in RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (Conventions — rate-limit headers): slow down as RateLimit-Remaining nears zero, rather than waiting for the 429.

Reading EIN0080

error_params[0] is a suggested back-off in milliseconds: floor(1000 / tenantRps), at least 1 (200 ms at the defaults), and it’s the same fixed value on every shed and every route — the quote bucket, the order bucket, and /quote/preview alike. error_reference carries informational text (“rate limit shed, retry after <n>ms”) — read the param, not the string. On the order routes, a rate-limit shed has reserved and forwarded nothing, so re-post the identical body after the wait.

Optimize your request workflow

  • Fetch the catalog once. GET /catalog changes only on restart — cache it, don’t call it per keystroke.
  • Debounce POST /quote/preview, and pause it entirely while a wallet prompt is open — a preview request the user can’t act on yet is pure budget spend.
  • One POST /quote/prepare per confirmation, not per render — a preparationId is meant to represent one deliberate signing attempt.
  • Poll every 2–3 seconds. Both status views (GET /order/{orderId}/status, GET /status/{orderId}) are cached for 2 seconds server-side — polling faster buys nothing.
  • Stop at a terminal state. See Order lifecycle for the stop set per view.

There are no webhooks

The hub has no push channel — no webhooks, no websockets. Polling at the cadence above is the only way to follow an order, and lost-ack recovery is by hubOrderId (Handling errors — a lost answer is not a lost order), not by callback.

Behind a proxy

Everyone your backend serves shares one budget at both layers: your organization’s pool at the gateway, and its buckets at the hub. Meter your users yourself before you forward their calls: the SDK’s createInteropProxy gives you beforeForward for exactly that (Partner backend — hardening). A page that uses your project id is limited per visitor IP by the gateway instead.

The per-IP limiter

A refusal that reads EMC0005 “DDoS check failed” with no request_id in the body is the hub’s own per-IP limiter, ahead of anything else in the hub. It counts the caller’s IP, which the gateway passes on: your server’s for key calls, the visitor’s for project-id calls. There’s no error_params back-off hint to read: back off and retry later.

Other busy signals

EIN0081 (“busy”) isn’t the rate limiter — it’s an admission shed with several possible causes: the in-flight quote cap (64 concurrent quotes, process-wide), the upstream-status projection table being full, the hub’s own outbound limiter to the upstream not admitting a forward in time, or the upstream answering its own 429. See Error reference — Hub codes (EIN) for the code row and Submit order (openFor) — Behavior for what each EIN0081 cause means for whether a re-post is safe.

See also