# Quick Start

Complete your first cross-chain trade with WheelX API in a few minutes.

This guide walks you through a cross-chain trade from USDC on Ethereum to USDT on Soneium, from quote to final status.

### Before You Start

Base URL:

```
https://api.wheelx.fi/v1
```

Requirements:

* Node.js `18+`
* an Ethereum RPC URL
* an EVM wallet address
* a signer or wallet client that can send transactions on Ethereum
* a wallet that holds USDC on Ethereum mainnet
* a small amount of ETH for gas

### System Flow

This is the execution flow for a standard WheelX cross-chain trade.

```mermaid
sequenceDiagram
  participant Script
  participant WheelX API
  participant Wallet
  participant Ethereum

  Script->>WheelX API: POST /quote
  WheelX API-->>Script: request_id + tx + approve
  Script->>Wallet: approve ERC20 if needed
  Wallet->>Ethereum: approval transaction
  Script->>Wallet: send tx.to + tx.data + tx.value
  Wallet->>Ethereum: source-chain transaction
  Script->>WheelX API: GET /order/{request_id}
  WheelX API-->>Script: Open -> Filled / Failed
```

### Try It Step by Step

{% stepper %}
{% step %}

#### Install the dependencies

This example uses `axios` to call the API and `viem` to check allowance, approve the token, and send the transaction. `viem` is not required by WheelX itself. It is included here because the sample script uses it end to end.

{% tabs %}
{% tab title="npm" %}

```bash
npm install axios viem
```

{% endtab %}

{% tab title="pnpm" %}

```bash
pnpm add axios viem
```

{% endtab %}

{% tab title="yarn" %}

```bash
yarn add axios viem
```

{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}

#### Configure your environment

Set the RPC endpoint and the wallet address before running the script.

```bash
export RPC_URL="https://your-ethereum-rpc"
export WALLET_ADDRESS="0xyourwalletaddress"
```

{% endstep %}

{% step %}

#### Request a quote

This example performs a bridge-and-swap flow from Ethereum USDC to Soneium USDT.

```json
{
  "from_chain": 1,
  "to_chain": 1868,
  "from_token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
  "to_token": "0x3A337a6adA9d885b6Ad95ec48F9b75f197b5AE35",
  "from_address": "0x1111111111111111111111111111111111111111",
  "to_address": "0x1111111111111111111111111111111111111111",
  "amount": "100000000",
  "affiliation": null,
  "to_platform_id": 0,
  "quote_only": true
}
```

You can use the placeholder address above to try the quote request first. Replace both address fields with your own EVM address before sending a real transaction. In the full script below, this value is read from `WALLET_ADDRESS`.

<details>

<summary>View the same request as CURL</summary>

```bash
curl --request POST 'https://api.wheelx.fi/v1/quote' \
  --header 'Content-Type: application/json' \
  --data '{
    "from_chain": 1,
    "to_chain": 1868,
    "from_token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "to_token": "0x3A337a6adA9d885b6Ad95ec48F9b75f197b5AE35",
    "from_address": "0x1111111111111111111111111111111111111111",
    "to_address": "0x1111111111111111111111111111111111111111",
    "amount": "100000000",
    "affiliation": null,
    "to_platform_id": 0,
    "quote_only": true
  }'
```

</details>
{% endstep %}

{% step %}

#### Review the response

A successful quote returns the transaction payload, the approval requirement, and the order identifier you will use later for status tracking.

```json
{
  "request_id": "0x00000000000000000000000000000000019ce54ecf0a7cd3a9cc98cc2b805241",
  "amount_out": "99324911",
  "tx": {
    "to": "0x7eC9672678509a574F6305F112a7E3703845a98b",
    "value": "0",
    "data": "0xb3c8c6da..."
  },
  "approve": {
    "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "spender": "0x7eC9672678509a574F6305F112a7E3703845a98b",
    "amount": 100000000
  },
  "min_receive": "94358665",
  "estimated_time": 10.0,
  "router": "Pancakeswap + WheelX + Uniswap"
}
```

