# Yield discovery with vaults.fyi

Build a React app that lets users discover the highest-yielding vaults across 80+ DeFi protocols, deposit directly into them, track open positions, and claim rewards — all signed by an Openfort embedded wallet.

:::tip
You'll build a single-page app where users authenticate with Openfort, browse top-ranked deposit options from the [vaults.fyi](https://vaults.fyi) API, and execute deposits, redemptions, and reward claims through one unified interface.
:::

<HoverCardLink
  title="View Sample Code"
  subtitle="GitHub Repository"
  description="Complete source code for the vaults.fyi integration with Openfort embedded wallets, wagmi, and React Query."
  href="https://github.com/openfort-xyz/recipes-hub/tree/main/vaults-fyi"
  img={{
  src: "/img/icons/github-icon.svg",
  alt: "GitHub Icon",
  className: "rounded-none",
}}
  color="#333"
  external
/>

## Getting started

::::steps

### Set up your project

Clone the recipe and install dependencies:

```bash
pnpx gitpick openfort-xyz/recipes-hub/tree/main/vaults-fyi openfort-vaults-fyi
cd openfort-vaults-fyi
pnpm install
```

### Set up the backend

This app requires an external backend for Openfort Shield authentication. The backend holds your secret key and creates encrypted sessions for the embedded wallet.

```bash
git clone https://github.com/openfort-xyz/openfort-backend-quickstart.git
cd openfort-backend-quickstart
cp .env.example .env
```

Add your Openfort credentials to `.env`:

```bash
OPENFORT_SECRET_KEY=sk_...
OPENFORT_SHIELD_SECRET_KEY=...
```

Start the backend:

```bash
pnpm install
pnpm dev
```

The backend runs on `http://localhost:3000` and provides the `/api/protected-create-encryption-session` endpoint.

### Get your credentials

You'll need credentials from two sources:

1. **Openfort** — from the [Openfort Dashboard](https://dashboard.openfort.io):
   * **Publishable Key** (starts with `pk_`)
   * **Shield Publishable Key**
   * **gas sponsorship ID** (optional — enables gas sponsorship)

2. **vaults.fyi** — request an API key from the [vaults.fyi portal](https://portal.vaults.fyi).

3. **WalletConnect** (optional) — a Project ID from [WalletConnect Cloud](https://cloud.walletconnect.com). The recipe authenticates through the Openfort embedded wallet, so this is only needed if you want to surface external WalletConnect wallets.

### Configure your environment

Return to the `openfort-vaults-fyi` directory and copy the template:

```bash
cp .env.example .env
```

```bash
# Openfort (from dashboard.openfort.io > Developers > API Keys)
VITE_OPENFORT_PUBLISHABLE_KEY=
VITE_OPENFORT_SHIELD_PUBLISHABLE_KEY=

# Optional: gas sponsorship policy id
VITE_OPENFORT_FEE_SPONSORSHIP_ID=

# WalletConnect (cloud.walletconnect.com)
VITE_WALLET_CONNECT_PROJECT_ID=

# Backend URL for openfort-backend-quickstart (Shield session endpoint)
VITE_BACKEND_URL=http://localhost:3000

# vaults.fyi API key (https://portal.vaults.fyi)
# Server-side only — read by vite.config.ts and injected into the dev proxy.
# Never prefix with VITE_ or it will be exposed in the browser bundle.
VAULTS_FYI_API_KEY=
```

:::warning
`VAULTS_FYI_API_KEY` is **not** prefixed with `VITE_`. It is read by `vite.config.ts` at server startup and injected into the dev proxy as the `x-api-key` header, so the key never reaches the browser bundle.
:::

### Run the application

```bash
pnpm dev
```

Sign in with Openfort, and start browsing deposit options.

::::

## How it works

The app combines three layers: Openfort for embedded wallet authentication and signing, wagmi for transaction execution, and the vaults.fyi SDK for vault discovery and transaction payload generation.

### Core workflow

| Step | vaults.fyi SDK method | Purpose |
| ---- | --------------------- | ------- |
| Discover | `sdk.getAllVaults()` | Transactional vaults for an asset/network, ranked by APY |
| Deposit | `sdk.getActions({ action: 'deposit' })` | Ready-to-sign deposit transaction payloads |
| Track | `sdk.getPositions()` | Position tracking across all protocols |
| Redeem | `sdk.getActions({ action: 'redeem' })` | Withdrawal transaction generation |
| Claim | `sdk.getRewardsTransactionsContext()` → `sdk.getRewardsClaimActions()` | Two-step reward discovery and claiming |

The deposit calldata returned by vaults.fyi targets the canonical protocol contract directly — there is no wrapper contract, no idle cash buffer, and no user-facing fee.

### Provider setup

The Openfort SDK plugs into wagmi via `getDefaultConfig` and the `OpenfortWagmiBridge`. Standard wagmi hooks like `useAccount()` and `useSendTransaction()` work as-is — Openfort handles signing behind the scenes.

```tsx
// src/Providers.tsx
const wagmiConfig = createConfig(
  getDefaultConfig({
    appName: 'Openfort × vaults.fyi',
    walletConnectProjectId: import.meta.env.VITE_WALLET_CONNECT_PROJECT_ID || 'demo',
    chains: [base],
    ssr: false,
  }),
)

<QueryClientProvider client={queryClient}>
  <WagmiProvider config={wagmiConfig}>
    <OpenfortWagmiBridge>
      <OpenfortProvider
        publishableKey={import.meta.env.VITE_OPENFORT_PUBLISHABLE_KEY}
        walletConfig={{
          shieldPublishableKey: import.meta.env.VITE_OPENFORT_SHIELD_PUBLISHABLE_KEY,
          createEncryptedSessionEndpoint: `${import.meta.env.VITE_BACKEND_URL}/api/protected-create-encryption-session`,
          ethereum: {
            ethereumFeeSponsorshipId: import.meta.env.VITE_OPENFORT_FEE_SPONSORSHIP_ID || undefined,
          },
        }}
      >
        {children}
      </OpenfortProvider>
    </OpenfortWagmiBridge>
  </WagmiProvider>
</QueryClientProvider>
```

### Secure SDK client

The vaults.fyi SDK points at a local proxy (`/api/vaults-fyi`) rather than the public endpoint. The proxy — defined in `vite.config.ts` — injects the API key from server-side environment variables, so the secret never ships in the browser bundle.

```ts
// src/lib/vaultsFyi.ts
import { VaultsSdk } from '@vaultsfyi/sdk'

export const sdk = new VaultsSdk(
  { apiKey: 'proxied' },
  { apiBaseUrl: `${window.location.origin}/api/vaults-fyi` },
)
```

### Discover deposit options

A React Query hook fetches the best USDC deposit options on Base, sorted by 7-day APY, and filters out vaults whose deposit or redeem flows are flagged as complex:

```ts
// src/hooks/useDepositOptions.ts
useQuery({
  queryKey: ['depositOptions'],
  queryFn: async () => {
    const result = await sdk.getAllVaults({
      query: {
        allowedAssets: ['USDC'],
        allowedNetworks: ['base'],
        onlyTransactional: true,
        sortBy: 'apy7day',
        sortOrder: 'desc',
        perPage: 20,
      },
    })

    return result.data.filter(
      (v) =>
        v.transactionalProperties?.depositStepsType !== 'complex' &&
        v.transactionalProperties?.redeemStepsType !== 'complex',
    )
  },
})
```

### Execute multi-step actions

Deposits and redemptions can return more than one transaction (for example, an approval followed by a deposit). vaults.fyi returns the full ordered list of `actions` plus a `currentActionIndex` marking where to start. The `useExecuteAction` hook runs the remaining steps sequentially with wagmi's `useSendTransaction`, waiting for two confirmations before moving on:

```ts
// src/hooks/useExecuteAction.ts
const remaining = actions.slice(currentActionIndex)

for (let i = 0; i < remaining.length; i++) {
  const step = remaining[i]
  setState((s) => ({ ...s, step: { current: i + 1, total: remaining.length } }))

  const hash = await sendTransactionAsync({
    to: step.tx.to as `0x${string}`,
    data: step.tx.data as `0x${string}` | undefined,
    value: step.tx.value ? BigInt(step.tx.value) : undefined,
    chainId: step.tx.chainId,
  })

  await client.waitForTransactionReceipt({ hash, confirmations: 2 })
  hashes.push({ hash, name: step.name })
}
```

State (`running`, `step`, `hashes`, `error`) is exposed so the UI can show progress for each transaction in the sequence.

### Track positions and claim rewards

`usePositions` calls `sdk.getPositions()` to list open positions across every protocol the user has touched. Rewards use a two-step flow: `useRewards` calls `sdk.getRewardsTransactionsContext()` to discover what's claimable, then `RewardsPanel` calls `sdk.getRewardsClaimActions()` to get the transactions to sign through the same `useExecuteAction` runner used for deposits.

Because every step funnels through `useExecuteAction`, deposits, redemptions, and reward claims all share the same signing, confirmation, and error-handling path.
