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

# Make a swap

> Swap into a requested token on the same chain or across chains.

export const VmCodeGroup = ({children, caption, label = "Virtual machine"}) => {
  const [vm, setVm] = useState("evm");
  const [open, setOpen] = useState(false);
  const selectorRef = useRef(null);
  const triggerRef = useRef(null);
  const menuRef = useRef(null);
  const focusIndex = useRef(0);
  const options = [{
    value: "evm",
    title: "EVM",
    icon: "/images/networks/ethereum.svg"
  }, {
    value: "solana",
    title: "Solana",
    icon: "/images/networks/solana.svg"
  }];
  if (typeof caption !== "string" || caption.trim() === "") {
    throw new Error("VmCodeGroup requires a non-empty caption.");
  }
  const selected = options.find(option => option.value === vm);
  const blocks = (Array.isArray(children) ? children : [children]).filter(child => typeof child !== "string" || child.trim() !== "");
  if (blocks.length !== 2 || !blocks.every(block => typeof block?.props?.language === "string" && block.props.numberOfLines !== undefined) || !options.every(option => blocks.filter(block => block.props.filename === option.title).length === 1)) {
    throw new Error("VmCodeGroup requires two fenced code blocks titled EVM and Solana, with no other content.");
  }
  useEffect(() => {
    const path = window.location.pathname;
    const selection = window.__rhinestoneDocsVm;
    if (selection?.path === path) setVm(selection.vm);
    const onChange = event => {
      if (event.detail.path === path) {
        setVm(event.detail.vm);
        setOpen(false);
      }
    };
    window.addEventListener("rhinestone-docs-vm", onChange);
    return () => window.removeEventListener("rhinestone-docs-vm", onChange);
  }, []);
  useEffect(() => {
    if (!open) return;
    menuRef.current?.querySelectorAll("button")[focusIndex.current]?.focus();
    const dismiss = event => {
      if (!selectorRef.current?.contains(event.target)) setOpen(false);
    };
    document.addEventListener("pointerdown", dismiss);
    return () => document.removeEventListener("pointerdown", dismiss);
  }, [open]);
  const selectVm = value => {
    const selection = {
      path: window.location.pathname,
      vm: value
    };
    window.__rhinestoneDocsVm = selection;
    window.dispatchEvent(new CustomEvent("rhinestone-docs-vm", {
      detail: selection
    }));
    triggerRef.current?.focus();
  };
  const openMenu = index => {
    focusIndex.current = index;
    setOpen(true);
    menuRef.current?.querySelectorAll("button")[index]?.focus();
  };
  const handleMenuKey = event => {
    const buttons = Array.from(menuRef.current.querySelectorAll("button"));
    const current = buttons.indexOf(document.activeElement);
    let next;
    if (event.key === "ArrowDown") next = (current + 1) % buttons.length;
    if (event.key === "ArrowUp") next = (current + buttons.length - 1) % buttons.length;
    if (event.key === "Home" || event.key.toLowerCase() === "e") next = 0;
    if (event.key === "End" || event.key.toLowerCase() === "s") next = buttons.length - 1;
    if (next !== undefined) {
      event.preventDefault();
      buttons[next].focus();
    }
    if (event.key === "Tab") setOpen(false);
  };
  return <div className="vm-code-group">
      <div className="vm-code-group-header">
        <span className="vm-code-caption" title={caption}>
          {caption}
        </span>
        <div className="vm-code-selector" ref={selectorRef} onBlur={event => {
    if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
  }} onKeyDown={event => {
    if (event.key === "Escape" && open) {
      event.preventDefault();
      setOpen(false);
      triggerRef.current?.focus();
    }
  }}>
          <button type="button" className="vm-code-trigger" ref={triggerRef} aria-label={`${label}: ${selected.title}`} aria-haspopup="menu" aria-expanded={open} onClick={() => open ? setOpen(false) : openMenu(options.indexOf(selected))} onKeyDown={event => {
    if (event.key === "ArrowDown" || event.key === "ArrowUp") {
      event.preventDefault();
      openMenu(event.key === "ArrowDown" ? 0 : options.length - 1);
    }
  }}>
            <img className="vm-code-icon" src={selected.icon} alt="" width="16" height="16" />
            <span>{selected.title}</span>
            <svg className="vm-code-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
              <path d="m7 10 5 5 5-5" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
            </svg>
          </button>
          {open && <div className="vm-code-menu" role="menu" aria-label={label} ref={menuRef} onKeyDown={handleMenuKey}>
              {options.map(option => <button type="button" role="menuitemradio" aria-checked={vm === option.value} tabIndex={-1} className="vm-code-option" key={option.value} onClick={() => selectVm(option.value)}>
                  <img className="vm-code-icon" src={option.icon} alt="" width="16" height="16" />
                  <span>{option.title}</span>
                  {vm === option.value && <svg className="vm-code-check" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                      <path d="m5 12 4 4L19 6" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
                    </svg>}
                </button>)}
            </div>}
        </div>
      </div>
      {options.map(option => {
    const block = blocks.find(block => block.props.filename === option.title);
    const CodeBlock = block.type;
    return <div key={option.value} className="vm-code-group-panel" data-vm={option.value} hidden={option.value !== vm}>
            <CodeBlock {...block.props} filename={undefined} />
          </div>;
  })}
    </div>;
};

