CCIP v1.6.0 SVM Router API Reference
Router
Below is a complete API reference for the ccip_send instruction from the CCIP Router program.
ccip_send
This instruction is the entry point for sending a cross-chain message from an SVM-based blockchain to a specified destination blockchain.
fn ccip_send(
    ctx: Context<CcipSend>,
    dest_chain_selector: u64,
    message: SVM2AnyMessage,
    token_indexes: Vec<u8>
) -> Result<[u8; 32]>;
Parameters
| Name | Type | Description | 
|---|---|---|
| dest_chain_selector | u64 | The unique CCIP blockchain identifier of the destination blockchain. | 
| message | SVM2AnyMessage | Read Messages for more details. | 
| token_indexes | Vec<u8> | Index offsets slicing the remaining accounts so each token's subset can be grouped (see Context). | 
Context (Accounts)
These are the required accounts passed alongside the instructions. For relevant PDAs, the instructions on how to derive seeds are given below.
| Field | Type | Writable? | Description | 
|---|---|---|---|
| config | Account<Config> | No | Router config PDA. Derivation: ["config"]under theccip_routerprogram. | 
| dest_chain_state | Account<DestChain> | Yes | Per-destination blockchain PDA for sequence_number, chain config, etc.Derivation: ["dest_chain_state", dest_chain_selector]under theccip_routerprogram. | 
| nonce | Account<Nonce> | Yes | Current nonce PDA for (authority, dest_chain_selector).Derivation: ["nonce", dest_chain_selector, authority_pubkey]under theccip_routerprogram. | 
| authority | Signer<'info> | Yes | The user/wallet paying for the ccip_sendtransaction. Also, it must match the seeds innonce. | 
| system_program | Program<'info, System> | No | Standard System Program. | 
| fee_token_program | Interface<'info, TokenInterface> | No | The token program used for fee payment (e.g., SPL Token). If paying with native SOL, this is just the SystemProgramID. | 
| fee_token_mint | InterfaceAccount<'info, Mint> | No | Fee token mint. If paying in SPL, pass your chosen token mint. If paying in native SOL, a special "zero" mint ( Pubkey::default()) or "native mint" (native_mint::ID) is used. | 
| fee_token_user_associated_account | UncheckedAccount<'info> | Yes | If fees are paid in SPL, this is the user's ATA. Derivation: It is derived via the Associated Token Program seeds: [authority_pubkey, fee_token_program.key(), fee_token_mint.key() ]under the relevant Token Program (Make sure you use the correct token program ID—Token-2022 vs.SPL Token). If paying with native SOL, pass the zero address (Pubkey::default())and do not mark it writable. | 
| fee_token_receiver | InterfaceAccount<'info, TokenAccount> | Yes | The ATA where all the fees are collected. Derivation: from [fee_billing_signer,fee_token_program.key(),fee_token_mint.key()]. | 
| fee_billing_signer | UncheckedAccount<'info> | No | PDA is the router's billing authority for transferring fees (native SOL or SPL tokens). from fee_token_user_associated_account to fee_token_receiver. Derivation: ["fee_billing_signer"]under theccip_routerprogram. | 
| fee_quoter | UncheckedAccount<'info> | No | The Fee Quoter program ID. | 
| fee_quoter_config | UncheckedAccount<'info> | No | The global Fee Quoter config PDA. Derivation: ["config"]under thefee_quoterprogram. | 
| fee_quoter_dest_chain | UncheckedAccount<'info> | No | Per-destination blockchain PDA in the Fee Quoter program. It stores chain-specific configuration (gas price data, limits, etc.) for SVM2Any messages. Derivation: ["dest_chain",dest_chain_selector]under thefee_quoterprogram. | 
| fee_quoter_billing_token_config | UncheckedAccount<'info> | No | A per-fee-token PDA in the Fee Quoter program stores token-specific parameters (price data, billing premiums, etc.) used to calculate fees. Derivation: If the message pays fees in native SOL, the seed uses the native_mint::ID; otherwise, it uses the SPL token'smintpublic key.["fee_billing_token_config", seed]under thefee_quoterprogram. | 
| fee_quoter_link_token_config | UncheckedAccount<'info> | No | PDA containing the Fee Quoter's LINK token billing configuration (LINK price data, premium multipliers, etc.). The fee token amount is converted into "juels" using LINK's valuation from this account during fee calculation. Derivation: ["fee_billing_token_config", link_token_mint]under thefee_quoterprogram. | 
| rmn_remote | UncheckedAccount<'info> | No | The RMN program ID used to verify if a given chain is cursed. | 
| rmn_remote_curses | UncheckedAccount<'info> | No | PDA containing list of curses chain selectors and global curses. Derivation: ["curses"]under thermn_remoteprogram. | 
| rmn_remote_config | UncheckedAccount<'info> | No | RMN config PDA, containing configuration that control how curse verification works. Derivation: ["config"]under thermn_remoteprogram. | 
| token_pools_signer | UncheckedAccount<'info> | Yes | PDA with the authority to CPI into token pool logic (mint/burn, lock/release). Derivation: ["external_token_pools_signer"]under theccip_routerprogram. | 
| remaining_accounts | &[AccountInfo] | Yes | You pass extra accounts for each token you wish to transfer (does not include fee tokens). Typically includes the sender ATA, the token pool config, token admin registry PDAs… etc. | 
How remaining_accounts and token_indexes Work
When you call the Router's ccip_send instruction, you pass:
- A list of token_amountsyou want to transfer cross-chain.
- A slice of remaining_accountscontaining the per-token PDAs (e.g., user token ATA, pool config, token admin registry PDA, etc.).
- A token_indexesarray tells the Router where inremaining_accountseach token's sub-slice begins.
Reason for remaining_accounts
On Solana, each Anchor instruction has a fixed set of named accounts. However, CCIP must handle any number of tokens, each requiring many accounts. Rather than define a massive static context, the Router uses Anchor's dynamic ctx.remaining_accounts: All token-specific accounts are packed into one slice.
Reason for token_indexes
The Router must figure out which segment of that slice corresponds to token #0, token #1, etc. So you provide an integer offset in token_indexes[i] indicating where the ith token's accounts begin inside remaining_accounts.
The Router:
- Slices out [start..end)for theith token's accounts. The subslice is from start up to but not including end. This is how you indicate that the token i's accounts occupy positions start, start+1, …, end-1.
- Validates each account.
- Calls the appropriate token pool to the lock-or-burn operation on them.
Structure of Each Token's Sub-slice
Inside each token's sub-slice, the Router expects:
- The user's token account (ATA).
- The token's chain PDAs.
- Lookup table PDAs, token admin registry, pool program, pool config, pool signer, token program, the mint, etc.
In total, it is typically 12 or more accounts per token. Repeat that "per-token chunk" of ~12 accounts for each token if you have multiple tokens. These accounts are extracted in this order:
| Index | Account | Description | 
|---|---|---|
| 0 | user_token_account | ATA for (authority, mint, token_program). | 
| 1 | token_billing_config | Per-destination blockchain-specific fee overrides for a given token. Note: In most cases, tokens do not have a custom billing fee structure. In these cases, CCIP uses the fallback default fee configuration. PDA ["per_chain_per_token_config", dest_chain_selector, mint]under the fee_quoter program. | 
| 2 | pool_chain_config | PDA ["ccip_tokenpool_chainconfig", dest_chain_selector, mint]under fee_quoter program. | 
| 3 | lookup_table | Address Lookup Table that the token's admin registry claims. Must match the Token Admin Registry's lookup_tablefield. | 
| 4 | token_admin_registry | PDA ["token_admin_registry", mint]under the ccip_router program. | 
| 5 | pool_program | The Token Pool program ID (for CPI calls). | 
| 6 | pool_config | PDA [ "ccip_tokenpool_config", mint ]under the pool_program. | 
| 7 | pool_token_account | ATA for ( pool_signer,mint,token_program). | 
| 8 | pool_signer | PDA [ "ccip_tokenpool_signer", mint ]under the pool_program. | 
| 9 | token_program | Token program ID (e.g. spl_tokenor 2022). Also, it must match the mint'sowner. | 
| 10 | mint | The SPL Mint (public key) for this token. | 
| 11 | fee_token_config | A token billing configuration account under the Fee Quoter program. It contains settings such as whether there is a specific pricing for the token, its pricing in USD, and any premium multipliers. Note: In most cases, tokens do not have a custom billing fee structure. In these cases, CCIP uses the fallback default fee configuration. PDA ["fee_billing_token_config", mint]under the fee_quoter program. | 
| 12 | … | Additional accounts are passed if required by the token pool. | 
Examples
One Token Transfer
Suppose you want to send one token (myMint) cross-chain:
- token_amounts: length = 1, e.g.- [{ token: myMint_pubkey, amount: 1000000 }].
- token_indexes:- [1]. Meaning:- The 0th token's remaining_accounts sub-slice will be [token_indexes[0] .. endOfArray), i.e.[1..].
- The user's Associated Token Account (ATA) for that token is found at remaining_accounts[0].
 
- The 0th token's remaining_accounts sub-slice will be 
- Your remaining_accountsmust have:- 1 user's ATA (the sender ATA for the single token).
- 12 pool-related accounts (pool config, chain config, token program, etc.). That is 13 total.
 
Two Token Transfers
Suppose you want to send two tokens (mintA and mintB) cross-chain:
- token_amounts: length = 2, e.g.- [{ token: mintA_pubkey, amount: 1000000 },{ token: mintB_pubkey, amount: 2000000 } ].
- token_indexesmust be length=2 since there are two tokens, and token_indexes = [2, 14]. Explanation:- After we skip the user ATAs at indices [0..2), we want the next 12 accounts for the first token to lie in [2..14), and then the next 12 for the second token to lie in[14..end).
- The Router program will use token_indexes:
- For the first token: The sub slice is [2..14).
- For the second token: The sub slice is [14…endOfArray).
 
 
- After we skip the user ATAs at indices [0..2), we want the next 12 accounts for the first token to lie in 
- Your remaining_accountsmust have:- 2 user ATAs (one for mintA, one formintB).
- 12 pool accounts for mintA.
- 12 pool accounts for mintB.
 
- 2 user ATAs (one for 
Thus 2 + 12 + 12 = 26 accounts in remaining_accounts.