new Sodax(...) accepts SodaxOptions — a deep-partial override of the static SodaxDefaultConfig data shape plus client-side options (logger, global fee, and per-feature partnerFee options). The merged result is SodaxConfig (exposed as sodax.instanceConfig). All three live in @sodax/types and are re-exported from @sodax/sdk.
Basic Configuration
Default Configuration
Initialize the SDK with default Sonic mainnet configurations (no fees):new Sodax(config?: SodaxOptions), where SodaxOptions = DeepPartial<SodaxDefaultConfig> & SodaxOptionalConfig — a deep-partial override of the SodaxDefaultConfig data contract plus the client-side options: the logger sink (see LOGGING.md), the global partner fee, per-feature options on swaps / bridge / moneyMarket / leverageYield (including partnerFee and useBackendSubmitTx — see Backend submit-tx 2-step), and radfi (see RadFi/Bound request signer). The logger, global fee, and radfi are kept off the data contract: they are resolved once and never fetched from or overwritten by the backend config. useBackendSubmitTx lives on the feature option slots (swaps / bridge / leverageYield) alongside partnerFee; the effective value is resolved on ConfigService — sodax.config.swapUseBackendSubmitTx / sodax.config.bridgeUseBackendSubmitTx (both default true) and sodax.config.leverageYieldUseBackendSubmitTx (default false). When called with no arguments the SDK merges your overrides with the packaged static defaults (sodaxConfig) using a recursive deepMerge. Omitted keys keep their default values.
Dynamic Configuration
Read effective configuration fromsodax.config, which includes packaged defaults and constructor overrides. initialize() is available for the initialization lifecycle:
initialize() returns Promise<Result<void>>. The current implementation returns success without fetching backend configuration; it preserves the constructor-merged configuration. Do not use it as a token-support refresh. A direct import of spokeChainConfig is only the packaged snapshot and does not include your overrides.
SodaxConfig overview
Top-level keys of the consolidatedSodaxConfig — the SodaxDefaultConfig data contract plus the per-feature client-side options merged over it. The feature rows are typed SwapsConfig / BridgeConfig / … (<Feature>DefaultConfig & <Feature>Options), so partnerFee and useBackendSubmitTx appear there even though neither is part of SodaxDefaultConfig and neither is ever fetched from the backend:
The global partner
fee is not a data key — it is a SodaxOptions client-side option (like logger). Set it via new Sodax({ fee }) and read the resolved value back on sodax.config.fee. It is the default applied to any feature whose own partnerFee is unset (see Partner Fees).
Partner Fees
Set a globalfee once, override it per feature, or both. The effective fee for a feature is featureFee ?? fee — a feature’s own partnerFee wins, otherwise the global fee applies. Services read the resolved value through ConfigService getters: SwapService reads config.swapPartnerFee, MoneyMarketService reads config.moneyMarketPartnerFee, BridgeService reads config.bridgePartnerFee, and LeverageYieldService reads config.leverageYieldPartnerFee. See Monetize SDK for usage details and per-request overrides.
Partner fee shapes
Partner fees are either percentage-based or amount-based (PartnerFee is a discriminated union—use one shape per fee object).
API key
The backend guards its API-keyed routes (starting with the Swaps API v2,POST /swaps/*) with an x-api-key header check; keys are minted through the partner portal. There is one key for every backend request — set it once at construction and the SDK sends it as x-api-key on the data API, the swaps API, the bridge API, the solver API, and the backend submit-tx legs of sodax.swaps.swap() / sodax.bridge.bridge():
apiKey in the trailing RequestOverrideConfig of any sodax.api.* method, or in extras on the high-level swap / bridge actions:
headers['x-api-key'] (any casing) → per-request apiKey → configured explicit x-api-key header (api.headers / setHeaders) → the configured apiKey. That full order is what the sodax.api.* transports resolve — tiers 1 and 3 exist only there, so the solver (below) carries the configured apiKey alone. An empty apiKey counts as unset and falls back; an explicit raw x-api-key header is authoritative and sent verbatim, a blank one included. The configured value is readable on sodax.config.apiKey.
Sponsoring is the one exception. Its own slice key (api.sponsoringApiConfig.apiKey) is the credential for that service and wins wherever the slice points. The global key is only inherited by sponsoring when the call actually targets a SODAX gateway — the packaged sponsoring default or the resolved shared root — and that check is made per request against the effective target, so a per-call baseURL override cannot carry the global key off-gateway. A custom sponsoring origin never receives it.
The solver API (solver.solverApiEndpoint, https://api.sodax.com/v1/intent by default) receives the configured key on /quote, /execute, and /status. It is the configured-apiKey tier only — those requests have no per-call override surface and no configured-header slot, so tiers 1–3 never apply to them. Solver auth failures surface through the solver’s own SolverErrorResponse contract rather than as an EXTERNAL_API_ERROR, so isAuthFailure does not recognize them.
Rotating a key at runtime with backendApi.setHeaders({ 'x-api-key': next }) reaches the data, swaps, and bridge clients only: sponsoring keeps its slice or inherited key (rotate it through sodax.api.sponsoring.setHeaders), and the solver keeps reading the configured ConfigService.apiKey.
Security note. The configured key follows the roots you configure — the data / swaps / bridge baseURL and solver.solverApiEndpoint — and it equally follows a per-call RequestOverrideConfig.baseURL on data / swaps / bridge, plaintext local targets such as http://localhost:3008 included: those three bake the key into their headers, so retargeting a single call carries it to that host. Sponsoring is the one gated exception described above. Point all of them — configured root and per-call override alike — only at trusted SODAX-related deployments.
Like the global fee, the global apiKey is a SodaxOptions client-side option, never part of the backend-fetched data contract. Keys bundled into a browser app are public by nature. Auth failures come back as EXTERNAL_API_ERROR results with context.status 401 (missing/invalid key) or 403 (suspended organisation / missing scope) — terminal until the key is fixed — while the transient verification 503 is retried automatically by the wire client.
Custom configuration
Solver (solver)
Intent-based swaps use the top-level solver block (not nested under swaps). Defaults match solverConfig in @sodax/types.
getSolverConfig() takes no parameters and returns the same object as the exported solverConfig constant from @sodax/sdk.
Partner fees for swaps belong in swaps.partnerFee, not inside solver.
Swaps token lists (swaps.supportedTokens)
SwapsConfig includes supportedTokens: Record<SpokeChainKey, readonly XToken[]>. Normally you rely on the packaged lists. If you override them, remember that deepMerge replaces arrays wholesale—provide the full list for any chain you touch, or omit supportedTokens to keep defaults.
Backend submit-tx 2-step (swaps.useBackendSubmitTx)
useBackendSubmitTx on swaps is a client-side runtime option on SodaxOptions (same slot as swaps.partnerFee) — it is NOT part of the backend-fetched SodaxDefaultConfig. Default true: after createIntent broadcasts the intent tx on the source chain, sodax.swaps.swap() hands the tx hash to the backend swaps API (sodax.api.swaps.submitTx), which relays and post-executes server-side; the SDK polls submit-tx status and returns the same SwapResponse. The SDK does not verify the tx on-chain first — the backend runs its own verification, so verifyTxHash would only delay every backend success. It runs on the client-side path only. Set false to force the fully client-side relay path.
swapsOptions / bridgeOptions keys are deprecated but still honoured, so an existing explicit opt-out keeps working; they apply only when the matching swaps / bridge flag is omitted. Move to swaps / bridge.
If the backend path does not reach solved for any reason (submission rejected, terminal failed/abandoned status, or poll timeout), swap() automatically falls back to the fully client-side relay + post-execution so the swap still completes — safely, because re-relaying / re-posting an already-processed swap is idempotent (no double-fill; verified by e2e-tests/e2e-relay.test.ts). timeout is a per-attempt budget: the backend attempt gets it, and the fallback relay then gets a fresh one that starts after on-chain verification, so neither a stalled backend nor a slow source-chain confirmation shortens the client-side wait, and raising timeout grows both. It does not bound intent creation, verification (the source chain’s pollingConfig.maxTimeoutMs) or post-execution. See SWAPS.md for the flow and How timeout bounds each attempt for the full breakdown.
Backend submit-tx (bridge.useBackendSubmitTx)
useBackendSubmitTx on bridge is the bridge counterpart — same slot as bridge.partnerFee, resolved live via config.bridgeUseBackendSubmitTx. Default true: sodax.bridge.bridge() routes the spoke-deposit through the backend bridge API (sodax.api.bridge.submitTx), which relays server-side; the SDK polls submit-tx status and returns the same TxHashPair. Set false to force the client-side relayTxAndWaitPacket path.
failed/abandoned, or poll timeout) bridge() falls back to the client-side relayTxAndWaitPacket flow so the bridge still completes — safe because re-relaying an already-relayed bridge tx is idempotent. timeout is per-attempt on the same terms as swaps: the backend attempt gets it and the fallback relay gets a fresh one. Bridge has no solver post-execution, so unlike swaps there is no 'posting_execution' step. See BRIDGE_API.md for the API client.
Backend submit-tx (leverageYield.useBackendSubmitTx)
useBackendSubmitTx on leverageYield is the leverage-yield counterpart — same slot as leverageYield.partnerFee, resolved live via config.leverageYieldUseBackendSubmitTx. It is the one that defaults false: the backend leverage-yield submit-tx path is opt-in while it beds in, where the swaps and bridge toggles default on. Set true and sodax.leverageYield.vaultSwap() (so deposit / withdraw too) routes the broadcast intent through the Leverage Yield API (sodax.api.leverageYield.submitTx, carrying the required operation: 'deposit' | 'withdraw'), which relays and post-executes server-side; the SDK polls submit-tx status until solved and returns the same VaultSwapResponse.
failed/abandoned, a rejected API key, or poll timeout) vaultSwap() falls back to the client-side verify → relay → notify-solver flow so the vault swap still completes — safe because re-relaying / re-posting an already-processed vault swap is idempotent. timeout is per-attempt on the same terms as swaps: the backend attempt gets it and the fallback relay gets a fresh one. A vault swap IS a solver swap, so unlike bridge the terminal status is 'solved' and the 'posting_execution' step applies. See LEVERAGE_YIELD.md for the flow and LEVERAGE_YIELD_API.md for the API client.
RadFi/Bound request signer (radfi.signRequest)
radfi is a client-side runtime option on SodaxOptions (like logger) — never part of the backend-fetched SodaxConfig. The SDK calls signRequest once per outbound Bound Exchange (RadFi) apiUrl request and merges the returned headers onto it, so a server-to-server caller can attach Bound’s x-api-signature HMAC header without the SDK ever holding the credential.
radfi and requests go out exactly as before.
The signer receives { method, path }, may be async, and is invoked per request (Bound’s signature embeds a timestamp valid for 60 s, so a cached one would replay). Its headers are merged last, so it must not return Authorization: that carries the per-user Bound access token, which is separate and passed per call via extras.bound.accessToken.
Money market (moneyMarket)
MoneyMarketConfig includes lendingPool, uiPoolDataProvider, poolAddressesProvider, bnUSD, bnUSDVault, bnUSDAToken, supportedTokens, supportedReserveAssets, and partnerFee. The packaged default is moneyMarketConfig.
Hub (hub)
The hub is a single HubConfig: chain metadata, hub contract addresses, and rpcUrl used when creating the hub JSON-RPC client. Override RPC or addresses with a partial under hub:
sodax.instanceConfig.hub (and sodax.hubProvider.chainConfig). sodax.config.getHubChainConfig() returns the static packaged hub snapshot, not the merged instance config—if you customize hub, treat instanceConfig.hub as the source of truth for your overrides.
Per-chain RPC and endpoints (chains)
There is no separate sharedConfig. Spoke RPC URLs and chain-specific settings live on each entry in chains[SpokeChainKey]. Partial objects are merged into the defaults for that key:
rpcUrl on their spoke config; Stellar uses horizonRpcUrl and sorobanRpcUrl; Sui uses grpc_url because it speaks gRPC-web rather than JSON-RPC (rpc_url is still accepted as a deprecated alias and wins when set — the packaged default always supplies grpc_url, so precedence rather than an either/or guard is what keeps overrides working — but the endpoint must serve gRPC; the packaged default is Sui’s public fullnode, which is rate-limited per IP, so override it for server-side traffic); Bitcoin includes radfi and related fields—mirror the shape of the default SpokeChainConfig for the chain you change.
Backend API (api)
ApiConfig controls baseURL, timeout, and headers for BackendApiService (the configuration service currently does not fetch through initialize()). It is either a flat BackendApiConfig (shown below — shared by sodax.backendApi, the swaps client sodax.api.swaps, and the bridge client sodax.api.bridge) or a nested CustomApiConfig ({ baseApiConfig?, swapsApiConfig?, sponsoringApiConfig? }) to point an individual client at its own endpoint.
How a request URL is composed
baseURL is the gateway root. Every service appends its own path below it — and the base API, swaps
and bridge all resolve the same root, so one value moves all three. Sponsoring is the exception: it
defaults to the same root but reaches it independently, so retargeting baseURL does not move it (see
the slice table below).
The version prefix is deployment-owned, which is why it lives in
baseURL rather than in the SDK’s
service paths: that is what lets a locally-run service be reached by swapping the host alone
(http://localhost:3008 mounts /swaps/* at its bare origin, with no version prefix at all). The
corollary is that https://api.sodax.com on its own is an incomplete base URL — it resolves every service
one segment short (/be/config/all, /swaps/tokens) and 404s. Only the data API has a basePath to
compensate, so the SDK warns at construction, naming each service whose resolved root on the packaged host
omits the prefix — including one reached through a swapsApiConfig or sponsoringApiConfig slice.
So a baseURL must never end in a service segment. If it ends in /be, the SDK trims it and logs a warning — the previous packaged default was https://api.sodax.com/v1/be, which nested the sibling services one level too deep (/v1/be/swaps/submit-tx).
basePath: '' for a service addressed directly at its origin rather than through the gateway:
baseURL moves all of them at once:
Every client method also takes a per-call
RequestOverrideConfig ({ baseURL?, timeout?, headers?, apiKey? }) as its last argument, which wins over the resolved config — useful for pointing one call at a canary host without touching app-wide config. A baseURL override replaces the gateway root; the calling service’s own path still applies, and a legacy /be suffix is trimmed from it just as it is from a configured base URL. Note that a timeout override replaces the resolved value rather than capping it.
Relayer (relay)
RelayConfig sets relayerApiEndpoint and relayChainIdMap (mapping each SpokeChainKey to the hub intent-relay bigint ID). Override only when pointing at a different relayer or custom map.
DEX (dex)
DexConfig holds concentrated-liquidity addresses and pool keys for Sonic. Most integrations keep the packaged dexConfig default.
Complete custom configuration
Combine the pieces that matter for your deployment:Service Properties
After construction, theSodax instance exposes the following read-only service properties:
Chain Keys
All chain constants live underChainKeys.* — import them from @sodax/sdk:
SpokeChainKey is the union type of all ChainKeys values. Use it to type any parameter that accepts a chain identifier.
Additional Resources
- Monetize SDK - Detailed fee configuration guide
- Architecture Reference - Spoke services, raw tx handling,
Result<T>, error conventions