WDK
Swap and Bridge in WDK: A Guide to Five Routers
Tether's Developer Relations

Swap and Bridge in WDK: A Guide to Five Routers

Raquel Carrasco Gonzalez
Article byRaquel Carrasco GonzalezDeveloper Relations Services
By the end of this post, you'll understand what WDK swidge protocol modules are, the specific interface that combines swap and bridge in one shape, and how to compose them into a real product.

What we've built so far

This series introduces complex blockchain interactions and makes them simple with WDK. If this is your first post, we recommend starting with More Than Transfers: Swaps, Bridges, Lending, and Fiat in WDK, the prerequisite for this protocol series. It separates wallet accounts from the protocols that use them. 


For the wallet basics, the core of WDK, you can start with Introduction to WDK: Tether's Wallet Development Kit, and the Wallet Fundamentals: Build, Sign, and Send Your First Transaction in WDK and Wallet Fundamentals: Sending USD₮ in WDK, to understand the basic architecture of WDK, as well as the Gasless Transactions in WDK: A Chain-by-Chain Guide, to keep the gasless overview nearby.

Step 1: Understand how Swidge works

Before getting into the Swidge modules API, let’s separate its two operations: swap and bridge.

  • A swap exchanges one asset for another at a quoted exchange rate (for example, USD₮ for Bitcoin). You sell one asset to buy another. Swaps can happen on-chain or off-chain; here, you use WDK to interact with protocols that execute on-chain.
  • A bridge moves value between blockchains, for example, USD₮ from Ethereum to Solana (or any other chain where USD₮ exists). You receive the corresponding asset on the destination chain. The amount you receive should be close to 1:1, but it depends on the route and its fees, so check the quote rather than assume a 1:1 result.

But in reality, the user usually holds USD₮ on Ethereum and wants, for example, SOL on Solana. An ordinary token transfer can't deliver that result. You need a route between chains and assets, and you may need to swap before or after bridging, with an interface that should describe the requested outcome without assuming the route's internal steps.


This is where SwidgeProtocol comes in. It gives you a shared interface for working with routing providers across chains. You discover supported chains and tokens with getSupportedChains() and getSupportedTokens(), preview a route with quoteSwidge(), execute it with swidge(), and track its status with getSwidgeStatus(). Each provider determines which operations it supports.


But keep in mind that cross-chain settlement is asynchronous, not atomic. A successful source transaction does not mean the funds are available on the destination chain. Your wallet should make that distinction clear, for example, from Ethereum → Solana mainnet.

  • User: “The Ethereum transaction succeeded. Are my funds available on Solana?”
  • Wallet: “Your source transaction succeeded. Your funds have not arrived on Solana yet.”

Step 2: Choose your router

Next, you need to distinguish between SwidgeProtocol, WDK’s shared API standard, and the provider modules that implement it. The API defines common methods for discovery, quoting, execution, and tracking, which improves the developer and integration experience. Second, each provider determines which operations, tokens, and networks its module supports. You use the same API method names across providers, but each module supports operations differently.


The provider list can grow as protocol teams contribute integrations to WDK. You can find the available providers in the Swidge index. Remember, they share an interface, but their token and chain coverage differs.


You also have modules with dedicated swap and bridge interfaces. Velora provides EVM DEX aggregation. USD₮0 supports token transfers from EVM sources to eligible EVM and non-EVM destinations. These modules use their own interfaces previous to the SwidgeProtocol API.

Step 3: Get your API keys

Before you start your Swidge project, check whether the provider you want to use requires an API key. Some providers allow public access; others require credentials or offer keys for higher request limits. You will add any required credentials to the protocol configuration later.


For example:

  • LI.FI: You can start without an API key. For higher request limits, obtain a key through the Partner Portal and keep it server-side. 
  • Symbiosis: Version 1.3.0 requires no API key. Its partnerId identifies your integration; it is not an authentication secret.
  • Rhino.fi: Create your project and manage keys in the Console. Public keys support quoting and bridge execution; secret keys also permit privileged access, including history.
  • Orchestra: Create a dashboard account. After Flashnet approves access, open API Keys. Choose a secret server key for backend use or a scoped client key for supported client integrations.
  • 0x: Create an account, team, and app in the 0x Dashboard, enable Swap API, and reveal your key. This module does not use the separate Gasless API.

Check your provider’s request limits before setting quote refresh and polling intervals. Public access and authenticated access may have different allowances.

Step 4: Register the module

