Skip to main content

YO Protocol Plugin

[!IMPORTANT] Complete the short Base MCP onboarding flow defined in SKILL.md before calling anything here. The user’s wallet address — required for positions and writes — is fetched lazily via get_wallets when a flow needs it. This plugin is 100% on-chain: it makes no HTTP calls and needs no allowlist.

Overview

YO Protocol is an ERC-4626 yield aggregator with ERC-7540-style async redemption, live on Base (8453), Ethereum (1), and Arbitrum (42161). Both deposit and withdraw go through the YO Gateway (0xF1EeE0957267b1A474323Ff9CfF7719E964969FA, same address on every chain) — never direct vault calls. The Gateway handles slippage, partner attribution, and instant/async routing: a single redeem settles instantly when the vault has idle assets, otherwise it queues and a YO solver fulfills it on-chain within ~24h — the user never sends a second transaction. This plugin builds unsigned calldata from a baked-in vault registry and submits it via Base MCP send_calls; all reads go through chain_rpc_request (eth_call). Everything is derived on-chain — no external service, no CLI, no allowlist. Live APY/yield is not on-chain data, so getVaults reports on-chain TVL and share price but not APY. Deposits are same-chain only — cross-chain/zap routing is out of scope; use the YO dapp for that.

Surface Routing

Everything routes through Base MCP’s own onchain tools, so the plugin behaves identically on every surface (Claude Code, Cursor, Codex, Claude.ai, ChatGPT). No shell, no allowlist. No shell-less or chat-only fallback is needed because chain_rpc_request and send_calls are Base MCP server tools available on all surfaces. Operate only on base, ethereum, arbitrum (the chains Base MCP supports where YO is deployed). YO also has deployments on chains Base MCP can’t reach (e.g. X Layer, Katana) — those are omitted; direct the user to the YO dapp for them.

Orchestration

All reads are chain_rpc_request{ chain, method:"eth_call", params:[{ to, data }, "latest"] }. Encode calldata by left-padding each argument to a 32-byte word and concatenating after the 4-byte selector (see Calldata reference). Always pass the chain name (base/ethereum/arbitrum), never the numeric ID.

getVaults

The vault set is the baked-in Vault registry (verified on-chain). For live numbers, read each vault on its chain:
APY/yield is not derivable on-chain, so don’t report it. If the user asks “vaults by APY,” say APY isn’t available from on-chain data and offer TVL/share-price instead, or point them at the YO dapp.

yoPositions

A user’s holdings, read on-chain — block-fresh. This mirrors the dapp (usePositionValuesuseChainSharesBalances + useChainConvertToAssetsBatch): a logical position aggregates every deployment that shares a vault id across chains.
Use convertToAssets, not previewRedeem, for displayed value — previewRedeem deducts the withdrawal fee and understates the holding. Report in underlying-asset units; do not attempt a USD conversion, and never use get_portfolio for YO vault tokens (its pricer mis-handles ERC-4626 shares — see Notes).

deposit

Same-chain deposit only. Approve the underlying to the Gateway, then call Gateway.deposit.
partnerId is uint32 — pass 0. For native ETH into yoETH there is no ERC-20 to approve: the underlying is WETH, so the user must hold WETH first (wrap, then the two-call batch above). This plugin is same-chain only — cross-chain/zap deposits are out of scope; use the YO dapp.

withdraw

Approve the vault share token to the Gateway (first time only), then call Gateway.redeem.
After submission the redeem either settled instantly or queued. To check progress, poll pendingRedeemRequest(user) on the vault — when pendingShares == 0 and the user’s underlying balance has grown, it settled. No user-side claim transaction is needed for async redeems — a YO solver fulfills them on-chain.

Submission

Target tool: send_calls (EIP-5792 batch of unsigned { to, value, data } calls). The plugin produces raw calldata locally — there is no semantic swap/send step and no external service in the path; reads use chain_rpc_request and never touch send_calls. Mapping into send_calls:
  • chain — the deployment’s chain name: base, ethereum, or arbitrum (never the numeric ID).
  • calls[] — ordered, approval before action:
    • deposit → [ approve(underlying → Gateway, amountIn), Gateway.deposit(...) ]
    • withdraw → [ approve(vault share → Gateway, maxUint256)?, Gateway.redeem(...) ] (approve omitted when allowance ≥ shares)
  • Each call: to = target contract, value = "0x0" (no native value — yoETH uses WETH), data = selector + 32-byte-padded args.
  • After submission, poll get_request_status(requestId) for confirmation. See ../references/approval-mode.md for the approval URL flow and ../references/batch-calls.md for batching.

Example Prompts

