Imported from openfort-xyz/recipes-hub (
yield-xyz/AGENTS.md). Install upstream withnpx skills add openfort-xyz/recipes-hub --skill yield-xyz. Copyright stays with the author.
AGENTS.md
Project overview
- Openfort + Yield.xyz integration with a Vite React frontend.
- Lets a Shield-managed embedded wallet discover, enter, track, and exit both native MON staking (
monad-mon-native-staking) and ERC-4626 vaults on Monad mainnet via the Yield.xyz v1 REST API. Monad Testnet lists one opportunity and zero vaults, which is why this targets mainnet - and why every action here moves real funds. - No other Yield.xyz partner recipe (Turnkey, Privy, Portal, Crossmint) has a working Monad demo as of Aug 2026 - verified by checking each one directly; Portal's docs list Monad only as a network-dropdown entry, no code/tx example.
Setup commands
pnpm icp .env.example .envpnpm dev(serves UI onhttp://localhost:5173)- Backend: clone and run openfort-backend-quickstart for Shield sessions. Defaults to
http://localhost:3000; if that's taken by another recipe, setPORTin the backend's.envand matchVITE_BACKEND_URL.
Environment
.envneedsVITE_OPENFORT_PUBLISHABLE_KEY,VITE_OPENFORT_SHIELD_PUBLISHABLE_KEY, optionalVITE_OPENFORT_FEE_SPONSORSHIP_ID,VITE_BACKEND_URL, andYIELD_XYZ_API_KEY(server-side only, noVITE_prefix - seevite.config.ts).- Populate with real Openfort credentials from the dashboard and a real Yield.xyz key - placeholders fail past the discovery screen.
openfort-backend-quickstart's own.env.exampleshipsOPENFORT_BASE_PATH=andSHIELD_BASE_PATH=as empty strings.OPENFORT_BASE_PATHis fine (the SDK falls back via||), butSHIELD_BASE_PATHbreaks:createEncryptionSession's default parameter only applies toundefined, not'', so an empty string produces a relative URL andnode-fetchthrows "Only absolute URLs are supported." Fix: don't declareSHIELD_BASE_PATHin the backend.envat all.
Testing instructions
pnpm lint/pnpm check(Biome)pnpm build(TypeScript + Vite build)- Full enter/exit verification needs a funded mainnet wallet and spends real MON. Mainnet has no faucet; the
fund walletaction only copies the address. To rehearse for free, follow README "Running against testnet" (staking only - the vault panel is empty on testnet). - Verified live end-to-end on 2026-08-14: real embedded wallet created, 2 MON delegated via
monad-testnet-mon-native-staking, confirmed on-chain viaeth_getTransactionReceiptagainsttestnet-rpc.monad.xyz, position picked up byGET /v1/yields/{yieldId}/balances.
Code style
- Vite + TypeScript, Biome for lint/format (
pnpm lint/pnpm check) - single quotes, no semicolons, 2-space, 120 col. - Prefer functional React components and hooks; wallet state via wagmi +
@openfort/react. - Yield.xyz data comes from a hand-rolled REST client (
src/lib/yieldXyz.ts) - there is no official browser SDK, so requests go through the Vite dev proxy to keep the API key server-side.
Integration notes (verified against the live API, Aug 2026)
POST /v1/actions/enter//exitreturns{ transactions: [{ id, stepIndex, unsignedTransaction, ... }] }.unsignedTransactionis a JSON-stringified plain tx object (to/data/value/chainId/nonce/gasLimit/maxFeePerGas/maxPriorityFeePerGas), not raw hex.useExecuteActionparses it and only forwardsto/data/value/chainIdto wagmi, letting it re-estimate gas/nonce fresh (the API's nonce is observed to be stale/0across multi-step actions).GET /v1/yields/{yieldId}/validatorsreturns{ items: [...], total, offset, limit }, not a raw array - same pagination envelope as/yields. (An earlier version of this recipe assumed a raw array based on a test that was silently unwrapped by a fallback in a throwaway script; the live proxy response corrected it.useValidatorsnow unwraps viaselect: (data) => data.items.)- Openfort's embedded wallet connector does not auto-switch chains per-transaction when
sendTransaction({ chainId })targets a chain other than the wallet's current active one - it throws "current chain does not match target chain."useExecuteActionexplicitly callsswitchChainAsync({ chainId })before signing ifactiveChainId !== chainId. - Solana staking on Yield.xyz is mainnet-only (no
solana-devnetyield opportunities) - that's why this recipe targets Monad instead of Solana. - Ethereum Sepolia only has Aave v3 lending yields on Yield.xyz - deliberately dropped from this recipe since it overlaps with the existing
aave/recipe's protocol and adds no differentiation; Monad staking does not overlap with anything else in recipes-hub. GET /v1/yields/{yieldId}/balances?address=is the single-address balance read; the batchPOST /v1/yields/balancesvariant takes aqueriesarray (notaddresses) with anetworkfield per query - not used here.PUT /v1/transactions/{id}/submit-hashaccepts any hash and moves status toBROADCASTED- it doesn't verify the hash on-chain itself, so treat it as best-effort bookkeeping, not a confirmation source (the recipe already confirms viawaitForTransactionReceiptbefore calling it).- Native wallet balance (
useBalanceinApp.tsx) doesn't auto-invalidate on its own after a stake/exit - it's a separate wagmi query from the Yield.xyz['balances', yieldId]query.StakePanel/PositionsPaneltake anonSettledprop (called in thefinallyblock of enter/exit) soApp.tsxcan refetch the lifteduseBalanceinstance immediately after any attempt, success or fail. - The Monad faucet (faucet.monad.xyz) sits behind a Vercel bot check plus X/Discord gates, exposes no public claim API, and ignores an
?address=query param - verified directly. A one-click in-app drip is therefore not possible;WalletBalance.tsxcopies the address to the clipboard instead. GET /yields?network=monadreturns 73 opportunities: 66vault, 6concentrated_liquidity_pool, 1staking.GET /yields?network=monad-testnetreturns exactly 1. Yield.xyz's only testnets areethereum-sepolia,monad-testnet,stellar-testnetandton-testnet, and of those only ethereum-sepolia (Aave v3 lending) has a deposit-shaped flow.- Vault enters are 2-step (
APPROVALthenSUPPLY); staking is 1 step.useExecuteActionalready loops overtransactions[]instepIndexorder, so both work unchanged. - Openfort's built-in wallet funding ("Add funds") returns "Funding isn't available on this network" for Monad Testnet - confirmed by testing the actual button. Mainnet support unverified (no explicit chain list in Openfort's docs). Because of this, the app does not use
OpenfortButton's built-in "Connected" panel at all once signed in - that panel (EthereumConnected.tsxin@openfort/react) hardcodes a Send/Deposit action row with no prop to hide just Deposit;ConnectUIOptionsonly exposeshideBalance/hideTooltips/hideRecentBadge, nothing for individual actions.WalletChip.tsxreplaces it:OpenfortButtonis rendered only when signed out (for login); once authenticated,WalletChipshows the address plus an icon-onlyuseSignOut()button, and clicking the address opensWalletModal.tsx(a self-built modal - avatar, address, big Send/Receive action buttons, styled after Openfort's own Connected panel) instead of Openfort's version.SendForm.tsx(rendered inside the modal's "send" view) is a plain address-to-address native transfer, same switch-chain-then-send pattern asuseExecuteActionbut single-step.WalletModal'sonSettledprop threads through toSendFormso a completed send also refreshes the native balance, same as stake/exit. - Yield.xyz has no separate testnet/mainnet API environment or key - same
api.yield.xyzbase URL and key work for bothmonad-testnetandmonadnetworks, confirmed by hittingGET /v1/yields?network=monadwith the shared demo key (200 OK, real ~14.8% APR data, 209 validators vs testnet's 1). Plan tiers (Trial/Standard/Pro, seedocs/rate-limits-and-plans) gate request throughput, not network access.
PR instructions
- Title format:
[yield-xyz] <summary>. - Run
pnpm lintandpnpm buildbefore requesting review. - Reflect new env vars, yieldIds, or chain support in
README.md.
Submitting this recipe to recipes-hub
This project was built standalone (not inside a checkout of openfort-xyz/recipes-hub), so it needs to be moved into that repo as a new yield-xyz/ folder before it's a real PR. Steps, in order:
- Fork and clone
openfort-xyz/recipes-hub, create a branch (e.g.yield-xyz-recipe). - Copy this folder in as
yield-xyz/at the repo root (sibling toaave/,vaults-fyi/, etc.). Copy everything exceptnode_modules/,dist/,.env, andpnpm-lock.yaml(the repo's own install will regenerate the lockfile). - Check these against the monorepo's actual conventions (read from the root
AGENTS.mdthere - re-verify it hasn't changed since Aug 2026):@openfort/reactversion pin. The rootAGENTS.mdsays to keep it at exactly2.0.1across every recipe for workspace consistency; this project used2.0.2(the latest at build time, same major/minor). Either downgrade to match, or call out the bump explicitly in the PR description so reviewers can decide - don't let it slide by silently.- Shared visual theme. The root
AGENTS.mdstates newer web recipes should track the demo-dashboard look: Geist font, neutral shadcn palette,0.625remradius. This recipe currently uses Figtree and a hand-picked neutral-900/950 palette (matchingvaults-fyi's font choice, but not the newer shadcn token convention some other recipes now share). Decide whether to restyle before submitting or leave it - it's a visual-consistency nit, not a functional blocker.
- Add a row to the root
README.md, in both tables:- Recipes table:
| **[Staking and vaults with Yield.xyz](./yield-xyz/)** | Native MON staking and ERC-4626 vaults on Monad via Yield.xyz's StakeKit API - discover, enter, track, and exit both, entirely non-custodial. No other Yield.xyz partner recipe has a working Monad demo. | \pnpx gitpick openfort-xyz/recipes-hub/tree/main/yield-xyz openfort-yield-xyz && cd openfort-yield-xyz` |` - Stack Overview table:
| **Yield.xyz** | Vite + React | Express.js (openfort-backend-quickstart) | Monad | \wagmi`, `viem`, hand-rolled REST client |`
- Recipes table:
- Re-run the install/build/lint cycle from a clean clone (
rm -rf node_modules && pnpm install && pnpm build && pnpm lint) inside the actual repo checkout - dependency resolution can differ once this sits in a real git repo/CI environment. Two gotchas already hit once during local development, expect they can recur on a fresh machine:pnpm installcan fail to link@rolldown/binding-*(vite 8's bundler) on the very first install - apnpm install --force(or a second plain install) resolves it. Known upstream pnpm optional-dependency quirk, not specific to this recipe.- Don't add
minimumReleaseAgetopnpm-workspace.yaml(some other recipes have it) - it blocks installing recently-published transitive packages like rolldown's platform bindings and will break a fresh install. Confirmed by testing; removed it from this recipe'spnpm-workspace.yamlfor that reason.
- Get real credentials one more time in the moved location and manually re-verify the full enter → track → exit loop, plus Send, from inside the new
yield-xyz/folder - don't assume the standalone verification carries over untouched after the file move and any version/theme changes from step 3. - Open the PR: title
[yield-xyz] Add native MON staking recipe via Yield.xyz, description linking thisAGENTS.md's "Integration notes" section for reviewers, and mention the@openfort/reactversion and theme decisions from step 3 explicitly so they're not missed in review.