In WDK, the structure is always the same: as with wallet modules, install and import the components your integration uses; the WDK core orchestrator, a compatible wallet module for your source chain, and your chosen Swidge provider module.

For this example, you will use the EVM wallet module and LI.FI:

npm install @tetherto/wdk @tetherto/wdk-wallet @tetherto/wdk-wallet-evm-erc-4337 @lifi/wdk-protocol-swidge-lifi @symbiosis-finance/wdk-protocol-swidge-symbiosis @tetherto/wdk-wallet-solana


What's happening:

  • @tetherto/wdk provides the core orchestrator for registering modules and accessing accounts.
  • @tetherto/wdk-wallet supplies the protocol base classes used in registration.
  • @tetherto/wdk-wallet-evm-erc-4337 provides your ERC-4337 smart account for sponsored EVM transactions.
  • @lifi/wdk-protocol-swidge-lifi provides the LI.FI implementation of SwidgeProtocol.

Once you have installed the WDK modules you are gonna need, you will have to import them to your project. Remember to use a wallet seed managed by your application's secret lifecycle. For sponsored gas, you also need a configured paymaster; switching the wallet import alone isn't enough. LI.FI compatibility

Your application supplies seedPhrase, Ethereum mainnet rpcUrl, bundlerUrl, paymasterUrl, and the applicable sponsorshipPolicyId; you can check more about gasless transactions and EVM gasless.

import WDK from '@tetherto/wdk'
import WalletManagerEvmErc4337 from '@tetherto/wdk-wallet-evm-erc-4337'
import {
  LifiSwidgeProtocol,
  LifiStatusError,
  NATIVE_VALUE_BRIDGE_DENY_LIST
} from '@lifi/wdk-protocol-swidge-lifi'

const wdk = new WDK(seedPhrase)
  .registerWallet('ethereum', WalletManagerEvmErc4337, {
    chainId: 1,
    provider: rpcUrl,
    safeModulesVersion: '0.3.0',
    bundlerUrl,
    paymasterUrl,
    isSponsored: true,
    sponsorshipPolicyId
  })
  .registerProtocol('ethereum', 'swidge', LifiSwidgeProtocol, {
    denyBridges: NATIVE_VALUE_BRIDGE_DENY_LIST,
    allowNativeValue: false,
    maxNetworkFeeBps: 100,
    maxProtocolFeeBps: 50
  })

const account = await wdk.getAccount('ethereum', 0)
const swidge = account.getSwidgeProtocol('swidge')

What's happening:

  • seedPhrase determines your account owner. Fund the resulting smart account with your input tokens.
  • 'ethereum' connects wallet registration, protocol registration, and account retrieval. Mismatched labels break that connection.
  • chainId: 1 selects Ethereum mainnet. Your infrastructure must use the same network.
  • provider: rpcUrl supplies Ethereum RPC access. A wrong network produces incorrect chain context.
  • safeModulesVersion: '0.3.0' selects the documented Safe module version. Unsupported values fail configuration.
  • bundlerUrl supplies the service that submits your UserOperations. An incompatible endpoint prevents submission.
  • paymasterUrl supplies sponsorship responses. An unavailable service or rejected request prevents sponsored execution.
  • isSponsored: true selects sponsored gas payment.
  • sponsorshipPolicyId identifies your provider's sponsorship policy where required. An invalid or ineligible policy can cause rejection. ERC-4337 configuration
  • 'swidge' is your application-defined protocol label. Retrieve it with the same label.
  • denyBridges filters bridges known to require native token value.
  • allowNativeValue: false rejects transactions that still require native value before sending approvals. Gas sponsorship does not cover the route's native token value.
  • maxNetworkFeeBps: 100 limits reported source gas to 1% of input USD value; missing pricing can weaken or skip this check.
  • maxProtocolFeeBps: 50 limits mapped protocol fees to 0.5%. Exceeding either applicable cap rejects execution. These checks do not cover every cost. LI.FI configuration
  • 0 selects your first derived smart account.
  • LifiStatusError is retained for the later polling example.

Step 5: Discover routes at runtime

Once you import the protocol into the project, you can request a quote and check which chains and tokens your provider currently lists. For this example, verify USD₮ on Ethereum and USD₮0 on Arbitrum.

const chains = await swidge.getSupportedChains()
const requiredChainIds = [1, 42161]

for (const chainId of requiredChainIds) {
  if (!chains.some(chain => Number(chain.id) === chainId)) {
    throw new Error(`Required chain unavailable: ${chainId}`)
  }
}