Key fields to check:

* `request_id`: save this value for order polling
* `tx.to`: contract address for the source-chain transaction
* `tx.data`: calldata to submit
* `tx.value`: native token value to include with the transaction
* `approve`: approval instructions for the input token; if not `null`, approve before the main transaction
* `amount_out`: quoted destination amount
* `min_receive`: minimum receive amount after slippage
* `estimated_time`: rough route duration
* `router`: readable route summary

{% hint style="info" %}
The quote already contains the transaction payload needed for execution. You do not need to build calldata yourself.
{% endhint %}

<details>

<summary>View a longer response example</summary>

```json
{
  "request_id": "0x00000000000000000000000000000000019ce54ecf0a7cd3a9cc98cc2b805241",
  "amount_out": "99324911",
  "fee": "0.22898808463509798380",
  "tx": {
    "to": "0x7eC9672678509a574F6305F112a7E3703845a98b",
    "value": "0",
    "data": "0xb3c8c6da...",
    "chainId": 1,
    "gas": null,
    "maxFeePerGas": null,
    "maxPriorityFeePerGas": null
  },
  "approve": {
    "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "spender": "0x7eC9672678509a574F6305F112a7E3703845a98b",
    "amount": 100000000
  },
  "slippage": 500,
  "min_receive": "94358665",
  "estimated_time": 10.0,
  "recipient": "0xac50fab2bf7a921a73298a691dc047a6fc2ad34a",
  "router_type": "swap",
  "price_impact": {
    "bridge_fee": "0.22544245870336198380",
    "swap_fee": "0",
    "dst_gas_fee": "0.00354562593173600000"
  },
  "router": "Pancakeswap + WheelX + Uniswap",
  "created_at": "2026-03-13T03:47:54.969734",
  "points": "22.54",
  "quote_message": null,
  "routes": [
    { "name": "Pancakeswap" },
    { "name": "WheelX" },
    { "name": "Uniswap" }
  ]
}
```

</details>
{% endstep %}

{% step %}

#### Execute the trade

Create a file named `wheelx-quickstart.mjs`, paste the script below, and connect your own signer before running it. The script focuses on the WheelX flow and leaves wallet setup to your application.

<details>

<summary>View the full Node.js script</summary>

