Skip to main content

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.

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. The requested tokens are automatically swept to the user after execution, but any tokens received as a result of the execution (such as vault shares) are not automatically swept.You must include an explicit transfer call to send the vault shares back to the user’s address.
import { encodeFunctionData, erc20Abi, parseUnits } from "viem";

const VAULT_ADDRESS = "0x..."; // ERC-4626 vault
const DEPOSIT_TOKEN = "0x..."; // Vault's underlying asset
const depositAmount = parseUnits("100", 6); // e.g. 100 USDC

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. Deposit into the vault
  {
    to: VAULT_ADDRESS,
    value: 0n,
    data: encodeFunctionData({
      abi: vaultAbi,
      functionName: "deposit",
      args: [depositAmount, VAULT_ADDRESS], // receiver is the intermediary
    }),
  },
  // 2. Transfer vault shares back to the user
  {
    to: VAULT_ADDRESS, // ERC-4626 vaults are also ERC-20 share tokens
    value: 0n,
    data: encodeFunctionData({
      abi: erc20Abi,
      functionName: "transfer",
      args: [USER_ADDRESS, depositAmount], // approximate share amount
    }),
  },
];
Since executions run outside the user’s account, you need the second transfer call to move vault shares to the user. This limitation will be addressed in a future API update with a new field for specifying expected output tokens.

Constructing the Meta Intent

const metaIntent = {
  destinationChainId: "eip155:42161", // e.g. Arbitrum
  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 = "YOUR_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.