sodax.swaps.swap() no longer runs a single linear pipeline. It creates the intent, then tries a
backend submit-tx attempt, and on any non-success falls back to the client-side relay so the
swap still completes.
If you drive the swap steps yourself — createIntent, then relay submission, then postExecution —
you get neither the backend path nor the fallback. This guide shows how to adopt both.
Who needs this
You can stop reading if your code callssodax.swaps.swap() or sodax.swaps.createLimitOrder().
Both already run the backend attempt and the fallback for you, and the shape of SwapResponse is
unchanged.
This guide is for you if you call createIntent and then handle the relay and solver notification
yourself — a bot, a backend orchestrator, or a frontend that persists each step. That code still works,
but it now takes the slow path every time and misses the backend’s server-side relay.
What changed
swap() runs these phases:
createIntent— build, sign and broadcast the intent transaction on the source chain. Unchanged.- Backend submit-tx attempt — hand the broadcast transaction to
sodax.api.swaps.submitTx. The backend verifies, relays and post-executes server-side; the SDK pollsgetSubmitTxStatusuntil the swap issolved. Controlled byswaps.useBackendSubmitTx, defaulttrue. - Client-side fallback — on any non-success in step 2: verify the transaction landed, relay it to
the hub and wait for the packet, then call
postExecution. This is the path your manual code already implements.
The orchestration helpers are internal.SwapService.submitTxandSwapService.fallbackSwapStepsare private, and the attempt budget and poll loop behind them are not exported from@sodax/sdk. The pieces they call —sodax.api.swaps.submitTx,getSubmitTxStatus,relayTxAndWaitPacket,postExecution— are public, so Option B rebuilds the loop in your own code rather than importing it.
Option A — hand the whole flow to swap()
The shortest migration, and the one to prefer unless you have a concrete reason to own the steps.
Delete the manual relay and post-execution code and call swap():
Option B — keep manual control
Use this when you must own the steps: a bot that persists every transition, a backend with its own signing boundary, or a UI that reports each phase. You reimplement whatswap() does internally.
Step 1 — create the intent
Unchanged. Keep all three returned values — the submit-tx request needs each of them. The bindings below are shared by every step in this section:Step 2 — build the submit-tx request
relayData is relayData.payload, and walletAddress is the
source address that signed the intent, not the destination.
Persist request before you submit it. If the process dies after broadcasting, this record is what
lets you resume; re-submitting the same (txHash, srcChainKey) is idempotent and answers
data.status: 'duplicate'. Never sign a second deposit to recover.
request.intent carries bigint fields (intentId, inputAmount, minOutputAmount, deadline,
srcChain, dstChain), so a plain JSON.stringify(request) throws — in the exact crash window
this step exists to survive. Serialize them explicitly and restore them on the way back in:
intent straight to
submitTx needs no conversion from you — only your own storage does.
Step 3 — open the attempt budget, then submit
The attempt starts before the POST, not after it: the submit request draws on the same budget as the poll, so a stalled POST costs the attempt rather than silently extending it. Every backend request is capped at the budget left, but never above the API service’s own timeout.verifyTxHash before this. The backend runs its own verification, so waiting for a
client-side confirmation delays every backend success by the source chain’s confirmation time and can
fail a swap the backend would have completed. Verification belongs to the fallback only.
Step 4 — poll until solved
Same budget, same cap on every request — and no sleep that the attempt cannot outlast, since that
would be dead wait standing between you and the fallback.
solved means “the backend did not finish it” — fall back.
Step 5 — fall back to the client-side relay
This is the flow you already have, with two additions: the relay gets its own fresh timeout, and it returns the sameSwapResponse the backend path does — so callers handle completion through one
contract no matter which path ran, exactly as swap() does.
data: relayData here takes the whole RelayExtraData object, while the submit-tx request in
Step 2 takes relayData.payload.
Two budgets, never one
timeout is a per-attempt budget, not an end-to-end deadline. The backend attempt gets one; if it
does not complete, the fallback relay gets a fresh one. Give your manual flow the same shape — a single
shared deadline leaves the fallback only what the backend did not spend, which is how a relay that needs
longer than the leftovers ends in a timeout.
Phases bounded by different things:
Read the constants from source rather than memorising them:
DEFAULT_RELAY_TX_TIMEOUT, DEFAULT_BACKEND_API_TIMEOUT and per-chain pollingConfig live in
@sodax/types;
RELAY_FALLBACK_FLOOR_MS lives in
IntentRelayApiService.ts.
The full breakdown is in
SWAPS.md § How timeout bounds each attempt.
Why falling back is safe
Re-relaying and re-posting an already-processed swap is idempotent: the relay deduplicates and returns the existingexecuted packet, and the solver re-affirms the intent rather than filling it
twice. This is covered by the SDK’s live relay end-to-end test.
The fallback is also load-bearing rather than belt-and-braces. The backend keeps processing at its own
pace after your poll gives up, so your fallback relay may race the backend’s — and that is fine, for the
same reason.
Outcome → action
Chain-specific notes
- Solana and Bitcoin deposits commit only a hash of the relay payload on-chain, so the relayer can
correlate a submission only with the exact original bytes. Keep the
relayDatathatcreateIntentreturned. If it is gone, recover byte-identical data withsodax.swaps.getIntentSubmitTxExtraData({ txHash })— note thattxHashthere is the hub-chain transaction hash, not the source-chainspokeTxHashused everywhere else in this guide, because the lookup reads the intent off the hub. From a populatedIntentyou already hold,sodax.swaps.reconstructRelayData(intent)derives the same bytes offline with no RPC call. - Sonic as the source chain has no relay leg — the spoke transaction already is the hub transaction.
The fallback must skip the relay and go straight to
postExecution. - Stellar destinations still need a trustline before the swap; see STELLAR_TRUSTLINE.md.
Status and recovery
Once a swap is in flight, read its state from the source-chain transaction hash:getDetailedStatus routes to the backend record or the solver, whichever can answer, which replaces
hand-rolled “try the backend, then try the solver” logic. getStatus and getSolvedIntentPacket are
unchanged.
Checklist
- Decided between Option A and Option B
- Submit-tx request built with
relayData.payloadand the source wallet address - The request is persisted before
submitTxis called - Both
result.okandresult.value.successare checked - Poll treats only
solved(with both result fields) as success - Poll bails on
failed/abandonedAtand on 401/403 - Every non-success path reaches the fallback
- No
verifyTxHashbefore the backend submit — only in the fallback - The fallback relay gets its own fresh timeout, floored at
RELAY_FALLBACK_FLOOR_MS - Hub-source swaps skip the relay leg
- Solana/Bitcoin keep or recover the exact
relayDatabytes
See also
- SWAPS.md — full
SwapServicereference, including § Backend 2-step submit and § Error Handling - HOW_TO_MAKE_A_SWAP.md — end-to-end walkthrough of a first swap
- CONFIGURE_SDK.md —
useBackendSubmitTx, API keys and timeouts - INTENT_RELAY_API.md — the relay layer the fallback uses