What it is
Swaps without Borders is a cross-network swap app where every swap pays a flat 0.1% partner fee, and 100% of that fee goes to a charity the community picks. No protocol cut, no ops skim. The whole charity mechanism is one object in the SDK config. SODAX handles the fee deduction, the routing and the accrual. My code handles the community side: points, votes and payouts. The loop:- You connect a wallet and swap across networks through the SODAX solver.
- The SDK deducts a 0.1% partner fee, which accrues on the Sonic hub to a public charity wallet (
0x95A8E0BcF616f7eF630b0D923667fbF52AA721AD). - Once the solver confirms your swap, you earn points based on its USD value.
- When the pot crosses a threshold, an admin opens a vote round with three charities from the shortlist. Connected wallets vote, weighted by their points.
- The winning charity takes the pot and the cycle restarts.
The design idea: the fee is the product
Most swap apps hide the fee. This one is built around it, so the money path is the most auditable part of the codebase:- One reviewable diff turns money on. For the first week the
Sodaxconfig had nopartnerFeeat all, and the build log said so explicitly (“partnerFeeconfig touched: 0”). The fee went live on Day 9 as its own commit. - One dedicated wallet. It is used only for charity fees, so every inflow is a fee and every outflow is a payout.
- Points only for real swaps. The server asks the solver whether an intent executed before crediting anything. The client cannot self-confirm.
Scaffolded against the Builders MCP
I pointed Claude Code at the SODAX Builders MCP and asked for a cross-network swap component with partner fees. Going frompnpm create next-app to a working mainnet swap took one afternoon, and I read zero SDK docs outside the MCP that day.
What the MCP handed over, per the Day 3 build log:
- The
SodaxProvider>QueryClientProvider>SodaxWalletProvidernesting, and why the config must be one stable reference. - The shapes of
useQuote,useSwap,useSwapAllowance,useSwapApprove, including the nestedparams.payloadpattern. CreateIntentParamsfield by field, includingsrcChainKey(request side) vssrcChain(read side).ChainKeysconstants and token addresses with decimals.
sodax_get_swap_tokens). On Day 11 it filled out my picker with 13 more tokens, taking the catalog from 103 to 116.
The SDK pieces that carried this
1. Partner fee as config
The whole charity mechanism is one object passed into the SDK config:amount * percentage / 10_000n).
2. The swap hooks from dapp-kit
Quote, allowance, approve, swap. The quote isexact_input, and the intent’s minimum output comes from the quote minus user-set slippage:
useSwap accepts a timeout, so the UI always gets an answer back within a bound I choose.
3. Fees accrue on the hub
Partner fees accumulate as wrapped tokens on the Sonic hub, one per source token people swapped from. The/charities panel reads the live basket server-side with one call:
feeClaim lifecycle on an admin page, and the claim card only acts for that wallet.
4. Server-side confirmation with getStatus
Points decide vote weight, so they cannot be farmable. After useSwap returns, the client logs every candidate hash (source tx, destination tx, solver intent hash). A server route then asks the solver which one it recognises:
SOLVED credits points, FAILED credits nothing, and NOT_FOUND moves on to the next hash. The solver is the source of truth, and getStatus keeps the check to a few lines.
5. One wallet layer for every ecosystem
@sodax/wallet-sdk-react covers EVM plus Solana, Sui, Injective, ICON, Stellar, NEAR, and Bitcoin (via Radfi) with the same hooks: useXAccount, useWalletProvider, useXBalances, useEvmSwitchChain. v1.0 lists swaps across 19 networks, and non-EVM routes quote live through the same hooks.
useEvmSwitchChain also drives a clean pre-sign guard: when the wallet is on a different network than the route’s source, the button becomes “Switch wallet to {network}”. Balance is checked before signing too, so every swap that reaches the wallet is one that can go through.
6. Self-serve recovery from the hub
v1.0.2 added a “Recover stuck funds” panel. Because assets that do not reach their destination wait in the user’s hub wallet on Sonic, users can always get them back themselves.recovery.ts scans the user’s EVM hub-wallet derivations, builds the withdrawal with EvmAssetManagerService.withdrawAssetData, decodes the payload to prove it is exactly one AssetManager.transfer of that asset back to the user, dry-runs it with eth_call, and only then asks for a signature. Every building block came from the SDK.
What made it easy
- The Builders MCP. An afternoon from an empty Next.js app to a real mainnet swap, with the provider tree, hook shapes and intent fields served straight to my coding agent.
- Partner fees as plain config. One object routes 100% of the fee to the charity across swaps, bridge and money market.
- Readable fee accrual.
fetchAssetsBalancesturns the hub basket into a live charity pot with one call. - The wallet layer. Eight ecosystems behind one hook shape.
- Smooth upgrades. The mid-build bump from
2.0.0-rc.1to2.0.0-rc.8was type-clean for my usage.
Tips for builders
- Check the SODAX Intents spec when building a token picker: native gas tokens are used on the hub, so offer ERC-20s on spoke networks.
- Pass a
timeouttouseSwapand validate network and balance before signing. - In Next.js, render the provider tree after a client mount, since wallet adapters touch browser globals.
Stack
Next.js 16 (webpack) · React 19 ·@sodax/sdk, @sodax/dapp-kit, @sodax/wallet-sdk-react, @sodax/types all at 2.0.0-rc.8 · TanStack Query · viem · Prisma + Supabase Postgres · CoinGecko pricing · Vercel
Try it
Live: charity-swap.vercel.app Repo (MIT): github.com/hazy2go/swaps-without-borders.BUILD-LOG.md and CHANGELOG.md have the full day-by-day story.
This is mainnet code. If you run it locally, test with small amounts.