const sourceTokens = await swidge.getSupportedTokens({ fromChain: 1 })
const destinationTokens = await swidge.getSupportedTokens({ fromChain: 42161 })

const fromToken = '0xdAC17F958D2ee523a2206206994597C13D831ec7'
const toToken = '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9'

for (const [tokens, address] of [
  [sourceTokens, fromToken],
  [destinationTokens, toToken]
]) {
  const token = tokens.find(
    token => token.token.toLowerCase() === address.toLowerCase()
  )

  if (!token || token.decimals !== 6) {
    throw new Error(`Expected token unavailable or decimals mismatch: ${address}`)
  }
}

What's happening:

  • requiredChainIds identifies Ethereum mainnet (1) and Arbitrum One (42161). If either is missing from discovery, your application stops before quoting.
  • fromChain selects the token catalog to fetch. In LI.FI's discovery method, you use this field for both catalogs, even when fetching tokens for your destination.
  • fromToken selects Ethereum USD₮; toToken selects Arbitrum USD₮0. Token addresses belong to a specific chain. Reusing the source address on the destination can select the wrong asset.
  • token.token contains the discovered identifier. The comparison ignores capitalization for these EVM addresses.
  • token.decimals !== 6 checks the precision your example expects. A mismatch stops the flow before you calculate amounts using the wrong number of decimal places.

These addresses define your worked example's selection, not a permanent supported-token list. Build your application's selectors from discovery, then request a quote for the selected pair. Finding both assets in the catalogs doesn't mean a route connects them. LI.FI implementation


But what if you want to swap USD₮ for SOL?


WDK is completely modular; you can add another provider to the same wallet. Keep LI.FI registered as 'swidge', then register Symbiosis under a separate label to explore USD₮ on Ethereum → SOL on Solana.


Multiple providers also let you compare quotes for routes they both support, or request an alternative quote when one provider cannot find a route.


Add the import below and the registration call before your existing getAccount() call:


import SymbiosisProtocol from '@symbiosis-finance/wdk-protocol-swidge-symbiosis'
// Add to the existing WDK instance, after LI.FI registration.
wdk.registerProtocol('ethereum', 'swidge-symbiosis', SymbiosisProtocol, {
  chain: 'Ethereum',
  maxProtocolFeeBps: 50
})
const account = await wdk.getAccount('ethereum', 0)
const swidge = account.getSwidgeProtocol('swidge')
const symbiosis = account.getSwidgeProtocol('swidge-symbiosis')

const chains = await symbiosis.getSupportedChains()
const solana = chains.find(chain => chain.name.toLowerCase() === 'solana')

if (!solana) {
  throw new Error('Solana unavailable in provider discovery')
}

const sourceTokens = await symbiosis.getSupportedTokens({ fromChain: 1 })
const destinationTokens = await symbiosis.getSupportedTokens({
  fromChain: solana.id
})

What's happening:

  • 'ethereum' attaches Symbiosis to your existing Ethereum wallet registration.
  • 'swidge-symbiosis' gives the additional provider its own application-defined label. Your LI.FI registration remains accessible as 'swidge'.
  • chain: 'Ethereum' declares Symbiosis's source network. It must match your wallet's network; the module does not verify that match.
  • maxProtocolFeeBps: 50 caps fees mapped as protocol fees at 0.5%. It does not cap affiliate fees or establish a total transaction-cost ceiling.
  • Account index 0 retrieves your first derived account, with both protocols available.
  • fromChain: 1 fetches Ethereum tokens; fromChain: solana.id fetches Solana tokens using the discovered identifier. Symbiosis configuration

Your application uses the same method names for both providers: discover with getSupportedChains() and getSupportedTokens(), preview with quoteSwidge(), execute with swidge(), and track with getSwidgeStatus().


For price comparison, quote the same input amount, destination asset, and recipient, with equivalent slippage settings using different imported providers. Compare output and fees together. This works only when both providers support the requested route. 


And from SOL to USD₮?


For SOL on Solana → USD₮ on Ethereum, you also need a Solana source wallet. Your Ethereum ERC-4337 account cannot sign the Solana transaction.

You can keep both existing providers and add another wallet registration to the same WDK instance:


import WalletManagerSolana from '@tetherto/wdk-wallet-solana'

wdk
  .registerWallet('solana', WalletManagerSolana, {
    provider: solanaRpcUrl
  })
  .registerProtocol('solana', 'swidge-symbiosis', SymbiosisProtocol, {
    chain: 'Solana',
    maxProtocolFeeBps: 50
  })

