> ## Documentation Index
> Fetch the complete documentation index at: https://skip-go.mintlify.site/go/llms.txt
> Use this file to discover all available pages before exploring further.

# Integrating USDC.n → USDC.inj Migration with the Skip API

> How to migrate USDC.n to USDC.inj (minted on Injective via CCTP v2) using the Skip API.

## 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.

```text Base URL theme={null}
https://go.skip.build/api/skip
```

### Target audience

| Audience | How signing works |
| - | - |
| **dApp developers** | You delegate signing to a connected wallet (e.g. Keplr) and simply hand the Skip API's response to that wallet. You don't need to implement low-level signing logic yourself. |
| **Developers implementing signing logic directly** (wallet developers, or backend/SDK developers with their own signing stack) | You are responsible for assembling and signing the transaction yourself using `SIGN_MODE_DIRECT`. |

Chapter 4 (transaction signing) will cover both perspectives separately.

<Note>
  **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.
</Note>

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

```text theme={null}
GET /v2/info/chains?include_evm=true&include_svm=true&only_testnets=false
GET /v2/fungible/assets?include_evm_assets=true&include_cw20_assets=true&include_svm_assets=true&only_testnets=false
```

Only two routes are possible for this migration.

| Route | Condition | Signed txs (reference) |
| - | - | - |
| USDC.n → USDC.inj within the same chain (e.g. dYdX → dYdX) | That chain must support **both** USDC.n and USDC.inj | 2 — except Injective → Injective, which only needs 1 |
| Noble → Injective | The one exception where source and destination chains differ | 1 |

<Info>
  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".
</Info>

As of this writing, the chains that support same-chain migration are roughly as follows:

| | | |
| - | - | - |
| Cosmos Hub | Initia | Injective |
| Osmosis | Terra | dYdX Protocol |
| Neutron | Xpla | ZIGChain |

<Warning>
  **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.
</Warning>

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

```json theme={null}
{
  "amount_in": "1000000",
  "source_asset_chain_id": "dydx-mainnet-1",
  "source_asset_denom": "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5",
  "dest_asset_chain_id": "dydx-mainnet-1",
  "dest_asset_denom": "ibc/BDC8EC8C66D3D784B2DC7E8F7DAE93A49961AE747CEEBBC7DC4BAFF564E40BD3",
  "smart_relay": true,
  "experimental_features": ["stargate", "eureka", "layer_zero"],
  "allow_multi_tx": true,
  "allow_unsafe": true,
  "smart_swap_options": {
    "split_routes": true,
    "evm_swaps": true
  },
  "go_fast": true,
  "fast_transfer": true
}
```

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.

<Warning>
  **`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.
</Warning>

<Warning>
  **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](#checking-which-route-was-selected).
</Warning>

<Warning>
  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.
</Warning>

`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)

```json theme={null}
{
  "amount_in": "1000000",
  "amount_out": "999200",
  "source_asset_chain_id": "dydx-mainnet-1",
  "source_asset_denom": "ibc/8E27BA2D...",
  "dest_asset_chain_id": "dydx-mainnet-1",
  "dest_asset_denom": "ibc/BDC8EC8C...",
  "operations": ["... individual steps that make up the route (omitted) ..."],
  "required_chain_addresses": ["dydx-mainnet-1", "noble-1", "<transit_evm_chain_id>", "injective-1", "dydx-mainnet-1"],
  "txs_required": 2
}
```

How the key fields are used in the next steps:

| Field | Role in the next step |
| - | - |
| `operations` | The route structure passed **unmodified** into the `/v2/fungible/msgs` request |
| `required_chain_addresses` | The order-fixed array of chain IDs that `address_list` for `/v2/fungible/msgs` must be built against |
| `txs_required` | The number of transactions the user must sign — lets you determine upfront whether you're in a 1-tx or 2-tx flow |
| `amount_out` | The expected output amount, passed as-is into the `/v2/fungible/msgs` request |

<Info>
  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).
</Info>

### Checking which route was selected

<Warning>
  **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.
</Warning>

**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**.

| `operations` contents | Selected route | What to do |
| - | - | - |
| Contains **both the CCTP v1 burn and the CCTP v2 burn/mint steps** | The migration (relayed flow) route this guide covers | If only one of the two is present and the other is missing, it should not be treated as the migration route — check that both are present **together**. Follow the message types, signing, and execution flow from chapter 3 onward as-is. |
| Has no CCTP steps, or only one of the two, and has swap/DEX steps | A **regular swap route was selected**, not the migration | The message types described from chapter 3 onward (IBC transfer, CCTP burn, etc.) don't apply as-is, but **the handling itself isn't anything special** — just like a regular Skip API swap integration, sign the messages in `operations`, submit them via `/v2/tx/submit`, and confirm completion with `/v2/tx/status`. However, since the message types and counts differ to fit the swap route, the specific message types and signing methods are not covered in this guide — see the general Skip swap integration guide. |

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):