Request the output token and amount on the target chain. Select one source chain and token. The wallet clients below quote the swap and any bridge, and deliver the requested output in one intent.

## Choose the intent shape

| Outcome | Destination delivery | Calls |
| - | - | - |
| Plain swap | Requested output token and amount | None |
| ERC-20 payment | Token needed on the target chain | Real `ERC20.transfer` calldata |
| Contract action funded by a trade | Tokens consumed by the contract | Real approval and application calls |

For a plain swap with the SDK, omit `destination.calls`. Direct Caucasus HTTP selects one `source` chain/token and one `destination` asset, and omits destination execution for a token-only quote. Do not add dummy calldata, a self-transfer, or `{ data: "0x" }`.

Use target-chain token addresses for the requested token. For EVM funding, select one source token and chain from the user's [portfolio](/transactions/multichain/unified-balance). Custom signer SDK portfolio reads are EVM-only; the embedded wallet can also [read managed Solana holdings](/transactions/multichain/unified-balance). Neither client searches or combines other balances; delivery requires a source even when sponsored.

## Request a token-only swap

This example requests 100 USDC on Arbitrum and restricts funding to WETH on Base:

```ts theme={null}
import { encodeFunctionData, erc20Abi, parseUnits } from "viem";
import { arbitrum, base } from "viem/chains";

const wethOnBase = "0x4200000000000000000000000000000000000006";
const usdcOnBase = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const usdcOnArbitrum = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";
const amount = parseUnits("100", 6);
```

For a self-send without destination execution, the amount can be treated as the target's final balance when existing destination funds are also eligible as funding. In Caucasus HTTP, they must belong to the selected source pair. Both examples restrict the source to Base, so any USDC already on Arbitrum is outside that scope and the full 100 USDC is delivered.

