Skip to main content

0. Overview

This document explains how to use the Skip API to migrate USDC.n (USDC issued by Noble) to USDC.inj (USDC minted on Injective via CCTP v2). The API flow itself applies uniformly across every supported source chain; this document uses converting USDC.n → USDC.inj within the dYdX chain (a dYdX → dYdX round trip) as the running example.
Base URL

Target audience

Chapter 4 (transaction signing) will cover both perspectives separately.
What this guide does not cover
  • How to sign transactions — the Skip API never returns an already-signed transaction at any step. /v2/fungible/msgs always returns unsigned message payloads, and how you sign them (delegating to a wallet such as Keplr, or signing directly with your own keys) is something you implement according to your own situation.
  • How to compute/derive per-chain user addresses — this guide does not cover how you obtain the addresses that go into address_list for Noble, the transit EVM chain, Injective, etc. (derivation paths, wallet integration, etc.). It assumes the API caller (the dApp) already has these values on hand.
  • The detailed execution steps when a regular swap route is selected — a /v2/fungible/route request considers not only the migration (relayed flow) route this guide covers but also regular swap (DEX conversion) routes, and which one comes back in the response is not guaranteed (see chapter 2). When a swap route is selected, you sign and execute it the same way as a regular swap integration using the Skip API, so those details are not covered in this guide.

1. Checking chains and assets — GET /v2/info/chains, GET /v2/fungible/assets

Only two routes are possible for this migration.
The signed-tx count above is only a reference to help your understanding. The actual count is always determined by the /route response’s txs_required (chapter 2). However, the same signature count doesn’t mean you can assume the migration route was selected — for example, 1 signature could be a Noble → Injective migration, but for some chain combinations a regular swap route can also come back with 1 signature. How to check which route was actually selected is covered in chapter 2, “Checking which route was selected”.
As of this writing, the chains that support same-chain migration are roughly as follows:
New chains may continue to be added, so don’t hardcode this list; always confirm the current state with the two APIs above. To target a new chain, always check /v2/fungible/assets first to confirm whether that chain supports USDC.n and USDC.inj respectively.
Noble is the issuer of USDC.n and does not support USDC.inj, so same-chain migration is not possible there — Noble can only be used via the Noble → Injective route. The values below, used in every subsequent request, must always come verbatim from these two APIs — never guess or assemble them yourself from a chain name or asset symbol.
  • source_asset_chain_id
  • source_asset_denom
  • dest_asset_chain_id
  • dest_asset_denom
amount_in is denominated in the asset’s base unit (1 USDC = 1000000). The migration minimum is more than 0.02 USDC (because of the relayed flow fee explained in chapter 2). The examples below use amount_in = "1000000".

2. Requesting a route — POST /v2/fungible/route

The /route API is a quote request: given a specific source/destination chain and asset combination and an input amount, it tells you which route the transfer would take and roughly how much output asset you’d end up with.

Request example — dYdX USDC.n → dYdX USDC.inj

This payload isn’t specially composed to force the migration route — it is identical to the standard payload used by the regular swap UI that Skip provides. Use this preset as-is for the migration flow.
allow_multi_tx must always be set to true. Some migration routes (the “USDC.n → USDC.inj within the same chain” route from chapter 1) require 2 user-signed transactions. If this field is false or omitted, the router won’t even consider routes that split into two transactions — for those chain combinations, the route request will either fail outright or fail to return the intended migration route.
This payload does not guarantee that the router will select the migration (relayed) route. The router also considers other routes available for the same chain and asset combination (e.g. a regular swap route through a DEX), and which route ends up in the response is decided by the routes’ estimated output amounts (amount_out) competing, with the better quote being selected. Therefore, check which route was selected in every /route response — the details are covered below in Checking which route was selected.
When the migration route (relayed flow) is actually selected, it uses a fee worth roughly 0.02 USD, so amount_in must always be greater than 0.02 USDC — anything smaller and the fee exceeds the transfer amount, or the quote itself stops making sense.
fast_transfer: true enables CCTP v2 Fast Transfer — if omitted or set to false, the CCTP v2 leg is processed via the standard (slower) path instead. This field also only matters when the migration route is actually selected.

Response example (abridged)

How the key fields are used in the next steps:
The response also includes a chain_ids field (omitted from the abridged example above), but it’s slated for deprecation. It’s purely a reference list of the chains that appear in this route and is not the basis for building address_list. address_list is always built against the order of required_chain_addresses (don’t confuse the two — required_chain_addresses can contain duplicate chain IDs, and you must match that exact length, duplicates included).

Checking which route was selected