```json Migration (relayed flow) route theme={null}
{
  "operations": [
    { "transfer": { "from_chain_id": "dydx-mainnet-1", "to_chain_id": "noble-1", "bridge_id": "IBC", ... }, "tx_index": 0, ... },
    { "cctp_transfer": { "from_chain_id": "noble-1", "to_chain_id": "<transit_evm_chain_id>", "bridge_id": "CCTP", ... }, "tx_index": 0, ... },
    { "cctp_transfer_v2": { "from_chain_id": "<transit_evm_chain_id>", "to_chain_id": "1776", "bridge_id": "CCTP_V2", ... }, "tx_index": 0, ... },
    { "transfer": { "from_chain_id": "injective-1", "to_chain_id": "dydx-mainnet-1", "bridge_id": "IBC", ... }, "tx_index": 1, ... }
  ]
}
```

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:

```json Regular swap route theme={null}
{
  "operations": [
    { "transfer": { "from_chain_id": "dydx-mainnet-1", "to_chain_id": "osmosis-1", "bridge_id": "IBC", ... }, "tx_index": 0, ... },
    { "swap": { "chain_id": "osmosis-1", "swap_venues": [{ "chain_id": "osmosis-1", "name": "osmosis-poolmanager" }], ... }, "tx_index": 0, ... },
    { "transfer": { "from_chain_id": "osmosis-1", "to_chain_id": "dydx-mainnet-1", "bridge_id": "IBC", ... }, "tx_index": 0, ... }
  ]
}
```

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**.

| Index | `required_chain_addresses` | `address_list` |
| - | - | - |
| 0 | `dydx-mainnet-1` | `dydx1...` |
| 1 | `noble-1` | `noble1...` |
| 2 | `<transit_evm_chain_id>` | `0x...` |
| 3 | `injective-1` | `inj1...` |
| 4 | `dydx-mainnet-1` | `dydx1...` |

<Warning>
  **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.
</Warning>

Use the chain's bech32 format for Cosmos addresses and `0x...` for EVM addresses.

### 3.2 Request payload

```tsx theme={null}
const msgsRequest = {
  // ★ source/dest chain IDs, denoms, and amount_in come from the /route response —
  //   not from your original request.
  source_asset_denom: route.source_asset_denom,
  source_asset_chain_id: route.source_asset_chain_id,
  dest_asset_denom: route.dest_asset_denom,
  dest_asset_chain_id: route.dest_asset_chain_id,
  amount_in: route.amount_in,
  amount_out: route.amount_out,

  // the array assembled in 3.1, unchanged
  address_list: addressList,

  // operations passed through from the /route response, unmodified — never hand-edit this value
  operations: route.operations,

  // slippage tolerance (%). Use the default "1" (1%) for the migration flow.
  slippage_tolerance_percent: '1',

  // Affiliate fee configuration field. Not used in the migration flow, so pass an empty object.
  chain_ids_to_affiliates: {},
};
```

**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.

<Warning>
  **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.
</Warning>

### 3.3 Response

The `/v2/fungible/msgs` response can include both a `txs` field and a `msgs` field.

<Warning>
  **`msgs` is a legacy response format slated for removal (deprecated) — don't use it. Always use `txs`.**
</Warning>

```json theme={null}
{
  "txs": [
    {
      "cosmos_tx": {
        "chain_id": "dydx-mainnet-1",
        "signer_address": "dydx1...",
        "msgs": [{ "msg_type_url": "/ibc.applications.transfer.v1.MsgTransfer", "msg": "{...}" }]
      }
    },
    {
      "cosmos_tx": {
        "chain_id": "injective-1",
        "signer_address": "inj1...",
        "msgs": [{ "msg_type_url": "/ibc.applications.transfer.v1.MsgTransfer", "msg": "{...}" }]
      }
    }
  ]
}
```

**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:

| `msg_type_url` | Description |
| - | - |
| `/ibc.applications.transfer.v1.MsgTransfer` | An IBC transfer leg (e.g. dYdX → Noble, Injective → dYdX) |
| `/circle.cctp.v1.MsgDepositForBurnWithCaller` | The CCTP burn when the route starts from Noble |
| `/cosmos.bank.v1beta1.MsgSend` | The relay-fee transfer bundled into the same tx as the burn above |

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.

<Warning>
  **A note on `timeout_timestamp`**

  An 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.
</Warning>

## 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.

```tsx theme={null}
const currentTx = txs[i].cosmos_tx;
```

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:

```jsx theme={null}
// 1) Convert each message from the msgs response into the EncodeObject({ typeUrl, value })
//    shape your wallet expects. toEncodeObject/safeJsonParse are out of scope for this guide —
//    implement them yourself (watch out for timeout_timestamp precision, see above).
const msgAnys = currentTx.msgs.map(({ msg_type_url, msg }) => toEncodeObject(msg_type_url, safeJsonParse(msg)));

// 2) msgAnys/currentTx alone aren't enough to build a SignDoc — account_number/sequence must
//    be fetched separately from that chain's account-query API (e.g. the LCD endpoint
//    /cosmos/auth/v1beta1/accounts/{address}), and the fee (denom/amount/gas limit) must be
//    decided separately too. Both are out of scope for this guide.
const accountInfo = await fetchAccountInfo(currentTx.chain_id, currentTx.signer_address); // account_number, sequence
const fee = getFeeFor(currentTx.chain_id); // denom, amount, gas limit

// 3) Keplr's signDirect performs "signing only" — it never broadcasts on its own.
//    (Don't use a high-level method like CosmJS's SigningStargateClient.signAndBroadcast,
//    which signs and broadcasts in one call — broadcasting must happen separately through the
//    Skip API in chapter 5.)
//    buildSignDoc (assembling TxBody/AuthInfo) is also out of scope — implement it yourself.
const { signed, signature } = await window.keplr.signDirect(
  currentTx.chain_id,
  currentTx.signer_address,
  buildSignDoc(msgAnys, accountInfo, fee),
);

// 4) Assemble the signed TxRaw from the result and base64-encode it.
//    encodeTxRawBase64 is also out of scope — implement it yourself.
const txRawBase64 = encodeTxRawBase64(signed, signature);
```

### 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`

```tsx theme={null}
const submitRequest = {
  chain_id: currentTx.chain_id,
  tx: '<base64-encoded TxRaw>',
};
```

`/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.

<Warning>
  **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.
</Warning>

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`

```text theme={null}
GET /v2/tx/status?chain_id=<chain_id>&tx_hash=<tx_hash>
```

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`.

| State | Meaning / action |
| - | - |
| `STATE_PENDING` | In progress — keep polling |
| `STATE_COMPLETED_SUCCESS` | This tx's transfer is complete — safe to proceed to the next tx |
| `STATE_COMPLETED_ERROR` | Failed — stop |
| `STATE_ABANDONED` | Failed (abandoned) — stop |
| `STATE_PENDING_ERROR` | Failed — stop |

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.

<Warning>
  Compare against the exact enum value. Don't judge by a partial substring match.

  ```ts theme={null}
  // Correct
  state === "STATE_COMPLETED_SUCCESS"

  // Wrong
  state.includes("COMPLETED")
  ```

  `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.**
</Warning>

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):

```text theme={null}
tx[0] signed → submit → STATE_PENDING → (IBC/CCTP relay processing) → STATE_COMPLETED_SUCCESS
  ↓
tx[1] signed → submit → STATE_PENDING → ... → STATE_COMPLETED_SUCCESS
```

**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.

<Note>
  **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.
</Note>

## 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.

| | Source | Destination |
| - | - | - |
| Original request | dYdX USDC.n | dYdX USDC.inj |
| Retry request | **Injective USDC.inj** | dYdX USDC.inj |

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

<Steps>
  <Step title="GET /v2/info/chains, GET /v2/fungible/assets">
    Confirm supported chains/assets, `chain_id`, `denom`.
  </Step>

  <Step title="POST /v2/fungible/route">
    Get a route quote + check in `operations` that the migration route was actually selected.
  </Step>

  <Step title="Build address_list">
    Exactly in `required_chain_addresses` order.
  </Step>

  <Step title="POST /v2/fungible/msgs">
    Generate the `txs[]` to sign.
  </Step>

  <Step title="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.
  </Step>

  <Step title="Done" />
</Steps>

### Pseudocode

