# stake.link AGENTS.md

> Knowledge base for AI agents building on or querying the stake.link protocol.
> MCP endpoint: https://www.stakedotlink.money/mcp

## What is stake.link

Liquid staking protocol on Ethereum for Chainlink (LINK), Polygon (POL), and Espresso (ESP). Users deposit tokens, receive rebasing liquid staking tokens (stLINK, stPOL, stESP), and earn staking rewards automatically. The protocol is operated by 15 professional Chainlink node operators and governed by the SDL token via the stake.link DAO.

## Core Concepts

### Liquid Staking Tokens (rebasing)

- **stLINK** = deposited LINK. Balance grows every ~2 days as rewards compound.
- **stPOL** = deposited POL. Same rebasing mechanic.
- **stESP** = deposited ESP. Same rebasing mechanic.

### Wrapped Tokens (non-rebasing)

- **wstLINK** = wrapped stLINK. Balance stays fixed, value per token increases. Used in DeFi (Morpho, Curve, Uniswap).
- **wstPOL**, **wstESP** = same pattern for POL and ESP.
- Wrapping/unwrapping is always available at the current exchange rate with no fees.

### SDL and reSDL (governance)

- **SDL** = fixed-supply governance token (100M total). Earns protocol fees when staked.
- **reSDL** = ERC-721 NFT representing locked SDL. Locking SDL for longer gives higher boost:
  - No lock = 1x
  - 12 months = ~3x
  - 24 months = ~5x
  - 36 months = ~7x
  - 48 months = ~9x
- reSDL holders get: boosted protocol fee share, Priority Pool priority, governance voting power.
- Unlock requires waiting half the lock duration before initiating, then another half-duration to complete.

### Priority Pool

- When Chainlink staking capacity is full, LINK deposits queue in the Priority Pool.
- Distribution is by reSDL priority, not first-come-first-served.
- When capacity opens, queued LINK is staked automatically via merkle distribution.

### Withdrawal Pool

- Redeeming stLINK for LINK goes through the Withdrawal Pool if Priority Pool liquidity is insufficient.
- FIFO queue, batched execution tied to Chainlink's unbonding cycle.

### Exchange Rate

- `StakingPool.getStakeByShares(1e18)` returns how much underlying token 1 share (1 stLINK at genesis) is now worth.
- This rate only goes up (unless slashing occurs). It increases on every rebase.
- `WrappedSDToken.getUnderlyingByWrapped(1e18)` returns how much stLINK 1 wstLINK is worth.

## MCP Server

Public site split:

- Novice start page: https://www.stakedotlink.money/start
- Developer page: https://www.stakedotlink.money/agents
- `/mcp-guide` now redirects to `/agents`

### Endpoint

```
https://www.stakedotlink.money/mcp
```

### Transport

Streamable HTTP. POST JSON-RPC to the endpoint. No authentication required.

### Connect

```bash
# Claude Code
claude mcp add sdl --transport http https://www.stakedotlink.money/mcp

# Codex
codex mcp add sdl --url https://www.stakedotlink.money/mcp
```

### Resources

Three docs are available directly over MCP resources:

- `https://www.stakedotlink.money/AGENTS.md`
- `https://www.stakedotlink.money/BUILDER.md`
- `https://www.stakedotlink.money/llms.txt`

Use `resources/list` to discover them, then `resources/read` with the URI you want.

### Available Tools

The public MCP now leads with the three simple strategy routes for regular users, plus the full read surface and unsigned transaction builders. The MCP never signs or broadcasts. Agents bring their own wallet. The live surface is 60 tools total, 50 read and planning plus 10 unsigned write builders. Call `tools/list` for the live catalog. The table below highlights the core tools and planner surface.