This is the most important section of chapter 2. Everything this guide explains from chapter 3 onward (message types, signing, /v2/tx/submit, status checking, etc.) assumes /route selected the migration route (relayed flow). But the same request can also lead the router to select a regular swap (DEX conversion) route. Both routes end with the user receiving USDC.inj at the destination, but the kinds and number of messages to sign along the way are completely different.
The number of signed transactions (txs_required) alone can’t tell you which route was selected. The same 1 signature could be a Noble → Injective migration, or, depending on the chain and asset combination, a regular swap route that also comes back with 1 signature. The only reliable method is to inspect the contents of the operations array directly. Each item in operations contains one key indicating the step’s type (transfer, cctp_transfer, cctp_transfer_v2, swap, evm_swap, etc.), alongside the common fields tx_index (which signed tx executes this step), amount_in, and amount_out. For the migration (relayed flow) route, cctp_transfer (CCTP v1 burn) and cctp_transfer_v2 (CCTP v2 burn/mint) appear as separate items, one right after the other in this order (the values below are examples to show the structure, with some fields omitted):
Migration (relayed flow) route
Looking at tx_index, the first three steps (IBC → CCTP v1 → CCTP v2) are bundled into the first signature (0), and only the final IBC transfer is the second signature (1). The user doesn’t sign the CCTP leg separately — the relayer continues it. For a regular swap route, on the other hand, there are no cctp_transfer/cctp_transfer_v2 keys at all, and keys like swap or evm_swap appear instead:
Regular swap route
Either route is a normal result. However, the messages to sign and the handling afterward differ depending on the route type, so every time you receive a /route response, check operations first to tell which one it is, then proceed with the matching flow.

3. Generating the transactions to sign — POST /v2/fungible/msgs

The /msgs API takes the route data computed by /route and generates the actual transaction payloads that need to be signed.

3.1 Building address_list

Iterate over required_chain_addresses in order and fill in the user’s address for each corresponding chain, preserving that exact order.
Never reorder this array, and never deduplicate repeated chain IDs on your own. As in the example above, if dydx-mainnet-1 appears twice, address_list must also be filled twice, without skipping either slot.
Use the chain’s bech32 format for Cosmos addresses and 0x... for EVM addresses.

3.2 Request payload

Don’t reuse the original request values you sent to /route — /msgs must match exactly the quote returned by /route, so source_asset_denom/source_asset_chain_id/dest_asset_denom/dest_asset_chain_id/amount_in/amount_out/operations must always come from the /route response.
The two most important rules at this step:
  1. Pass operations through exactly as received by /route. Do not modify any fields. Modifying it can cause the request to fail, or produce a transaction that doesn’t match the quote /route calculated.
  2. Build address_list in the exact order required_chain_addresses specifies, with no slot left empty. A missing or misordered slot can cause the request to fail, or generate a message addressed to the wrong account.

3.3 Response

The /v2/fungible/msgs response can include both a txs field and a msgs field.
msgs is a legacy response format slated for removal (deprecated) — don’t use it. Always use txs.
What’s inside txs[] is not an already-signed tx — it’s an unsigned transaction payload. The API caller must take this value and sign it directly (or delegate signing to a connected wallet) — signing logic itself is covered in chapter 4. Message types that appear in this migration flow: A single tx’s msgs can contain more than one message (e.g. the Noble burn tx = MsgDepositForBurnWithCaller + MsgSend). msg is the protobuf message identified by msg_type_url, expressed as a JSON string. Depending on the route, the number of transactions that need signing can be 1 or 2 (see txs_required in chapter 2). Even when there are multiple, process the txs array one at a time, in order — see chapter 6 for the detailed sequencing.
A note on timeout_timestampAn IBC MsgTransfer can contain a large uint64 value like 1788152143572186559.Don’t convert this value to a JavaScript Number — it comfortably exceeds Number.MAX_SAFE_INTEGER, so you can silently lose precision. Use BigInt, Long, or another type/parser that safely handles uint64.

4. Signing the transactions

The txs[] you got from /msgs are not yet signed. How you sign them depends on your implementation — this guide only gives an overview of the two perspectives below; the detailed implementation is out of scope. For each transaction, pull out its Cosmos transaction.
Sign the Cosmos messages in currentTx.msgs using SIGN_MODE_DIRECT to produce a signed TxRaw.

4.1 dApp developers — delegating signing to a wallet

Most dApps don’t implement their own signing logic; instead they hand currentTx.msgs straight to a connected wallet (e.g. Keplr) and ask it to sign. Pseudocode using Keplr as the example:

4.2 Developers implementing signing logic directly

If you’re using your own key management/signing stack without a wallet, follow the standard Cosmos SDK SIGN_MODE_DIRECT procedure (TxBody → AuthInfo → SignDoc → sign → TxRaw) as usual. The detailed implementation of Cosmos transaction assembly and wallet integration is beyond the scope of this guide.

5. Broadcasting the transaction — POST /v2/tx/submit

