Skip to main content
With Rhinestone intents, you can deposit into any ERC-4626 vault on a destination chain using tokens from any supported chain. This is powered by destinationExecutions — arbitrary calls that run on the destination chain as part of the intent settlement. The flow works as follows:
  1. Encode the vault deposit call
  2. Build the meta intent with the deposit execution
  3. Get a quote from Rhinestone
  4. Fulfill any token requirements
  5. Sign the intent
  6. Submit and poll for completion

Encoding the Vault Deposit

For EOAs, destination executions run in an intermediary contract — not in the user’s account context — so any tokens the execution produces (like vault shares) would otherwise be stranded there.ERC-4626’s deposit(assets, receiver) sidesteps this: set receiver to the user’s address and shares mint directly to them. First approve the vault to pull the deposit token from the intermediary, then deposit.
import { encodeFunctionData, erc20Abi, parseUnits } from "viem";

const VAULT_ADDRESS = "0xbeef0e0834849aCC03f0089F01f4F1Eeb06873C9"; // Steakhouse Prime USDC vault (ERC-4626) on Base
const DEPOSIT_TOKEN = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // USDC on Base (the vault's underlying asset)
const USER_ADDRESS = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; // the EOA receiving the vault shares
const depositAmount = parseUnits("100", 6); // 100 USDC (6 decimals)

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

const destinationExecutions = [
  // 1. Approve the vault to pull USDC from the intermediary
  {
    to: DEPOSIT_TOKEN,
    value: "0",
    data: encodeFunctionData({
      abi: erc20Abi,
      functionName: "approve",
      args: [VAULT_ADDRESS, depositAmount],
    }),
  },
  // 2. Deposit, minting shares straight to the user via the receiver arg
  {
    to: VAULT_ADDRESS,
    value: "0",
    data: encodeFunctionData({
      abi: vaultAbi,
      functionName: "deposit",
      args: [depositAmount, USER_ADDRESS],
    }),
  },
];
Because executions run in the intermediary — not the user’s account — tokens produced onchain aren’t swept back automatically. ERC-4626’s receiver argument handles this: shares mint straight to the user. For outputs from calls that don’t expose a recipient (e.g. a swap), add an explicit transfer back to the user.

Constructing the Meta Intent

const metaIntent = {
  destinationChainId: "eip155:8453", // Base (where the vault lives)
  tokenRequests: [
    {
      tokenAddress: DEPOSIT_TOKEN,
      amount: depositAmount.toString(),
    },
  ],
  account: {
    address: USER_ADDRESS,
    accountType: "EOA",
  },
  destinationExecutions,
};

Getting a Quote

Submit the meta intent to the /quotes endpoint:
const baseUrl = "https://v1.orchestrator.rhinestone.dev";
const apiKey = process.env.RHINESTONE_API_KEY;

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

const res = await fetch(`${baseUrl}/quotes`, {
  method: "POST",
  headers,
  body: JSON.stringify(metaIntent),
});

const { routes } = await res.json();
const route = routes[0];
const { intentId, cost, signData, tokenRequirements } = route;
The response is a server-ranked routes array. Each route carries:
  • intentId: server-stored handle, used to submit
  • cost: input/output amounts and fee breakdown
  • signData: EIP-712 typed data to sign
  • tokenRequirements: prerequisite token operations (EOA only)

Getting a Quote

See the full guide for advanced options like sponsorship and source chain filtering

Fulfilling Token Requirements

Before submitting, EOAs must fulfill the token requirements returned in the quote. There are two types:ERC-20 Approvals — approve tokens to the Permit2 contract:
import { maxUint256 } from "viem";

const PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";

const hash = await walletClient.writeContract({
  address: tokenAddress,
  abi: erc20Abi,
  functionName: "approve",
  args: [PERMIT2, maxUint256],
});
ETH Wrapping — wrap native ETH to WETH:
const hash = await walletClient.writeContract({
  address: WETH_ADDRESS,
  abi: [
    {
      name: "deposit",
      type: "function",
      stateMutability: "payable",
      inputs: [],
      outputs: [],
    },
  ],
  functionName: "deposit",
  value: wrapAmount,
});
Approvals are only ever to the Permit2 contract. We recommend using max approvals for the best UX. Alternatively, inspect cost.input for the exact amount needed.

Token Requirements

Full details on fulfilling token requirements

Signing the Intent

Forward signData.origin[] and signData.destination directly to your wallet. One signature per source chain, plus the destination signature.
const originSignatures = await Promise.all(
  signData.origin.map((typedData) =>
    walletClient.signTypedData(typedData),
  ),
);
const destinationSignature = await walletClient.signTypedData(
  signData.destination,
);

Submitting and Polling

Submit the signed intent to /intents using the intentId from the quote:
const res = await fetch(`${baseUrl}/intents`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    intentId,
    signatures: {
      origin: originSignatures,
      destination: destinationSignature,
    },
  }),
});

const { intentId: submittedId } = await res.json();
Poll for status using the intentId:
const poll = async (intentId: string) => {
  const res = await fetch(`${baseUrl}/intents/${intentId}`, { headers });
  return res.json();
};
See the tracking intents guide for the full list of intent statuses.