Read the code, not the status
The hub answers200 for success and 400 for every failure of its own — availability and busy
codes included — and the gateway in front of it passes those answers through unchanged. The gateway
answers its own refusals in the same envelope with a real status (400, 401, 403, 404, 413,
429, 502, 503, 504) and an EPX code. Either way the status never tells a retryable failure from a
terminal one: branch on the code. An answer without the envelope (an HTML page, a plain-text status)
did not come from a route; treat it as the service being unavailable. See
Conventions — the response envelope for the
envelope shape itself (not restated here).
The three blocks, and what a retry means
The codes fall into three blocks — terminal, availability, busy — and the hub’sEIN codes and the
gateway’s EPX codes share them. Terminal codes need a changed request (or, for the gateway, a fixed
credential or origin), availability codes a later retry, busy codes a back-off (EIN0080,
EPX0080 and EPX0082: error_params[0] ms; EPX0081: Retry-After seconds) — the table on
Errors is the spec.
The TypeScript SDK encodes the block rule as HubError.retryable and retryAfterMs
(SDK errors). On any route, an EIN0050 whose error_params[0]
is upstreamAuth means the upstream refused the hub’s own credential — a configuration problem an
operator has to clear, and no client-side back-off fixes it. A refused key or origin (401, 403)
is the same kind of problem on your side: fix the configuration, don’t retry. Retrying a refused
credential makes it worse: 20 of them within a minute lock the caller’s IP out of every call for the
rest of that minute (EPX0081).
The request id
Every answer carries a request id, in the body’srequest_id and in the X-Request-Id header. The
gateway makes a fresh one for every call it sends to the hub, retries included: you never send one,
and nothing about a retry depends on it. Log it next to the call, and quote it when you report a
problem.
Terminal codes on the happy path
A few terminal codes show up in normal flow, not just on mistakes:EIN0005(quote expired) — normal flow, not a bug: re-quote and re-sign. Not once the quote’s own transaction is sent (the open flow, or a proxy lane — see the known issue): the funds have already moved, and a new quote would move them a second time.EIN0008(no viable route) — a fact about the amount or the destination, not the form; change the request.EIN0010(bad signature) — covers an unknown quote too (one code on purpose); re-sign the exact bytes disclosed, never a re-derived version. OnPOST /order/openfora bad EVM escrow signature is not checked by the hub and comes back asEIN0021instead.EIN0001on the winner’s own kind (“the winner completes viaPOST /order”) — a bridge winner posted to the wrong route; the fix is the other route, not different input.
Submissions: re-post, never re-quote
A re-post of the identical body never opens a second order, and it is safe — except afterEIN0021: a declined signed order must be re-quoted and signed again, because a re-post answers
EIN0050 and then EIN0052. This is the client rule behind two route-level specs — the reservation mechanics on
Submit order (openFor) — Behavior and the idempotency bullet on
Create order — Behavior — and it’s worth stating once, in
plain terms:
EIN0052means an earlier submission of this quote went unanswered and may have opened the order. Stop. Never re-quote, never ask the user to sign again. Re-posting the identical body is safe and keeps answeringEIN0052until an operator reconciles the reservation — surfacerequest_idto them; there is no client-side fix. This rule is for a submission that went unanswered. After an explicitEIN0021nothing was opened: do not re-post (that only answersEIN0050and thenEIN0052) — re-quote and sign again, as above.- A bare
EIN0050orEIN0081onPOST /order/openforcan mean two different things: nothing was sent (back off and re-post — it forwards again), or something was sent and the hub is still waiting (the reservation is kept, and the next re-post answersEIN0052, which tells you which case it was). You can’t tell which one happened from the first error alone — only from what the re-post answers. - The gateway’s availability codes on a submission (
EPX0050,EPX0051andEPX0052, a502,503or504) say nothing about whether the order was opened: treat them as a lost answer (below).EPX0053is the exception: the gateway could not check the credential, and the call never reached the hub. POST /orderreplays the first ack even afterexpiresAt, for a quote that already has an order — so a lostPOST /orderanswer is recovered the same way.
A lost answer is not a lost order
If thePOST /order/openfor answer never arrives, or comes
back as one of the gateway’s availability codes, compute
hubOrderId = hex(sha256("infinity:v1:order:" + quoteId)) and read
GET /order/{hubOrderId}/status. The SDK’s
deriveHubOrderId does the hashing. A not-found in the
narrow window right after a crash does not prove there was no submission — the alias record is
written best-effort after upstream acceptance, so retry the read once, then, if it’s still
not-found, re-post the identical request. It never opens a second order.
The gateway keeps an order submission running at the hub when your client times out or
disconnects, so a client that gives up only loses the answer: the read above finds the order. The
gateway does cut the hub’s call at its own 10 s (EPX0052), and a submission cut while the hub is
forwarding it is cancelled with it: the read then answers EIN0009, and every re-post EIN0052
until an operator reconciles it (known issue).
Give submissions a client timeout longer than the gateway’s 10 s, so that you get the gateway’s
answer rather than none.
If the POST /order answer never arrives, re-post the identical request: a
quote that already has its order answers with that order’s ack, even after expiresAt. Never send
the transaction again.
Example error responses
Illustrative values; formats follow Conventions — the response envelope.EIN0008 is documented on Conventions already, so these
four use different codes.
EIN0001 — a form field, here amount on POST /quote. Terminal codes carry the top-level
message:
EIN0080 — the hub’s rate limit on POST /order/openfor; error_params[0] is the suggested
back-off in milliseconds (floor(1000 / tenantRps), at least 1; 200 at the defaults). The hub’s busy
codes carry no top-level message, but the hub always sets error_reference on this code (the same
informational text as the RateLimitShed error, not a hint to parse):
EIN0052 — outcome unknown, on a different POST /order/openfor submission (an earlier,
unanswered submission of the same quote is what produced this outcome, not a retry of the EIN0080
call above). Availability codes also carry no top-level message:
EPX0080 — the gateway’s rate limit: your pool is spent. It comes with HTTP 429 and a
Retry-After header in seconds; error_params[0] is the same back-off in milliseconds: