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.
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 /catalogchanges 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/prepareper confirmation, not per render — apreparationIdis 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 byhubOrderId
(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’screateInteropProxy 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 readsEMC0005 “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.