```js
import axios from 'axios'
import {
  createPublicClient,
  erc20Abi,
  http
} from 'viem'
import { mainnet } from 'viem/chains'

const API_BASE_URL = 'https://api.wheelx.fi/v1'

async function getWalletConnection() {
  // Replace this with your own wallet setup.
  // Return an object in this shape:
  // {
  //   account: { address: '0x...' },
  //   walletClient: yourViemWalletClient
  // }
  throw new Error('Replace getWalletConnection() with your own wallet setup.')
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

async function fetchQuote(accountAddress) {
  const response = await axios.post(`${API_BASE_URL}/quote`, {
    from_chain: 1,
    to_chain: 1868,
    from_token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
    to_token: '0x3A337a6adA9d885b6Ad95ec48F9b75f197b5AE35',
    from_address: accountAddress,
    to_address: accountAddress,
    amount: '100000000',
    affiliation: null,
    to_platform_id: 0,
    quote_only: true
  })

  if (!response.data.tx) {
    throw new Error('Quote returned without an executable tx payload.')
  }

  return response.data
}

async function ensureApproval({ quote, account, walletClient, publicClient }) {
  if (!quote.approve) return

  const approveAmount = BigInt(quote.approve.amount)

  const allowance = await publicClient.readContract({
    address: quote.approve.token,
    abi: erc20Abi,
    functionName: 'allowance',
    args: [account.address, quote.approve.spender]
  })

  if (allowance >= approveAmount) {
    console.log('✅ Approval already sufficient.')
    return
  }

  console.log('📝 Sending approval transaction...')

  const approvalHash = await walletClient.writeContract({
    account,
    address: quote.approve.token,
    abi: erc20Abi,
    functionName: 'approve',
    args: [quote.approve.spender, approveAmount],
    chain: mainnet
  })

  await publicClient.waitForTransactionReceipt({ hash: approvalHash })
  console.log('✅ Approval confirmed:', approvalHash)
}

async function sendMainTransaction({ quote, account, walletClient, publicClient }) {
  console.log('🚀 Sending source-chain transaction...')

  const txHash = await walletClient.sendTransaction({
    account,
    chain: mainnet,
    to: quote.tx.to,
    data: quote.tx.data,
    value: BigInt(quote.tx.value)
  })

  await publicClient.waitForTransactionReceipt({ hash: txHash })
  console.log('✅ Source transaction confirmed:', txHash)
}

async function pollOrder(requestId) {
  while (true) {
    try {
      const response = await axios.get(`${API_BASE_URL}/order/${requestId}`)
      const order = response.data

      console.log('⏳ Current order status:', order.status)

      if (order.status === 'Filled' || order.status === 'Failed') {
        return order
      }
    } catch (error) {
      if (axios.isAxiosError(error) && error.response?.status === 404) {
        console.log('⌛ Order not indexed yet. Retrying...')
        await sleep(3000)
        continue
      }

      throw error
    }

    await sleep(3000)
  }
}

async function main() {
  const rpcUrl = process.env.RPC_URL
  const walletAddress = process.env.WALLET_ADDRESS

  if (!rpcUrl) {
    throw new Error('Missing RPC_URL')
  }

  if (!walletAddress) {
    throw new Error('Missing WALLET_ADDRESS')
  }

  const { account, walletClient } = await getWalletConnection()

  if (!account?.address) {
    throw new Error('Missing account.address from getWalletConnection()')
  }

  if (!walletClient) {
    throw new Error('Missing walletClient from getWalletConnection()')
  }

  if (walletAddress.toLowerCase() !== account.address.toLowerCase()) {
    throw new Error('WALLET_ADDRESS must match the connected wallet address')
  }

  const publicClient = createPublicClient({
    chain: mainnet,
    transport: http(rpcUrl)
  })

  const quote = await fetchQuote(walletAddress)

  console.log('🆔 request_id:', quote.request_id)
  console.log('👛 wallet:', walletAddress)
  console.log('🧭 router:', quote.router)
  console.log('💰 amount_out:', quote.amount_out)
  console.log('🛡️ min_receive:', quote.min_receive)

  await ensureApproval({
    quote,
    account,
    walletClient,
    publicClient
  })

  await sendMainTransaction({
    quote,
    account,
    walletClient,
    publicClient
  })

  const finalOrder = await pollOrder(quote.request_id)
  console.log('🎉 Final order:', finalOrder)
}

main().catch((error) => {
  console.error('❌ Execution failed:', error)
  process.exit(1)
})
```

</details>

<details>

<summary>View a local testing version that uses PRIVATE_KEY</summary>

If you prefer to run this flow locally without adding wallet connection code first, you can use a private key from an environment variable.

```bash
export PRIVATE_KEY="0xyourprivatekey"
```

