Skip to main content
This guide walks through the full intent flow for EOA users: get a quote, fulfill token requirements, sign, and submit.
Using the Rhinestone SDK? The wallet quickstart already covers crosschain intents. Come back here when you need API-specific configuration.Using an existing smart account (non-Rhinestone SDK)? You’ll need to install the Intent Executor on each account before using Warp, then follow the smart account signing guide for the signing step.

Prerequisites

Steps

1

Get a quote

Submit a meta intent to the /intents/route endpoint. Specify your destination chain, the token and amount you want on that chain, and your account:
const BASE_URL = "https://v1.orchestrator.rhinestone.dev";
const API_KEY = "YOUR_RHINESTONE_API_KEY";
const EOA_ADDRESS = "0xYourEOAAddress";

const res = await fetch(`${BASE_URL}/intents/route`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": API_KEY,
    Accept: "application/json",
  },
  body: JSON.stringify({
    account: {
      address: EOA_ADDRESS,
      accountType: "EOA",
    },
    destinationChainId: 8453, // Base
    tokenRequests: [
      {
        tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
        amount: "5000000", // 5 USDC (6 decimals)
      },
    ],
  }),
});

const { intentOp, intentCost, tokenRequirements } = await res.json();
The response includes:
  • intentOp: the intent elements for the user to sign
  • intentCost: the total cost in input tokens
  • tokenRequirements: any approvals or wrapping the user needs to complete before signing
2

Fulfill token requirements

Before signing, the user must fulfill any tokenRequirements returned in the quote. There are two types:ERC-20 approvals — approve tokens to the Permit2 contract:
import { maxUint256 } from "viem";

for (const [chainId, tokens] of Object.entries(tokenRequirements)) {
  for (const [tokenAddress, requirement] of Object.entries(tokens)) {
    if (requirement.type === "approval") {
      await walletClient.writeContract({
        address: tokenAddress,
        abi: erc20Abi,
        functionName: "approve",
        args: [requirement.spender, maxUint256],
      });
    }
  }
}
ETH wrapping — wrap native ETH to WETH:
for (const [chainId, tokens] of Object.entries(tokenRequirements)) {
  for (const [tokenAddress, requirement] of Object.entries(tokens)) {
    if (requirement.type === "wrap") {
      await walletClient.writeContract({
        address: WETH_ADDRESS,
        abi: wethAbi,
        functionName: "deposit",
        value: requirement.amount,
      });
    }
  }
}
Use max approvals to the Permit2 contract. This is the only contract you ever approve — future intents won’t need a new approval.
3

Sign the intent

Get a fresh quote (to avoid stale pricing), then sign each intent element using EIP-712. You need one signature per input chain. The last signature also doubles as the destination signature.
import { signTypedData } from "@wagmi/core";

// Refresh the quote before signing
const { intentOp: freshIntentOp } = await fetch(`${BASE_URL}/intents/route`, {
  method: "POST",
  headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
  body: JSON.stringify({ /* same payload as before */ }),
}).then((r) => r.json());

const signatures = [];

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

  const signature = await signTypedData(wagmiConfig, typedData);
  signatures.push(signature);
}

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

Signing guide

Full getTypedData implementation and type definitions
4

Submit the intent

Post the signed intent to /intent-operations:
const submitRes = await fetch(`${BASE_URL}/intent-operations`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": API_KEY,
    Accept: "application/json",
  },
  body: JSON.stringify(
    {
      signedIntentOp: {
        ...freshIntentOp,
        originSignatures,
        destinationSignature,
      },
    },
    (_, value) => (typeof value === "bigint" ? value.toString() : value),
  ),
});

const { bundleResults } = await submitRes.json();
const operationId = bundleResults[0].bundleId;
5

Poll for completion

Track execution status using the operation ID:
async function pollStatus(operationId: string) {
  while (true) {
    const res = await fetch(`${BASE_URL}/intent-operation/${operationId}`, {
      headers: { "x-api-key": API_KEY },
    });
    const { status } = await res.json();

    if (["COMPLETED", "FILLED", "FAILED", "EXPIRED"].includes(status)) {
      console.log("Final status:", status);
      return status;
    }

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

await pollStatus(operationId);
Typical execution time is under 2 seconds. See Submitting the Intent for the full list of statuses.

Next steps