/v2/tx/submit takes the signed transaction bytes, actually broadcasts them to the chain, and simultaneously registers the transaction so the Skip API server can track it. In other words, this one API does both “broadcast” and “lifecycle tracking registration” together.
Every transaction in this migration must be submitted through /v2/tx/submit. Do not broadcast directly through a chain’s RPC/LCD.Why — this is the single most important point in this guide. If you broadcast a signed tx directly through chain RPC/LCD, the transaction is successfully included on-chain, but Skip’s relayer has no way of detecting that this tx exists. Registering it via /v2/tx/submit is what makes the following possible: the Noble CCTP burn gets recognized, the relayer’s next step (transit chain burn → Injective mint) gets triggered, and the tx becomes queryable via /v2/tx/status. Broadcasting directly leaves funds stuck at an intermediate chain with nothing to move them forward, and /v2/tx/status will report that it can’t find the tx at all.
Use the tx_hash from the response in the next step (status checking). For migration routes that require 2 signed transactions, you must confirm the first transaction’s /v2/tx/status as successful before signing and broadcasting the second one. Otherwise the route’s progress can get out of sync — see chapter 6 for details.

6. Checking transaction status — GET /v2/tx/status

Use the same chain_id you used when submitting. /v2/tx/status is not simply telling you “was the submitted transaction correctly included in a block.” It tells you the progress of the entire route that transaction triggered (IBC transfer → CCTP v1 burn → CCTP v2 burn/mint, etc.). So while the state is sitting at STATE_PENDING, the relayer may well be working through the CCTP leg behind the scenes. Use response.transfers[0].state as the migration transfer’s state. Don’t use the root-level state/status fields in its place — these can hold a different value than transfers[0].state. If you see a value not in this table (e.g. STATE_SUBMITTED right after submission), or transfers is still empty, treat it as still in progress and keep polling.
Compare against the exact enum value. Don’t judge by a partial substring match.
STATE_COMPLETED_ERROR also contains the substring "COMPLETED", so this kind of check leads to a serious bug: mistaking a failed tx for a successful one and firing off the next tx anyway.
Recommended polling interval: 3–5 seconds.

When to move on to the next transaction

For a route that needs 2 signatures (e.g. dYdX → dYdX):
Only sign and submit the second transaction after the first transaction’s transfer reaches STATE_COMPLETED_SUCCESS. For dYdX → dYdX, following this rule is what ensures the final Injective → dYdX IBC transfer only runs after USDC.inj has actually arrived on Injective — submitting the second transaction before that point means the signer (the Injective account) has no funds yet, so the broadcast itself will fail.

7. Handling Injective gas

Broadcasting the final Injective transaction (tx[1] in a route that needs a second signature) requires INJ for gas. When a leg of the migration route needs signing on the Injective chain, if the user’s Injective address used there doesn’t have enough INJ to cover the tx fee, Skip Gas Station automatically tops up a small amount of INJ. This is not an additional transaction the user has to sign, and it’s applied automatically once the first transaction completes and funds arrive on Injective.
Exception: for a route like Injective → Injective, where the first (and only) signature itself happens on the Injective account, the precondition for this gas support (a prior tx completing and funds arriving on Injective first) doesn’t hold. In that case the user needs to already hold INJ from the start.

8. Incomplete migration state

If the first transaction and the automatic relay processing that follows it (CCTP burn/mint) have completed, but the second (final) transaction hasn’t been executed yet, USDC.inj remains on Injective. The migration to the original destination chain is incomplete. This is a different situation from the first transaction failing, and should be handled separately. Inform the user that their funds remain on Injective and that the final transfer step is still pending. If the second transaction step fails, don’t reuse the existing tx payload or only call /msgs again — request a fresh quote from /v2/fungible/route from the start. When doing so, don’t resend the original request (e.g. dYdX USDC.n → dYdX USDC.inj) — the funds have already moved to USDC.inj on Injective, so change the source to Injective USDC.inj and request a new quote. Reusing the original request leaves the source mismatched with where the funds actually are (Injective), so the request can be processed in an unintended way. In addition, the existing payload’s timeout_timestamp may have already expired, and the exchange rate/fees need to be recalculated as of the retry time anyway.

9. The full process

1

GET /v2/info/chains, GET /v2/fungible/assets

Confirm supported chains/assets, chain_id, denom.
2

POST /v2/fungible/route

Get a route quote + check in operations that the migration route was actually selected.
3

Build address_list

Exactly in required_chain_addresses order.
4

POST /v2/fungible/msgs

Generate the txs[] to sign.
5

For each txs[i], in order

  1. Sign (delegate to wallet or sign directly)
  2. POST /v2/tx/submit — broadcast + register for tracking
  3. Poll GET /v2/tx/status until transfers[0].state === "STATE_COMPLETED_SUCCESS"
Only move to the next tx once the current one is fully done — for an Injective-signed tx, also confirm the automatic gas top-up landed.
6

Done

Pseudocode