> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rhinestone.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Execute a crosschain swap

> Bridge and swap tokens across chains in a single intent using the Rhinestone API.

In this tutorial you will swap USDC on Base for ETH on Arbitrum in a single intent. Warp handles the bridge and swap automatically — one signature, one operation, one confirmation.

By the end you will have a working end-to-end implementation you can adapt for any crosschain swap.

## Prerequisites

* A Rhinestone API key ([request one here](https://tally.so/r/wg22x4))
* An EOA with USDC on Base (or another [supported chain and token](/home/resources/supported-chains))
* A viem `WalletClient` configured for signing

## Setup

```ts theme={null}
import { createWalletClient, http, erc20Abi, maxUint256, type Hex } from "viem";
import { base } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";

const BASE_URL = "https://v1.orchestrator.rhinestone.dev";
const API_KEY = process.env.RHINESTONE_API_KEY;

const headers = {
  "Content-Type": "application/json",
  "x-api-key": API_KEY,
  "x-api-version": "2026-04.blanc",
};

const account = privateKeyToAccount(process.env.PRIVATE_KEY as Hex);

const walletClient = createWalletClient({
  account,
  chain: base,
  transport: http(),
});

const EOA_ADDRESS = account.address;
```

## Steps

<Steps>
  <Step title="Get a quote">
    Request a quote for swapping USDC on Base into ETH on Arbitrum. Specify the destination chain, the token you want, and the amount:

    ```ts theme={null}
    const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
    const ETH_ARBITRUM = "0x0000000000000000000000000000000000000000"; // native ETH
    const ETH_AMOUNT = "10000000000000000"; // 0.01 ETH (18 decimals)

    const quoteRes = await fetch(`${BASE_URL}/quotes`, {
      method: "POST",
      headers,
      body: JSON.stringify({
        account: {
          address: EOA_ADDRESS,
          accountType: "EOA",
        },
        destinationChainId: "eip155:42161", // Arbitrum
        tokenRequests: [
          {
            tokenAddress: ETH_ARBITRUM,
            amount: ETH_AMOUNT,
          },
        ],
        accountAccessList: {
          chainTokens: {
            "eip155:8453": [USDC_BASE], // Base
          },
        },
      }),
    });

    const { routes } = await quoteRes.json();
    const route = routes[0];
    const { intentId, cost, signData, tokenRequirements } = route;
    ```

    <Info>
      `accountAccessList` constrains which tokens on which chains the router can spend.
      Without it, the API may route through multiple chains and tokens (including wrapped ETH),
      which can lead to unexpected gas requirements. Specify the exact source token(s) you want to use.
    </Info>

    Because the source token (USDC on Base) differs from the destination token (ETH on Arbitrum), `cost.input` and `cost.output` show different tokens. This is how you know Warp is routing through a swap.

    ```ts theme={null}
    console.log("Spending:", cost.input);
    console.log("Receiving:", cost.output);
    ```
  </Step>

  <Step title="Approve token spending">
    Check `tokenRequirements` for any approvals needed. For a USDC source, you will typically need a Permit2 approval:

    ```ts theme={null}
    if (tokenRequirements) {
      for (const [chainId, tokens] of Object.entries(tokenRequirements)) {
        for (const [tokenAddress, requirement] of Object.entries(tokens as Record<string, any>)) {
          if (requirement.type === "approval") {
            console.log(`Approving ${tokenAddress} on ${chainId}...`);

            const { request } = await walletClient.simulateContract({
              address: tokenAddress as `0x${string}`,
              abi: erc20Abi,
              functionName: "approve",
              args: [requirement.spender, maxUint256],
            });

            const hash = await walletClient.writeContract(request);
            console.log("Approval tx:", hash);
          }
        }
      }
    }
    ```

    <Info>
      This approves to the [Permit2](https://github.com/Uniswap/permit2) contract. Once approved, future intents spending the same token on the same chain will not need another approval.
    </Info>
  </Step>

  <Step title="Sign the intent">
    Forward `signData.origin[]` and `signData.destination` directly to your wallet. One signature per source chain, plus the destination signature.

    ```ts theme={null}
    const originSignatures = await Promise.all(
      signData.origin.map((typedData) =>
        walletClient.signTypedData(typedData),
      ),
    );
    const destinationSignature = await walletClient.signTypedData(
      signData.destination,
    );
    ```

    <Card title="Signing guide" icon="signature" href="../guides/signing">
      Smart account signing and validator wrapping.
    </Card>
  </Step>

  <Step title="Submit the intent">
    Post the signed intent to `/intents` using the `intentId` from the quote:

    ```ts theme={null}
    const submitRes = await fetch(`${BASE_URL}/intents`, {
      method: "POST",
      headers,
      body: JSON.stringify({
        intentId,
        signatures: {
          origin: originSignatures,
          destination: destinationSignature,
        },
      }),
    });

    const { intentId: submittedId } = await submitRes.json();

    console.log("Intent submitted:", submittedId);
    ```

    If the submit returns 404, the quote TTL elapsed — re-quote and re-sign.
  </Step>

  <Step title="Poll for completion">
    Track the intent status until it reaches a final state:

    ```ts theme={null}
    async function pollUntilComplete(intentId: string) {
      const FINAL_STATUSES = ["COMPLETED", "FAILED", "EXPIRED"];

      while (true) {
        const res = await fetch(`${BASE_URL}/intents/${intentId}`, { headers });

        const data = await res.json();
        console.log("Status:", data.status);

        if (FINAL_STATUSES.includes(data.status)) {
          if (data.status === "COMPLETED") {
            console.log("Swap complete!");
            console.log("Fill tx:", data.fillTransactionHash);
          } else {
            console.error("Intent failed with status:", data.status);
          }
          return data.status;
        }

        await new Promise((resolve) => setTimeout(resolve, 2000));
      }
    }

    await pollUntilComplete(submittedId);
    ```

    Typical execution time is under 2 seconds. `FILLED` is an intermediate state — the relayer has delivered funds on the destination, but the source-chain claim hasn't settled yet. `COMPLETED` means everything is settled.
  </Step>
</Steps>

## Next steps

<CardGroup cols={2}>
  <Card title="Sponsor fees" icon="circle-dollar-sign" href="../guides/getting-a-quote">
    Cover bridge and swap fees for your users using `sponsorSettings`.
  </Card>

  <Card title="Execute crosschain calls" icon="arrow-right-arrow-left" href="../features/execute-crosschain-calls">
    Add destination chain executions to your intent — deposit into a vault, buy an NFT, and more.
  </Card>
</CardGroup>
