Earn
Earn modules come in two shapes, and every module of a shape has the same methods:
| Interface | Modules | Flow |
|---|---|---|
StakingProtocol |
Lido (ETH), SSV Network, Tonstakers (TON), SPL stake pools (Jito on Solana), native SOL staking, native MON staking (Monad) | Stake → request a withdrawal → wait for the queue → claim |
LendingProtocol |
Aave v3, Morpho, Mellow, Kamino | Supply / withdraw, and on markets that allow it, borrow / repay |
This page uses Lido and Aave on Ethereum. The other modules work the same way, with
different packages and chains (see Packages). Snippets
come from examples/earn, which our CI typechecks and tests.
npm install @anywalletkit/protocol-staking-lido-evm @anywalletkit/protocol-lending-aave-evmSet up
Section titled “Set up”Register each module as a protocol, bound to the chain its account lives on:
import { Wallet } from '@anywalletkit/kit'import { WalletManagerEvm } from '@anywalletkit/wallet-evm'import LidoProtocolEvm from '@anywalletkit/protocol-staking-lido-evm'import { AaveProtocolEvm } from '@anywalletkit/protocol-lending-aave-evm'
export function walletWithEarn(seedPhrase: string): Wallet { const wallet = new Wallet(seedPhrase) wallet.registerWallet('eip155:1', WalletManagerEvm, { provider: 'https://ethereum-rpc.publicnode.com', chainId: 1 }) // A protocol is bound to the account of one chain. wallet.registerProtocol('lido', LidoProtocolEvm, { chain: 'eip155:1' }) wallet.registerProtocol('aave', AaveProtocolEvm, { chain: 'eip155:1', config: { market: 'core' } }) return wallet}Every write has a quote… twin that returns the gas fee without signing anything. Show it
before the user confirms. Policies apply to every write a module
makes, because modules sign through the same account.
Staking
Section titled “Staking”export async function stakeEth(wallet: Wallet, amount: bigint) { const lido = await wallet.getProtocol<LidoProtocolEvm>('lido') const { fee } = await lido.quoteStake({ amount }) // gas in wei, nothing is signed const { hash } = await lido.stake({ amount }) // ETH → stETH return { hash, fee }}Position and rate
Section titled “Position and rate”export async function stakingPosition(wallet: Wallet) { const lido = await wallet.getProtocol<LidoProtocolEvm>('lido') const position = await lido.getStakedBalance() return { staked: position.total, // the whole position in stETH, wei apr: await lido.getApr(), // percent: 2.7 = 2.7 % }}Unstake: two phases
Section titled “Unstake: two phases”Staked funds do not come back in one transaction. A withdrawal request joins the protocol’s
queue, finalizes later (hours to days, depending on the protocol), and only then can be
claimed. The interface names the queue instead of pretending withdraw() returns money:
export async function unstake(wallet: Wallet, amount: bigint) { const lido = await wallet.getProtocol<LidoProtocolEvm>('lido') // Phase 1: join the withdrawal queue. await lido.requestWithdrawal({ amount })}
export async function claimIfReady(wallet: Wallet) { const lido = await wallet.getProtocol<LidoProtocolEvm>('lido') const queue = await lido.getWithdrawalRequests() // queue.pendingAmount: still in the queue; queue.claimableAmount: ready to claim if (queue.claimableIds.length === 0) return null // Phase 2: claim the finalized requests back as ETH. return lido.claimWithdrawals({ ids: queue.claimableIds })}Show pendingAmount as “unstaking” and claimableAmount with a Claim button. Check
the queue when the screen opens; there is nothing to watch in between.
Lending
Section titled “Lending”Supply and withdraw
Section titled “Supply and withdraw”const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
export async function lendUsdc(wallet: Wallet, amount: bigint) { const aave = await wallet.getProtocol<AaveProtocolEvm>('aave') const apy = await aave.getSupplyApy(USDC) // 4.1 = 4.1 % const { fee } = await aave.quoteSupply({ token: USDC, amount }) const result = await aave.supply({ token: USDC, amount }) // permit when the token supports it, else an exact approval return { apy, fee, hash: result.hash }}
export async function withdrawAll(wallet: Wallet) { const aave = await wallet.getProtocol<AaveProtocolEvm>('aave') return aave.withdraw({ token: USDC, amount: 'max' }) // principal + interest}Tokens are contract addresses; 'native' supplies the chain’s coin (ETH) through Aave’s
gateway. Where the token supports EIP-2612, the module signs a permit instead of sending a
separate approval transaction. amount: 'max' withdraws or repays everything, interest
included.
Position and health
Section titled “Position and health”export async function lendingPosition(wallet: Wallet) { const aave = await wallet.getProtocol<AaveProtocolEvm>('aave') const { supplies, borrows, account } = await aave.getPosition() return { supplies, // [{ token, symbol, amount, supplyApy, collateral }] borrows, // [{ token, symbol, amount, borrowApy }] healthFactor: account.healthFactor, // < 1 = liquidation; Infinity without debt }}With debt, keep an eye on healthFactor: below 1.0 the position can be liquidated. Show
it on every borrow and withdraw screen, and warn well before it gets close (for example
under 1.5).
Native SOL staking
Section titled “Native SOL staking”@anywalletkit/protocol-staking-native-solana delegates SOL straight to validators through
Solana’s Stake program: no pool, no token. Your app decides which validators it offers; the
module has no default list and refuses to start without one.
npm install @anywalletkit/protocol-staking-native-solana @anywalletkit/wallet-solanaChoose the validators
Section titled “Choose the validators”import { Wallet } from '@anywalletkit/kit'import { WalletManagerSolana } from '@anywalletkit/wallet-solana'import { NativeStakingProtocolSolana } from '@anywalletkit/protocol-staking-native-solana'
const SOLANA = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'
export function walletWithNativeStaking(seedPhrase: string, rpc = 'https://api.mainnet-beta.solana.com'): Wallet { const wallet = new Wallet(seedPhrase) wallet.registerWallet('solana', WalletManagerSolana, { provider: rpc }) wallet.registerProtocol('sol-staking', NativeStakingProtocolSolana, { chain: SOLANA, config: { // Required: the vote accounts your wallet delegates to. Replace these with your own choice. validators: [ { voteAccount: 'he1iusunGwqrNtafDtLdhsUQDFvo13z9sUa36PauBtk', name: 'Validator A' }, { voteAccount: 'CatzoSMUkTRidT5DwBxAC2pEtnwMBTpkCepHkFgZDiqb', name: 'Validator B' }, ], maxCommission: 10, // refuse a validator whose commission is above 10 % (checked at every delegation) }, }) return wallet}Each stake goes to a validator from this list (or any voting validator with
validators: 'any', when your users pick from the whole cluster). Before every delegation the
module reads the validator live and refuses it when it is delinquent or its commission is above
maxCommission (10 % unless you set it). enrich can attach an explorer’s APR or a logo to each
validator; the on-chain checks do not depend on it.
export async function stakeSol(wallet: Wallet, lamports: bigint) { const staking = await wallet.getProtocol<NativeStakingProtocolSolana>('sol-staking') const validators = await staking.listValidators() // live commission, stake, APR; `eligible` and `refusals` const pick = validators.find((v) => v.eligible)! const quote = await staking.quoteStake({ amount: lamports, validator: pick.voteAccount }) // ≥ 1 SOL; nothing is signed // quote.total = amount + the stake account's rent (returned on withdraw); quote.fee = network fee const result = await staking.stake({ amount: lamports, validator: pick.voteAccount }) return { hash: result.hash, stakeAccount: result.stakeAccount, activeFrom: result.readyEpoch }}A stake is one transaction: a new stake account at an address derived from the wallet (seed
stake:<i>), initialized with the wallet as its only authority and delegated. Only the wallet
signs, and the position can be rebuilt from the wallet address alone. The minimum is 1 SOL; the
account also holds about 0.0017 SOL of rent, which comes back on withdrawal. The stake starts
earning from the next epoch boundary (an epoch is about two days).
Position
Section titled “Position”export async function nativePosition(wallet: Wallet) { const staking = await wallet.getProtocol<NativeStakingProtocolSolana>('sol-staking') const accounts = await staking.getStakeAccounts() // one per delegation: status, validator, amounts, readyAt const { balance, total } = await staking.getStakedBalance() // delegated lamports; everything in stake accounts return { accounts, balance, total, apr: await staking.getApr() }}status is computed from the StakeHistory sysvar the way the Stake program computes it:
activating, active, deactivating or inactive, with readyEpoch / readyAt while it
changes. Rewards compound into each account at every epoch boundary; getRewards() lists them
per epoch.
Unstake and claim
Section titled “Unstake and claim”export async function unstakeSol(wallet: Wallet, lamports: bigint) { const staking = await wallet.getProtocol<NativeStakingProtocolSolana>('sol-staking') // Whole accounts are deactivated; a partial amount is split into a new account first. const { steps, readyAt } = await staking.requestWithdrawal({ amount: lamports }) return { steps, readyAt } // the SOL can be claimed after the epoch boundary (~2 days)}
export async function claimSol(wallet: Wallet) { const staking = await wallet.getProtocol<NativeStakingProtocolSolana>('sol-staking') const { claimableIds } = await staking.getWithdrawalRequests() if (claimableIds.length === 0) return null return staking.claimWithdrawals({ ids: claimableIds }) // every lamport, rent included, back to the wallet}Unstaking deactivates whole accounts and, for a partial amount, splits it off one account into
a new one first; both parts must keep the 1 SOL minimum. The SOL can be withdrawn after the
cool-down (the next epoch boundary). A stake deactivated in the epoch it was made is free at
once, and delegate({ account }) in the same epoch undoes an unstake. merge joins two
compatible accounts on the same validator.
Native MON staking
Section titled “Native MON staking”@anywalletkit/protocol-staking-native-monad delegates MON straight to Monad validators through
the staking precompile (0x…1000): no pool, no token. Your app decides which validator ids it
offers; the module has no default list and refuses to start without one.
npm install @anywalletkit/protocol-staking-native-monad @anywalletkit/wallet-evmChoose the validators
Section titled “Choose the validators”import { Wallet } from '@anywalletkit/kit'import { WalletManagerEvm } from '@anywalletkit/wallet-evm'import { NativeStakingProtocolMonad } from '@anywalletkit/protocol-staking-native-monad'
export function walletWithMonadStaking(seedPhrase: string, rpc = 'https://rpc.monad.xyz'): Wallet { const wallet = new Wallet(seedPhrase) wallet.registerWallet('eip155:143', WalletManagerEvm, { provider: rpc, chainId: 143 }) wallet.registerProtocol('mon-staking', NativeStakingProtocolMonad, { chain: 'eip155:143', config: { // Required: the validator ids your wallet delegates to. Replace them with your own choice. validators: [{ id: 97n, name: 'Validator A' }, { id: 21n, name: 'Validator B' }], defaultValidator: 97n, maxCommission: 10, // refuse a validator whose commission is above 10 % (checked at every delegation) }, }) return wallet}validators: 'any' lets users pick from the whole active set (listValidators()). Before every
delegation the module reads the validator live and refuses it when it does not exist, is not in the
consensus set in good standing, or charges more than maxCommission (10 % unless you set it; many
validators charge 10–15 %). enrich can attach an explorer’s APR or a logo.
Stake and position
Section titled “Stake and position”export async function stakeMon(wallet: Wallet, wei: bigint) { const staking = await wallet.getProtocol<NativeStakingProtocolMonad>('mon-staking') const validators = await staking.listValidators() // live commission, standing, APR; `eligible` and `refusals` const quote = await staking.quoteStake({ amount: wei }) // simulated, nothing is signed; keeps the 10 MON reserve const result = await staking.stake({ amount: wei }) // delegate(97) with the amount as value return { validators, fee: quote.fee, hash: result.hash, activeFrom: result.activatesAtEpoch } // epoch n+1, or n+2 after the boundary block}
export async function monadPosition(wallet: Wallet) { const staking = await wallet.getProtocol<NativeStakingProtocolMonad>('mon-staking') const positions = await staking.listPositions() // per validator: active, activating, unclaimed rewards const { balance, total, unclaimedRewards } = await staking.getStakedBalance() return { positions, balance, total, unclaimedRewards, apr: await staking.getApr() }}A stake becomes active at the next epoch, or the one after when it is sent after the epoch’s
boundary block (getEpoch().activatesAtEpoch; epochs are about 4 hours). Monad keeps a 10 MON
reserve on every account: a stake that would leave less is refused (getMaxStakeable()). Rewards
accrue separately in unclaimedRewards; claim them or compound them into the stake.
Unstake and claim
Section titled “Unstake and claim”export async function unstakeMon(wallet: Wallet, wei: bigint) { const staking = await wallet.getProtocol<NativeStakingProtocolMonad>('mon-staking') // Only active stake can be undelegated; the module picks a free withdrawal slot (0..255). const { id, withdrawableAtEpoch } = await staking.requestWithdrawal({ amount: wei, validator: 97n }) return { id, withdrawableAtEpoch } // claimable one epoch after the stake deactivates}
export async function claimMon(wallet: Wallet) { const staking = await wallet.getProtocol<NativeStakingProtocolMonad>('mon-staking') const { claimableIds } = await staking.getWithdrawalRequests({ validator: 97n }) if (claimableIds.length > 0) await staking.claimWithdrawals({ ids: claimableIds }) // one withdraw per request const position = (await staking.listPositions()).find((p) => p.validatorId === 97n) if (position && position.unclaimedRewards > 0n) await staking.claimRewards({ validator: 97n }) // or compound({ validator }) return { claimed: claimableIds.length }}Only active stake can be undelegated. Each unstake takes a withdrawal slot (0–255 per validator)
and can be withdrawn one epoch after the stake deactivates. The precompile withdraws one request
per transaction, so claimWithdrawals may send several. After a full exit the precompile no longer
lists the validator among the wallet’s delegations: keep the id that requestWithdrawal returns,
or pass the validator in validators, so the withdrawal is found again.
Module-specific extras
Section titled “Module-specific extras”The shared interfaces cover what every module can do. Modules add their own methods on top: Lido wraps stETH into wstETH, Aave toggles collateral and lists reserves with their rates and caps, Morpho lists vaults, SSV reports APR history. They are listed in each package’s API reference.