| Tool                                  | Description                                                                       | Data Source                                  |
| ------------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------- |
| `sdl_health_check`                    | Server health, block number, subgraph sync                                        | RPC + subgraph                               |
| `sdl_get_registry`                    | Canonical contract, source, repo, docs, and DeFi venue registry                   | Ethereum + Sourcify + official APIs          |
| `sdl_get_contract`                    | Live contract resolution: proxy, implementation, source coverage                  | Ethereum + Sourcify                          |
| `sdl_get_contract_abi`                | Verified ABI for a live contract or implementation                                | Sourcify                                     |
| `sdl_get_contract_source`             | Verified Solidity source for a live contract or implementation                    | Sourcify                                     |
| `sdl_protocol_summary`                | Full protocol snapshot: TVL, prices, rates, queues, SDL state                     | 8 contracts + 3 oracles + subgraph           |
| `sdl_get_prices`                      | LINK, ETH, POL from Chainlink; SDL from subgraph; wstLINK adapters                | Chainlink AggregatorV3 + adapters            |
| `sdl_get_exchange_rates`              | stLINK/share, wstLINK/stLINK, stPOL, stESP rates                                  | StakingPool + WrappedSDToken view functions  |
| `sdl_get_pool`                        | Per-pool deep dive (LINK/POL/ESP): TVL, capacity, fees, strategies                | StakingPool + VCS + FundFlowController       |
| `sdl_get_peg`                         | stLINK/LINK exchange rate and deviation                                           | StakingPool + WLSTAdapter                    |
| `sdl_get_priority_pool`               | Deposit queue: depth, status, oracle progress, merkle root, recent updates        | PriorityPool + DistributionOracle + IPFS     |
| `sdl_get_priority_pool_distribution`  | Full Priority Pool distribution flow: thresholds, root, IPFS tree, recent batches | PriorityPool + DistributionOracle + IPFS     |
| `sdl_get_priority_pool_wallet`        | Wallet claim view: queued LINK, claimable stLINK, current tree status             | PriorityPool + current merkle tree           |
| `sdl_get_priority_pool_batch_compare` | Latest batch diff: compare current and previous cumulative trees                  | PriorityPool + IPFS + priority-pool-ea logic |
| `sdl_get_withdrawal_pool`             | Withdrawal queue: total queued, execution timing                                  | WithdrawalPool contract                      |
| `sdl_get_sdl_pool`                    | SDL staking: effective balance, reward tokens, boost params                       | SDLPool + LinearBoostController + subgraph   |
| `sdl_get_resdl_positions`             | Wallet's reSDL NFT locks with boost, status, expiry                               | SDLPool.getLockIdsByOwner + getLocks         |
| `sdl_get_lock_tiers`                  | Boost multiplier per lock duration (0/12/24/36/48 months)                         | LinearBoostController.getBoostAmount         |
| `sdl_get_positions`                   | Full wallet scan: all token balances + locks + queues                             | Multicall across 12 contracts                |
| `sdl_get_defi_morpho`                 | Morpho Blue wstLINK/LINK market state                                             | Morpho Blue + MetaMorpho vault               |
| `sdl_get_defi_curve`                  | Curve pool TVL, volume, balances, and gauge rewards                               | Curve source API                             |
| `sdl_get_defi_folks`                  | Folks Finance wstLINK pool TVL, APY, borrow rate, utilization                     | Folks Finance xApp indexer                   |
| `sdl_get_defi_uniswap`                | Uniswap SDL/LINK pool TVL, volume, prices, and trade counts                       | GeckoTerminal API                            |
| `sdl_get_defi_beefy`                  | Beefy Curve LP autocompound vault APY and TVL                                     | Beefy API                                    |
| `sdl_get_merkl_campaigns`             | Merkl campaign APR, status, and daily SDL rewards                                 | Merkl API                                    |
| `sdl_get_governance_proposals`        | Live SLURPs and council proposals with states, thresholds, and URLs               | Snapshot GraphQL                             |
| `sdl_get_fpb_framework`               | Incentive budget, markets, stewards, and live campaign metrics                    | stake.link config + live DeFi reads          |
| `sdl_search_protocol`                 | Search conceptual stake.link knowledge and historical explainers                  | Local knowledge base index                   |
| `sdl_get_top_holders`                 | Largest stakers by stLINK or reSDL balance                                        | LinkPool subgraph                            |
| `sdl_get_reward_history`              | Reward distribution timestamps, amounts, rates                                    | LinkPool subgraph                            |
| `sdl_strategies_list`                 | Public strategy menu, 3 simple routes plus canonical advanced Uniswap LP           | Public strategy planner                      |
| `sdl_strategy_plan`                   | Ordered plan for one public strategy, with previous-step output refs when needed  | Public strategy planner                      |

### Write Tools (10)

All write tools return an unsigned `AgentTransaction`. They never sign, broadcast, or access private keys.

| Tool                        | Description                                                                  | Target Contract     |
| --------------------------- | ---------------------------------------------------------------------------- | ------------------- |
| `sdl_build_stake_link`      | LINK → stLINK via PriorityPool (ERC677 transferAndCall, no approval needed)  | LINK → PriorityPool |
| `sdl_build_unstake_link`    | Queue stLINK for redemption via WithdrawalPool (FIFO, includes approval)     | WithdrawalPool      |
| `sdl_build_wrap_stlink`     | stLINK → wstLINK (rebasing → non-rebasing, includes approval)                | wstLINK             |
| `sdl_build_unwrap_wstlink`  | wstLINK → stLINK (no approval; wstLINK holds the underlying)                 | wstLINK             |
| `sdl_build_morpho_deposit`  | Deposit LINK into Morpho Alpha LINK Enhanced V2 Vault (ERC-4626, + approval) | Alpha LINK Vault    |
| `sdl_build_morpho_withdraw` | Withdraw LINK from Morpho vault by burning shares                            | Alpha LINK Vault    |
| `sdl_build_curve_swap`      | Swap LINK and stLINK in the canonical Curve pool                             | Curve Pool          |
| `sdl_build_curve_lp_add`    | Add balanced LINK and stLINK liquidity on Curve                              | Curve Pool          |
| `sdl_build_curve_gauge_deposit` | Stake Curve LP tokens in the live gauge                                  | Curve Gauge         |
| `sdl_build_uniswap_v3_lp`   | Mint the canonical LINK and SDL Uniswap V3 LP NFT, explicit min or zero-min ack required | Position Manager    |