const solanaAccount = await wdk.getAccount('solana', 0)
const solanaSwidge = solanaAccount.getSwidgeProtocol('swidge-symbiosis')


What's happening:

  • 'solana' associates this provider instance with your Solana wallet.
  • provider: solanaRpcUrl connects that wallet to Solana. Use the network supported by your selected route.
  • 'swidge-symbiosis' identifies the protocol within this wallet registration. It can match the label used on your Ethereum wallet.
  • chain: 'Solana' tells Symbiosis that the source account is on Solana.
  • maxProtocolFeeBps: 50 caps mapped protocol fees at 0.5%; it is not a total-cost ceiling.
  • 0 selects your first derived Solana account.

You then use the same discovery, quote, execution, and status methods. Set the destination to Ethereum, select USD₮ at 0xdAC17F958D2ee523a2206206994597C13D831ec7, and explicitly supply your Ethereum recipient.


Step 6: Quote, execute, poll

Now we return to our LI.FI instance, swidge, and request a quote for Ethereum USD₮ → Arbitrum USD₮0. Keep the route inputs together with the quote your user can review.

Your application supplies arbitrumRecipient: a validated Arbitrum address controlled by the intended recipient.

const route = Object.freeze({
  fromToken,
  toToken,
  toChain: 42161,
  recipient: arbitrumRecipient,
  fromTokenAmount: 100_000_000n,
  slippage: 0.01
})

const quote = await swidge.quoteSwidge(route)

What's happening:

  • fromToken selects Ethereum USD₮. A wrong address selects the wrong input asset.
  • toToken selects Arbitrum USD₮0. Its contract must belong to the destination chain.
  • toChain: 42161 selects Arbitrum One. Omitting it requests a same-chain route in LI.FI.
  • recipient explicitly selects the destination address. Do not assume your Ethereum smart-account address is usable on Arbitrum. Confirm control of the destination account before sending.
  • fromTokenAmount is 100 USD₮ in six-decimal base units. Passing 100n requests only 0.0001 USD₮.
  • slippage: 0.01 sets a 1% tolerance.
  • Object.freeze() prevents changes to these route fields while confirmation is pending. It does not lock the provider's price.

Display the chains, assets, recipient, input amount, expected output, minimum output, and itemized fees. Convert quote.toTokenAmount and quote.toTokenAmountMin using the destination token's decimals.


Explain that approvals may precede the route transaction. Your ERC-4337 paymaster must accept the required source operations. A successful route quote does not guarantee sponsorship.


Call the following function only after the user confirms. saveRoute is your application's durable-storage callback, not a WDK method.


async function executeConfirmedRoute(saveRoute) {
  const result = await swidge.swidge({
    ...route,
    minAmountOut: quote.toTokenAmountMin
  })

  try {
    await saveRoute({ id: result.id, hash: result.hash })
  } catch (cause) {
    throw Object.assign(new Error('Route storage failed', { cause }), {
      operation: result
    })
  }

  const deadline = Date.now() + 10 * 60_000

  while (Date.now() < deadline) {
    try {
      const state = await swidge.getSwidgeStatus(result.id)
      if (state.status !== 'pending') return state
    } catch (error) {
      const awaitingIndex =
        error instanceof LifiStatusError &&
        error.lifiStatus === 'NOT_FOUND'

      if (!awaitingIndex) throw error
    }

    await new Promise(resolve => setTimeout(resolve, 10_000))
  }

  throw new Error(
    'Settlement unresolved; resume tracking the saved operation'
  )
}

What's happening:

  • ...route carries the reviewed inputs into execution.
  • minAmountOut passes the displayed minimum to LI.FI's fresh-quote check. If the new quote's minimum falls below it, execution is rejected before approvals.
  • id identifies the route operation; hash records its source transaction. Save both before polling.
  • cause preserves the storage error. operation carries the returned execution result so your error handler can recover it without sending again.
  • deadline limits this polling session to ten minutes. It does not establish a settlement deadline.
  • 10_000 sets a ten-second polling interval.
  • LifiStatusError with lifiStatus === 'NOT_FOUND' allows for indexing delay. Other errors propagate.
  • state.status !== 'pending' hands other states to your application. Returning does not necessarily mean success. LI.FI execution and status handling

Interpret the returned state before updating your UI:

State

Your next action

pending

Continue bounded polling.

completed

Report settlement complete.

failed

Inspect the recorded transactions and explain the failure.

partial

Explain which asset arrived; do not report full completion.

refunded

Report the refund outcome.

refund-pending

Continue tracking through your recovery flow.

