Across is an intent-based crosschain bridge with a clean REST API, integrator fee support (appFee as decimal percentage 0-1), and 10 overlapping chains with ShapeShift. Integration complexity is comparable to Relay. Blocker: we need to register for an integrator ID before going to production.
- 1. API Overview
- 2. Supported Chains & Overlap
- 3. Affiliate / Integrator Fees
- 4. SDK vs REST API
- 5. Types & Response Shapes
- 6. Registration & Access Requirements
- 7. Brand Assets / Press Kit
- 8. Feature Flag & Env Vars Plan
- 9. Treasury Addresses (Per-Chain)
- 10. Comparison with Relay
- 11. Implementation Plan
- 12. Open Questions & Blockers
Base URLs:
- Mainnet:
https://app.across.to/api - Testnet:
https://testnet.across.to/api
Architecture: REST API, serverless, no state storage. Returns ready-to-execute calldata.
| Endpoint | Method | Purpose |
|---|---|---|
/swap/approval |
GET | Get executable swap calldata (recommended for integrators) |
/swap/approval |
POST | Same, but with embedded destination chain actions |
/swap/chains |
GET | List supported chains |
/swap/tokens |
GET | List supported tokens |
/swap/sources |
GET | List available liquidity sources |
/deposit/status |
GET | Track deposit/fill lifecycle |
/deposits |
GET | Get all deposits for a depositor |
/suggested-fees |
GET | Lower-level fee quote (bridge-only, no swap calldata) |
/limits |
GET | Transfer min/max limits for a route |
/available-routes |
GET | Discover supported routes |
GET /swap/approvalwith params → returnsapprovalTxns[]+swapTxcalldata- Sign & send approval txns (if any)
- Sign & send
swapTx - Poll
GET /deposit/status?depositTxnRef={txHash}for fill status
| Parameter | Type | Required | Description |
|---|---|---|---|
tradeType |
string | Yes | exactInput, minOutput, or exactOutput |
amount |
string | Yes | Amount in smallest units |
inputToken |
string | Yes | Token address on origin chain |
outputToken |
string | Yes | Token address on destination chain |
originChainId |
integer | Yes | Origin chain ID |
destinationChainId |
integer | Yes | Destination chain ID |
depositor |
string | Yes | Sender wallet address |
recipient |
string | No | Output recipient (defaults to depositor) |
appFee |
number | No | Integrator fee as decimal percentage (0-1 range) |
appFeeRecipient |
string | No | Required if appFee set |
integratorId |
string | No | 2-byte hex identifier (e.g. "0xdead") |
slippage |
string/float | No | "auto" (default) or 0-1 decimal |
refundAddress |
string | No | Defaults to depositor |
refundOnOrigin |
boolean | No | Control refund location |
excludeSources |
array | No | Sources to exclude |
includeSources |
array | No | Sources to include |
| Status | Meaning |
|---|---|
filled |
Complete, funds received on destination |
pending |
Not yet filled |
expired |
Won't be filled, eligible for refund |
refunded |
Expired deposit refunded on origin chain |
slowFillRequested |
No relayer filled; Across fills without relayer capital |
Across explicitly requests no caching of /swap/approval and /suggested-fees responses. Only /deposit/status can be cached (it's stateful). We should use a minimal/no cache for quote requests.
{
"type": "string",
"code": "string",
"status": 400,
"message": "human-readable description",
"id": "unique error identifier"
}| Chain | Chain ID | In ShapeShift? |
|---|---|---|
| Arbitrum | 42161 | YES |
| Base | 8453 | YES |
| Blast | 81457 | No |
| BNB Smart Chain | 56 | YES |
| Ethereum | 1 | YES |
| HyperEVM | 999 | YES |
| Ink | 57073 | No |
| Lens | 232 | No |
| Linea | 59144 | No |
| Lisk | 1135 | No |
| MegaETH | 4326 | No |
| Mode | 34443 | No |
| Monad | 143 | YES |
| Optimism | 10 | YES |
| Plasma | 9745 | YES |
| Polygon | 137 | YES |
| Scroll | 534352 | No |
| Soneium | 1868 | No |
| Solana | 34268394551451 | YES |
| Unichain | 130 | No |
| World Chain | 480 | No |
| zkSync | 324 | No |
| Zora | 7777777 | No |
- Ethereum (1)
- Arbitrum (42161)
- Base (8453)
- BNB Smart Chain (56)
- Optimism (10)
- Polygon (137)
- HyperEVM (999)
- Monad (143)
- Plasma (9745)
- Solana (34268394551451)
- Across supports Solana but uses a custom chain ID (
34268394551451), not standard. ShapeShift uses CAIP-2 format (solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp). Need a mapping. - Across does NOT support: Gnosis, Avalanche, Bitcoin, Tron, Sui, NEAR, Starknet, TON, Katana, or any UTXO/Cosmos chains.
- ShapeShift chains NOT in Across: Gnosis, Avalanche, Bitcoin, Bitcoin Cash, Litecoin, Dogecoin, Zcash, Cosmos, Thorchain, Mayachain, Binance, Tron, Sui, NEAR, Starknet, TON, Katana.
CRITICAL: Across uses appFee as a decimal percentage from 0 to 1, NOT basis points.
| Desired Fee | appFee value |
Basis Points Equivalent |
|---|---|---|
| 0.01% | 0.0001 |
1 bps |
| 0.1% | 0.001 |
10 bps |
| 1% | 0.01 |
100 bps |
| 5% | 0.05 |
500 bps |
Conversion from our internal basis points:
const appFee = affiliateBps / 10000 // e.g. 66 bps → 0.0066appFee: decimal percentage (0-1)appFeeRecipient: address on the destination chain that receives the fee- Fee is paid in the output token on the destination chain
- Fee is deducted from the output amount (user receives less)
The /swap/approval response includes detailed fee breakdown:
{
"fees": {
"total": {
"amount": "string",
"amountUsd": "string",
"pct": "string",
"details": {
"app": { "amount", "amountUsd", "pct", "token" },
"bridge": {
"details": {
"lp": { "amount", "pct" },
"relayerCapital": { "amount", "pct" },
"destinationGas": { "amount", "pct" }
}
},
"swapImpact": { "amount", "amountUsd", "pct" }
}
}
}
}Since appFeeRecipient receives fees on the destination chain, we MUST use per-chain treasury addresses. Use the existing getTreasuryAddressFromChainId(buyAsset.chainId) helper from @shapeshiftoss/utils.
| Aspect | Details |
|---|---|
| Version | 0.4.4 (pre-1.0) |
| Last published | Nov 11, 2025 (~3 months ago) |
| Runtime deps | Zero |
| Peer deps | viem ^2.31.2 (we have 2.40.3, compatible) |
| Bundle size | ~845 KB unpacked |
| TypeScript | Yes, 99.3% TS |
| License | Ambiguous: MIT in package.json, AGPL-3.0 in repo LICENSE |
What the SDK actually does:
- Wraps the same REST endpoints (
/swap/approval,/suggested-fees, etc.) - Adds
executeQuote()lifecycle orchestrator (approve → deposit → fill with progress callbacks) waitForFillTx()watches on-chain contract events via viem (more real-time than REST polling)buildMulticallHandlerMessage()for cross-chain actions- Zod validation of API responses
- Singleton client pattern (
createAcrossClient/getAcrossClient)
Pros:
- Zero runtime deps, types included, Zod validation
waitForFillTxgives real-time fill detection via on-chain events- Small, tree-shakeable
Cons:
- License ambiguity (MIT vs AGPL-3.0 — needs clarification from Risk Labs)
- Pre-1.0, modest activity (3 months since last publish)
- Singleton pattern doesn't match our swapper architecture
executeQuoteorchestrator is the main value-add but we'd never use it — we have our own execution pipeline via chain adapters- We'd mainly use it for types only
Pros:
- Consistent with existing swappers (Relay, Chainflip, ZRX, CowSwap, Portals, etc.)
- Full control over execution flow, error handling, retries
- No license concerns
/swap/approvalreturns ready-to-execute calldata — same pattern as Relay/deposit/statusgives simple polling-based status tracking- Across is primarily EVM — types are simpler than Relay (no UTXO/Solana/Tron variants)
Cons:
- Need to write our own types (~200-300 lines, comparable to Relay's
types.ts) - No Zod validation (but we don't use it for other swappers either)
- Status polling via REST has ~10s indexer cadence vs SDK's on-chain event watching
REST API (Option B) — matches every other swapper in the codebase. The SDK's main value-add (executeQuote orchestrator) is something we'd never use. We'd essentially import the SDK just for types, which doesn't justify the dependency + license risk. Worth looking at the SDK source for type inspiration though.
If the license is confirmed MIT, we could reconsider for waitForFillTx (better UX for fill detection), but that's a nice-to-have optimization, not a blocker.
type AcrossSwapApprovalResponse = {
crossSwapType: 'bridgeableToBridgeable' | 'bridgeableToBridgeableIndirect' | 'bridgeableToAny' | 'anyToBridgeable' | 'anyToAny'
amountType: string
// Executable transactions
approvalTxns: Array<{
chainId: number
to: string
data: string
}>
swapTx: {
simulationSuccess: boolean
chainId: number
to: string
data: string
gas: string
maxFeePerGas: string
maxPriorityFeePerGas: string
}
// Balance/allowance checks
checks: {
allowance: { token: string; spender: string; actual: string; expected: string }
balance: { token: string; actual: string; expected: string }
}
// Route steps
steps: {
originSwap?: SwapStep
bridge: BridgeStep
destinationSwap?: SwapStep
}
// Token info
inputToken: TokenInfo
outputToken: TokenInfo
refundToken: TokenInfo
// Amounts
inputAmount: string
maxInputAmount: string
expectedOutputAmount: string
minOutputAmount: string
// Fees (detailed breakdown)
fees: {
total: FeeBreakdown
totalMax: FeeBreakdown
originGas: { amount: string; amountUsd: string; token: TokenInfo }
}
expectedFillTime: number // seconds
quoteExpiryTimestamp: number
id: string
}
type TokenInfo = {
address: string
decimals: number
symbol: string
name: string
chainId: number
}
type SwapStep = {
tokenIn: TokenInfo
tokenOut: TokenInfo
inputAmount: string
outputAmount: string
minOutputAmount: string
maxInputAmount: string
swapProvider: { name: string; sources: string[] }
slippage: number
}
type BridgeStep = {
inputAmount: string
outputAmount: string
tokenIn: TokenInfo
tokenOut: TokenInfo
fees: {
amount: string
pct: string
token: TokenInfo
details: {
type: 'across'
relayerCapital: FeePart
destinationGas: FeePart
lp: FeePart
}
}
provider: string
}
type FeePart = { amount: string; pct: string; token: TokenInfo }
type FeeBreakdown = {
amount: string
amountUsd: string
token: TokenInfo
pct: string
details: {
type: string
swapImpact: { amount: string; amountUsd: string; token: TokenInfo; pct: string }
app: { amount: string; amountUsd: string; pct: string; token: TokenInfo }
bridge: BridgeStep['fees']
}
}type AcrossDepositStatus = {
status: 'filled' | 'pending' | 'expired' | 'refunded' | 'slowFillRequested'
fillTxnRef?: string // only if status === 'filled'
destinationChainId: number
originChainId: number
depositId: number
depositTxnRef: string
depositRefundTxnRef?: string
actionsSucceeded?: boolean
pagination: { currentIndex: number; maxIndex: number }
}type AcrossSuggestedFees = {
totalRelayFee: { pct: string; total: string } // pct scaled by 1e18 (1% = 1e16)
relayerCapitalFee: { pct: string; total: string }
relayerGasFee: { pct: string; total: string }
lpFee: { pct: string; total: string }
timestamp: string
isAmountTooLow: boolean
quoteBlock: string
spokePoolAddress: string
exclusiveRelayer: string
exclusivityDeadline: string
expectedFillTimeSec: string
fillDeadline: string
limits: {
minDeposit: string
maxDeposit: string
maxDepositInstant: string
maxDepositShortDelay: string
recommendedDepositInstant: string
}
}type AcrossError = {
type: string
code: string
status: number
message: string
id: string
}Two different formats depending on endpoint:
| Endpoint | Fee Format | Example for 1% |
|---|---|---|
/swap/approval (appFee param) |
Decimal percentage (0-1) | 0.01 |
/swap/approval response (fees.*.pct) |
String decimal | "0.01" |
/suggested-fees response (*.pct) |
Scaled by 1e18 | "10000000000000000" |
Be careful not to mix these up. The Swap API uses human-readable decimals. The Suggested Fees API uses 1e18-scaled values.
Registration form: https://docs.google.com/forms/d/e/1FAIpQLSe-HY6mzTeGZs91HxObkQmwkMQuH7oy8ngZ1ROiu-f4SR4oMw/viewform
- Required before production deployment
- You receive a 2-byte hex string (e.g.
"0xdead") as your integrator ID - Passed as
integratorIdquery parameter on API calls - You can start building before receiving the ID — just add it later
- Registration also enables "co-marketing efforts" with Across team
- The API appears to be open for development (no auth headers, no API keys)
- The integrator ID is the only "gating" mechanism, and it's optional for development
- No IP whitelisting mentioned
- No rate limits documented (but "don't cache" policy suggests they expect reasonable usage)
- Testnet available at
https://testnet.across.to/apifor testing
- Fill out the form NOW — it's a blocker for production
- Development can proceed in parallel using the API without an integrator ID
- Once received, add the ID as an env var (e.g.
VITE_ACROSS_INTEGRATOR_ID)
| Asset | Variants | Formats | Use Case |
|---|---|---|---|
| Logotype (full wordmark) | Aqua, Dark, Gradient | SVG, PNG | Marketing only (gradient) |
| Logomark (circle symbol) | Aqua, Dark | SVG, PNG | App icon, token icon |
| "Powered By Across" | Black, White | SVG, PNG | Attribution badges |
https://docs.across.to/user-docs/additional-info/across-brand-assets
Use the Logomark (Aqua Circle) SVG — it's explicitly noted as suitable "for Apps and as a Token Icon."
Download URL (Primary Aqua Symbol SVG):
https://3563890891-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fo33kX1T6RRp4inOcEH1d%2Fuploads%2FvmOkOcLgbiXv7hVNSlNT%2FAcross%20Logomark%20Aqua%20Circle.svg?alt=media&token=b63972b2-174f-4ee8-9a58-08fb8792cd17
We'll need to download this and add it to src/assets/ or wherever swapper icons live in the codebase.
Following the existing pattern (see Relay as reference):
| Variable | Type | Default | Description |
|---|---|---|---|
VITE_FEATURE_SWAPPER_ACROSS |
boolean | false |
Feature flag to enable/disable |
VITE_ACROSS_API_URL |
url | https://app.across.to/api |
API base URL |
VITE_ACROSS_INTEGRATOR_ID |
string | "" |
2-byte hex integrator ID (e.g. "0xdead") |
-
src/config.ts— Add env var validation:VITE_FEATURE_SWAPPER_ACROSS: bool({ default: false }), VITE_ACROSS_API_URL: url({ default: 'https://app.across.to/api' }), VITE_ACROSS_INTEGRATOR_ID: str({ default: '' }),
-
src/state/slices/preferencesSlice/preferencesSlice.ts— Add to FeatureFlags type + initial state -
src/test/mocks/store.ts— Add mock value -
.env,.env.development,.env.production— Set appropriate values -
src/state/helpers.ts— Add swapper enablement logic:[SwapperName.Across]: AcrossSwapper && (!isCrossAccountTrade || isCrossAccountTradeSupported(SwapperName.Across)),
Across pays fees on the destination chain in the output token. We need per-chain treasury addresses.
Existing treasury addresses from @shapeshiftoss/utils (packages/utils/src/treasury.ts):
| Chain | Treasury Address | Across Support? |
|---|---|---|
| Ethereum | 0x90a48d5cf7343b08da12e067680b4c6dbfe551be |
YES |
| Optimism | 0x6268d07327f4fb7380732dc6d63d95F88c0E083b |
YES |
| Polygon | 0xB5F944600785724e31Edb90F9DFa16dBF01Af000 |
YES |
| BNB Smart Chain | 0x8b92b1698b57bEDF2142297e9397875ADBb2297E |
YES |
| Arbitrum | 0x38276553F8fbf2A027D901F8be45f00373d8Dd48 |
YES |
| Base | 0x9c9aA90363630d4ab1D9dbF416cc3BBC8d3Ed502 |
YES |
| Solana | Bh7R3MeJ98D7Ersxh7TgVQVQUSmDMqwrFVHH9DLfb4u3 |
YES |
| Gnosis | 0xb0E3175341794D1dc8E5F02a02F9D26989EbedB3 |
Not in Across |
| Avalanche | 0x74d63F31C2335b5b3BA7ad2812357672b2624cEd |
Not in Across |
| Bitcoin | bc1qr2whxtd0gvq... |
Not in Across |
| Starknet | 0x052a1132ea4db8... |
Not in Across |
We have treasury addresses for all overlapping EVM chains. For Monad, HyperEVM, and Plasma — these are newer chains and may not have treasury addresses yet. Need to check with the team or add them to treasury.ts.
import { getTreasuryAddressFromChainId } from '@shapeshiftoss/utils'
// In Across quote request:
const appFeeRecipient = getTreasuryAddressFromChainId(buyAsset.chainId)
const appFee = affiliateBps / 10000 // Convert bps → decimal percentage| Aspect | Relay | Across |
|---|---|---|
| API Style | REST (POST /quote/v2) |
REST (GET /swap/approval) |
| HTTP Method | POST with JSON body | GET with query params |
| Returns Calldata | Yes (per step) | Yes (swapTx + approvalTxns) |
| Fee Format | Basis points (integer) | Decimal percentage (0-1 float) |
| Fee Recipient | Hardcoded DAO_TREASURY_BASE |
Per-chain via appFeeRecipient |
| Fee Paid In | Output token | Output token |
| Status Endpoint | GET /intents/status/v2?requestId= |
GET /deposit/status?depositTxnRef= |
| Status Values | success/failed/pending/refund/delayed/waiting | filled/pending/expired/refunded/slowFillRequested |
| Tx Notification | POST /transactions/single (optional, helps indexing) |
Not needed |
| Supported Chains | 14 (EVM + BTC + Solana + Tron) | 23 (mostly EVM + Solana) |
| UTXO Support | Yes (Bitcoin) | No |
| Non-EVM | BTC, Solana, Tron | Solana only |
| Max Steps | 2 (rejects 3+) | N/A (single swapTx) |
| Cache | 15s via axios-cache-interceptor | No caching allowed |
| Auth | None (open API) | Integrator ID (for prod) |
| SDK Available | No official SDK | Yes (@across-protocol/app-sdk) |
| Registration Required | No | Yes (Google Form) |
- Simpler execution model — Across returns a single
swapTxobject vs Relay's multi-stepsteps[].items[]approach. No need to handle UTXO PSBTs, Solana instructions, or Tron transactions. - GET vs POST — Across uses GET with query params. Relay uses POST with JSON body.
- Fee format — MUST convert our basis points to decimal percentage for Across.
affiliateBps / 10000. - No tx notification needed — Relay benefits from calling
/transactions/singleafter broadcast. Across doesn't need this. - Status tracking is simpler — Just poll with the deposit tx hash, no separate
requestIdto track.
packages/swapper/src/swappers/AcrossSwapper/
├── AcrossSwapper.ts # Main swapper export
├── endpoints.ts # SwapperApi (getTradeQuote, getTradeRate, checkTradeStatus)
├── constant.ts # Chain mappings, API URL, error codes
├── index.ts # Package export
├── getTradeQuote/
│ └── getTradeQuote.ts # Quote request handler
├── getTradeRate/
│ └── getTradeRate.ts # Rate request handler
└── utils/
├── types.ts # Across-specific types
├── getTrade.ts # Core quote/rate logic
├── fetchAcrossTrade.ts # GET /swap/approval
├── acrossService.ts # Axios instance (no cache per their policy)
└── acrossTokenToAssetId.ts # Convert Across token → AssetId
- Register for integrator ID (blocker for production)
- Add feature flag + env vars (config.ts, preferencesSlice, .env files)
- Download and add Across icon to swapper assets
- Add
SwapperName.Acrossto swapper types - Create chain ID mapping (ShapeShift CAIP-2 → Across chain IDs)
- Implement types (~200-300 lines based on API response shapes above)
- Implement
acrossService.ts— Axios instance with NO cache - Implement
fetchAcrossTrade.ts— GET/swap/approvalwith proper params - Implement
getTrade.ts— Core logic mapping our types → Across params → our trade quote - Implement
getTradeQuote.ts+getTradeRate.ts— Thin wrappers aroundgetTrade.ts - Implement
endpoints.ts— SwapperApi withcheckTradeStatuspolling/deposit/status - Register swapper in
packages/swapper/src/constants.ts - Add swapper enablement logic in
src/state/helpers.ts - Test end-to-end under feature flag
| Decision | Choice | Rationale |
|---|---|---|
| SDK vs REST | REST API | Consistent with all other swappers, SDK's main value-add conflicts with our execution pipeline, license ambiguity |
| Solana | Include | Swap API DOES support Solana (docs were outdated). EVM→Solana is pure EVM tx. Solana→EVM returns base64 tx (simpler than Relay). 34 tokens supported. No dest swaps on Solana yet. |
| Chains without treasury | Support without fees | Monad/HyperEVM/Plasma supported but no appFee param sent (skip affiliate fees) |
| Fee recipient | Per-chain treasury | Use getTreasuryAddressFromChainId(buyAsset.chainId), graceful fallback for chains without treasury |
| All overlapping chains | Yes, all 10 | Support every chain where ShapeShift and Across overlap |
- Register for integrator ID — https://docs.google.com/forms/d/e/1FAIpQLSe-HY6mzTeGZs91HxObkQmwkMQuH7oy8ngZ1ROiu-f4SR4oMw/viewform
- Solana execution model — INVESTIGATED via live curls: Swap API DOES support Solana (docs/blog were outdated). See Section 14 for full details. EVM→Solana returns EVM calldata (no Solana code needed). Solana→EVM returns base64 Solana tx in
swapTx.datawithecosystem: "svm". No destination swaps on Solana yet. 34 tokens supported. - Cross-account trades —
recipientparam exists on/swap/approval, so cross-account works for EVM chains. - Slippage — Across supports
"auto"slippage (default) or a manual decimal (0-1). Our auto-slippage maps to their"auto". - Quote expiry — Response includes
quoteExpiryTimestamp. We should respect this and re-fetch if expired. slowFillRequestedstatus — Unique to Across (no relayer fills, protocol fills directly). Display as "processing" state with "Taking longer than usual" message.- Treasury addresses for Monad/HyperEVM/Plasma — Not a blocker; skip affiliate fees on these chains until treasury addresses are deployed.
Despite the blog post saying "Solana is on the roadmap", live API testing proves the Swap API fully supports Solana now. Here's what we found via actual curl requests:
Test 1: EVM → Solana (ETH USDC → Solana USDC)
GET /swap/approvalwithoriginChainId=1, destinationChainId=34268394551451- WORKS. Returns EVM calldata (
swapTx.ecosystem: "evm",swapTx.chainId: 1) - User signs an EVM tx on origin chain. Across handles Solana delivery.
crossSwapType: "bridgeableToBridgeable"
Test 2: Solana → EVM (Solana USDC → ETH USDC)
GET /swap/approvalwithoriginChainId=34268394551451, destinationChainId=1- WORKS. Returns Solana transaction data (
swapTx.ecosystem: "svm",swapTx.chainId: 34268394551451) swapTx.datais a base64-encoded Solana transaction (NOT EVM calldata)swapTx.toisDLv3NggMiSaef97YCkew5xKUHDh13tVGZ7tydt3ZeAru(SVM SpokePool program)simulationSuccess: false(expected — test used a dummy depositor)crossSwapType: "bridgeableToBridgeable"
Test 3: EVM → Solana (ETH native → Solana USDC, with origin swap)
- WORKS. Returns EVM calldata with origin swap step.
crossSwapType: "anyToBridgeable",swapTx.ecosystem: "evm"
Test 4: EVM → Solana (ETH native → Solana SOL, destination swap needed)
- FAILS. Error:
"Destination swaps are not supported yet for routes involving Solana." - This means: bridgeable→bridgeable and any→bridgeable work, but no destination-side swaps on Solana
Test 5: Token support
/swap/tokensreturns 34 Solana tokens including SOL, USDC, USDT, cbBTC, JUP, BONK, PENGU, RAY, JitoSOL, JupSOL, WBTC, ETH (Portal), and various others. Way beyond just USDC.
| Route | Works? | swapTx.ecosystem |
Notes |
|---|---|---|---|
| EVM → Solana (bridgeable→bridgeable) | YES | evm |
User signs EVM tx |
| EVM → Solana (any→bridgeable) | YES | evm |
Origin swap + bridge |
| EVM → Solana (any→any, dest swap) | NO | — | "Destination swaps not supported for Solana" |
| Solana → EVM (bridgeable→bridgeable) | YES | svm |
User signs Solana tx (base64) |
| Solana → EVM (any→any) | Untested | likely svm |
Probably works if origin is bridgeable |
-
swapTx.ecosystemfield — This is the discriminator. When"evm", thedatafield is hex calldata. When"svm", thedatafield is a base64-encoded Solana transaction. -
For EVM → Solana routes: Execution is identical to EVM→EVM. The user signs an EVM transaction. Across handles the Solana delivery via relayers. No Solana-specific code needed on our end for this direction.
-
For Solana → EVM routes: The
swapTx.datais a base64 Solana transaction targeting the SVM SpokePool program (DLv3NggMiSaef97YCkew5xKUHDh13tVGZ7tydt3ZeAru). We need to:- Decode the base64 string into a Solana
TransactionorVersionedTransaction - Sign it with the user's Solana wallet
- Submit to Solana RPC
- Decode the base64 string into a Solana
-
recipientparameter — When destination is Solana, pass a Solana Pubkey (base58). When destination is EVM, pass an EVM address. The API handles the format. -
Allowance/approval — For Solana→EVM, the
checks.allowance.spenderis the SVM SpokePool program. TheapprovalTxnsarray should contain Solana approve instructions if needed.
Relay returns RelayQuoteSolanaItemData with:
{ instructions: RelaySolanaInstruction[], addressLookupTableAddresses: string[] }We then construct a VersionedTransaction from these instructions.
Across takes a different approach — they return a complete serialized Solana transaction as base64 in swapTx.data. This is simpler from our perspective: just decode, sign, submit. No instruction assembly needed.
Solana IS supported by the Swap API and is actually simpler than expected:
- EVM → Solana: No Solana code needed (pure EVM tx)
- Solana → EVM: Decode base64 tx, sign, submit (simpler than Relay's instruction assembly)
- Limitation: No destination swaps on Solana yet (can't do ETH→SOL, only ETH→USDC on Solana)
- 34 Solana tokens supported in the token list
Chain count: 10 (9 EVM + Solana)
No pre-filtering at startup. Swappers don't call external APIs to discover available routes. Instead:
- Upstream: Feature flags enable/disable swappers via
getEnabledSwappers()insrc/state/helpers.ts - Downstream: Each swapper validates pairs at quote time. If no route → return
TradeQuoteError.UnsupportedTradePair - Generic utils exist:
filterCrossChainEvmBuyAssetsBySellAssetId()andfilterSameChainEvmBuyAssetsBySellAssetId()inpackages/swapper/src/swappers/utils/
For Across: Use the existing pattern — define a chainIdToAcrossChainId mapping (like Relay's chainIdToRelayChainId), filter by supported chains, let the API return errors for unsupported pairs. No need to call /available-routes at startup.
Confirmed NOT supported by Across. Live curl test returned:
{ "message": "Failed to fetch swap quote: No bridge routes found for 1 -> 1" }Across is cross-chain only. Use filterCrossChainEvmBuyAssetsBySellAssetId() helper.
Pattern: Swappers return allowanceContract in TradeQuoteStep. The app handles approvals via the standard chain adapter flow — no swapper-specific approval logic needed.
For Across: Set allowanceContract to the checks.allowance.spender value from the API response (e.g., 0x5c7BCd6E7De5423a257D81B442095A1a6ced35C5 which is the Across SpokePool on Ethereum). This is the same pattern as Bebop (quote.approvalTarget) and Portals (target).
Existing pattern (Jupiter/Relay): Both swappers store TransactionInstruction[] + addressLookupTableAddresses in solanaTransactionMetadata. At execution time:
getUnsignedSolanaTransaction()extracts instructions from metadataadapter.getFeeData()computes compute units + priority feesadapter.buildSendApiTransaction()serializes intoSolanaSignTxexecuteSolanaTransaction()→signAndBroadcastTransaction()(wallet callback)
Key difference: Jupiter/Relay return individual instructions (Jupiter: base64 data, Relay: hex data). Across returns a full base64-encoded serialized transaction in swapTx.data.
Approach for Across Solana→EVM:
- Option A: Deserialize the base64 tx into a
VersionedTransaction, extract instructions, feed into existing pipeline - Option B: Skip instruction extraction, directly pass the serialized tx to the wallet for signing (would need a slightly different path)
- Recommendation: Option A — deserialize to instructions to stay consistent with Jupiter/Relay pattern. Use
VersionedTransaction.deserialize()then extract.message.compiledInstructions.