When and how the deposit comes back, as an FAQ.

What is a refund?

The escrow contract returns the user’s deposit. refund(order) on the settler is permissionless — anyone can call it, but only the order’s own user is paid — and whoever sends it pays the gas. The hub never sends it, and there is no automatic refund: the hub can tell you when an order is refundable and hand you the tuple to call with, but the user (or anyone) has to call refund(order).

When does an order become refundable?

After the escrow’s on-chain expires (or a later refundableAfter a refund extender set) has passed, for as long as the order is neither settled nor refunded. The escrow doesn’t check fills: offering a withdrawal only when no fill was observed is the client’s withdraw rule (below), not the escrow’s. On the proxy lanes, a refused registration doesn’t touch the deposit: the user’s own transaction already opened the order in escrow, the solver still fills it as usual, and if nothing fills it the user waits out that same deadline like any other unfilled order — it doesn’t refund early. Never re-quote meanwhile: a new quote means a second deposit (known issue). The exact expires / refundableAfter semantics are on List orders — Response (not restated here).

What is refunded, and to whom?

Whatever token the escrow actually holds — not necessarily the token the user thinks they paid with. On a native-coin lane (oif-wrap-native), the user paid in the chain’s native coin, but the escrow holds (and a refund returns) the wrapped token named by the catalog’s wrappedKey — never the native coin, and never the token paid with on a token-in order either. It’s paid to the order’s user, as recorded on List orders — Response (chain.user); the settler itself pays only that user (Get escrow order — Behavior, Get on-chain fill).

How do I know it was not filled?

The dating rule that says whether an absent fill is trustworthy lives on Get on-chain fill — Behavior — read it there. The client-side withdraw rule built on top of it: offer a withdrawal only if all three hold —
  1. filled is absent from GET /onchain/{onchainOrderId}/fill, and
  2. the order’s deadline has passed, and
  3. the destination chain’s watermark updatedAt is at or after that deadline.
updatedAt is the server’s publish time, not a block timestamp, so it trails chain time by the observers’ finality depth — if it’s close to the deadline, wait one more poll before deciding. EIN0051 on this route means “cannot tell”, never “not filled” — never offer a withdrawal on the strength of an error. Pass onchainOrderId exactly as the hub serves it (0x + 64 lowercase hex): the route doesn’t validate it, and a miscased or unprefixed id answers with no facts, which reads as “not filled” (known issue).

How do I withdraw?

These steps are for EVM-origin orders only: for a non-EVM origin (Solana), GET /onchain/{onchainOrderId}/order answers EIN0009 permanently, even once its Open, fill and settle are observed, so there is no tuple to send (known issue). A Solana-origin order refunds through GET /onchain/{onchainOrderId}/refund instead: the hub builds the unsigned refund transaction, whose fee payer and only signer is the order’s user, for their wallet to sign and send (unwrap=true also turns wrapped SOL back into SOL). The SDK checks that transaction before the wallet sees it (SDK — Refunds).
  1. Take onchainOrderId from GET /orders — or, for a proxy-lane deposit the hub never recorded, from GET /onchain/tx/{chainDomain}/{txHash}/order with the send transaction’s hash.
  2. GET /onchain/{onchainOrderId}/fill — apply the withdraw rule above.
  3. GET /onchain/{onchainOrderId}/order for the tuple and the settler; verify the settler is one of the catalog’s inputSettlers for that chain, and check the live contract’s own deadline (not just the cached projection).
  4. The user sends refund(order) to the settler, from whichever device holds the wallet — the on-chain views aren’t scoped to the caller, precisely so a refund can be sent from a second device.
Steps 2 and 3 take your secret key or your project id, so a page can run them itself. Step 1’s GET /orders needs the key: a page takes onchainOrderId from the order it submitted instead (on the escrow lanes, the openFor ack’s orderId), or from GET /onchain/tx/…, which is open to it.

Is refundEligible enough?

No. GET /orders’ chain.refundEligible is an advisory estimate from refundEligibilitySource: "stored_projection" — undated stored observations, not live contract state — and refundRequiresOnchainCheck is always true. Verify the live contract (step 3 above) before offering, or sending, a refund.

How does a refund show up?

Three places agree once a refund is observed: OnchainFill.refunded (an EventFact), GET /orders’ chain.status: "REFUNDED", and the upstream status’s state: "refunded".

Can I force a refund for testing?

There is no way to force a refund for testing: an order becomes refundable only once its on-chain deadline has passed, and the hub has no test-refund path.

See also