All skills

SODAX

Community builtSKILL.md

Build cross-network trading, lending, and asset transfers between Stellar and 20+ other networks with the @sodax/sdk TypeScript package.

About

>-

Links
Tags
www-sodax-com · community
SKILL.md

SODAX (umbrella)

Self-contained entry point for AI coding agents building on SODAX. Routes tasks to the right granular sodax-* skill and supplies chain keys, the Result<T, SodaxError> model, and the gotchas that cause silent failures. This umbrella gives you the shape; the docs (docs.sodax.com) and granular skills are the source of truth for runnable code.

Read before writing any code

Six hard-failure gotchas. Every SODAX integration hits at least one of these.

  1. await sodax.config.initialize() is mandatory before any config-driven lookup. Otherwise findSupportedTokenBySymbol returns undefined.
  2. raw discriminates the payload. { raw: false, walletProvider } for signing, { raw: true } for unsigned. Passing walletProvider with raw: true fails to compile; forgetting raw: false on a signed call rejects walletProvider.
  3. deadline is Unix seconds, not milliseconds. Use BigInt(Math.floor(Date.now() / 1000) + 300).
  4. sodax.moneyMarket.data is a sub-namespace. Use sodax.moneyMarket.data.getReservesHumanized(), not sodax.moneyMarket.getReservesHumanized().
  5. Reserve-data pipeline order matters. getReservesHumanized → buildReserveDataWithPrice → formatReservesUSD → buildUserSummaryRequest → formatUserSummary. Skipping the intermediate builds throws.
  6. Result<T, E> is checked on result.ok, not caught. Never try/catch a Result-returning service call. Never branch on error.message. Discriminate on the narrow error.code union. Not every method is Result-wrapped: synchronous getters return their value directly (sodax.config.getChains(), sodax.config.findSupportedTokenBySymbol(...), sodax.leverageYield.getVault(name)), and sodax.hubProvider.getUserHubWalletAddress(...) is async but resolves to a plain Address. Mind the name collisions: sodax.swaps.getPartnerFee(bigint) is synchronous and returns bigint, while sodax.api.swaps.getPartnerFee(query) is async and Result-wrapped.

Key handling: read private keys from environment variables only. Never hardcode, never log, never send to an external API.

