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.
CapabilityPathSurface notes
getVaults (list, TVL, share price)static registry + chain_rpc_request (eth_calltotalAssets/totalSupply)Works everywhere. APY/yield unavailable (off-chain only).
yoPositions (balances, pending redeems)chain_rpc_request (eth_call)Works everywhere; block-fresh.
depositchain_rpc_request (read) → send_calls (write)Works everywhere. Same-chain only.
withdraw / redeemchain_rpc_request (read) → send_calls (write)Works everywhere.
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

ChainIDBase MCP chain
Base8453base
Ethereum1ethereum
Arbitrum42161arbitrum
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.
VaultChainShare token (vault)Sh. decUnderlyingUnderlying address
yoETHbase0x3A43AEC53490CB9Fa922847385D82fe25d0E9De718WETH0x4200000000000000000000000000000000000006
yoETHethereum0x3A43AEC53490CB9Fa922847385D82fe25d0E9De718WETH0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
yoBTCbase0xbCbc8cb4D1e8ED048a6276a5E94A3e952660BcbC8cbBTC0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf
yoBTCethereum0xbCbc8cb4D1e8ED048a6276a5E94A3e952660BcbC8cbBTC0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf
yoUSDbase0x0000000f2eB9f69274678c76222B35eEc7588a656USDC0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
yoUSDethereum0x0000000f2eB9f69274678c76222B35eEc7588a656USDC0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
yoUSDarbitrum0x0000000f2eB9f69274678c76222B35eEc7588a656USDC0xaf88d065e77c8cC2239327C5EDb3A432268e5831
yoEURbase0x50c749aE210D3977ADC824AE11F3c7fd10c871e96EURC0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42
yoEURethereum0x50c749aE210D3977ADC824AE11F3c7fd10c871e96EURC0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c
yoloUSDbase0x5DD8BFa6C5C68D05d25EF6143E05C11E26c4cDB76USDC0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
yoloUSDethereum0x5DD8BFa6C5C68D05d25EF6143E05C11E26c4cDB76USDC0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
yoloUSDarbitrum0x5DD8BFa6C5C68D05d25EF6143E05C11E26c4cDB76USDC0xaf88d065e77c8cC2239327C5EDb3A432268e5831
yoGOLDethereum0x586675A3a46B008d8408933cf42d8ff6c9CC61a16XAUt0x68749665FF8D2d112Fa859AA293F07A622782F38
yoUSDTethereum0xb9a7da9e90D3B428083BAe04b860faA6325b721e6USDT0xdAC17F958D2ee523a2206206994597C13D831ec7
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):
FunctionSelectorArgsReturns
balanceOf(address)0x70a08231usershares
convertToAssets(uint256)0x07a2d13asharesassets (gross, no fee)
convertToShares(uint256)0xc6e6f592assetsshares
previewRedeem(uint256)0x4cdad506sharesassets (fee-adjusted)
pendingRedeemRequest(address)0x53dc1dd3user(assets, pendingShares)
allowance(address,address)0xdd62ed3eowner, spendercurrent allowance
totalAssets()0x01e1d114vault TVL (underlying units)
totalSupply()0x18160dddtotal shares
Write selectors (send_calls calldata):
FunctionSelectorTarget
ERC-20 approve(address,uint256)0x095ea7b3underlying or vault
Gateway deposit(address,uint256,uint256,address,uint32)0x82b78ba7Gateway
Gateway redeem(address,uint256,uint256,address,uint32)0x99519ab8Gateway
[!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).