Painless cross-chain swaps: Solana builder stack map
What makes a cross-chain swap feel local for Solana apps — stack layers (Jupiter, LI.FI, Relay, Wormhole, Pyth), status machine, and a thin quote→status PoC.
devrels.xyz/a/251short linkPainless cross-chain swaps: Solana builder stack map. Users want “type amount, confirm once, money shows up.” Builders still assemble DEX liquidity, bridges or solvers, gas on two chains, and a status machine that does not lie. This piece is for app builders shipping Solana-first product with optional EVM (and other) legs — not a vendor bake-off.
What “painless” can mean
Cross-chain is not same-chain atomicity. A route can still hop bridges and intermediate tokens. Pain drops when product hides the tourism:
| User feel | What is actually allowed |
|---|---|
| One in-amount, one out-amount | Multi-hop route; show hops only when risk needs trust |
| One primary confirm | May still need dest gas top-up or permit; say so up front |
| Progress that finishes | Status API: pending / stuck / done / failed — not fake % bars |
| Failure with a money path | Refund, retry, or open support with tx ids — not dead end |
| Same-chain is instant-ish | Keep Jupiter-class path; never force bridge UX for SOL↔SPL |
Pain inventory (ship these away)
- Wrong destination gas token → stuck after source burn
- Silent pending with no ETA or explorer deep links
- Four signatures for one “swap” (approve, swap, bridge, claim)
- Showing bridge picker to non-power users
- Same-chain pair forced through a cross-chain router
- Price shown from a stale CEX ticker while route uses different pool math
Stack map (Solana primary)
| Layer | Job | Examples | When |
|---|---|---|---|
| Same-chain DEX / meta | Best SOL/SPL execution | Jupiter | Default for any pair that never leaves Solana |
| Omnichain liquidity router | Quote + execute across bridges/DEXs | LI.FI | App needs many corridors without owning each bridge |
| Intent / solver | User signs intent; solvers compete to fill | Relay, SODAX | Payments/swaps where fill quality matters more than path UI |
| Messaging / asset bridge | Move messages or lock/mint assets | Wormhole, CCTP-class USDC, native bridge product APIs | You own corridor policy or need custom message payloads |
| Price / risk | Mark, slippage bounds, liquidation-adjacent UX | Pyth | Show fair value and reject toxic quotes before sign |
| Wallet / session | Sign Solana + optional EVM | Kit / adapters, embedded wallets | Always — multi-chain auth is half the pain |
Rule of thumb: compose up only as far as the corridor needs. Jupiter alone if both legs are Solana. Router or solver when the user starts on Base and ends in USDC on Solana. Wormhole (or a specialist bridge) when you need a specific message or asset path, not a generic “best price” shopper. Pyth sits beside the quote so the UI can refuse nonsense output before the wallet opens.
Chooser for Solana apps
| Corridor | Start here |
|---|---|
| SOL ↔ SPL on Solana | Jupiter (or meta that already nests Jupiter engines) |
| EVM stable → Solana USDC/SOL | LI.FI quote or Relay/SODAX-class intent; verify Solana in chain list |
| App-controlled message + custom receiver | Wormhole messaging + your program; not a swap aggregator alone |
| USDC-native burn/mint corridors | CCTP-style path (often inside a router) |
| Show “fair” mid while routing | Pyth price feed for the pair; separate from execution venue |
Status machine (minimum)
QUOTE_OK
→ USER_REVIEW (amount, est. out, fee, ETA, steps collapsed)
→ SIGNED_SOURCE
→ IN_FLIGHT (poll status; show source tx + dest expectation)
→ DONE (dest balance or receipt link)
→ FAILED (reason + funds location + retry/refund CTA)
→ STUCK (past ETA; escalate with both chain explorers)Integrators that stop at “tx submitted” create support debt. Router products expose status endpoints for a reason — use them (LI.FI /status, Relay execute status). Messaging layers need their own finality rules (Wormhole VAA attested ≠ user-visible balance until redeemed).
Where Pyth and Wormhole sit
Pyth is not a bridge. It is the price layer you use to bound slippage UI, check that a quoted out-amount is sane vs mark, and power any on-chain check that should not trust the frontend. See Pyth: the price layer and what/why.
Wormhole is messaging and (via connected apps) asset movement — guardian-attested messages, NTT-style transfers, and a large connected-chain set. Use it when you need a protocol message or a specific transfer primitive; do not reinvent a multi-bridge shopper on top of raw VAAs unless that is the product. Docs: wormhole.com/docs.
Thin PoC — quote → status
Sketch for a server or agent using an omnichain quote API (LI.FI shape). Swap base URL and chain ids for your vendor; keep the state machine.
// Thin PoC: painless path = quote + review fields + poll status
// Not production signing — shows the control flow only.
const LIQUEST = "https://li.quest/v1"
type Quote = {
id?: string
toolDetails?: { name?: string }
estimate?: { toAmount?: string; executionDuration?: number }
transactionRequest?: { data?: string; to?: string; value?: string }
// …vendor-specific Solana instruction payloads also appear here
}
export async function getCrossChainQuote(p: {
fromChain: string
toChain: string
fromToken: string
toToken: string
fromAmount: string // base units
fromAddress: string
}): Promise<Quote> {
const q = new URLSearchParams({
fromChain: p.fromChain,
toChain: p.toChain,
fromToken: p.fromToken,
toToken: p.toToken,
fromAmount: p.fromAmount,
fromAddress: p.fromAddress,
})
const res = await fetch(`${LIQUEST}/quote?${q}`)
if (!res.ok) throw new Error(`quote ${res.status}`)
return res.json()
}
/** Optional: sanity-check quoted out vs Pyth mark (pseudo). */
export function reviewAgainstMark(quotedOutHuman: number, pythMark: number, maxBps = 100) {
if (!pythMark) return { ok: true as const }
const bps = Math.abs(quotedOutHuman - pythMark) / pythMark * 10_000
return bps <= maxBps
? { ok: true as const }
: { ok: false as const, bps, reason: "quote far from mark — refuse or warn hard" }
}
export async function pollStatus(p: {
txHash: string
fromChain: string
toChain: string
bridge?: string
}): Promise<{ status: string; substatus?: string }> {
const q = new URLSearchParams({
txHash: p.txHash,
fromChain: p.fromChain,
toChain: p.toChain,
...(p.bridge ? { bridge: p.bridge } : {}),
})
for (let i = 0; i < 60; i++) {
const res = await fetch(`${LIQUEST}/status?${q}`)
const body = await res.json()
const status = body.status || body.detail || "UNKNOWN"
if (status === "DONE" || status === "FAILED") return body
await new Promise((r) => setTimeout(r, 3000))
}
return { status: "STUCK" }
}
// App wiring (conceptual):
// 1) if fromChain===toChain===solana → Jupiter, skip this path
// 2) quote = await getCrossChainQuote(...)
// 3) reviewAgainstMark(...) using Pyth HTTP/Hermes or on-chain price
// 4) show collapsed steps + ETA from quote.estimate
// 5) wallet signs quote.transactionRequest / Solana ixs
// 6) pollStatus until DONE | FAILED | STUCKSame-chain branch (do not skip in production):
# Same-chain SOL/SPL — Jupiter quote shape (illustrative)
# Prefer current Jupiter Metis / Ultra docs for live paths.
curl -s "https://quote-api.jup.ag/v6/quote?inputMint=So11111111111111111111111111111111111111112&outputMint=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v&amount=100000000&slippageBps=50" | headAnti-patterns
- Dual-integrate three routers just for logos — pick one meta path
- Hide “pending” past ETA with no stuck state
- Ignore destination gas and claim steps until users file tickets
- Use Pyth as execution venue (it is price, not liquidity)
- Parse Wormhole yourself when a maintained router already covers the corridor
People and links
| Piece | Link |
|---|---|
| Jupiter | article · station.jup.ag |
| LI.FI | article · docs.li.fi |
| Relay | article |
| SODAX | article |
| Pyth | price layer · docs.pyth.network |
| Wormhole | wormhole.com/docs · @wormhole |
Resources
Keep reading
Define an intent; Relay solvers fill across 69+ chains — integrate via Quote, Execute, and Status.
Single integration for same-chain swaps, cross-chain bridges, intents, and DeFi deposits — li.quest API with SVM among supported chain types.
Pyth is not “a price API with a logo” — it is publisher-sourced market data you can verify on-chain, usually by pulling an update into the same transaction that needs the price.
Get new articles in your inbox
Technical deep-dives on Solana tooling, infrastructure, and ecosystem. No noise.