Canonical agent references

  • AI Integration Guide — docs.sodax.com/ai-integration-guide.md — canonical entry for agents. Explains the @sodax/skills bundle (one mode-gated broad skill per @sodax/* package, four packages in all, each bundling two knowledge subtrees: integration/knowledge/ for new v2 code and migration-v1-to-v2/knowledge/ for v1→v2 porting, plus the sodax-build front-door skill when the package isn't chosen yet).
  • Documentation index (LLM-friendly) — docs.sodax.com/llms.txt — full page list. Fetch first when discovering topics.
  • API & SDK stack overview — docs.sodax.com/home/sdk-stack.md — routes to the HTTP API or the open-source SDK. Both hit the same execution layer.
  • @sodax/skills package (GitHub source) — github.com/icon-project/sodax-sdks/tree/main/packages/skills

Identity

SODAX is a cross-chain DeFi hub, mainnet-only (no testnet chains or RPC endpoints). Sonic is the hub; 21 spoke chains connect to it. From one wallet on any supported chain a user can swap, lend, borrow, bridge, stake SODA, or migrate ICON-era assets to a new spoke.

Non-custodial by design. SODAX is protocol and software: it routes and settles. Independent solvers on an open marketplace fill intents. Never write "our solver", "the SODAX solver", "SODAX trades", "SODAX takes custody", or "SODAX fills orders" — those are inaccurate.

The SDK is intent-based: the user signs on a spoke, the SDK relays to Sonic, a solver or hub contract fulfills, and settlement lands on the destination chain. Feature-service methods return Result<T, SodaxError>; see Result and error handling for the direct-return helpers and balnSwapService exceptions that do not. For React frontends, prefer @sodax/dapp-kit hooks over calling @sodax/sdk directly.

Install

Two integration surfaces (both hit the same execution layer — see the API & SDK stack overview):

HTTP API — any language

Partner REST endpoints. Overview: docs.sodax.com/developers/http-api.md. Pick this if you're not on TypeScript or you want a thin wire integration. Route directly to the sub-endpoint that matches the task:

Sub-APIEndpoint prefixDocs
Swaps (quote / build-intent / submit-tx / status)/v1/swaps/*docs.sodax.com/developers/http-api/swaps.md
Leverage yield vaults over HTTP/v1/leverage-yield/*docs.sodax.com/developers/http-api/leverage.md
Oracle candles + solver oracle prices—docs.sodax.com/developers/http-api/oracle.md
Protocol stats (networks / partners / volume / supply)—docs.sodax.com/developers/http-api/stats.md
Stellar account sponsoring/v1/sponsorships/* (API key required)docs.sodax.com/developers/how-to/stellar-sponsoring-getting-started.md

Shared conventions (BigInts as decimal strings, error shape at the edge, caching): see the HTTP API root page.

API keys and partner fees: docs.sodax.com/developers/how-to/api-keys.md · docs.sodax.com/developers/how-to/monetize_sdk.md.

Open-source SDK — TypeScript

Overview: docs.sodax.com/developers/packages.md. Three layers across four packages:

LayerPackageDocs
Foundation (core services + types)@sodax/sdkdocs.sodax.com/developers/packages/foundation/sdk/index.md
Connection — wallet providers (Node / non-React)@sodax/wallet-sdk-coredocs.sodax.com/developers/packages/connection/wallet-sdk-core.md
Connection — React adapter (context, state)@sodax/wallet-sdk-reactdocs.sodax.com/developers/packages/connection/wallet-sdk-react.md
Experience — React hooks over the SDK@sodax/dapp-kitdocs.sodax.com/developers/packages/experience/dapp-kit.md

Install with your JS package manager (npm / pnpm / yarn / bun):

  • Core: @sodax/sdk
  • React dapps: also @sodax/dapp-kit + @sodax/wallet-sdk-react
  • Backend / Node signing without a browser wallet: also @sodax/wallet-sdk-core

Roles:

  • @sodax/sdk core services (sodax.swaps, sodax.moneyMarket, sodax.bridge, sodax.staking, sodax.dex, sodax.leverageYield, sodax.migration, sodax.partners, sodax.recovery, sodax.sponsoring, sodax.config, sodax.api). Re-exports every @sodax/types symbol; do not install @sodax/types separately.
  • @sodax/dapp-kit React hooks wrapping the SDK (useSwap, useSupply, useBridge, etc.).
  • @sodax/wallet-sdk-core chain-specific I*WalletProvider implementations for Node / backend / non-React.
  • @sodax/wallet-sdk-react React wallet connect layer (useWalletProvider, wallet-modal, WalletConnect).

Route by intent

Pick the row that matches the user's task. Load the named granular skill for depth; this umbrella covers the fast path.

IntentWhere to go next
Not sure what to buildsodax-build (guided interview, produces brief)
Backend, script, indexer, bot, non-React swap / bridge / lend / stakesodax-sdk (broad) or sodax-sdk/<feature> if the feature is fixed
One SDK feature only (swap, money-market, bridge, staking, dex, leverage-yield, migration, partner, recovery, sponsoring, backend-api, swaps-api, leverage-yield-api, bridge-api)sodax-sdk/<feature>
React dapp with hookssodax-wallet-sdk-react then sodax-dapp-kit (or the feature-specific sodax-dapp-kit/<feature>)
React dapp calling SDK directly (no dapp-kit)sodax-wallet-sdk-react then sodax-sdk
Backend needs one chain's walletsodax-wallet-sdk-core/<chain> then sodax-sdk/<feature>
React wallet concern only (connect button, modal, chain switch, sign message, WalletConnect)sodax-wallet-sdk-react/<concern>
Porting an existing v1 dappsodax-wallet-sdk-react (migration) then sodax-dapp-kit (migration) then sodax-sdk (migration)
Porting an existing v1 backendsodax-sdk (migration) then sodax-wallet-sdk-core (migration; usually a no-op)

If both migration and new features are in play, do migration first. Otherwise stale v1 patterns leak into new code.

Bootstrap once

The SDK is constructed once and reused. Minimal form (new Sodax()) uses packaged defaults for hub + spoke RPCs with no failover. Production form (recommended) passes { hub: { rpcUrls: [...] }, chains: { [ChainKeys.X]: { rpcUrls: [...] } } } for multi-endpoint failover. Same rotation shape for the hub (Sonic) and every spoke chain you touch. Call await sodax.config.initialize() before any config-driven lookup — it loads fresh config, is idempotent, and falls back to packaged defaults.

Canonical example (kept current with the SDK):

Config-driven token lookup: use sodax.config.findSupportedTokenBySymbol(chainKey, symbol). Do not hand-construct XToken. Sanity-check chain keys with sodax.config.isValidSpokeChainKey(...).

Every feature service is wired after construction: sodax.swaps, sodax.moneyMarket, sodax.bridge, sodax.staking, sodax.dex, sodax.leverageYield, sodax.migration, sodax.partners, sodax.recovery, sodax.sponsoring, sodax.config, sodax.api, sodax.hubProvider, sodax.spoke.

Version-gated behaviour lives in Version notes at the bottom of this skill.

Wallet providers

Methods that sign take a walletProvider matching the source chain family. The srcChainKey on the payload narrows its type at compile time. Read methods (sodax.config.*, sodax.bridge.getBridgeableAmount, sodax.staking.getStakingConfig, quotes) and raw: true flows need no provider.

Node / backend (private-key, via @sodax/wallet-sdk-core) — one I*WalletProvider class per chain family. Constructor field conventions vary per chain (EVM: privateKey + chainId + rpcUrl; Solana: 64-byte privateKey + endpoint; Sui: mnemonics only; Bitcoin / Stellar: uppercase type: 'PRIVATE_KEY' + network; ICON / NEAR / Stacks: privateKey + rpcUrl/endpoint + optional accountId; Injective: nested secret: { privateKey | mnemonics }).

Browser / React — construct with a supplied client/adapter instead of a private key (EVM: viem wallet+public clients; Solana: adapter context with publicKey + signTransaction; Sui: wallet-standard client). In a React dapp, prefer the umbrella hook useWalletProvider({ xChainId }) from @sodax/wallet-sdk-react.

Canonical examples (per chain, kept current):

Common flows

Every signed call takes the discriminator { raw: false, walletProvider }. For unsigned-tx (Gnosis Safe, HW wallet, custom relay) use { raw: true }; passing walletProvider alongside raw: true is a compile error.

1. Cross-chain swap (spoke to spoke)

Optional read-only preview via sodax.swaps.getQuote(...) — returns { ok, value: { quoted_amount, ... } }. Then sodax.swaps.swap({ params, raw: false, walletProvider }) with params: CreateIntentParams — needs srcChainKey, dstChainKey, srcAddress, dstAddress, inputToken (address, not XToken object), outputToken (address), inputAmount, minOutputAmount (from quote.value.quoted_amount × slippage), deadline (Unix seconds, not ms), allowPartialFill, solver (zero address = auto), data ('0x' unless custom). On success returns { solverExecutionResponse, intent, intentDeliveryInfo }.

2. Money market supply (single chain, or cross-chain via the same params shape)

sodax.moneyMarket.supply({ params: { srcChainKey, srcAddress, token, amount, action: 'supply' }, raw: false, walletProvider }). Returns { srcChainTxHash, dstChainTxHash }. Solana, Sui, Stellar, Bitcoin, ICON, Injective, NEAR, Stacks do NOT need allowance/approve. On EVM sources, gate with isAllowanceValid and call approve first.

3. Money market borrow to another chain

Same as supply, but action: 'borrow' plus dstChainKey + dstAddress for cross-chain delivery. Omit dstChainKey/dstAddress for a same-chain borrow.

4. Money market reserves + user position (formatted USD summary)

Four-step pipeline living on the sodax.moneyMarket.data sub-namespace (not on sodax.moneyMarket directly). Order matters: getReservesHumanized + getUserReservesHumanized(userHubAddress) → buildReserveDataWithPrice(reserves) → formatReservesUSD(request) → buildUserSummaryRequest(reserves, formattedReserves, userReserves) → formatUserSummary(request). Output includes totalCollateralUSD, totalBorrowsUSD, netWorthUSD, healthFactor, availableBorrowsUSD. Get the user hub address via sodax.hubProvider.getUserHubWalletAddress(userAddress, srcChainKey) — it resolves to the address itself, not a Result. aToken balances: sodax.moneyMarket.data.getATokensBalances(aTokens, userHubAddress) → Map<Address, bigint>. Aave RAY (27-decimal) precision is preserved; use formatted responses for display, raw for on-chain math.

5. SODA staking (stake, unstake, claim)

sodax.staking.stake({ params: { srcChainKey, srcAddress, amount, minReceive (xSODA slippage floor), action: 'stake' }, raw: false, walletProvider }). unstake uses action: 'unstake' with the xSODA amount. claim (after unstaking period) needs a requestId (source it from getUnstakingInfoWithPenalty or the staking data API). Read the penalty curve before UI with sodax.staking.getUnstakingInfoWithPenalty(user, chainKey). getStakeRatio(amount) → [xSodaAmount, previewDepositAmount]. instantUnstake skips the waiting period at a slippage cost via StakingRouter.

6. Direct bridge (asset transfer between two spokes)

sodax.bridge.bridge({ params: { srcChainKey, srcAddress, srcToken, amount, dstChainKey, dstToken, recipient }, raw: false, walletProvider }). Preflight gates: sodax.bridge.isBridgeable({ from, to }) (must be true — tokens sharing a hub vault bridge to each other; different-vault tokens do not, use swap instead) and sodax.bridge.getBridgeableAmount(from, to) for the current limit. Vaults are the bridgeability unit.

7. Migration: bnUSD (legacy ICON to new EVM bnUSD)

sodax.migration.migratebnUSD({ params: { srcChainKey, srcAddress, srcbnUSD, dstChainKey, dstbnUSD, amount, dstAddress }, raw: false, walletProvider }). ICX and BALN migrations use their own param types (IcxMigrateParams, BalnMigrateParams) and are not generic over K. sodax.migration.balnSwapService.* lock-management methods (stake, unstake, claim, cancelUnstake, getDetailedUserLocks) still throw rather than return Result<T> (tech debt) — wrap in try / catch.

8. Partner fees

PartnerService exposes operations on sodax.partners.feeClaim. Hub chain key ('sonic') is required. Flow: (1) isTokenApproved({ srcChainKey: 'sonic', srcAddress: partnerAddress, token }); (2) approveToken(...) if not; (3) setSwapPreference(...) to set the auto-swap target asset for collected fees; (4) per-action override at swap time via extras: { partnerFee: { address, percentage } } on sodax.swaps.swap. Same-token claims (output equals fee token) are rejected up front with VALIDATION_FAILED — bridge the wrapped hub asset off Sonic instead via sodax.bridge.bridge(...).

ChainKey reference

The canonical spoke chain keys (import { ChainKeys } from '@sodax/sdk';). Keep this table inline — string values are unguessable and hallucinated constantly.

ChainKeyString
SONIC_MAINNET (hub)'sonic'
ETHEREUM_MAINNET'ethereum'
ARBITRUM_MAINNET'0xa4b1.arbitrum'
BASE_MAINNET'0x2105.base'
BSC_MAINNET'0x38.bsc'
OPTIMISM_MAINNET'0xa.optimism'
POLYGON_MAINNET'0x89.polygon'
AVALANCHE_MAINNET'0xa86a.avax'
HYPEREVM_MAINNET'hyper'
LIGHTLINK_MAINNET'lightlink'
REDBELLY_MAINNET'redbelly'
KAIA_MAINNET'0x2019.kaia'
HEDERA_MAINNET'hedera'
ROBINHOOD_MAINNET'robinhood'
INJECTIVE_MAINNET'injective-1'
ICON_MAINNET'0x1.icon'
SUI_MAINNET'sui'
SOLANA_MAINNET'solana'
STELLAR_MAINNET'stellar'
NEAR_MAINNET'near'
BITCOIN_MAINNET'bitcoin'
STACKS_MAINNET'stacks'

ChainTypeArr: 'ICON' | 'EVM' | 'INJECTIVE' | 'SUI' | 'STELLAR' | 'SOLANA' | 'STACKS' | 'NEAR' | 'BITCOIN'.

Result and error handling

Feature-service methods (swaps, money market, bridge, staking, dex, leverage-yield, migration except balnSwapService) return Promise<Result<T, SodaxError>>. Branch on result.ok before touching result.value (success) or result.error (failure); nothing throws across those service boundaries. That contract does not extend to direct-return helpers — sodax.hubProvider.getUserHubWalletAddress resolves to the hub address itself and the synchronous sodax.config.* getters return their value, so do not read .ok / .value on either — nor to sodax.migration.balnSwapService, which still throws. Use isSodaxError(e) (not bare instanceof) across bundle boundaries.

SodaxErrorCode (full set): USER_REJECTED | VALIDATION_FAILED | INTENT_CREATION_FAILED | EXECUTION_FAILED | TX_VERIFICATION_FAILED | TX_SUBMIT_FAILED | RELAY_TIMEOUT | RELAY_FAILED | APPROVE_FAILED | ALLOWANCE_CHECK_FAILED | GAS_ESTIMATION_FAILED | LOOKUP_FAILED | EXTERNAL_API_ERROR | UNKNOWN.

Feature tags (SodaxFeature, on e.feature): swap | moneyMarket | bridge | staking | migration | dex | partner | recovery | backend | sponsoring | leverageYield.

Context fields: carry the discriminator you actually need in a handler — action ('supply', 'stake', 'migrateBaln', ...), phase (validate | intentCreation | verify | submit | relay | destinationExecution | execution | postExecution | approve | allowanceCheck | gasEstimation | lookup), srcChainKey, dstChainKey, relayCode (SUBMIT_TX_FAILED | RELAY_TIMEOUT | RELAY_POLLING_FAILED | UNKNOWN), api (solver | backend | swaps | sponsoring), method, field, reason. Solver read-side errors (sodax.swaps.getQuote) return a different shape (SolverErrorResponse) — read error.detail.code / error.detail.message.

Canonical error-handling example (switch-per-code pattern):

Gotchas

Full list (the top six are promoted to Read before writing any code above).

  • await sodax.config.initialize() is mandatory before any config-driven lookup.
  • srcChainKey narrows walletProvider at compile time. Passing a Solana wallet with ChainKeys.ARBITRUM_MAINNET is a TypeScript error. Cast EVM addresses at the boundary: (await wp.getWalletAddress()) as \0x${string}``.
  • raw discriminates the payload. { raw: false, walletProvider } for signing, { raw: true } for unsigned.
  • deadline is Unix seconds, not milliseconds. Use BigInt(Math.floor(Date.now() / 1000) + 300).
  • No allowances on Solana, Sui, Stellar, Bitcoin, ICON, Injective, NEAR, Stacks. Only EVM sources gate through isAllowanceValid + approve.
  • sodax.moneyMarket.data is a sub-namespace.
  • Reserve-data pipeline order matters. getReservesHumanized → buildReserveDataWithPrice → formatReservesUSD → buildUserSummaryRequest → formatUserSummary.
  • Solana intents via createIntent + submit need relay extra data. The one-shot sodax.swaps.swap(...) handles it. If splitting for a backend flow, submit relayData.payload (a string) to sodax.api.swaps.submitTx, not the full relayData object.
  • Same-token partner-fee claim is validated closed. Bridge the wrapped hub asset off Sonic; do not createIntentAutoSwap with the fee token as output.
  • sodax.config is the only source of truth for config after overrides. Direct imports of spokeChainConfig / sodaxConfig from @sodax/types / @sodax/sdk are packaged-default snapshots and miss both initialize() updates and new Sodax(config) overrides.
  • Cross-chain destination quirks. Stellar destinations need a trustline set for the receiving asset. NEAR needs NEP-141 storage registration. Bitcoin uses PSBT.
  • Supplied collateral on money market is NOT counted twice for TVL. Explain to the user if they see MM supplied assets alongside vault TVL and expect double-counting.
  • sodax.hubProvider.getUserHubWalletAddress resolves to an address, not Result<T>. Await it and use the value; do not branch on .ok.
  • getPartnerFee exists twice. sodax.swaps.getPartnerFee(inputAmount: bigint): bigint is synchronous; sodax.api.swaps.getPartnerFee(query) is async and Result-wrapped. Same for getSupportedSwapTokens on sodax.config and sodax.swaps.
  • sodax.migration.balnSwapService still throws. Wrap claim, claimUnstaked, stake, unstake, cancelUnstake, getDetailedUserLocks in try / catch; every other migration method returns Result<T>.

Verify

Read-only smoke test (no wallet required): after sodax.config.initialize(), call sodax.swaps.getQuote(...) for a small SOL→USDC quote (Solana → Arbitrum). If quote.value.quoted_amount prints as a bigint, the SDK, config init, and solver connectivity are all working. If !quote.ok, read quote.error.detail.message.

Version notes

Version-gated behaviour and v1→v2 migration crosswalk. Refresh from source before relying on any specific version.

  • Multi-RPC failover (hub: { rpcUrls: [...] } and per-chain rpcUrls): requires @sodax/sdk >= 2.1.0-rc.3. Without it, only single-endpoint construction is available.
  • ROBINHOOD_MAINNET ('robinhood') is the newest spoke. Present from @sodax/sdk 2.2.0-rc.1. Guard with sodax.config.isValidSpokeChainKey(...) if you must support older pins.
  • ChainKeys string values are stable across versions; enum member names are v2. Do not use v1 SONIC_MAINNET_CHAIN_ID etc. Migration crosswalk lives in the v1-to-v2 knowledge subtree of sodax-sdk.
  • sodax.migration.balnSwapService methods still throw rather than return Result<T> (tech debt tracked; every other migration method returns Result<T>).
  • sodax.backendApi is aliased as sodax.api. Use sodax.api in new code.

Docs & granular skills

The intent table above and the per-flow pointers under each Common Flow route to the specific granular skill for depth.

When SODAX is the wrong tool

Not for chain work that does not involve SODAX. For Solana-native swap-only, use Jupiter. For Solana-native lending, use Kamino, MarginFi, or Solend. For generic message-passing between chains, use Wormhole or LayerZero. For pure bridging without lending / swap / staking overlap, use deBridge or Across. If the user has explicitly named another SDK, defer to it; this skill does not substitute.

Related skills