Add Solana Support to Your Bridge
Across is expanding to Solana, unlocking seamless USDC bridging between Ethereum Mainnet, supported EVM chains, and Solana. If you’re an integrator, builder, here’s what you need to know to get started quickly.

The Core Workflow: Building a Deposit Transaction
Executing a deposit from a user involves three main steps:
Fetch Fees
Get the latest transaction details by making a call to the /suggested-fees
API.
Add Integrator ID
Include your unique integratorID
in the transaction data.
Execute Deposit
Call the deposit()
function in the SVM SpokePool contract to initiate the bridge.
Please ensure that you derive the required Program Derived Addresses (PDAs) (e.g., vault, depositorTokenAccount) and prepare parameters like inputAmount
, outputAmount
, destinationChainId
, etc. We have attached a utility script below to help you out!
Returns suggested fees based inputToken
+outputToken
, originChainId
, destinationChainId
, and amount
. Also includes data used to compute the fees.
Address of token to bridge on the origin chain. Must be used together with parameter outputToken
. Here we are using USDC.
Note that the address provided must exist on the specified originChainId
below.
{"value":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"}
Address of token to bridge on the destination chain. Must be used together with parameter inputToken
. For ETH, use the wrapped address, like WETH.
Note that the address provided must exist on the specified destinationChainId
below.
{"value":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"}
Chain ID where the specified token
or inputToken
exists.
{"value":1}
The desired destination chain ID of the bridge transfer.
{"value":34268394551451}
Amount of the token to transfer.
Note that this amount is in the native decimals of the token. So, for WETH, this would be the amount of human-readable WETH multiplied by 1e18. For USDC, you would multiply the number of human-readable USDC by 1e6.
{"value":"50000000"}
Recipient of the deposit. Can be an EOA or a contract. If this is an EOA and message is defined, then the API will throw a 4xx error.
Example: GsiZqCTNRi4T3qZrixFdmhXVeA4CSUzS7c44EQ7Rw1Tw
Calldata passed to the recipient
if recipient
is a contract address. This calldata is passed to the recipient via the recipient's handleAcrossMessage() public function.
Example: 0xABC123
Optionally override the relayer address used to simulate the fillRelay() call that estimates the gas costs needed to fill a deposit. This simulation result impacts the returned suggested-fees. The reason to customize the EOA would be primarily if the recipientAddress is a contract and requires a certain relayer to submit the fill, or if one specific relayer has the necessary token balance to make the fill.
Example: 0x428AB2BA90Eba0a4Be7aF34C9Ac451ab061AC010
The quote timestamp used to compute the LP fees. When bridging with across, the user only specifies the quote timestamp in their transaction. The relayer then determines the utilization at that timestamp to determine the user's fee. This timestamp must be close (within 10 minutes or so) to the current time on the chain where the user is depositing funds and it should be <= the current block timestamp on mainnet. This allows the user to know exactly what LP fee they will pay before sending the transaction.
If this value isn't provided in the request, the API will assume the latest block timestamp on mainnet.
Example: 1653547649
Suggested fees for the transaction and supporting data
Bad request due to invalid input parameter.
Unexpected error within the API
curl -L \
'https://app.across.to/api/suggested-fees?inputToken=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&outputToken=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v&originChainId=1&destinationChainId=34268394551451&recipient=GsiZqCTNRi4T3qZrixFdmhXVeA4CSUzS7c44EQ7Rw1Tw&amount=50000000&skipAmountLimit=true&allowUnmatchedDecimals=true'
{
"estimatedFillTimeSec": 2,
"capitalFeePct": "78750000000001",
"capitalFeeTotal": "78750000000001",
"relayGasFeePct": "155024308002",
"relayGasFeeTotal": "155024308002",
"relayFeePct": "78905024308003",
"relayFeeTotal": "78905024308003",
"lpFeePct": "0",
"timestamp": "1754342087",
"isAmountTooLow": false,
"quoteBlock": "23070320",
"exclusiveRelayer": "0x394311A6Aaa0D8E3411D8b62DE4578D41322d1bD",
"exclusivityDeadline": 1754342267,
"spokePoolAddress": "0x5c7BCd6E7De5423a257D81B442095A1a6ced35C5",
"destinationSpokePoolAddress": "0x6f26Bf09B1C792e3228e5467807a900A503c0281",
"totalRelayFee": {
"pct": "78905024308003",
"total": "78905024308003"
},
"relayerCapitalFee": {
"pct": "78750000000001",
"total": "78750000000001"
},
"relayerGasFee": {
"pct": "155024308002",
"total": "155024308002"
},
"lpFee": {
"pct": "0",
"total": "0"
},
"limits": {
"minDeposit": "134862494200912",
"maxDeposit": "1661211802629989209324",
"maxDepositInstant": "231397155893653275446",
"maxDepositShortDelay": "1661211802629989209324",
"recommendedDepositInstant": "231397155893653275446"
},
"fillDeadline": "1754353917",
"outputAmount": "999921094975691997",
"inputToken": {
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"symbol": "ETH",
"decimals": 18,
"chainId": 1
},
"outputToken": {
"address": "0x4200000000000000000000000000000000000006",
"symbol": "ETH",
"decimals": 18,
"chainId": 10
},
"id": "xn8fx-1754342218143-67be35cfbdb6"
}
The structure of the API response body remains the same, but there are crucial changes to how you interact with it for Solana routes:
Recipient Address Format: When calling the
/suggested-fees
API, therecipient
parameter must be a Solana Public Key (a base-58 encoded string). Failure to provide a valid Solana address will result in a400
error.Solana Addresses in Response: Be prepared to handle Solana Pubkey addresses in response fields like
recipient
,inputToken
,spokePoolAddress
, and others.
Test Your Codebase
You can check that you have updated your relayer codebase properly by simply following the below script to bridge 10 USDC from Solana to Base.
Please ensure you set your relayer address as the exclusiveRelayer
. Be sure to use a testing wallet with less than 20 USDC on Solana mainnet.
If you notice any issues or bugs, please reach out to us here.
Running the script
Now, make 2 files in the same directory:
index.js
: Main code for the script should be here. feel free to simply copy paste the script given below and add your relayer address online 102
as theexclusiveRelayer
..env
: Put your Solana Wallet's private key here to be able to bridge funds for testing your relayer.
import { Connection, Keypair, PublicKey, SystemProgram } from "@solana/web3.js";
import { getU64Encoder } from "@solana/kit";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
getAssociatedTokenAddressSync,
} from "@solana/spl-token";
import BN from "bn.js";
import anchor from "@coral-xyz/anchor";
import fetch from "node-fetch";
import dotenv from "dotenv";
import bs58 from "bs58";
import { SvmSpokeIdl } from "@across-protocol/contracts";
dotenv.config();
const u64Encoder = getU64Encoder();
const { Program, AnchorProvider } = anchor;
// 1️⃣ Provider + Program
const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed");
const wallet = Keypair.fromSecretKey(bs58.decode(process.env.SOLANA_PRIVATE_KEY));
const provider = new AnchorProvider(connection, {
publicKey: wallet.publicKey,
signTransaction: tx => wallet.signTransaction(tx),
signAllTransactions: txs => txs.map(t => wallet.signTransaction(t)),
}, {});
const program = new Program(SvmSpokeIdl, {connection, provider});
// 2️⃣ Fetch quote
const API_URL = "https://app-frontend-v3-git-epic-solana-v1-uma.vercel.app/api/suggested-fees";
function evmToSolanaPK(evmAddress) {
const hex = evmAddress.replace(/^0x/, "").toLowerCase();
if (hex.length !== 40) throw new Error("Invalid EVM address");
const buf = Buffer.alloc(32);
Buffer.from(hex, "hex").copy(buf, 12); // right-align, zero-pad left 12 bytes
return new PublicKey(buf);
}
async function getQuote() {
const params = new URLSearchParams({
inputToken: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
outputToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
destinationChainId: "8453",
originChainId: "34268394551451",
amount: "10000000",
skipAmountLimit: "true",
allowUnmatchedDecimals: "true",
});
const resp = await fetch(`${API_URL}?${params}`);
if (!resp.ok) throw new Error(`Quote API error ${resp.status}`);
return resp.json();
}
// 3️⃣ PDAs
async function derivePdas(depositor, inputToken) {
const seed = 0;
const programId = new PublicKey(SvmSpokeIdl.address);
const [statePda] = PublicKey.findProgramAddressSync(
[Buffer.from("state"), Buffer.from(u64Encoder.encode(seed))],
programId
);
const depositorTokenAccount = getAssociatedTokenAddressSync(
inputToken,
depositor,
true,
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
);
const [vaultPda] = PublicKey.findProgramAddressSync(
[statePda.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), inputToken.toBuffer()],
programId
);
const [eventAuthority] = PublicKey.findProgramAddressSync(
[Buffer.from("__event_authority")],
programId
);
return { statePda, depositorTokenAccount, vaultPda, eventAuthority };
}
// 4️⃣ Call deposit
(async () => {
const quote = await getQuote();
console.log("Quote data:", JSON.stringify(quote, null, 2));
const depositor = wallet.publicKey;
// Convert EVM addresses to Solana base58 addresses
const recipient = wallet.publicKey;
const inputToken = new PublicKey(quote.inputToken.address);
const outputToken = evmToSolanaPK(quote.outputToken.address);
const exclusiveRelayer = evmToSolanaPK(<add-your-relayer-address-here>); //replace this with your relayer address as a string
const inputAmount = new BN(quote.inputAmount);
const outputAmount = new BN(quote.outputAmount); // must be length 32
const destinationChainId = new BN(quote.outputToken.chainId);
const quoteTimestamp = quote.quoteTimestamp >>> 0; // u32
const fillDeadline = quote.fillDeadline >>> 0; // u32
const exclusivityParameter = quote.exclusivityParameter >>> 0; // u32
const message = new Uint8Array([]);
const { statePda, depositorTokenAccount, vaultPda, eventAuthority } =
await derivePdas(depositor, inputToken);
const txSig = await program.methods
.deposit(
depositor,
recipient,
inputToken,
outputToken,
inputAmount,
outputAmount,
destinationChainId,
exclusiveRelayer,
quoteTimestamp,
fillDeadline,
exclusivityParameter,
message
)
.accounts({
signer: depositor,
state: statePda,
delegate: depositor,
depositorTokenAccount,
vault: vaultPda,
mint: inputToken,
tokenProgram: TOKEN_PROGRAM_ID,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
eventAuthority,
program: program.programId,
})
.signers([wallet])
.rpc();
console.log("✅ Deposit TX:", txSig);
})();
To run the script, simply head over to your terminal and run the following command and you can see the deposit transaction go forward and get filled by your relayer:
node index.js
Next Steps
Get start with building your crosschain apps using Across V4 today:
Check the Bridge Integration Guide: For a step-by-step walkthrough of how to build a bridging experience in your application using our SDK.
Review the API & SDK: The best way to start is with our high-level tools. They are V4-compatible and abstract away the protocol's complexity.
API Reference: For direct HTTP integration to get quotes and check transaction status.
App-SDK Guide: For deeper integrations within a TypeScript or JavaScript application.
Join the Community: Have a technical question or want to connect with the team and other developers? Our Discord and telegram are the best places to get support.
Last updated