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/msgsalways 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_listfor 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/routerequest 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
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”.
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_idsource_asset_denomdest_asset_chain_iddest_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
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)
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
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
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
/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.
Use the chain’s bech32 format for Cosmos addresses and
0x... for EVM addresses.
3.2 Request payload
/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.
3.3 Response
The/v2/fungible/msgs response can include both a txs field and a msgs field.
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.
4. Signing the transactions
Thetxs[] 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.
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 handcurrentTx.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 SDKSIGN_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.
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
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.
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):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
- Sign (delegate to wallet or sign directly)
POST /v2/tx/submit— broadcast + register for tracking- Poll
GET /v2/tx/statusuntiltransfers[0].state === "STATE_COMPLETED_SUCCESS"
6
Done