Some write tools return a single `approval`. Multi-leg builders may return `approvals`, plural, when more than one token allowance is required.

Strategy plans can now return structured previous-step references for multi-leg routes. Example:

```json
{
  "amount_stlink": {
    "kind": "previous_step_output",
    "step_id": "swap-to-stlink",
    "output": "received_stlink",
    "description": "Use the actual stLINK received from the previous swap step."
  }
}
```

That keeps the planner machine-usable. No more fake placeholder amounts in executable tool arguments.

For the Uniswap V3 route, the planner only emits an executable LP builder call after the caller provides explicit minimums or deliberately passes `allow_zero_mins: true`. Without that guard, the planner still returns the route and warnings, but it does not hand back a ready-to-run mint call.

### Response Format

Every response includes provenance:

```json
{
  "block": 24837915,
  "tvl": {
    "link": {
      "raw": "6445571331190321344682425",
      "formatted": "6445571.33",
      "contract": "0xb8b295df2cd735b15BE5Eb419517Aa626fc43cD5",
      "function": "totalStaked()"
    }
  }
}
```

- `raw`: uint256 from the contract, no rounding
- `formatted`: human-readable (divided by 1e18)
- `contract`: Ethereum address that was called
- `function`: Solidity function name
- `block`: Ethereum block number at read time

## Smart Contract Architecture

### Ethereum Mainnet Contracts

| Contract                 | Address                                      | Key View Functions                                                                                                                                                           |
| ------------------------ | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| StakingPool (stLINK)     | `0xb8b295df2cd735b15BE5Eb419517Aa626fc43cD5` | `totalStaked()`, `getStakeByShares(uint256)`, `getSharesByStake(uint256)`, `canDeposit()`, `canWithdraw()`, `getUnusedDeposits()`, `balanceOf(address)`, `sharesOf(address)` |
| WrappedSDToken (wstLINK) | `0x911D86C72155c33993d594B0Ec7E6206B4C803da` | `getUnderlyingByWrapped(uint256)`, `getWrappedByUnderlying(uint256)`, `totalSupply()`, `balanceOf(address)`                                                                  |
| SDLPool (reSDL NFT)      | `0x0B2eF910ad0b34bf575Eb09d37fd7DA6c148CA4d` | `totalEffectiveBalance()`, `effectiveBalanceOf(address)`, `getLockIdsByOwner(address)`, `getLocks(uint256[])`, `lastLockId()`, `supportedTokens()`                           |
| LinearBoostController    | `0x14b2F86c159199b6CBa593438aE89078dfB83698` | `getBoostAmount(uint256, uint64)`, `maxBoost()`, `maxLockingDuration()`                                                                                                      |
| PriorityPool (LINK)      | `0xDdC796a66E8b83d0BcCD97dF33A6CcFBA8fd60eA` | `totalQueued()`, `poolStatus()`, `depositsSinceLastUpdate()`, `merkleRoot()`, `paused()`                                                                                     |
| WithdrawalPool           | `0xa60B5146E44ff755e32BD51532842ceB41D0C248` | `getTotalQueuedWithdrawals()`, `getAccountTotalQueuedWithdrawals(address)`, `getFinalizedWithdrawalIdsByOwner(address)`                                                      |
| OperatorVCS              | `0x4852e48215A4785eE99B640CACED5378Cc39D2A4` | `getTotalDeposits()`, `getDepositChange()`, `operatorRewardPercentage()`                                                                                                     |
| CommunityVCS             | `0xAc12290b097f6893322F5430627e472131fBC1B5` | `getTotalDeposits()`, `getDepositChange()`, `canDeposit()`                                                                                                                   |
| FundFlowController       | `0xd2e7381d8d3FcC97C1b4d88761bDBc8Dd26a0200` | `claimPeriodActive()`, `shouldUpdateVaultGroups()`                                                                                                                           |
| SDL Token                | `0xA95C5ebB86E0dE73B4fB8c47A45B792CFeA28C23` | `totalSupply()`, `balanceOf(address)` (ERC-20)                                                                                                                               |
| StakingPool (stPOL)      | `0x2ff4390dB61F282Ef4E6D4612c776b809a541753` | Same as stLINK StakingPool                                                                                                                                                   |
| StakingPool (stESP)      | `0x5273a75694311A6c4F2AcF5C5B8566D965cb6e50` | Same as stLINK StakingPool                                                                                                                                                   |