“Show me the YO vaults.”
  1. List the Vault registry.
  2. For each deployment on its chain: chain_rpc_request totalAssets() + totalSupply() → TVL and share price.
  3. Present vault, chain(s), underlying, TVL, share price. Note APY isn’t available on-chain.
“What’s my position in yoUSD?”
  1. get_wallets → user.
  2. For yoUSD on base, ethereum, arbitrum (all USDC): balanceOf(user) (skip 0) → convertToAssets(shares); sum the three (same underlying).
  3. pendingRedeemRequest(user) on each → flag any in-flight redeem.
  4. Report the summed total as USDC. If the user also holds yoUSDT, report it as a separate USDT line.
“Deposit 1 USDC into yoUSD on Base.”
  1. get_wallets → user; amountIn = 1000000.
  2. convertToShares(1000000) on yoUSD/base → expectedShares; minSharesOut = expectedShares*9950/10000.
  3. send_calls(chain="base", calls=[ approve(USDC → Gateway, 1000000), Gateway.deposit(yoUSD, 1000000, minSharesOut, user, 0) ]).
  4. User approves → get_request_status.
“Withdraw all my yoUSD on Base.”
  1. get_wallets → user.
  2. balanceOf(user) on yoUSD/base → shares; previewRedeem(shares)expectedAssets; minAssetsOut = expectedAssets*9950/10000.
  3. allowance(user, Gateway) → approve needed?
  4. send_calls(chain="base", calls=[ approve(yoUSD → Gateway, max)?, Gateway.redeem(yoUSD, shares, minAssetsOut, user, 0) ]).
  5. User approves → get_request_status.

Risks & Warnings

  • slippage — deposits set minSharesOut and withdraws set minAssetsOut from a fresh on-chain quote with a default 50 bps tolerance; the call reverts rather than filling materially worse. Show the user the expected shares/assets before they approve. Never silently widen slippage — if a quote looks off, stop and surface it.
  • irreversiblesend_calls broadcasts on-chain writes that cannot be undone. Verify vault address, amount, recipient (user), and chain before presenting the approval. A queued async redeem settles on its own (~24h) — don’t resubmit it as a second transaction.
  • Never ask for or use a private key. Never use a local signer, cast send, or browser-wallet helper. Do not sign or broadcast outside Base MCP.
  • Approvals go to the Gateway, not the vault: underlying-token approve for deposits, vault-share approve for withdraws.
  • amountIn / shares are raw base-unit integer strings — never scientific notation.

Notes

Chains

The same vault id is deployed at the same share-token address on every chain (e.g. yoUSD is 0x0000000f… on Base, Ethereum, and Arbitrum) — the differentiator is the chain you call it on.

Vault registry

Baked-in and verified on-chain (symbol, decimals, and asset() read live on each chain). One row per deployment; the share token is the ERC-4626 vault you read/redeem, the underlying is what you approve for deposits. The share-token address is identical across a vault’s chains; only the underlying address differs per chain (e.g. WETH is 0x4200…0006 on Base but 0xC02a…56Cc2 on Ethereum). Deployments that share a vault id and the same underlying symbol sum into one position — yoUSD (USDC on base/eth/arb), yoETH (WETH on base/eth), yoBTC (cbBTC on base/eth), yoEUR (EURC on base/eth), yoloUSD (USDC on base/eth/arb). yoUSDT is a yoUSD secondary with a different underlying (USDT) — report it as its own USDT line, never added to the USDC total. yoUSD is also deployed on X Layer (196, USDTO) and Katana (747474, vbUSDC); Base MCP can’t reach those, so they’re omitted from on-chain totals — note that to the user and point them at the YO dapp. Gateway: 0xF1EeE0957267b1A474323Ff9CfF7719E964969FA — verified deployed on Base, Ethereum, and Arbitrum (identical bytecode). It is the spender for all approvals: approve the underlying for deposits, the vault share token for withdraws.
[!CAUTION] Never report YO positions from get_portfolio. Its pricer does not understand ERC-4626 vault shares and mis-prices them by 10–10,000×. Report positions in underlying-asset units from convertToAssets(shares) instead. get_portfolio is fine for plain ERC-20 holdings — just not YO vault tokens.

Calldata reference

Encode args by left-padding each value to a 32-byte word and concatenating after the 4-byte selector. Read selectors (eth_call against the share token): Write selectors (send_calls calldata):
[!WARNING] partnerId is uint32. Encoding it as uint256 changes the selector to a function that does not exist on the Gateway and the call reverts. The signature must be exactly (address,uint256,uint256,address,uint32). Pass partnerId = 0 when no partner ID is assigned. maxUint256 = 0xff…ff (64 f’s).