<Tabs>
  <Tab title="Embedded wallet">
    Use the initialized `wallet` client from [Accounts](/wallets/embedded-wallets/accounts) after the user signs in; sends always use the authenticated account:

    ```ts {2} theme={null}
    const result = await wallet.sendIntent({
      source: { chain: base.id, token: wethOnBase },
      destination: { chain: arbitrum.id, token: usdcOnArbitrum, amount },
    });

    console.log(result);
    ```

    This embedded-wallet swap uses numeric EVM chain IDs, one `source` token, and one `destination` token, with no calls for a plain swap. `source.chain` applies the WETH address to Base; omitting it funds from the destination chain rather than searching other chains. `sendIntent()` manages quote preparation and does not expose the custom signer SDK's ranked `prepared.quotes` object.

    Sponsorship is required by default. Reuse the funding, review, and completion options from [Send a transaction](/transactions/multichain/send-a-transaction). Preserve the whole result, including `intentId`, submission flags, native destination identifier, and refund evidence; never blindly retry an uncertain submission.

    ### Managed Solana boundary

    Managed Solana same-cluster delivery requires matching SPL mints, so it is not a different-mint swap. Native SOL token delivery and cross-cluster transfers are unsupported. For an EVM → Solana token-delivery example, see [Send a transaction](/transactions/multichain/send-a-transaction); supply a destination mint and recipient, without calls or instructions. Do not rely on Solana → EVM delivery as a swap route yet; see [Networks](/wallets/embedded-wallets/networks#availability) for current limitations. Solana SmartSessions and cross-VM contract execution are unsupported.

    <Note>
      Omit `destination.amount` to request the maximum output from the source token.
      `source.maxAmount` caps the observed source balance, not the total spend.
      Neither client's cap guarantees exact-input spend.
    </Note>
  </Tab>

  <Tab title="Custom signer">
    Use `account` from the [Custom signer quickstart](/wallets/custom-signer/quickstart) with `@rhinestone/sdk@3.0.0` and the constants above. The later preparation examples are alternatives to this request:

    <VmCodeGroup caption="Prepare a cross-chain swap" label="Source VM">
      ```ts EVM theme={null}
      const prepared = await account.prepareTransaction({
        source: { chain: base, token: wethOnBase },
        destination: { chain: arbitrum, token: usdcOnArbitrum, amount },
      });
      ```

      ```ts Solana theme={null}
      import { baseSepolia } from "viem/chains";
      import { solanaAddress, solanaDevnet } from "@rhinestone/sdk/solana";

      // Use a deployed, authorized managed Solana account.
      // Swap native SOL for USDC on Base Sepolia.
      // Same-chain Solana swaps use destination.instructions instead.
      // Solana-origin requests do not accept quoter or settlement filters.
      const prepared = await account.prepareTransaction({
        source: {
          chain: solanaDevnet,
          token: solanaAddress("11111111111111111111111111111111"),
          // Cap includes user-paid fees; the rent reserve is not spendable.
          maxAmount: 50_000_000n,
        },
        destination: {
          chain: baseSepolia,
          token: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
          // Exact output: receive 1 USDC within the source debit cap.
          // For max-output routing instead, remove amount.
          amount: 1_000_000n,
          recipient: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
        },
      });
      ```
    </VmCodeGroup>

    See [Send a transaction](/transactions/multichain/send-a-transaction) for other delivery directions and recipient configuration.

    Review and authorize either preparation with the same SDK flow:

    ```ts theme={null}
    const quote = prepared.quotes.best;
    console.log(quote.cost.input, quote.cost.output, quote.cost.fees);

    const signed = await account.signTransaction(prepared, {
      intentId: quote.intentId,
    });
    const submitted = await account.submitTransaction(signed);
    const result = await account.waitForExecution(submitted);

    console.log(result.status, result.operations);
    ```

    In the EVM example, the SDK uses viem chain objects. `source` selects WETH on Base; omitting `source.chain` uses the destination chain, not another portfolio balance.

    Review the selected quote's input, output, fees, and expiry before signing. The requested output is not reduced by fees; the quote includes fees in its input cost. This example is user-paid. Set `sponsored: true` during preparation if your application pays the fees.

    If no route is available, check the balance, selected source, output amount, supported tokens, and liquidity. Prepare and sign a new route after expiry or a price-related execution failure. If submission has an uncertain result, query the submitted intent before creating another one.

    <span id="spend-an-exact-input-amount" />

    ### Cap the selected source balance

    The Solana debit-cap behavior is shown above. For an EVM source, set `source.maxAmount` to cap the observed WETH balance and omit `destination.amount` for max-output routing:

    ```ts theme={null}
    const prepared = await account.prepareTransaction({
      source: { chain: base, token: wethOnBase, maxAmount: parseUnits("0.05", 18) },
      destination: { chain: arbitrum, token: usdcOnArbitrum },
    });
    ```

    This is not a binding exact-input swap: the cap does not guarantee a particular spend or that the entire cap is consumed. Declared auxiliary funds and source-call `provides` are added after capping the observed balance. Read the selected quote to show actual input, output, and fees before signing.

    ### Pin the swap venue

    This option is EVM-origin only.

    The orchestrator sources swap routes from every venue the chain supports. `quoters` restricts that set:

    ```ts theme={null}
    const prepared = await account.prepareTransaction({
      source: { chain: base, token: wethOnBase },
      destination: { chain: arbitrum, token: usdcOnArbitrum, amount },
      quoters: { include: ["1inch", "0x"] },
    });
    ```

    Supported venues are `1inch`, `0x`, `velora`, `kyberswap`, `fynd`, `fynd-hosted`, `bebop`, and `relay`. `{ exclude: [...] }` inverts the filter against the full set, and an empty allow-list fails the request rather than falling back to an unconstrained route.

    A venue-scoped session derives this pin on its own; see [Swap sessions](/wallets/session-keys/custom-setup/swap-sessions#pinning-venues-without-a-session) for how the two interact.

    ### Deliver the output elsewhere

    A plain swap credits the account itself. Set `destination.recipient` to deliver the output to another address instead:

    ```ts theme={null}
    const prepared = await account.prepareTransaction({
      source: { chain: base, token: wethOnBase },
      destination: {
        chain: arbitrum,
        token: usdcOnArbitrum,
        amount,
        recipient: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
      },
    });
    ```

    Pass a managed `EvmAccountConfig` from `@rhinestone/sdk/evm` instead of an address to deliver into another smart account; do not pass the composite config. An explicit recipient is delivery-only and cannot coexist with destination calls. For a trade that funds calls, leave it unset and encode the receiving address in calldata, as `shareRecipient` does below.

    For EVM routes, settlement layer filters, ranked-quote selection, declared incoming balances, and destination gas limits work the same way here — see [Send a transaction](/transactions/multichain/send-a-transaction#choose-a-different-route).
  </Tab>

  <Tab title="REST API">
    ### Estimate a swap

    Run this request server-side; never expose the API key to a browser. This exact-in example estimates spending 0.05 WETH on Base for USDC on Arbitrum:

    ```ts theme={null}
    const apiKey = process.env.RHINESTONE_API_KEY;
    if (!apiKey) throw new Error("Missing server-side API key");

    const estimateBody = {
      account: { evm: { type: "erc7579" } },
      source: {
        vm: "evm",
        chainId: "eip155:8453",
        token: "0x4200000000000000000000000000000000000006",
        amount: "50000000000000000",
        assumptions: { deployed: true },
      },
      destination: {
        vm: "evm",
        chainId: "eip155:42161",
        token: "0xaf88d065e77c8cc2239327c5edb3a432268e5831",
      },
    };

    const response = await fetch(
      "https://v1.orchestrator.rhinestone.dev/quotes/estimate",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "x-api-key": apiKey,
          "x-api-version": "2026-09.caucasus",
        },
        body: JSON.stringify(estimateBody),
      },
    );
    if (!response.ok) throw new Error(`Estimate failed: ${response.status}`);

    const estimate = await response.json();
    const route = estimate.routes[0];
    if (!route) {
      console.log("No estimate available", estimate.unavailableReason);
    } else if (route.status === "over_capacity") {
      console.log("Indicative route exceeds current capacity", route.accuracy);
    } else if (route.status === "ok") {
      console.log(route.cost.input, route.cost.output, route.cost.fees);
      console.log(route.accuracy, route.assumptions);
    } else {
      throw new Error("Unsupported estimate status");
    }
    ```

    For exact-out, remove `source.amount` and set `destination.amount` to a base-unit decimal string, such as `"100000000"` for 100 USDC. Send exactly one amount side, not both or neither; do not send `direction`, `amountIn`, or `amountOut`.

    The account descriptor is hypothetical: supply types, not wallet addresses or signing configuration. `source.assumptions.deployed` affects only an ERC-7579 source; omission assumes undeployed, and EOA/Swig estimates ignore it. Returned `assumptions` describe what was priced; `accuracy` is `exact` or `approximated`, not a guarantee of execution.

    Estimates do not use actual destination-balance credit or accept caps, auxiliary funds, recipients, or execution. Even an `ok` estimate is not an executable quote: it has no intent ID or signing requests. Binding quotes use `source.maxAmount` as an observed-balance cap, not exact input. Follow the [HTTP lifecycle](/transactions/multichain/end-to-end-transaction-flow#follow-the-flow) before signing or submitting; see [estimate migration](/api-reference/api-versioning#migrate-estimates) for the contract.
  </Tab>
</Tabs>

## Trade and deposit

This procedure is EVM-only.

When the output feeds another action, include the actual calls and the matching destination token and amount in one intent. This example swaps WETH into 100 USDC on Base, approves the Steakhouse Prime USDC ERC-4626 vault, and deposits for `shareRecipient`:

```ts {23-24,32-33} theme={null}
const vault = "0xbeef0e0834849aCC03f0089F01f4F1Eeb06873C9";
const shareRecipient = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045";

const vaultAbi = [
  {
    type: "function",
    name: "deposit",
    stateMutability: "nonpayable",
    inputs: [
      { name: "assets", type: "uint256" },
      { name: "receiver", type: "address" },
    ],
    outputs: [{ name: "shares", type: "uint256" }],
  },
] as const;

const fundedCalls = [
  {
    to: usdcOnBase,
    value: 0n,
    data: encodeFunctionData({
      abi: erc20Abi,
      functionName: "approve",
      args: [vault, amount],
    }),
  },
  {
    to: vault,
    value: 0n,
    data: encodeFunctionData({
      abi: vaultAbi,
      functionName: "deposit",
      args: [amount, shareRecipient],
    }),
  },
] as const;
```

<Tabs>
  <Tab title="Embedded wallet">
    Submit the shared calls with the embedded wallet's field names:

    ```ts {7} theme={null}
    const result = await wallet.sendIntent({
      source: { chain: base.id, token: wethOnBase },
      destination: {
        chain: base.id,
        token: usdcOnBase,
        amount,
        calls: [...fundedCalls],
      },
    });
    ```
  </Tab>

  <Tab title="Custom signer">
    Submit the same calls with the custom signer SDK's field names:

    ```ts theme={null}
    const prepared = await account.prepareTransaction({
      source: { chain: base, token: wethOnBase },
      destination: { chain: base, token: usdcOnBase, amount, calls: [...fundedCalls] },
    });

    const quote = prepared.quotes.best;
    const signed = await account.signTransaction(prepared, {
      intentId: quote.intentId,
    });
    const submitted = await account.submitTransaction(signed);
    const result = await account.waitForExecution(submitted);
    ```
  </Tab>
</Tabs>

The output delivery and calls belong to one intent. The approval and deposit execute in order and revert together if either call fails. This is not a queue of separate trades.

See [Sponsor fees](/transactions/sponsorship/sponsor-fees) for funding modes. Continue to the [end-to-end transaction flow](/transactions/multichain/end-to-end-transaction-flow) for status and recovery details.


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