### Chainlink Oracles

| Pair         | Address                                      | Decimals | Notes                                                                                          |
| ------------ | -------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------- |
| LINK/USD     | `0x2c1d072e956AFFC0D435Cb7AC38EF18d24d9127c` | 8        | Standard Chainlink feed                                                                        |
| ETH/USD      | `0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419` | 8        | Standard Chainlink feed                                                                        |
| POL/USD      | `0x7bAC85A8a13A4BcD8abb3eB7d6b4d632c5a57676` | 8        | Standard Chainlink feed                                                                        |
| wstLINK/LINK | `0xf534813F0e94De9718c75c6FE3bbd6583c46BB0A` | 18       | stake.link adapter. Reads `getUnderlyingByWrapped(1e18)` internally. Used by Morpho as oracle. |
| wstLINK/USDC | `0xBA2A4765934Ad29f27631fcF3117360FE28217a5` | 8        | stake.link adapter. Combines LINK/USD + USDC/USD + wstLINK rate.                               |

### Morpho Blue Integration

| Component                  | Address                                                              |
| -------------------------- | -------------------------------------------------------------------- |
| Morpho Blue                | `0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb`                         |
| wstLINK Vault (MetaMorpho) | `0x610f5B68bD1EED68Af649A3fD3DC2CAa1ee4Ae7E`                         |
| Market ID                  | `0x987134eb4716fc3dee2cb9ca8d8fba692389f7e43c717db4fe0cedaf287816e9` |

Read market state: `Morpho.market(marketId)` returns `(totalSupplyAssets, totalSupplyShares, totalBorrowAssets, totalBorrowShares, lastUpdate, fee)`.

### Subgraph

```
Primary:  https://graph-readonly.linkpool.pro/subgraphs/name/stakedotlink-ethereum-production
Fallback: https://graph.linkpool.pro/subgraphs/name/stakedotlink-ethereum-production
```

No API key required. GraphQL POST. Key entities: `tokenHolders`, `linkStakingDistributions`, `polStakingDistributions`, `wsdstakingPools`, `price`, `locks`, `initiateUnlocks`.

## Common Patterns for AI Agents

### Get TVL

```
Call: sdl_protocol_summary
Read: result.tvl.link.formatted (LINK staked)
Multiply by result.prices.link_usd.value for USD TVL
```

### Check if user can stake

```
Call: sdl_get_pool with pool="LINK"
Read: result.deposit_room.formatted
If > 0: direct deposit available
If = 0: deposits queue in Priority Pool
```

### Check how close the next Priority Pool batch is

```
Call: sdl_get_priority_pool
Read: result.distribution_oracle.progress.progress_pct
Read: result.distribution_oracle.progress.remaining_to_threshold.formatted
Read: result.distribution_oracle.progress.should_pause_for_update_now
```

### Check what a wallet can claim from the latest batch

```
Call: sdl_get_priority_pool_wallet with address="0x..."
Read: result.claimable_stlink_now.formatted
Read: result.queued_link_now.formatted
Read: result.current_distribution.in_current_merkle_tree
```

### Check who got added in the latest batch

```
Call: sdl_get_priority_pool_batch_compare
Read: result.summary.total_delta_amount.formatted
Read: result.summary.matches_event_amount
Read: result.top_changes
```

### Find the live implementation and source for a contract

```
Call: sdl_get_contract with contract="PriorityPool"
Read: result.live.implementation_address
Read: result.verification.primary_source_path

Call: sdl_get_contract_source with contract="PriorityPool"
Read: result.primary_source.content
```

### Calculate wstLINK value in LINK

```
Call: sdl_get_exchange_rates
Read: rate where pair = "wstLINK→stLINK"
Multiply user's wstLINK balance by that rate
```

### Check user's full position

```
Call: sdl_get_positions with address="0x..."
Returns all balances in one call
```

### Understand lock boost

```
Call: sdl_get_lock_tiers
Returns multiplier for each duration
Example: 48 months = ~9x means locking 1000 SDL gives 9000 effective balance for rewards
```

## Writing Transactions

Every `sdl_build_*` tool returns an unsigned transaction the agent can sign. The MCP never touches private keys.

### AgentTransaction Shape