```js
import axios from 'axios'
import {
  createPublicClient,
  createWalletClient,
  erc20Abi,
  http
} from 'viem'
import { mainnet } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'

const API_BASE_URL = 'https://api.wheelx.fi/v1'

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

async function fetchQuote(accountAddress) {
  const response = await axios.post(`${API_BASE_URL}/quote`, {
    from_chain: 1,
    to_chain: 1868,
    from_token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
    to_token: '0x3A337a6adA9d885b6Ad95ec48F9b75f197b5AE35',
    from_address: accountAddress,
    to_address: accountAddress,
    amount: '100000000',
    affiliation: null,
    to_platform_id: 0,
    quote_only: true
  })

  if (!response.data.tx) {
    throw new Error('Quote returned without an executable tx payload.')
  }

  return response.data
}

async function ensureApproval({ quote, account, walletClient, publicClient }) {
  if (!quote.approve) return

  const approveAmount = BigInt(quote.approve.amount)

  const allowance = await publicClient.readContract({
    address: quote.approve.token,
    abi: erc20Abi,
    functionName: 'allowance',
    args: [account.address, quote.approve.spender]
  })

  if (allowance >= approveAmount) {
    console.log('✅ Approval already sufficient.')
    return
  }

  console.log('📝 Sending approval transaction...')

  const approvalHash = await walletClient.writeContract({
    account,
    address: quote.approve.token,
    abi: erc20Abi,
    functionName: 'approve',
    args: [quote.approve.spender, approveAmount],
    chain: mainnet
  })

  await publicClient.waitForTransactionReceipt({ hash: approvalHash })
  console.log('✅ Approval confirmed:', approvalHash)
}

async function sendMainTransaction({ quote, account, walletClient, publicClient }) {
  console.log('🚀 Sending source-chain transaction...')

  const txHash = await walletClient.sendTransaction({
    account,
    chain: mainnet,
    to: quote.tx.to,
    data: quote.tx.data,
    value: BigInt(quote.tx.value)
  })

  await publicClient.waitForTransactionReceipt({ hash: txHash })
  console.log('✅ Source transaction confirmed:', txHash)
}

async function pollOrder(requestId) {
  while (true) {
    try {
      const response = await axios.get(`${API_BASE_URL}/order/${requestId}`)
      const order = response.data

      console.log('⏳ Current order status:', order.status)

      if (order.status === 'Filled' || order.status === 'Failed') {
        return order
      }
    } catch (error) {
      if (axios.isAxiosError(error) && error.response?.status === 404) {
        console.log('⌛ Order not indexed yet. Retrying...')
        await sleep(3000)
        continue
      }

      throw error
    }

    await sleep(3000)
  }
}

async function main() {
  const rpcUrl = process.env.RPC_URL
  const privateKey = process.env.PRIVATE_KEY
  const walletAddress = process.env.WALLET_ADDRESS

  if (!rpcUrl) {
    throw new Error('Missing RPC_URL')
  }

  if (!privateKey) {
    throw new Error('Missing PRIVATE_KEY')
  }

  if (!walletAddress) {
    throw new Error('Missing WALLET_ADDRESS')
  }

  const account = privateKeyToAccount(privateKey)

  if (walletAddress.toLowerCase() !== account.address.toLowerCase()) {
    throw new Error('WALLET_ADDRESS must match the address derived from PRIVATE_KEY')
  }

  const publicClient = createPublicClient({
    chain: mainnet,
    transport: http(rpcUrl)
  })

  const walletClient = createWalletClient({
    account,
    chain: mainnet,
    transport: http(rpcUrl)
  })

  const quote = await fetchQuote(walletAddress)

  console.log('🆔 request_id:', quote.request_id)
  console.log('👛 wallet:', walletAddress)
  console.log('🧭 router:', quote.router)
  console.log('💰 amount_out:', quote.amount_out)
  console.log('🛡️ min_receive:', quote.min_receive)

  await ensureApproval({
    quote,
    account,
    walletClient,
    publicClient
  })

  await sendMainTransaction({
    quote,
    account,
    walletClient,
    publicClient
  })

  const finalOrder = await pollOrder(quote.request_id)
  console.log('🎉 Final order:', finalOrder)
}

main().catch((error) => {
  console.error('❌ Execution failed:', error)
  process.exit(1)
})
```

</details>

Run it:

```bash
node wheelx-quickstart.mjs
```

{% endstep %}

{% step %}

#### Check the order status

For this flow, the order status is simple:

* `Open`: the order has started and is still in progress
* `Filled`: the cross-chain trade is complete
* `Failed`: the order did not complete successfully

Use this rule in your application:

1. Submit the source-chain transaction
2. Keep the `request_id`
3. Poll `/order/{request_id}`
4. Mark the trade as complete only when the order reaches `Filled`

{% hint style="warning" %}
For cross-chain flows, the source-chain transaction receipt is not the final completion signal. Use the order status.
{% endhint %}
{% endstep %}
{% endstepper %}
