[Swap] Add VULT-scaled affiliate fee to Android Jupiter + route native SOL via THORChain
See 00-shared-spec.md for the cross-platform routing/fee spec.
Context
Android already uses Jupiter as its Solana swap provider (and currently the only
Solana route), but with zero affiliate fee — the quote sends only
inputMint/outputMint/amount/slippageBps and the swap body has no feeAccount:
data/.../api/JupiterApi.kt — getSwapQuote() (no platformFeeBps, no feeAccount)
data/.../repositories/swap/JupiterQuoteSource.kt — quote source
app/.../ui/models/swap/SwapQuoteManager.kt:549-558 — providerPriority (Jupiter = lowest, Solana-only)
Every other provider charges a VULT-scaled 50 bps (GetDiscountBpsUseCase,
THORChainSwaps, KyberApi, OneInchApi, LiFiChainApi). We want Jupiter to match, and
native SOL cross-chain to prefer THORChain (consistency with iOS/SDK).
Scope
JupiterApi.kt — add affiliate fee:
- Add
affiliateBps: Int? (and the output mint) to getSwapQuote(...).
- Quote:
parameter("platformFeeBps", affiliateBps) when > 0.
- Swap body: add
put("feeAccount", <ATA(feeOwner, outputMint)>).
JupiterQuoteSource.kt — pass the VULT-scaled bps through. Compute via
GetDiscountBpsUseCase → affiliateBps = max(0, 50 − discountBps) (same as Kyber/1inch/LiFi).
- Routing (
SwapQuoteManager.kt providerPriority):
- Native SOL ↔ other chain: THORChain first (confirm THORChain SOL support is wired
on Android; the SDK enables it), then SwapKit/LiFi.
- SOL ↔ SPL / SPL ↔ SPL: Jupiter first, LiFi fallback.
- SPL ↔ other chain: THORChain (if supported) → SwapKit → LiFi (Jupiter not eligible).
- Keep Jupiter gated to same-chain Solana only.
- feeAccount derivation — ATA of
(owner = fee wallet, mint = output mint). Use
wallet-core / Solana SPL ATA derivation. Create-if-missing or pre-provision common mints.
Fee account
Fee owner wallet: 8iqhrtBzMcYLR6c6FkzeoMHibedYDkHvLKnX2ArNie5z.
Not using Jupiter's on-chain referral program (we keep 100% of the fee).
Files
data/src/main/kotlin/com/vultisig/wallet/data/api/JupiterApi.kt
data/src/main/kotlin/com/vultisig/wallet/data/repositories/swap/JupiterQuoteSource.kt
app/src/main/java/com/vultisig/wallet/ui/models/swap/SwapQuoteManager.kt
app/.../data/usecases/GetDiscountBpsUseCase.kt (reuse)
Tests
- Jupiter quote includes
platformFeeBps = VULT-scaled bps; swap includes feeAccount.
- 0-discount user → 50 bps; tiered user → reduced; never negative.
- Native SOL→BTC/ETH routes via THORChain, not Jupiter.
- SOL↔SPL / SPL↔SPL still route via Jupiter.
Acceptance
See 00-shared-spec.md.
Shared spec (cross-platform)
Jupiter / Solana swap alignment — shared spec (read first)
This is the common spec referenced by all four platform issues. Goal: make Solana
swap routing identical across SDK (Windows/extension), iOS, and Android, route
on-Solana token swaps through Jupiter (no aggregator markup), keep native SOL
cross-chain on THORChain, and charge a VULT-scaled affiliate fee on Jupiter.
Routing rules (apply on every platform)
| Swap shape |
Provider preference order |
| Native SOL ↔ other chain (cross-chain) |
THORChain → SwapKit → LiFi |
| SOL ↔ SPL or SPL ↔ SPL (stays on Solana) |
Jupiter → LiFi (fallback) |
| SPL token ↔ other chain (cross-chain) |
THORChain (if pair supported) → SwapKit → LiFi |
Hard constraint: Jupiter is Solana-only and same-chain — it cannot do any
cross-chain route. Only offer Jupiter when from.chain == Solana && to.chain == Solana.
Affiliate fee on Jupiter
Jupiter the aggregator takes 0%; the only fee charged is the one we add.
- Quote (
GET .../swap/v1/quote): add platformFeeBps=<affiliateBps>.
- Swap (
POST .../swap/v1/swap): add feeAccount=<our fee ATA for the fee mint>.
- Fee-mint constraint (ExactIn): the fee mint must be the input or output mint.
Use the OUTPUT mint as the fee mint. (ExactOut would force the input mint —
we run ExactIn, so output-mint is fine.)
Fee account
- Fee owner wallet (Solana):
8iqhrtBzMcYLR6c6FkzeoMHibedYDkHvLKnX2ArNie5z
feeAccount = the Associated Token Account (ATA) of (owner = fee wallet, mint = output mint).
Derive per output mint: getAssociatedTokenAddressSync(outputMint, feeOwner).
- Provisioning decision: create-if-missing, per swap, per asset. Each swap prepends an
idempotent create-ATA instruction (createAssociatedTokenAccountIdempotent) for the
fee ATA (output mint, owner = fee wallet). No pre-provisioning. We are not using the
Jupiter on-chain Referral Program (so Jupiter keeps 0% of our fee), just a self-owned ATA.
- ⚠️ Jupiter does NOT auto-create the
feeAccount ATA. Its /swap endpoint auto-creates
the user's output ATA, but the fee account is a precondition you must supply already
initialized. So we MUST prepend our own createAssociatedTokenAccountIdempotent for the
fee ATA. There is no request flag for this. (Confirmed against Jupiter docs.)
- ⚠️ Rent (~0.002 SOL) is paid by whoever signs the create instruction. Prepended to the
user's swap tx → the user pays (first time per fee mint). If we'd rather absorb it,
create the fee ATA in a separate Vultisig-funded tx (one-time per mint) instead of per
swap. Default per this ticket = create-if-missing in the user's tx. Implementer should
inspect the returned tx instructions to confirm the rent-payer.
Fee consolidation (USDC) — NOT possible via the fee mechanism
Jupiter requires the fee mint to be one of the swap-pair mints (input or output for
ExactIn). So you cannot point every swap's feeAccount at a single USDC account — for a
pair where neither leg is USDC (e.g. SOL→BONK) the fee can only land in SOL or BONK.
Therefore fees accrue across many mints (the output mint of each swap). If consolidating
to USDC is desired later, that's a separate periodic sweep/swap job, independent of the
swap fee mechanism — not a config flag. (Confirmed against Jupiter docs.)
Fee scaling by VULT holding (reuse existing logic — do NOT reinvent)
Every other provider already does: affiliateBps = max(0, baseBps(50) − vultTierDiscountBps).
Apply the same value to Jupiter's platformFeeBps.
- VULT tiers / discount bps (SDK
swap/affiliate/config.ts): base 50; tier discount
Bronze 5 / Silver 10 / Gold 20 / Platinum 25 / Diamond 35 / Ultimate 50 (NFT = +1 tier).
- SDK:
getSwapAffiliateBps(vultDiscountTier).
- Android:
GetDiscountBpsUseCase → max(0, 50 − discount).
- iOS:
bps(for: vultTierDiscount) pattern (see LiFiService 141-142).
API base (already proxied — keep using the proxy)
- Quote:
https://api.vultisig.com/jup/swap/v1/quote
- Swap:
https://api.vultisig.com/jup/swap/v1/swap
- Params:
inputMint, outputMint, amount (raw atomic units, no decimals),
slippageBps (default 50), platformFeeBps (new).
- Swap body:
quoteResponse, userPublicKey, compute-unit config, feeAccount (new).
- NOTE: legacy
quote-api.jup.ag/v6 was deprecated 2025-10-01; we already use swap/v1.
Acceptance (all platforms)
[Swap] Add VULT-scaled affiliate fee to Android Jupiter + route native SOL via THORChain
Context
Android already uses Jupiter as its Solana swap provider (and currently the only
Solana route), but with zero affiliate fee — the quote sends only
inputMint/outputMint/amount/slippageBpsand the swap body has nofeeAccount:data/.../api/JupiterApi.kt—getSwapQuote()(noplatformFeeBps, nofeeAccount)data/.../repositories/swap/JupiterQuoteSource.kt— quote sourceapp/.../ui/models/swap/SwapQuoteManager.kt:549-558—providerPriority(Jupiter = lowest, Solana-only)Every other provider charges a VULT-scaled 50 bps (
GetDiscountBpsUseCase,THORChainSwaps,KyberApi,OneInchApi,LiFiChainApi). We want Jupiter to match, andnative SOL cross-chain to prefer THORChain (consistency with iOS/SDK).
Scope
JupiterApi.kt— add affiliate fee:affiliateBps: Int?(and the output mint) togetSwapQuote(...).parameter("platformFeeBps", affiliateBps)when > 0.put("feeAccount", <ATA(feeOwner, outputMint)>).JupiterQuoteSource.kt— pass the VULT-scaled bps through. Compute viaGetDiscountBpsUseCase→affiliateBps = max(0, 50 − discountBps)(same as Kyber/1inch/LiFi).SwapQuoteManager.ktproviderPriority):on Android; the SDK enables it), then SwapKit/LiFi.
(owner = fee wallet, mint = output mint). Usewallet-core / Solana SPL ATA derivation. Create-if-missing or pre-provision common mints.
Fee account
Fee owner wallet:
8iqhrtBzMcYLR6c6FkzeoMHibedYDkHvLKnX2ArNie5z.Not using Jupiter's on-chain referral program (we keep 100% of the fee).
Files
data/src/main/kotlin/com/vultisig/wallet/data/api/JupiterApi.ktdata/src/main/kotlin/com/vultisig/wallet/data/repositories/swap/JupiterQuoteSource.ktapp/src/main/java/com/vultisig/wallet/ui/models/swap/SwapQuoteManager.ktapp/.../data/usecases/GetDiscountBpsUseCase.kt(reuse)Tests
platformFeeBps= VULT-scaled bps; swap includesfeeAccount.Acceptance
See
00-shared-spec.md.Shared spec (cross-platform)
Jupiter / Solana swap alignment — shared spec (read first)
This is the common spec referenced by all four platform issues. Goal: make Solana
swap routing identical across SDK (Windows/extension), iOS, and Android, route
on-Solana token swaps through Jupiter (no aggregator markup), keep native SOL
cross-chain on THORChain, and charge a VULT-scaled affiliate fee on Jupiter.
Routing rules (apply on every platform)
Hard constraint: Jupiter is Solana-only and same-chain — it cannot do any
cross-chain route. Only offer Jupiter when
from.chain == Solana && to.chain == Solana.Affiliate fee on Jupiter
Jupiter the aggregator takes 0%; the only fee charged is the one we add.
GET .../swap/v1/quote): addplatformFeeBps=<affiliateBps>.POST .../swap/v1/swap): addfeeAccount=<our fee ATA for the fee mint>.Use the OUTPUT mint as the fee mint. (ExactOut would force the input mint —
we run ExactIn, so output-mint is fine.)
Fee account
8iqhrtBzMcYLR6c6FkzeoMHibedYDkHvLKnX2ArNie5zfeeAccount= the Associated Token Account (ATA) of(owner = fee wallet, mint = output mint).Derive per output mint:
getAssociatedTokenAddressSync(outputMint, feeOwner).idempotent create-ATA instruction (
createAssociatedTokenAccountIdempotent) for thefee ATA (output mint, owner = fee wallet). No pre-provisioning. We are not using the
Jupiter on-chain Referral Program (so Jupiter keeps 0% of our fee), just a self-owned ATA.
feeAccountATA. Its/swapendpoint auto-createsthe user's output ATA, but the fee account is a precondition you must supply already
initialized. So we MUST prepend our own
createAssociatedTokenAccountIdempotentfor thefee ATA. There is no request flag for this. (Confirmed against Jupiter docs.)
user's swap tx → the user pays (first time per fee mint). If we'd rather absorb it,
create the fee ATA in a separate Vultisig-funded tx (one-time per mint) instead of per
swap. Default per this ticket = create-if-missing in the user's tx. Implementer should
inspect the returned tx instructions to confirm the rent-payer.
Fee consolidation (USDC) — NOT possible via the fee mechanism
Jupiter requires the fee mint to be one of the swap-pair mints (input or output for
ExactIn). So you cannot point every swap's
feeAccountat a single USDC account — for apair where neither leg is USDC (e.g. SOL→BONK) the fee can only land in SOL or BONK.
Therefore fees accrue across many mints (the output mint of each swap). If consolidating
to USDC is desired later, that's a separate periodic sweep/swap job, independent of the
swap fee mechanism — not a config flag. (Confirmed against Jupiter docs.)
Fee scaling by VULT holding (reuse existing logic — do NOT reinvent)
Every other provider already does:
affiliateBps = max(0, baseBps(50) − vultTierDiscountBps).Apply the same value to Jupiter's
platformFeeBps.swap/affiliate/config.ts): base50; tier discountBronze 5 / Silver 10 / Gold 20 / Platinum 25 / Diamond 35 / Ultimate 50 (NFT = +1 tier).
getSwapAffiliateBps(vultDiscountTier).GetDiscountBpsUseCase→max(0, 50 − discount).bps(for: vultTierDiscount)pattern (seeLiFiService141-142).API base (already proxied — keep using the proxy)
https://api.vultisig.com/jup/swap/v1/quotehttps://api.vultisig.com/jup/swap/v1/swapinputMint,outputMint,amount(raw atomic units, no decimals),slippageBps(default 50),platformFeeBps(new).quoteResponse,userPublicKey, compute-unit config,feeAccount(new).quote-api.jup.ag/v6was deprecated 2025-10-01; we already useswap/v1.Acceptance (all platforms)
platformFeeBps= VULT-scaled affiliate bps.feeAccount= ATA(feeOwner, outputMint).