Skip to main content

Overview

Relay is a cross-chain protocol for bridging tokens and swapping assets across networks. This guide demonstrates integrating Turnkey wallets with Relay to enable cross-chain bridging and same-chain token swaps, covering authentication, quote fetching, multi-step execution, and intent status polling. The relay turnkey example is a Next.js application that bridges ETH from Base to Arbitrum and swaps ETH for USDC on Base.

Prerequisites

Complete the Turnkey Quickstart first. You’ll need:
  • A Turnkey organization and Auth Proxy Config ID
  • (Optional) A Relay API key for higher rate limits

Installation

npm install @turnkey/react-wallet-kit @turnkey/viem viem wagmi

Wallet configuration

The @turnkey/react-wallet-kit package handles browser-based authentication. Each new user gets a Turnkey sub-organization with an HD wallet provisioned automatically on first login. Use @turnkey/viem’s createAccount to turn the active session into a viem Account:
"use client";

import { useTurnkey } from "@turnkey/react-wallet-kit";
import { createAccount } from "@turnkey/viem";
import { createWalletClient, createPublicClient, http } from "viem";

export function useTurnkeyWallet() {
  const { httpClient, session, fetchWalletAccounts, wallets } = useTurnkey();

  const accounts = await fetchWalletAccounts({ wallet: wallets[0] });
  const ethAccount = accounts[0];

  const turnkeyAccount = await createAccount({
    client: httpClient!,
    organizationId: ethAccount.organizationId,
    signWith: ethAccount.address,
    ethereumAddress: ethAccount.address,
  });

  function makeWalletClient(chain: Chain) {
    return createWalletClient({ account: turnkeyAccount, chain, transport: http() });
  }

  function makePublicClient(chain: Chain) {
    return createPublicClient({ chain, transport: http() });
  }
}

Relay integration

Call the Relay API from Next.js server actions to keep the API key server-side. The /quote/v2 endpoint returns an executable quote with all transaction calldata and signature payloads:
"use server";

export async function getQuote(params: QuoteRequest): Promise<QuoteResponse> {
  const res = await fetch("https://api.relay.link/quote/v2", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      ...(process.env.RELAY_API_KEY
        ? { Authorization: `Bearer ${process.env.RELAY_API_KEY}` }
        : {}),
    },
    body: JSON.stringify(params),
  });
  return res.json();
}
Request a bridge quote by specifying origin and destination chains and currencies:
import { parseEther } from "viem";
import { base, arbitrum } from "viem/chains";

const NATIVE = "0x0000000000000000000000000000000000000000";

const quote = await getQuote({
  user: address,
  originChainId: base.id,
  destinationChainId: arbitrum.id,
  originCurrency: NATIVE,
  destinationCurrency: NATIVE,
  amount: parseEther("0.001").toString(),
  tradeType: "EXACT_INPUT",
});

Executing a quote

A Relay quote contains an array of steps, each of kind "transaction" or "signature". Iterate through all steps and dispatch them in order using the Turnkey-backed wallet client:
"use client";

export async function executeQuote({ quote, account, chains, makeWalletClient, makePublicClient }) {
  for (const step of quote.steps) {
    for (const item of step.items) {
      if (item.status === "complete") continue;

      const chain = chains.find((c) => c.id === item.data.chainId) ?? chains[0];

      if (step.kind === "signature") {
        const client = makeWalletClient(chain);
        const signature = await signItem(client, item);
        await submitSignature(item.data.post.endpoint, signature, item.data.post.body);

      } else if (step.kind === "transaction") {
        const client = makeWalletClient(chain);
        const publicClient = makePublicClient(chain);

        const hash = await client.sendTransaction({
          account,
          chain,
          to: item.data.to,
          data: item.data.data ?? "0x",
          value: item.data.value ? BigInt(item.data.value) : 0n,
        });

        await publicClient.waitForTransactionReceipt({ hash });
      }
    }
  }
}

Polling for completion

After submitting a step (e.g. the deposit transaction), Relay’s solver detects the deposit and fills the request on the destination chain. To know when the full flow is done, poll the intent status endpoint. Each step item in the quote can include a check object with an endpoint to call:
// From the quote response: item.check
{
  "endpoint": "/intents/status?requestId=0x8a9b3c...",
  "method": "GET"
}
Call this endpoint (e.g. https://api.relay.link/intents/status/v3?requestId=<requestId>) periodically (e.g. once per second) until the status indicates completion. The requestId is available on each step in the quote response. Status lifecycle: Typical values include waiting (user submitted deposit, not yet indexed), depositing (deposit confirmed, preparing fill), pending (deposit indexed, solver preparing fill on destination), and success (fill executed, funds reached the recipient). For the full list and behavior, see Relay’s status lifecycle documentation. For a real-time stream instead of polling, you can use Relay’s WebSocket API.

Handling EIP-191 and EIP-712 signatures

Some Relay steps (particularly for swaps) require off-chain signatures before the deposit transaction. Handle both signature kinds with the Turnkey-backed viem wallet client:
"use client";

import { type WalletClient, type Hex } from "viem";

async function signItem(walletClient: WalletClient, item: StepItem): Promise<Hex> {
  const sign = item.data.sign!;

  if (sign.signatureKind === "eip191") {
    return walletClient.signMessage({
      account: walletClient.account!,
      message: sign.message,
    });
  }

  // EIP-712: strip EIP712Domain — viem constructs it from the domain object
  const { EIP712Domain: _, ...types } = sign.types ?? {};

  return walletClient.signTypedData({
    account: walletClient.account!,
    domain: sign.domain as any,
    types,
    primaryType: sign.primaryType!,
    message: sign.value as any,
  });
}

Key takeaways

✅ Successfully implemented:
  • Turnkey authentication using @turnkey/react-wallet-kit with passkey and email OTP support
  • Relay cross-chain bridging and same-chain swaps via the /quote/v2 API
  • Multi-step quote execution handling both transaction and signature step kinds
  • Intent status polling to confirm completion using the quote’s check endpoint and requestId
  • EIP-191 and EIP-712 signing through the Turnkey-backed viem account
  • Server-side API key protection using Next.js server actions
To dive deeper into Relay’s API (chain config, quoting, execution, and monitoring), see the Relay API Quickstart.