Error handling conventions: Direct callers ofSolverApiService(used by lower-level scripts and tests) still receiveSolverErrorResponsewithdetail.code/detail.message. The swap module’spostExecutionwraps these intoSodaxErrorwith codeEXTERNAL_API_ERROR; the originalSolverIntentErrorCodeis onresult.error.context.solverCodeand the fulldetailis onresult.error.context.solverDetail— see SWAPS.md Error Handling.
Mainnet production
URL: https://api.sodax.com/v1/intent This is the only solver host integrators should target. It is the packaged default, so no configuration is needed unless you are deliberately overriding the endpoint.Overview
The solver API drives the intent-based swap feature.SwapService (accessed via sodax.swaps) is the public entry point — it delegates all HTTP communication to the stateless SolverApiService class. External callers should use SwapService rather than calling SolverApiService directly.
Three endpoints are exposed:
All three carry the configured backend API key as the
x-api-key header when new Sodax({ apiKey }) is set — the same instance-wide key every other backend service uses. This is a config-level tier only: these requests take no per-call override. Solver auth failures come back through the SolverErrorResponse contract below rather than as an EXTERNAL_API_ERROR. See CONFIGURE_SDK.md § API key.
Error handling
All three solver methods returnPromise<Result<T, SolverErrorResponse>>. On HTTP errors or network failures, result.ok is false and result.error is a SolverErrorResponse:
SolverIntentErrorCode is an enum defined in @sodax/sdk. On unhandled exceptions the code is SolverIntentErrorCode.UNKNOWN.
To branch on solver errors, inspect result.error.detail.code:
POST /quote — Get a price quote
Called via SwapService.getQuote(payload).
Request (SolverIntentQuoteRequest)
SwapService.getQuote automatically adjusts amount by the configured partner fee before forwarding to the solver, so the returned quoted_amount reflects the net output the user receives.
Token addresses are validated against the active ConfigService and translated to their hub (Sonic) equivalents before the request is sent.
Response (SolverIntentQuoteResponse)
quoted_amount is in the destination token’s smallest unit.
Example
POST /execute — Notify solver of a live intent
Called via SwapService.postExecution(request). Invoked automatically by SwapService.swap() after the relay packet lands on the hub — call this manually only when orchestrating swap steps yourself.
Request (SolverExecutionRequest)
The request is retried automatically on transient network failures.
Response (SolverExecutionResponse)
Example
POST /status — Poll intent execution status
Called via SwapService.getStatus(request).
Request (SolverIntentStatusRequest)
Response (SolverIntentStatusResponse)
SolverIntentStatusCode is an enum in @sodax/sdk. The value 3 (SOLVED) indicates the solver has filled the intent.
When the solver returns NOT_FOUND or the request fails, sodax.swaps.getStatus checks the backend’s durable intent
record: if a fill that consumed the whole input was recorded there (intentState.remainingInput === '0') it returns
SOLVED with that hub-chain fill hash.
What happens otherwise depends on whether that check could be made. A record showing no such fill — or a 404, the
backend saying it holds none — answers the question, so the solver’s result is returned unchanged. If the record could
not be read at all (5xx, transport failure, unusable body), a solver NOT_FOUND is reported as a failed Result
instead: the fill may exist and simply be unreadable, so returning NOT_FOUND would present an unverified miss as a
definitive one. A poller that stops after N consecutive NOT_FOUND reads would otherwise spend that budget during a
backend outage and give up on a swap that had in fact completed. A failed solver request is returned as-is, since its
own error is the more useful diagnostic. An intent created with
allowPartialFill emits a fill event per fill, so a partial fill is deliberately not reported as SOLVED — that
would mark an unfinished swap complete and stop useStatus from polling it. The static SolverApiService.getStatus
does not do any of this.
SolverApiService.getStatus(request, config, logger?, timeoutMs?) takes an optional request budget. Omit it and the
call is unbounded, as it has always been; supply it when a stalled solver must not hold the caller open. An expiry is
reported as UNKNOWN, like any other failure. sodax.swaps.getStatus leaves it unset — a one-shot read is the
caller’s to bound — while getDetailedStatus, which is meant to be polled, sets it.
Example
Full swap flow
SwapService.swap() orchestrates the complete lifecycle. The steps below show what happens internally and where each solver endpoint is called:
swap() returns:
Complete example
Chain keys
UseChainKeys.* from @sodax/sdk for all chain references. SpokeChainKey is the union of ChainKeys values. XToken.chainKey (not xChainId) carries the chain key on token objects.
Intent.srcChain and Intent.dstChain are bigint relay chain IDs (not chain keys) — use getIntentRelayChainId(chainKey) from @sodax/sdk to convert between them.
Related source files
packages/sdk/src/swap/SolverApiService.ts— stateless HTTP client for the three solver endpointspackages/sdk/src/swap/SwapService.ts— public service facade; usesodax.swapspackages/sdk/src/swap/EvmSolverService.ts— EVM-level intent ABI encoding/decoding and event parsingpackages/sdk/docs/SWAPS.md— full swap feature documentationpackages/sdk/docs/ARCHITECTURE_REFACTOR_SUMMARY.md— v2 architecture reference (chain keys,Result<T>, error convention)packages/sdk/CHAIN_ID_MIGRATION.md— mapping from old*_CHAIN_IDconstants toChainKeys.*