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. 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: 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 /intents/route endpoint to get a quote:
const baseUrl = "https://v1.orchestrator.rhinestone.dev";
const apiKey = "YOUR_RHINESTONE_API_KEY";

const res = await fetch(`${baseUrl}/intents/route`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": apiKey,
    Accept: "application/json",
  },
  body: JSON.stringify(metaIntent, (_, value) =>
    typeof value === "bigint" ? value.toString() : value,
  ),
});

const data = await res.json();
const { intentOp, intentCost, tokenRequirements } = data;
The response includes:
  • intentOp: the intent operation elements to sign
  • intentCost: the cost of the intent in input tokens
  • tokenRequirements: any 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 the tokensSpent field on the intent elements for the exact amount needed.

Token Requirements

Full details on fulfilling token requirements

Signing the Intent

EOAs sign each intent element using EIP-712 typed data via Permit2. You need one signature per input chain. The last origin signature doubles as the destination signature.
import { type Hex } from "viem";

const signatures: Hex[] = [];

for (const element of intentOp.elements) {
  const typedData = getTypedData(
    element,
    BigInt(intentOp.nonce),
    BigInt(intentOp.expires),
  );

  const signature = await walletClient.signTypedData({
    domain: typedData.domain,
    types: typedData.types,
    primaryType: typedData.primaryType,
    message: typedData.message,
  });

  signatures.push(signature);
}

const originSignatures = signatures;
const destinationSignature = signatures.at(-1)!;

Signing

See the full signing guide for the getTypedData implementation and type definitions

Submitting and Polling

Submit the signed intent to the /intent-operations endpoint:
const res = await fetch(`${baseUrl}/intent-operations`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": apiKey,
    Accept: "application/json",
  },
  body: JSON.stringify(
    {
      intentOp,
      originSignatures,
      destinationSignature,
    },
    (_, value) => (typeof value === "bigint" ? value.toString() : value),
  ),
});

const { bundleResults } = await res.json();
const operationId = bundleResults[0].bundleId;
Poll for status using the operation ID:
const poll = async (operationId: string) => {
  const res = await fetch(`${baseUrl}/intent-operation/${operationId}`, {
    headers: { "x-api-key": apiKey },
  });
  return res.json();
};
See the submitting guide for the full list of intent statuses.