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 withnew Sodax({...configuration}).
This applies to sodax.swaps / sodax.moneyMarket / sodax.bridge / sodax.leverageYield only
(not sodax.api.*):
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 configuredswaps.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
undefinedis not “no fee” — it falls back to the configured fee.partnerFee: undefinedand omittingpartnerFeebehave 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)(orsodax.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.
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. Passextras.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
Onsodax.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. Thesodax.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 futurecreateIntentAutoSwap 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 returntrue 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.
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:
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
Allpartners.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 onPartnerFeeClaimService 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.