```json
{
  "to": "0x514910771AF9Ca656af840dff83E8264EcF986CA",
  "data": "0x4000aea0000000000000000000000000ddc796a66e8b83d0bccd97df33a6ccfba8fd60ea0000000000000000000000000000000000000000000000056bc75e2d63100000...",
  "value": "0",
  "chainId": 1,
  "approval": {
    "to": "0xb8b295df2cd735b15BE5Eb419517Aa626fc43cD5",
    "data": "0x095ea7b3000000000000000000000000a60b5146e44ff755e32bd51532842ceb41d0c248ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
    "value": "0",
    "reason": "stLINK allowance for WithdrawalPool is 0, need 100000000000000000000",
    "simulation": {
      "success": true,
      "gasUsed": 46218,
      "gasLimit": 70000
    }
  },
  "metadata": {
    "action": "stake_link",
    "inputAmount": "100",
    "contractName": "LINK (ERC677) → PriorityPool",
    "functionName": "transferAndCall",
    "blockNumber": 24887309,
    "notes": [
      "LINK is ERC677. transferAndCall delivers LINK + invokes PriorityPool.onTokenTransfer in one tx.",
      "If pool has capacity, LINK is staked immediately (mints stLINK to sender).",
      "If pool is full and shouldQueue=true, LINK enters the Priority Pool queue."
    ],
    "simulation": {
      "success": true,
      "gasUsed": 185432,
      "gasLimit": 250000,
      "blockNumber": 24887310
    }
  }
}
```

### Simulation (default ON)

Every `sdl_build_*` tool runs a Tenderly mainnet-fork simulation by default and attaches the result to `metadata.simulation` (and `approval.simulation` if an approval is needed). This lets the agent — and the human approving the tx — see up front whether the transaction will succeed and how much gas it will consume.

On success:

```json
"simulation": { "success": true, "gasUsed": 185432, "gasLimit": 250000 }
```

On failure:

```json
"simulation": {
  "success": false,
  "gasUsed": 23107,
  "gasLimit": 250000,
  "revertReason": "ERC20: transfer amount exceeds balance"
}
```

Common revert reasons the simulation will surface before you burn gas:

- `Transfer amount exceeds balance` — sender doesn't hold enough tokens
- `ERC20: insufficient allowance` — approval missing or too small (the build tool normally adds an `approval` automatically, but simulation catches the edge case where allowance logic disagreed)
- `PoolNotOpen` — staking pool is DRAINING or CLOSED
- `InsufficientLiquidity` — Morpho market too utilized for a full withdraw
- `MinLockingDuration` / `MaxLockingDuration` — bad reSDL lock params

**Opt out:** pass `simulate: false` to skip simulation (saves ~300-800ms). Useful for hot paths where the agent has already verified state, or for builds you're batching into a Safe transaction where the Safe itself will simulate.

**Soft-fail:** if Tenderly is misconfigured, rate-limited, or down, `simulation` is replaced with `{ "error": "..." }`. The tx itself is still returned — agents should decide whether to proceed without a simulation check.

### Approval Flow

When an ERC20 allowance is insufficient, the response includes an `approval` object. Sign and broadcast the approval first, then the main transaction:

1. Build: `sdl_build_wrap_stlink({ amount: "100", from: "0x..." })`
2. If `response.approval` is present: sign + broadcast `approval` first (waits 1 block).
3. Sign + broadcast the main `{to, data, value}` tx.

Actions that typically don't need approval:

- `sdl_build_stake_link` — ERC677 `transferAndCall` combines transfer + callback
- `sdl_build_unwrap_wstlink` — wstLINK contract holds the underlying stLINK
- `sdl_build_morpho_withdraw` — caller owns the vault shares

### Example: Build a Yield Loop

A common agent pattern: stake LINK, wrap to wstLINK, deposit to Morpho vault for organic lending yield on top of staking APY.

```typescript
import { createWalletClient, http } from 'viem';
import { mainnet } from 'viem/chains';

const mcp = 'https://www.stakedotlink.money/mcp';

async function callTool(name: string, args: any) {
  const res = await fetch(mcp, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      jsonrpc: '2.0',
      id: 1,
      method: 'tools/call',
      params: { name, arguments: args },
    }),
  });
  const body = await res.json();
  return JSON.parse(body.result.content[0].text);
}

// 1. Stake 1000 LINK
const stake = await callTool('sdl_build_stake_link', {
  amount: '1000',
  from: wallet.address,
});
await wallet.sendTransaction({ to: stake.to, data: stake.data, value: 0n });

// 2. Wrap 1000 stLINK to wstLINK
const wrap = await callTool('sdl_build_wrap_stlink', {
  amount: '1000',
  from: wallet.address,
});
if (wrap.approval) {
  await wallet.sendTransaction({ to: wrap.approval.to, data: wrap.approval.data, value: 0n });
}
await wallet.sendTransaction({ to: wrap.to, data: wrap.data, value: 0n });

// 3. Check Morpho market state before depositing
const morpho = await callTool('sdl_get_defi_morpho', {});
if (morpho.utilization_rate > 0.95) {
  console.log('Market near full utilization, withdraws may be delayed');
}

// 4. Deposit LINK to Morpho vault (separate LINK, not the staked position)
const deposit = await callTool('sdl_build_morpho_deposit', {
  amount: '500',
  from: wallet.address,
});
if (deposit.approval) {
  await wallet.sendTransaction({ to: deposit.approval.to, data: deposit.approval.data, value: 0n });
}
await wallet.sendTransaction({ to: deposit.to, data: deposit.data, value: 0n });
```