action-required

Surface the required recovery action.

⚠️ A tracking timeout is not a failed transfer. Preserve the operation and resume status checks. Do not automatically execute through Symbiosis or another provider because polling stopped. If swidge() itself fails after a possible submission, investigate the source account before retrying.

Keep cleanup in the lifecycle that owns your shared WDK instance. Call dispose() when you are finished with its key-bearing accounts. Because LI.FI and Symbiosis now share that instance, disposing it inside this helper would also end other flows using those accounts. Ensure cancellation and error paths reach cleanup too.

Step 7: Swap the router

You already registered LI.FI and Symbiosis on the same Ethereum wallet. To select another provider, retrieve its registration label:

const selectedSwidge = account.getSwidgeProtocol('swidge-symbiosis')
const alternativeQuote = await selectedSwidge.quoteSwidge(route)

What's happening:

  • 'swidge-symbiosis' selects your existing Symbiosis registration. Use 'swidge' to select LI.FI.
  • route preserves the Ethereum input, Arbitrum output, recipient, amount, and slippage from Step 6.
  • quoteSwidge() requests a new indicative quote. Review it before choosing a provider; your user's earlier confirmation covered the LI.FI quote.

The method names stay the same. Both instances expose discovery, quoting, execution, and status methods. You can also request quotes from both providers for the same supported route, then compare expected output, minimum output, and itemized fees.

Keep the provider's identity alongside each quote. Execute through the provider that produced the selected quote, and use that provider to track the resulting operation.

⚠️ The execution protections differ. Do not pass the Step 6 execution block to Symbiosis unchanged:

Behavior

LI.FI

Symbiosis 1.3.0

Reviewed minimum

minAmountOut checks the fresh quote against the accepted minimum.

Does not enforce minAmountOut.

Network-fee cap

Checks reported source gas, subject to pricing limitations.

Maps no fees as network; this cap does not constrain source gas.

Protocol-fee cap

Applies to mapped protocol fees.

Applies to mapped protocol fees; excludes affiliate fees.

Status lookup

NOT_FOUND requires the LI.FI-specific error handling shown earlier.

HTTP 404 maps to pending; retain a polling deadline.

Gasless route filtering

Documents denyBridges and allowNativeValue.

Does not document equivalent configuration fields.

These differences follow the LI.FI configuration and Symbiosis execution reference.


Provider fallback starts with a new quote. If LI.FI cannot quote a route, you can try Symbiosis and show its result for confirmation. If an execution attempt has an uncertain outcome, investigate it before switching providers and submitting again.


You reuse the application flow, but must preserve the guarantees your product requires. If you need LI.FI's accepted-minimum check or native-value rejection, keep execution on LI.FI until the alternative provides verified equivalent protection.


Common Errors

  • Missing registration: Check the protocol label, registration order and resolved base-wallet versions.
  • Unsupported asset or no route: Refresh discovery, verify chain-specific token identifiers, then try another amount or provider.
  • Slippage or fee limit exceeded: Request a fresh quote. Never silently raise the user's limits.
  • Approval or sponsorship failure: Check allowance, receipts and paymaster policy. Ethereum USD₮ may require an allowance reset.
  • Polling stays pending: Keep a deadline and inspect the source transaction. With ERC-4337, distinguish the UserOperation hash from the EVM transaction hash. Do not resend because tracking stopped.
  • Solana gasless rejected: Symbiosis 1.3.0 failed the offline compatibility test with Solana gasless beta.4.

Provider error references: LI.FI, Symbiosis, 0x.


Key Insights

  • Discover, then quote: Listed assets do not guarantee an available route.
  • Quote before sending: Preserve reviewed inputs and verify your provider's execution protections.
  • Track settlement: Save operation IDs and distinguish UserOperation hashes from transaction hashes.
  • Switch deliberately: Shared methods do not guarantee the same routes, gasless support, or safeguards.
  • Pin compatible versions: Dependency mismatches can break registration.
  • Check ownership: All five routers compared here are independently maintained Community modules.


What's Next

Follow WDK's official X account for announcements. Join the community on Discord to ask questions, share what you're building, and get help from the team.


Explore all documented Swidge modules in the documentation. For another guided walkthrough, coming up next: Lending in WDK: Aave, Morpho, and Building a Savings App. You will move from tracking cross-chain routes to managing lending positions.


Resources: core registration, Swidge modules, LI.FI usage, Symbiosis usage.


Receive the latest WDK news

Click to subscribe to the WDK newsletter and stay updated!