Skip to main content
Learn how to configure fees and monetize your SODAX SDK integration. When using the SODAX SDK, you can monetize your integration by collecting fees from the transactions processed through your application.

Two integration paths

On the Swaps API path there is no default: omit partnerFee and the swap succeeds with no fee. See Swaps API monetization. Bridge API v2 is different — omitted partnerFee falls back to the backend’s bridgePartnerFee. The sections immediately below describe the orchestrator path. Fees are configured globally per feature when creating the Sodax instance, and the swap, bridge and leverage-yield features additionally accept a per-action override: swap’s getQuote() and leverage-yield’s getQuote() take an optional partnerFee, swap’s swap() / createIntent() and bridge’s bridge() / createBridgeIntent() read extras.partnerFee, and leverage-yield’s deposit() / vaultSwap() / createVaultIntent() take partnerFee directly. When omitted, the configured fee applies.

Defining Fee

Global fee configuration

On the orchestrator path, configure fees globally per feature with new Sodax({...configuration}). This applies to sodax.swaps / sodax.moneyMarket / sodax.bridge / sodax.leverageYield only (not sodax.api.*):
Each feature’s fee is independent: swaps.partnerFee applies to sodax.swaps only and does not apply to leverage-yield vault flows, even though those are executed as solver intents. A feature with no partnerFee of its own falls back to the global fee.

Per-request fee configuration

The swap feature supports a per-action fee override that beats the configured swaps.partnerFee (per-feature override, else global). When omitted, the configured fee applies. This is what lets a backend construct swap intents on behalf of partners whose fee differs per request. Precedence is the same for every feature that accepts an override: per-action fee → feature fee → global fee → no fee. Two consequences worth knowing:
  • An override of undefined is not “no fee” — it falls back to the configured fee. partnerFee: undefined and omitting partnerFee behave identically, so a lookup that misses (partnerFee: feesByPartner[id]) charges the configured fee rather than nothing. If you need a specific request to charge nothing while a fee is configured, pass a zero fee explicitly: { address, percentage: 0 }.
  • To read the fee without placing an order, use sodax.swaps.getPartnerFee(amount) (or sodax.bridge.getFee(amount)) rather than quoting with a zero fee — quotes already return the net amount, so they cannot tell you the fee that was deducted.
Money market is the exception: it has no per-action override, so every money-market flow charges the configured moneyMarket.partnerFee (else the global fee).

Quote request

SwapService.getQuote() deducts the partner fee from the amount before forwarding to the solver, so quoted_amount reflects the net output. No fee field appears in the solver request payload — the deduction happens client-side. Pass an optional partnerFee second argument to match a per-action override used on createIntent / swap; omit it to use the configured swap fee.

Swap request

The fee is applied automatically by the service, which encodes it into the intent it builds, so no fee field appears on the wire. Pass extras.partnerFee to override the configured swaps.partnerFee for this single action — omit extras (or extras.partnerFee) to use the configured fee.

Leverage-yield vault requests

Vault deposits and withdrawals are solver intents, but they are priced off the leverage-yield fee (leverageYield.partnerFee, else the global fee) — never swaps.partnerFee. Both directions are charged. Pass partnerFee to deposit() or withdraw() (it rides on the returned payload), or directly to vaultSwap() / createVaultIntent(), to override the configured fee for one intent. The fee always comes out of inputAmount, so its denomination differs by direction: a deposit’s input is the token being paid in, while a withdraw’s input is the vault itself — that fee is taken in lsoda* shares, so the receiver accrues vault shares rather than the output token. Both are hub-side ERC20s and both show up in sodax.partners.feeClaim. Quote through sodax.leverageYield.getQuote(), not sodax.swaps.getQuote(): it deducts the same effective leverage-yield fee the intent will charge, so the quote and the intent agree. Quoting a vault flow through the swap service deducts the swap fee instead. You can make the two agree by passing the leverage-yield fee to swaps.getQuote explicitly — using a zero fee ({ address, percentage: 0 }) when that effective fee is undefined, because an explicit undefined falls back to the configured swap fee — but leverageYield.getQuote() resolves it for you and is the canonical path. Pass the same partnerFee to the quote and to the deposit. The fee is deducted from the input before the swap, so a quote taken with a different fee is sized on a different net input. If the intent’s fee is the larger of the two, minOutputAmount derived from that quote is higher than the intent can deliver and the intent will not fill. Omitting partnerFee on both calls is equally safe — both then resolve the same effective leverage-yield fee.