## Integration Patterns

### Yield Optimizer Agent

Continuously rebalances LINK between staking (stLINK rebase) and Morpho lending based on rate spread.

```
Loop every N blocks:
  1. Call sdl_protocol_summary → get stLINK APY
  2. Call sdl_get_defi_morpho → get Morpho supply APY
  3. If Morpho APY > stLINK APY + spread_threshold:
     Call sdl_build_unstake_link, then sdl_build_morpho_deposit
  4. If stLINK APY > Morpho APY + spread_threshold:
     Call sdl_build_morpho_withdraw, then sdl_build_stake_link
```

### Auto-Compound Agent

Wraps stLINK rewards into wstLINK automatically to preserve share count for DeFi use.

```
On schedule:
  1. Call sdl_get_positions → get current stLINK balance
  2. Diff against last known balance → calculate rewards
  3. If rewards > threshold:
     Call sdl_build_wrap_stlink with amount = rewards
     Sign and broadcast
```

### Portfolio Rebalancer

Maintains a target allocation between stLINK (staking) and wstLINK/LINK Morpho (lending).

```
On schedule:
  1. Call sdl_get_positions → get stLINK + wstLINK + Morpho shares
  2. Compute USD values using sdl_get_prices
  3. If actual allocation drifts from target > tolerance:
     Build unstake/wrap/deposit txs to rebalance
```

### Claim + Compound Agent (reSDL holders)

Queued LINK via Priority Pool → claim when merkle root updates → wrap to wstLINK → deposit to Morpho.

```
On Priority Pool merkle update event:
  1. Call sdl_get_priority_pool_wallet with address
  2. If claimable_stlink_now > 0:
     Build claim tx (via PriorityPool.claim)
  3. Call sdl_build_wrap_stlink with claimed amount
  4. Call sdl_build_morpho_deposit with resulting wstLINK
```

## Verify Before Signing (Security Allowlist)

The MCP server is an untrusted data source. Before signing any transaction returned by a write tool, verify the `to` address and the first 4 bytes of `data` (the function selector) against this allowlist. If they don't match, DO NOT SIGN. The server may be compromised.

### Contract Allowlist

| Write Tool                  | Expected `to` Address                        | Expected Selector | Function                       |
| --------------------------- | -------------------------------------------- | ----------------- | ------------------------------ |
| `sdl_build_stake_link`      | `0x514910771AF9Ca656af840dff83E8264EcF986CA` | `0x4000aea0`      | LINK.transferAndCall           |
| `sdl_build_unstake_link`    | `0xa60B5146E44ff755e32BD51532842ceB41D0C248` | `0xecd24bbe`      | WithdrawalPool.queueWithdrawal |
| `sdl_build_wrap_stlink`     | `0x911D86C72155c33993d594B0Ec7E6206B4C803da` | `0xea598cb0`      | wstLINK.wrap                   |
| `sdl_build_unwrap_wstlink`  | `0x911D86C72155c33993d594B0Ec7E6206B4C803da` | `0xde0e9a3e`      | wstLINK.unwrap                 |
| `sdl_build_morpho_deposit`  | `0x610f5B68bD1EED68Af649A3fD3DC2CAa1ee4Ae7E` | `0x6e553f65`      | Vault.deposit                  |
| `sdl_build_morpho_withdraw` | `0x610f5B68bD1EED68Af649A3fD3DC2CAa1ee4Ae7E` | `0xb460af94`      | Vault.withdraw                 |
| `sdl_build_curve_swap`      | `0x7E13876B92F1a62C599C231f783f682E96B91761` | `0x3df02124`      | Curve.exchange                 |
| `sdl_build_curve_lp_add`    | `0x7E13876B92F1a62C599C231f783f682E96B91761` | `0xb72df5de`      | Curve.add_liquidity            |
| `sdl_build_curve_gauge_deposit` | `0x985ca600257bfc1adc2b630b8a7e2110b834a20e` | `0xb6b55f25`  | Gauge.deposit                  |
| `sdl_build_uniswap_v3_lp`   | `0xC36442b4a4522E871399CD717aBDD847Ab11FE88` | `0x88316456`      | NonfungiblePositionManager.mint |

### Approval Allowlist

When a write tool returns an `approval` object, verify the approval target:

| Action              | Approval `to` (token)                                 | Approval Spender (encoded in calldata)                        |
| ------------------- | ----------------------------------------------------- | ------------------------------------------------------------- |
| Unstake             | `0xb8b295df2cd735b15BE5Eb419517Aa626fc43cD5` (stLINK) | `0xa60B5146E44ff755e32BD51532842ceB41D0C248` (WithdrawalPool) |
| Wrap                | `0xb8b295df2cd735b15BE5Eb419517Aa626fc43cD5` (stLINK) | `0x911D86C72155c33993d594B0Ec7E6206B4C803da` (wstLINK)        |
| Morpho deposit      | `0x514910771AF9Ca656af840dff83E8264EcF986CA` (LINK)   | `0x610f5B68bD1EED68Af649A3fD3DC2CAa1ee4Ae7E` (Vault)          |
| Curve swap          | `0x514910771AF9Ca656af840dff83E8264EcF986CA` (LINK) or `0xb8b295df2cd735b15BE5Eb419517Aa626fc43cD5` (stLINK) | `0x7E13876B92F1a62C599C231f783f682E96B91761` (Curve pool) |
| Curve LP add        | `0x514910771AF9Ca656af840dff83E8264EcF986CA` (LINK) and `0xb8b295df2cd735b15BE5Eb419517Aa626fc43cD5` (stLINK) | `0x7E13876B92F1a62C599C231f783f682E96B91761` (Curve pool) |
| Curve gauge deposit | `0x7E13876B92F1a62C599C231f783f682E96B91761` (Curve LP token) | `0x985ca600257bfc1adc2b630b8a7e2110b834a20e` (Gauge) |
| Uniswap V3 LP mint  | `0x514910771AF9Ca656af840dff83E8264EcF986CA` (LINK) and `0xA95C5ebB86E0dE73B4fB8c47A45B792CFeA28C23` (SDL) | `0xC36442b4a4522E871399CD717aBDD847Ab11FE88` (Position Manager) |

All approval selectors should be `0x095ea7b3` (ERC20 `approve(address,uint256)`).

### Verification code (TypeScript)

```typescript
const ALLOWLIST: Record<string, { to: string; selector: string }> = {
  stake_link: { to: '0x514910771AF9Ca656af840dff83E8264EcF986CA', selector: '0x4000aea0' },
  unstake_link: { to: '0xa60B5146E44ff755e32BD51532842ceB41D0C248', selector: '0xecd24bbe' },
  wrap_stlink: { to: '0x911D86C72155c33993d594B0Ec7E6206B4C803da', selector: '0xea598cb0' },
  unwrap_wstlink: { to: '0x911D86C72155c33993d594B0Ec7E6206B4C803da', selector: '0xde0e9a3e' },
  morpho_deposit: { to: '0x610f5B68bD1EED68Af649A3fD3DC2CAa1ee4Ae7E', selector: '0x6e553f65' },
  morpho_withdraw: { to: '0x610f5B68bD1EED68Af649A3fD3DC2CAa1ee4Ae7E', selector: '0xb460af94' },
  curve_swap: { to: '0x7E13876B92F1a62C599C231f783f682E96B91761', selector: '0x3df02124' },
  curve_lp_add: { to: '0x7E13876B92F1a62C599C231f783f682E96B91761', selector: '0xb72df5de' },
  curve_gauge_deposit: { to: '0x985ca600257bfc1adc2b630b8a7e2110b834a20e', selector: '0xb6b55f25' },
  uniswap_v3_lp_mint: { to: '0xC36442b4a4522E871399CD717aBDD847Ab11FE88', selector: '0x88316456' },
};

const APPROVAL_ALLOWLIST: Record<string, Record<string, string>> = {
  unstake_link: {
    '0xb8b295df2cd735b15be5eb419517aa626fc43cd5': '0xa60b5146e44ff755e32bd51532842ceb41d0c248',
  },
  wrap_stlink: {
    '0xb8b295df2cd735b15be5eb419517aa626fc43cd5': '0x911d86c72155c33993d594b0ec7e6206b4c803da',
  },
  morpho_deposit: {
    '0x514910771af9ca656af840dff83e8264ecf986ca': '0x610f5b68bd1eed68af649a3fd3dc2caa1ee4ae7e',
  },
  curve_swap: {
    '0x514910771af9ca656af840dff83e8264ecf986ca': '0x7e13876b92f1a62c599c231f783f682e96b91761',
    '0xb8b295df2cd735b15be5eb419517aa626fc43cd5': '0x7e13876b92f1a62c599c231f783f682e96b91761',
  },
  curve_lp_add: {
    '0x514910771af9ca656af840dff83e8264ecf986ca': '0x7e13876b92f1a62c599c231f783f682e96b91761',
    '0xb8b295df2cd735b15be5eb419517aa626fc43cd5': '0x7e13876b92f1a62c599c231f783f682e96b91761',
  },
  curve_gauge_deposit: {
    '0x7e13876b92f1a62c599c231f783f682e96b91761': '0x985ca600257bfc1adc2b630b8a7e2110b834a20e',
  },
  uniswap_v3_lp_mint: {
    '0x514910771af9ca656af840dff83e8264ecf986ca': '0xc36442b4a4522e871399cd717abdd847ab11fe88',
    '0xa95c5ebb86e0de73b4fb8c47a45b792cfea28c23': '0xc36442b4a4522e871399cd717abdd847ab11fe88',
  },
};

function extractApprovalSpender(data: string): string {
  return `0x${data.slice(34, 74)}`.toLowerCase();
}

function verifyApproval(action: string, approval: ApprovalTransaction): boolean {
  if (!approval.data.startsWith('0x095ea7b3')) return false;
  const expectedSpender = APPROVAL_ALLOWLIST[action]?.[approval.to.toLowerCase()];
  if (!expectedSpender) return false;
  return extractApprovalSpender(approval.data) === expectedSpender;
}

function verifyTx(tx: AgentTransaction): boolean {
  const expected = ALLOWLIST[tx.metadata.action];
  if (!expected) return false;
  if (tx.to.toLowerCase() !== expected.to.toLowerCase()) return false;
  if (!tx.data.startsWith(expected.selector)) return false;
  if (tx.approval && !verifyApproval(tx.metadata.action, tx.approval)) return false;
  if (tx.approvals?.some((approval) => !verifyApproval(tx.metadata.action, approval))) return false;
  return true;
}

// Usage: refuse to sign if verification fails
const tx = await mcp('sdl_build_stake_link', { amount: '100', from: wallet.address });
if (!verifyTx(tx)) throw new Error('Transaction failed allowlist verification — DO NOT SIGN');
```

