Skip to main content
Error handling conventions: Direct callers of SolverApiService (used by lower-level scripts and tests) still receive SolverErrorResponse with detail.code / detail.message. The swap module’s postExecution wraps these into SodaxError with code EXTERNAL_API_ERROR; the original SolverIntentErrorCode is on result.error.context.solverCode and the full detail is on result.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 return Promise<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:
Polling intent status and waiting for fill delivery are separate steps the caller performs after swap() returns:

Complete example


Chain keys

Use ChainKeys.* 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.
  • packages/sdk/src/swap/SolverApiService.ts — stateless HTTP client for the three solver endpoints
  • packages/sdk/src/swap/SwapService.ts — public service facade; use sodax.swaps
  • packages/sdk/src/swap/EvmSolverService.ts — EVM-level intent ABI encoding/decoding and event parsing
  • packages/sdk/docs/SWAPS.md — full swap feature documentation
  • packages/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_ID constants to ChainKeys.*