> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://www.openfort.io/api/mcp` to find what you need.
>
> **Have feedback?** Use `submit_feedback` on the same MCP server.

# Stake and earn with Yield.xyz

[Yield.xyz](https://yield.xyz) is one API for thousands of earn opportunities across 80+ networks: native staking, liquid staking, lending markets, and ERC-4626 vaults. This recipe builds a React app that lets users discover those opportunities, enter them, track their position live, and exit - all signed by an Openfort embedded wallet.

Yield.xyz never signs or holds funds. Every `enter`/`exit` call returns **unsigned** transactions that your app signs and broadcasts itself, so the user's key stays in the embedded wallet and the position stays non-custodial end to end.

:::tip
You'll build a single-page app where users authenticate with Openfort, review validator commission and live APR from the Yield.xyz API, and stake, deposit, track, and exit their positions through one unified interface.
:::

The integration is network-agnostic - every component reads from a single config object naming the network, chain id, and opportunity.

[View Sample Code](https://github.com/openfort-xyz/recipes-hub/tree/main/yield-xyz) — GitHub Repository. Complete source code for the Yield.xyz integration with Openfort embedded wallets, wagmi, and React Query.

:::warning
The sample ships configured for **Monad mainnet, and moves real funds**. Monad Testnet lists exactly one Yield.xyz opportunity (native staking, one validator, 0% reported APR) and no vaults at all, which is why both panels target mainnet. Read [Pointing it at another network](#pointing-it-at-another-network) before running this against anyone's wallet.
:::

## Getting started

::::steps
### Set up your project

Clone the recipe and install dependencies:

```bash
pnpx gitpick openfort-xyz/recipes-hub/tree/main/yield-xyz openfort-yield-xyz
cd openfort-yield-xyz
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_...
SHIELD_PUBLISHABLE_KEY=...
SHIELD_SECRET_KEY=...
SHIELD_ENCRYPTION_KEY=...
```

:::warning
Leave `OPENFORT_BASE_PATH` and `SHIELD_BASE_PATH` **out of `.env` entirely** rather than blank. The Shield SDK's base-path default only kicks in for an unset variable, not an empty string - an empty `SHIELD_BASE_PATH` makes `createEncryptionSession` build a relative URL and fail.
:::

Start the backend:

```bash
pnpm install
pnpm dev
```

The backend runs on `http://localhost:3000` and provides the `/api/protected-create-encryption-session` endpoint. If you already have another Openfort recipe's backend on that port, set `PORT` in its `.env` and match `VITE_BACKEND_URL` below.

### 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**
* **Fee sponsorship ID** (optional - enables gas sponsorship, scoped to your target chain)

**2. Yield.xyz** - Yield.xyz requires an API key, which you can generate from the [Yield.xyz Dashboard](https://dashboard.yield.xyz). For quick testing, you can use the shared, rate-limited demo key `ec241b09-2f93-43b0-b0ba-ebf166c80e48`. For production, we recommend creating a dedicated project and API key.

You can also explore the [Yield.xyz Products Overview](https://stakekit.notion.site/Yield-xyz-Products-Overview-3135318e934e8042be5be2a6aa4c78bd) to learn more about the API, available products, and integration options.

:::warning
**New Yield.xyz projects do not have yields enabled by default.** After creating your project and API key, open the Yield.xyz dashboard and enable the opportunities your project should access. Opportunities that aren't enabled for your project fail at `POST /v1/actions/enter` with a 400.
:::

### Configure your environment

Return to the `openfort-yield-xyz` 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=

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

# Yield.xyz API key (dashboard.yield.xyz, or contact hello@yield.xyz)
# 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.
YIELD_XYZ_API_KEY=
```

### Run the application

```bash
pnpm dev
```

Sign in with Openfort, then fund the wallet with the network's native token - the `fund wallet` link copies the address to your clipboard. Then stake or deposit.
::::

## How it works

The app combines three layers: Openfort for embedded wallet authentication and signing, wagmi for transaction execution, and a small hand-rolled client for Yield.xyz's REST API (Yield.xyz has no official browser SDK).

### Two mechanics, one execution path

Yield.xyz describes every opportunity with a `mechanics.type`. This recipe drives two of them through identical code:

| Mechanic | Enter | Exit | Validator |
| --- | --- | --- | --- |
| `staking` | `DELEGATE` | `UNDELEGATE` | Pick one, with commission and APR from the API |
| `vault` | `APPROVAL` + `SUPPLY` (2 steps) | Withdraw | None - the vault issues shares |

You never branch on the mechanic to execute it. Both call the same `POST /v1/actions/enter` → sign → `PUT /v1/transactions/{id}/submit-hash` loop in `src/hooks/useExecuteAction.ts`; the step count and whether a validator is required simply come back in the response. Adding lending or liquid staking is a config change, not new execution code.

In the shipped sample, the staking panel uses the network's native staking opportunity, and the vaults panel lists the top 8 `vault` opportunities by APY.

### Core workflow

| Step | Yield.xyz endpoint | Purpose |
| ---- | --------------------- | ------- |
| Discover | `GET /v1/yields?network=` | Opportunities available on a network, with live rates |
| Inspect | `GET /v1/yields/{yieldId}` | Yield mechanics, argument schema, minimum entry amount, warmup/cooldown and other metadata |
| Validators | `GET /v1/yields/{yieldId}/validators` | Available validators and their metadata, including commission and applicable rate information |
| Enter | `POST /v1/actions/enter` | Returns an ordered list of unsigned transactions in `transactions[]` |
| Sign & broadcast | - | Sign and broadcast each unsigned transaction sequentially with the Openfort embedded wallet |
| Report | `PUT /v1/transactions/{id}/submit-hash` | Report each externally broadcast transaction hash back to Yield.xyz so the action can be tracked |
| Track | `GET /v1/yields/{yieldId}/balances?address=` | Retrieve the user's current yield position/balance information |
| Exit | `POST /v1/actions/exit` | Returns an ordered list of unsigned transactions to exit the position; execute them sequentially using the same flow as enter |

`unsignedTransaction` is a JSON-stringified plain transaction object (`to`, `data`, `value`, `chainId`, `gasLimit`), not raw hex. Nonce and fee fields are deliberately dropped in favor of fresh estimation at send time - they go stale between when Yield.xyz builds the transaction and when the user signs it.

:::warning
`gasLimit` is the exception and **must be passed through**. Yield.xyz sizes it for its own targets - a native staking precompile can need ~300,000 - while a failed local estimate falls back to the 21,000 bare-transfer floor, and the node then rejects the transaction with a misleading `Gas limit too low` instead of the real reason.
:::

### Provider setup

The Openfort SDK plugs into wagmi via `embeddedWalletConnector()`. Only the embedded wallet connector is registered - no external-wallet connectors - so the login widget only offers Guest, email, and social sign-in.

Swap the `viem/chains` import for your target network; the sample uses Monad:

```tsx
// src/Providers.tsx
import { monad } from 'viem/chains' // the sample's network - swap for yours

const wagmiConfig = createConfig({
  chains: [monad],
  connectors: [embeddedWalletConnector()],
  transports: { [monad.id]: http() },
})

<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: {
            rpcUrls: { [monad.id]: monad.rpcUrls.default.http[0] },
            ethereumFeeSponsorshipId: import.meta.env.VITE_OPENFORT_FEE_SPONSORSHIP_ID || undefined,
          },
        }}
      >
        {children}
      </OpenfortProvider>
    </OpenfortWagmiBridge>
  </WagmiProvider>
</QueryClientProvider>
```

:::warning
Set `walletConfig.ethereum.rpcUrls` for any chain outside [Openfort's supported chains](https://www.openfort.io/docs/configuration/chains). The embedded signer builds its provider during wallet creation, before the wagmi bridge has resolved your `transports`, and that first provider is the one the session keeps - so a `transports` entry alone does not reach the signer on the first connect. Without an `rpcUrls` entry the chain switch fails with `No RPC URL configured for chain <id>`, and because `useExecuteAction` switches chains before signing, every enter and exit fails there. Setting it costs nothing on supported chains, so set it either way.
:::

### Secure API client

The Yield.xyz client points at a local proxy (`/api/yield-xyz`) rather than the public endpoint. The proxy - defined in `vite.config.ts` - injects the API key from a server-side environment variable, so the secret never ships in the browser bundle.

```ts
// vite.config.ts
server: {
  proxy: {
    '/api/yield-xyz': {
      target: 'https://api.yield.xyz/v1',
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/api\/yield-xyz/, ''),
      configure: (proxy) => {
        proxy.on('proxyReq', (proxyReq) => {
          if (env.YIELD_XYZ_API_KEY) proxyReq.setHeader('X-API-KEY', env.YIELD_XYZ_API_KEY)
        })
      },
    },
  },
}
```

This proxy only runs under `vite dev`. For a production build, replace it with a backend route that adds the same `X-API-KEY` header server-side, and point `BASE_PATH` in `src/lib/yieldXyz.ts` at that route.

### Execute the enter/exit action

Both `enter` and `exit` return an ordered list of unsigned transaction steps. **The order must be preserved:** each transaction should be signed, broadcast, confirmed, and reported back to Yield.xyz before moving to the next transaction in the action. `useExecuteAction` does exactly that, switching chains first if the wallet isn't already on the target chain - Openfort's embedded connector doesn't do this implicitly per-transaction:

```ts
// src/hooks/useExecuteAction.ts
if (activeChainId !== chainId) {
  await switchChainAsync({ chainId })
}

for (const step of steps) {
  const parsed = parseUnsignedTransaction(step.unsignedTransaction)
  const hash = await sendTransactionAsync(parsed)
  await publicClient.waitForTransactionReceipt({ hash })
  await yieldXyz.submitHash(step.id, hash)
}
```

Transactions are built client-side: Yield.xyz returns the payload, wagmi's `useSendTransaction` signs it through Openfort's embedded EIP-1193 provider and broadcasts it. These are plain EOA transactions, not UserOperations, so a wallet with no native token cannot transact even with a fee sponsorship policy set - fund the address first.

### Wallet UI comes from the SDK

The recipe writes no wallet management UI of its own. `<OpenfortButton />` renders the connect button and, once signed in, the connected panel: address with copy, balance, Send, and Deposit. Deposit is Openfort's funding hub - on Monad mainnet it offers transfer from a wallet, from an address, or from an exchange.

Funding availability is per-network, so check it for your own target chain: on Monad Testnet, for instance, the hub reports "Funding isn't available on this network" and you fund from the faucet instead. Use `uiConfig.funding.methods` to choose which rails appear, or `uiConfig.customPageComponents[routes.CONNECTED]` to replace the connected panel wholesale.

### Pointing it at another network

Every component reads from one config object, so switching networks - to another chain entirely, or to a testnet to rehearse for free - touches two files:

| File | Setting | What it is |
| --- | --- | --- |
| `src/config/demos.ts` | `network` | Yield.xyz network slug, e.g. `monad` or `monad-testnet` |
| `src/config/demos.ts` | `chainId` | EVM chain id, e.g. `143` or `10143` |
| `src/config/demos.ts` | `yieldId` | The opportunity to enter, from `GET /v1/yields?network=` |
| `src/config/demos.ts` | `tokenSymbol` / `tokenDecimals` | The input token for that opportunity |
| `src/config/demos.ts` | `explorerUrl` | Block explorer base URL for transaction links |
| `src/Providers.tsx` | `viem/chains` import | The matching viem chain, plus its `rpcUrls` entry |

Two things to check before you commit to a network. First, that the opportunity exists there - `GET /v1/yields?network=<slug>` is the fastest way to see. Monad Testnet, for example, has one staking opportunity and **no vaults**, so the Vaults panel comes up empty. Second, that your Yield.xyz project has that opportunity enabled.

There is no Yield.xyz sandbox environment: the same API key and base URL serve both testnet and mainnet, and which one you hit is determined entirely by the `network` field in the request, not by the key or endpoint.