```tsx theme={null}
const BASE_URL = 'https://go.skip.build/api/skip';

// 1) Confirm chains/assets — never hardcode, always resolve via the API.
const chainsRes = await fetch(`${BASE_URL}/v2/info/chains?include_evm=true&include_svm=true&only_testnets=false`);
const { chains } = await chainsRes.json();

const assetsRes = await fetch(
  `${BASE_URL}/v2/fungible/assets?include_evm_assets=true&include_cw20_assets=true&include_svm_assets=true&only_testnets=false`,
);
// Response shape: { chain_to_assets_map: { [chain_id]: { assets: [{ denom, origin_chain_id, origin_denom, ... }] } } }
const { chain_to_assets_map } = await assetsRes.json();

function assertChainSupported(chainId: string) {
  if (!chains.some((c) => c.chain_id === chainId)) {
    throw new Error(`"${chainId}" is not a chain_id present in /v2/info/chains.`);
  }
}

// Don't tell USDC.n and USDC.inj apart by symbol — distinguish them by which chain/denom the
// asset was originally issued on (origin_chain_id/origin_denom). USDC.n's origin is Noble's
// (noble-1) uusdc; USDC.inj's origin is the USDC minted on Injective (injective-1) via CCTP v2.
function findDenomByOrigin(chainId: string, originChainId: string, originDenom: string): string {
  const asset = chain_to_assets_map[chainId]?.assets.find(
    (a) => a.origin_chain_id === originChainId && a.origin_denom === originDenom,
  );
  if (!asset) {
    throw new Error(`Could not find an asset on "${chainId}" with origin (${originChainId}, ${originDenom}).`);
  }
  return asset.denom;
}

const sourceChainId = 'dydx-mainnet-1';
const destChainId = 'dydx-mainnet-1';
assertChainSupported(sourceChainId);
assertChainSupported(destChainId);

const sourceDenom = findDenomByOrigin(sourceChainId, 'noble-1', 'uusdc'); // USDC.n
const destDenom = findDenomByOrigin(destChainId, 'injective-1', 'erc20:0xa00C59fF5a080D2b954d0c75e46E22a0c371235a'); // USDC.inj

// 2) Request a route
const routeRes = await fetch(`${BASE_URL}/v2/fungible/route`, {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({
    amount_in: '1000000',
    source_asset_chain_id: sourceChainId,
    source_asset_denom: sourceDenom,
    dest_asset_chain_id: destChainId,
    dest_asset_denom: destDenom,
    smart_relay: true,
    experimental_features: ['stargate', 'eureka', 'layer_zero'],
    allow_multi_tx: true,
    allow_unsafe: true,
    smart_swap_options: { split_routes: true, evm_swaps: true },
    go_fast: true,
    fast_transfer: true,
  }),
});
// Error handling for routeRes.ok etc. is omitted here — a real implementation must handle it properly.
const route = await routeRes.json();
// Check whether operations actually contains a CCTP leg — a swap route can be selected even with
// this payload, so this check is the only way to tell whether it's the migration route
// (see chapter 2). assertRouteContainsCctp is out of scope — implement it yourself.
assertRouteContainsCctp(route.operations);

// 3) Build address_list — exactly in required_chain_addresses order, with no slot skipped.
// getUserAddressFor (how you obtain each chain's user address) is out of scope — implement it
// yourself (see chapter 0).
const addressList = route.required_chain_addresses.map((chainId) => getUserAddressFor(chainId));

// 4) Generate the transactions to sign — source/dest/amount_in/amount_out/operations come from the route response
const msgsRes = await fetch(`${BASE_URL}/v2/fungible/msgs`, {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({
    source_asset_denom: route.source_asset_denom,
    source_asset_chain_id: route.source_asset_chain_id,
    dest_asset_denom: route.dest_asset_denom,
    dest_asset_chain_id: route.dest_asset_chain_id,
    amount_in: route.amount_in,
    amount_out: route.amount_out,
    address_list: addressList,
    operations: route.operations,
    slippage_tolerance_percent: '1',
    chain_ids_to_affiliates: {},
  }),
});
const msgsResponse = await msgsRes.json();

// 5) Process each tx in order: sign → submit → confirm status
for (const { cosmos_tx: currentTx } of msgsResponse.txs) {
  // 5-1) Sign — delegate to a wallet (4.1) or sign directly (4.2). signCosmosTx is out of
  // scope — implement it yourself. The result is a base64 TxRaw.
  const txRawBase64 = await signCosmosTx(currentTx);

  // 5-2) Broadcasting must go only through /v2/tx/submit. Error handling for network errors,
  // 4xx/5xx responses, etc. is omitted here — a real implementation must handle it properly.
  const submitRes = await fetch(`${BASE_URL}/v2/tx/submit`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ chain_id: currentTx.chain_id, tx: txRawBase64 }),
  });
  const { tx_hash } = await submitRes.json();

  // 5-3) Poll until complete — judge by an exact string comparison on transfers[0].state.
  // The while(true) below is a simplification for explanatory purposes — a real implementation
  // should cap the number of retries/timeout.
  while (true) {
    const statusRes = await fetch(`${BASE_URL}/v2/tx/status?chain_id=${currentTx.chain_id}&tx_hash=${tx_hash}`);
    const status = await statusRes.json();
    const state = status.transfers?.[0]?.state;

    if (['STATE_COMPLETED_ERROR', 'STATE_ABANDONED', 'STATE_PENDING_ERROR'].includes(state)) {
      throw new Error(`tx ${tx_hash} failed: ${state}`);
    }
    if (state === 'STATE_COMPLETED_SUCCESS') {
      break;
    }
    await new Promise((resolve) => setTimeout(resolve, 4000)); // re-check every 3–5 seconds
  }

  // Move on to the next tx — if this was an Injective-signed tx, also confirm Skip Gas
  // Station's automatic INJ top-up landed (chapter 7).
}

// 6) All txs complete — migration done.
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.