### Why this matters

The MCP server runs on a web server. If that server is compromised, it could return calldata that sends your tokens to an attacker instead of staking them. The simulation would still show "success" because the malicious transaction would technically execute.

This allowlist is published in this document (AGENTS.md), which your agent loads as context BEFORE calling the MCP. The verification runs client-side, not on the server. A compromised server cannot fake the allowlist because it lives in a separate document the agent already has.

No other DeFi agent protocol publishes a client-side verification allowlist. This is a stake.link-first security pattern.

## Edge Cases and Gotchas

1. **stLINK rate only goes up** (unless slashing). If the exchange rate drops, something is very wrong.
2. **Pool status DRAINING** means deposits are disabled but withdrawals continue. CLOSED means both disabled.
3. **Priority Pool merkle root = 0x00...** means no distribution has happened yet or the pool was just reset.
4. **Withdrawal queue timing** depends on Chainlink's unbonding cycle. `FundFlowController.claimPeriodActive()` tells you if withdrawals can currently execute.
5. **LinearBoostController.minLockingDuration()** reverts on the deployed contract (older version). Use `maxLockingDuration()` and `maxBoost()` which work.
6. **reSDL unlock timeline**: a 48-month lock can initiate unlock after 24 months, then takes another 24 months to complete. `lock.expiry > 0` means unlock has been initiated.
7. **SDL price** comes from the subgraph (DEX-derived), not a Chainlink oracle. There is no Chainlink feed for SDL.
8. **Stake tx path** — Don't call `StakingPool.deposit()` directly. Use LINK's ERC677 `transferAndCall(PriorityPool, amount, encoded_flags)`. Going through the PriorityPool is required for queue semantics when the pool is full.
9. **Unwrap doesn't need approval.** The wstLINK contract custody the underlying stLINK. Approve-then-unwrap will work but burns gas for nothing.
10. **Morpho withdrawals can fail at high utilization.** If `sdl_get_defi_morpho.utilization_rate` is near 1.0, withdrawing the full supply may revert. Check before building the tx.
11. **stLINK is rebasing** — if you quote "I have 100 stLINK" to a user, their balance next week will be higher. Use wstLINK if you need a static-balance receipt for accounting.
12. **ERC20 approval amounts** — write tools request exact-amount approvals, not infinite. Agents that prefer `type(uint256).max` approvals must override the approval tx before signing.
13. **Block number on AgentTransaction** — the `metadata.blockNumber` tells you when the MCP read state. If significant time passes before signing, re-call the build tool to get fresh approval/allowance data.
14. **Priority Pool claim** — after queueing via `sdl_build_stake_link`, the LINK enters the Priority Pool. Claiming stLINK requires calling `PriorityPool.claimLSDTokens(amount, sharesAmount, merkleProof)` — no build tool for this yet (add in a future wave).

## Links

- Protocol: https://stake.link
- Docs: https://docs.stake.link
- Contracts: https://github.com/stakedotlink/contracts
- Site: https://www.stakedotlink.money
- Developer page: https://www.stakedotlink.money/agents
- Start page: https://www.stakedotlink.money/start
- Governance: https://talk.stake.link
- Snapshot: https://snapshot.box/#/s:council.stakedotlink.eth