Swaps API monetization

On sodax.api.swaps / @sodax/swaps-api / raw /swaps/*, put partnerFee on the request body — SDK fee config does not apply. Send the same value on quote and create-intent:
checkAllowance / approve accept the field via the shared body but ignore it. Use amount (decimal string) instead of percentage for a flat fee; if both are set the backend uses amount. Reference: SWAPS_API.md, @sodax/swaps-api README.

Partner Fee Claiming

Partners earn fees from every swap or bridge operation they facilitate. Those fees accrue as wrapped ERC-20 tokens on the Sonic hub chain. The sodax.partners service exposes the full lifecycle for retrieving and converting those balances.

Accessing the partner service

Step 1 — Query accrued balances

fetchAssetsBalances issues a multicall to the hub chain and returns only non-zero balances, keyed by the wrapped asset address on Sonic.

Step 2 — Configure auto-swap preferences

Before claiming, configure where swapped proceeds should be delivered. Preferences are stored on-chain and applied automatically to every future createIntentAutoSwap call.
setSwapPreference supports both signed execution (raw: false) and raw transaction building (raw: true). When raw: true, walletProvider must be omitted — the method returns the unsigned transaction object instead.

Step 3 — Approve the fee token

Before swapping, ensure the ProtocolIntents contract is approved to spend the fee token. Native tokens are pre-approved and always return true from isTokenApproved. Some tokens take two transactions. A few ERC-20s of the 2017 TetherToken lineage reject an allowance change from one non-zero value to another, so approveToken sends approve(0) first and waits for it to be mined before the real approval — the user signs twice. This applies here more often than elsewhere, because fee approval always requests an unlimited allowance, so a second claim is always a non-zero to non-zero change. The returned value is still a single transaction hash, the last one’s.

Step 4 — Claim fees (end-to-end swap)

swap is the high-level method that submits the auto-swap intent on-chain and notifies the solver to execute it in one call.
Use createIntentAutoSwap instead of swap when you need manual control over the solver notification step (e.g. to retry independently).

Step 5 — Same-token claims (no conversion) and recovery

The solver cannot fill a swap whose output token equals its input token. If a partner’s configured output token is the same asset as the fee token they are claiming (e.g. claiming BTC fees while the auto-swap output is BTC), swap rejects it up front with VALIDATION_FAILED instead of creating an unfillable intent that would lock the funds. To deliver such a fee as-is, skip the swap and move the wrapped fee token off Sonic with the bridge — to its native chain, or to a Sonic address for same-chain delivery:
Bridging from Sonic pulls the token via the partner’s hub-wallet router, so it needs a bridge allowance first (sodax.bridge.isAllowanceValid / sodax.bridge.approve) — a different spender than the ProtocolIntents approval used by swap. If a same-token claim was already submitted before this guard existed, the funds sit in an unfillable intent. Recover them with cancelIntent, which calls ProtocolIntents’ own cancelIntent(fromToken, toToken) and refunds the locked amount to the partner. This is the only authorized cancel path: the intent’s creator is the ProtocolIntents contract, so the generic SwapService.cancelIntent reverts Unauthorized().

Error handling

All partners.feeClaim methods return Promise<Result<T, SodaxError<PartnerErrorCode>>> from the unified vocabulary. Discriminate on error.code (closed reason-only union) and error.feature === 'partner'. The original lower-level failure is preserved on error.cause; operation/method partition is on error.context.action / error.context.method.
PartnerErrorCode is the narrow union 'VALIDATION_FAILED' | 'LOOKUP_FAILED' | 'APPROVE_FAILED' | 'EXECUTION_FAILED' | 'UNKNOWN'. Use isPartnerError(e) instead of instanceof SodaxError in dapp/app code (bundle-safe).

Raw transaction mode

Every write method on PartnerFeeClaimService supports raw: true to obtain the unsigned transaction instead of broadcasting it. When raw: true, the walletProvider field must be omitted — TypeScript enforces this at compile time.