# stake.link Builder Guide

> Deep integration guide for developers building agents and applications on stake.link.
> For the agent-focused knowledge base, see [AGENTS.md](./AGENTS.md).

## Prerequisites

- Node.js 18+ or Python 3.10+
- Ethereum RPC (public or private)
- MCP client: Claude Code, Cursor, or any MCP-compatible tool
- Wallet: any Ethereum signer (viem, ethers, Safe SDK, Fireblocks, Privy, etc.)

## Quick Start

### 1. Connect the MCP

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

Or add to your `.mcp.json`:

```json
{
  "mcpServers": {
    "sdl": {
      "type": "http",
      "url": "https://www.stakedotlink.money/mcp"
    }
  }
}
```

### 2. Verify Connection

```bash
curl -X POST https://www.stakedotlink.money/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

You should see the public read surface plus the strategy planner and write builders.

You can also pull the published docs over MCP itself:

```bash
curl -X POST https://www.stakedotlink.money/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"resources/list"}'
```

Use `resources/read` with one of the returned URIs to fetch AGENTS.md, BUILDER.md, or llms.txt inside the MCP session.

Public site split:

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

### 3. Build a Staking Agent in 50 Lines

```typescript
import { createWalletClient, http, parseEther, type Hex } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';

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

async function mcp(name: string, args: Record<string, unknown>) {
  const res = await fetch(MCP, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      jsonrpc: '2.0',
      id: Date.now(),
      method: 'tools/call',
      params: { name, arguments: args },
    }),
  });
  const body = await res.json();
  if (body.error) throw new Error(body.error.message);
  return JSON.parse(body.result.content[0].text);
}

async function main() {
  const account = privateKeyToAccount(process.env.PRIVATE_KEY as Hex);
  const wallet = createWalletClient({ account, chain: mainnet, transport: http() });

  // 1. Check current pool state
  const summary = await mcp('sdl_protocol_summary', {});
  console.log(`Staking APY: ${summary.apy.link.formatted}%`);

  // 2. Build the stake transaction
  const tx = await mcp('sdl_build_stake_link', {
    amount: '100',
    from: account.address,
  });

  // 3. Handle approvals if needed (stake_link is ERC677, so this stays empty)
  const approvals = tx.approvals ?? (tx.approval ? [tx.approval] : []);
  for (const approval of approvals) {
    const approvalHash = await wallet.sendTransaction({
      to: approval.to,
      data: approval.data,
      value: BigInt(approval.value),
    });
    console.log('Approval tx:', approvalHash);
  }

  // 4. Sign and broadcast the main transaction
  const hash = await wallet.sendTransaction({
    to: tx.to,
    data: tx.data,
    value: BigInt(tx.value),
  });
  console.log('Stake tx:', hash);
}

main().catch(console.error);
```

## Write Tool Reference

All write tools return an `AgentTransaction`. See [AGENTS.md § Writing Transactions](./AGENTS.md#writing-transactions) for the full shape.

When you use `sdl_strategy_plan` for multi-step routes, later tool arguments can include a `previous_step_output` reference instead of a fake placeholder string. Resolve that reference from the real output of the earlier step before calling the next builder.

For the Uniswap V3 route, the planner only emits an executable `sdl_build_uniswap_v3_lp` call after the caller supplies `amount_link_min` and `amount_sdl_min`, or explicitly passes `allow_zero_mins: true`. Without that guard, the planner still returns the route, but it leaves the builder step non-executable on purpose.

### sdl_build_stake_link

**Purpose:** Stake LINK via PriorityPool (LINK → stLINK).
**Flow:** ERC677 `transferAndCall` on LINK, which invokes `PriorityPool.onTokenTransfer()`. No separate approval needed.
**Contract:** `LINK` → `PriorityPool` (`0xDdC796a66E8b83d0BcCD97dF33A6CcFBA8fd60eA`)
**Function:** `LINK.transferAndCall(PriorityPool, amount, encoded(shouldQueue, []))`

```typescript
const tx = await mcp('sdl_build_stake_link', {
  amount: '1000', // LINK
  from: '0x...',
  shouldQueue: true, // Default: true — enter Priority Pool if staking is full
});
```

**Returns:**

- `to`: LINK contract
- `data`: encoded transferAndCall
- No approval (ERC677)
- If staking capacity is available: mints stLINK 1:rate immediately
- If pool is full + shouldQueue: LINK enters priority queue, claim later via merkle proof

### sdl_build_unstake_link

**Purpose:** Queue stLINK for redemption to LINK.
**Flow:** Approve stLINK → WithdrawalPool, then `WithdrawalPool.queueWithdrawal(account, amount)`.
**Contract:** `WithdrawalPool` (`0xa60B5146E44ff755e32BD51532842ceB41D0C248`)

```typescript
const tx = await mcp('sdl_build_unstake_link', {
  amount: '100', // stLINK
  from: '0x...',
});
```

**Returns:**

- Optional `approval` tx if current allowance < amount
- Main tx queues stLINK in FIFO withdrawal queue
- Execution tied to Chainlink unbonding cycles (check `FundFlowController.claimPeriodActive()`)
- stLINK continues rebasing while queued

### sdl_build_wrap_stlink / sdl_build_unwrap_wstlink

**Purpose:** Convert between rebasing (stLINK) and non-rebasing (wstLINK) representations.
**Wrap flow:** Approve stLINK → wstLINK, then `wstLINK.wrap(amount)`.
**Unwrap flow:** `wstLINK.unwrap(amount)` — no approval needed.

```typescript
// Wrap — need approval
const wrap = await mcp('sdl_build_wrap_stlink', { amount: '50', from: '0x...' });
// wrap.approval present if allowance is insufficient

// Unwrap — no approval
const unwrap = await mcp('sdl_build_unwrap_wstlink', { amount: '50', from: '0x...' });
```

Rate: `wstLINK.getUnderlyingByWrapped(1e18)` = how much stLINK 1 wstLINK is worth. Grows every rebase.

### sdl_build_morpho_deposit / sdl_build_morpho_withdraw

**Purpose:** Deposit/withdraw LINK to/from the Alpha LINK Enhanced V2 Vault (ERC-4626).
**Deposit flow:** Approve LINK → Vault, then `Vault.deposit(assets, receiver)`.
**Withdraw flow:** `Vault.withdraw(assets, receiver, owner)` — caller owns shares.
**Contract:** `0x610f5B68bD1EED68Af649A3fD3DC2CAa1ee4Ae7E`

```typescript
const deposit = await mcp('sdl_build_morpho_deposit', {
  amount: '500', // LINK
  from: '0x...',
  receiver: '0x...', // Optional, defaults to from
});

const withdraw = await mcp('sdl_build_morpho_withdraw', {
  amount: '500', // LINK
  from: '0x...',
});
```

**Withdraw caveats:**

- Subject to market utilization — check `sdl_get_defi_morpho.utilization_rate` first
- If utilization > 0.95, partial withdraw or wait for borrow repayment

### sdl_build_curve_swap

**Purpose:** Swap LINK and stLINK inside the canonical Curve pool.
**Contract:** `0x7E13876B92F1a62C599C231f783f682E96B91761`

```typescript
const swap = await mcp('sdl_build_curve_swap', {
  amount_in: '100',
  from: '0x...',
  direction: 'link_to_stlink',
  slippage_bps: 50,
});
```

### sdl_build_curve_lp_add

**Purpose:** Add balanced LINK and stLINK liquidity to the canonical Curve pool.
**Contract:** `0x7E13876B92F1a62C599C231f783f682E96B91761`

```typescript
const lp = await mcp('sdl_build_curve_lp_add', {
  amount_link: '50',
  amount_stlink: '49.8',
  from: '0x...',
  slippage_bps: 50,
});
```

This builder can return `approvals`, plural, because both LINK and stLINK may need allowances.

### sdl_build_curve_gauge_deposit

**Purpose:** Deposit Curve LP tokens into the canonical gauge.
**Gauge:** `0x985ca600257bfc1adc2b630b8a7e2110b834a20e`

```typescript
const gauge = await mcp('sdl_build_curve_gauge_deposit', {
  amount_lp: '10',
  from: '0x...',
});
```

### sdl_build_uniswap_v3_lp

**Purpose:** Mint the canonical LINK and SDL Uniswap V3 LP NFT.
**Pool:** `0x51d1026e35d0F9aa0fF243ebC84bb923852c1fC3`
**Position Manager:** `0xC36442b4a4522E871399CD717aBDD847Ab11FE88`

```typescript
const uniswap = await mcp('sdl_build_uniswap_v3_lp', {
  amount_link: '10',
  amount_sdl: '50',
  allow_zero_mins: true, // or provide amount_link_min + amount_sdl_min
  from: '0x...',
});
```

Notes:

- Uses the canonical mainnet LINK token0 and SDL token1 order.
- Requires either explicit min amounts or `allow_zero_mins: true` so the caller acknowledges mint slippage risk.
- Mints a full range LP position for the current canonical public route.
- Returns approvals when LINK or SDL allowance is missing.
- Does not encode NFT staking. The planner only adds that step when a live incentive program is actually detected.

## Reading Protocol State

### Live TVL, APY, prices

```typescript
const summary = await mcp('sdl_protocol_summary', {});
// summary.tvl.link.formatted      → "6445571.33"  (LINK staked)
// summary.apy.link.formatted      → "4.7"         (staking APY %)
// summary.prices.link_usd.value   → 13.45         (LINK/USD from Chainlink)
```

### Wallet position

```typescript
const pos = await mcp('sdl_get_positions', { address: '0x...' });
// pos.stlink.balance.formatted       → stLINK balance
// pos.wstlink.balance.formatted      → wstLINK balance
// pos.wstlink.underlying.formatted   → equivalent stLINK
// pos.resdl.locks[].boost_multiplier → reSDL lock boosts
// pos.queued.priority_pool.formatted → queued LINK
// pos.finalized_withdrawals.formatted → claimable LINK
```

### Morpho market state

```typescript
const morpho = await mcp('sdl_get_defi_morpho', {});
// morpho.supply_assets.formatted      → total LINK supplied
// morpho.borrow_assets.formatted      → total LINK borrowed
// morpho.utilization_rate             → 0.0 to 1.0
// morpho.supply_apy                   → % APY for suppliers
// morpho.borrow_apy                   → % APY paid by borrowers
```

## Common Gotchas (Quick Reference)

| Gotcha                            | Impact                                                           |
| --------------------------------- | ---------------------------------------------------------------- |
| `StakingPool.deposit` direct call | Wrong path. Use `LINK.transferAndCall(PriorityPool, ...)`        |
| Infinite approvals                | Not default. Override `approval.data` if you prefer max approval |
| stLINK balance appears to "grow"  | Rebasing. Expected. Use wstLINK for static accounting            |
| Morpho withdraw reverts           | Market at max utilization. Partial withdraw or wait              |
| `minLockingDuration()` revert     | Contract bug on live version. Use `maxLockingDuration()`         |
| `metadata.blockNumber` stale      | Re-call build tool after long delays                             |
| wstLINK unwrap approval           | Not needed — wastes gas                                          |

## Error Recovery

### Transaction reverted

If a transaction reverts, decode the revert reason before retrying:

```typescript
try {
  await wallet.sendTransaction({ to: tx.to, data: tx.data });
} catch (e) {
  if (e.message.includes('insufficient balance')) {
    // User doesn't have enough tokens
  } else if (e.message.includes('InsufficientLiquidity')) {
    // Morpho market too utilized for full withdraw
    // Retry with smaller amount
  } else if (e.message.includes('PoolNotOpen')) {
    // Staking pool is DRAINING or CLOSED
    // Check sdl_get_pool first
  }
}
```

### Approval race conditions

If an agent submits the main tx before the approval confirms, it will revert. Wait for the approval receipt:

```typescript
const approvals = tx.approvals ?? (tx.approval ? [tx.approval] : []);
for (const approval of approvals) {
  const approvalHash = await wallet.sendTransaction({ to: approval.to, data: approval.data });
  await publicClient.waitForTransactionReceipt({ hash: approvalHash });
}
const mainHash = await wallet.sendTransaction({ to: tx.to, data: tx.data });
```

### Stale block number

If significant time passes between calling `sdl_build_*` and signing, re-call the build tool. Allowances may have changed, and the approval logic may flip.

## Safe Multisig Integration

If the signer is a Safe, construct a batch:

```typescript
// Build individual txs
const stake = await mcp('sdl_build_stake_link', { amount: '1000', from: SAFE_ADDRESS });
const wrap = await mcp('sdl_build_wrap_stlink', { amount: '1000', from: SAFE_ADDRESS });

// Convert to Safe transaction format
const safeTx = {
  safe: SAFE_ADDRESS,
  transactions: [
    { to: stake.to, data: stake.data, value: stake.value, operation: 0 },
    // wrap.approval if needed
    ...(wrap.approval
      ? [{ to: wrap.approval.to, data: wrap.approval.data, value: '0', operation: 0 }]
      : []),
    { to: wrap.to, data: wrap.data, value: wrap.value, operation: 0 },
  ],
};

// Submit to Safe Transaction Service
// (full Safe SDK example in Wave 4 documentation)
```

> A dedicated `sdl_build_safe_batch` tool is planned in a future wave.

## Transaction Simulation

Every `sdl_build_*` tool runs a Tenderly mainnet-fork simulation by default. The result is attached to `metadata.simulation`:

```typescript
const tx = await mcp('sdl_build_stake_link', {
  amount: '100',
  from: account.address,
});

const sim = tx.metadata.simulation;
if (!sim) {
  // Simulation not configured on this MCP instance — proceed with caution
} else if ('error' in sim) {
  console.warn('Simulation unavailable:', sim.error);
} else if (!sim.success) {
  throw new Error(`Transaction would revert: ${sim.revertReason}`);
} else {
  console.log(`Simulation OK — gas=${sim.gasUsed}/${sim.gasLimit}`);
}
```

When an approval is needed, the simulation runs the approval + main tx as a bundle so the main tx sees the allowance change. Both simulation results are attached:

```typescript
// tx.approval.simulation   → { success, gasUsed, gasLimit }
// tx.metadata.simulation   → { success, gasUsed, gasLimit }
```

**Opt out:** pass `simulate: false` to skip simulation and save ~300-800ms. Use this when:

- The agent has already verified state via `sdl_get_positions` + `sdl_get_defi_morpho`
- You're batching into a Safe transaction (the Safe will simulate via its own UI)
- You're running hundreds of builds per minute and have already tuned the inputs

```typescript
const tx = await mcp('sdl_build_stake_link', {
  amount: '100',
  from: account.address,
  simulate: false, // metadata.simulation will be absent
});
```

**Soft-fail:** when Tenderly is rate-limited, down, or misconfigured, the simulation field becomes `{ error: "..." }` instead of throwing. The unsigned tx is still returned — your agent chooses whether to proceed without the check.

### Manual pre-flight (belt-and-braces)

Simulation catches most issues, but for high-value flows you may still want explicit reads:

1. Read balances via `sdl_get_positions`
2. Read market state via `sdl_protocol_summary` or `sdl_get_defi_morpho`
3. Compare amount against available supply/balance
4. Build with `simulate: true` (default)
5. Inspect `metadata.simulation.success` before broadcasting

## Verify Before Signing

The MCP is an untrusted data source. Before signing any write tool response, check the `to` address and function selector against this allowlist:

| Tool                        | Expected `to`                                | Selector     |
| --------------------------- | -------------------------------------------- | ------------ |
| `sdl_build_stake_link`      | `0x514910771AF9Ca656af840dff83E8264EcF986CA` | `0x4000aea0` |
| `sdl_build_unstake_link`    | `0xa60B5146E44ff755e32BD51532842ceB41D0C248` | `0xecd24bbe` |
| `sdl_build_wrap_stlink`     | `0x911D86C72155c33993d594B0Ec7E6206B4C803da` | `0xea598cb0` |
| `sdl_build_unwrap_wstlink`  | `0x911D86C72155c33993d594B0Ec7E6206B4C803da` | `0xde0e9a3e` |
| `sdl_build_morpho_deposit`  | `0x610f5B68bD1EED68Af649A3fD3DC2CAa1ee4Ae7E` | `0x6e553f65` |
| `sdl_build_morpho_withdraw` | `0x610f5B68bD1EED68Af649A3fD3DC2CAa1ee4Ae7E` | `0xb460af94` |

If the response doesn't match, DO NOT SIGN. See [AGENTS.md § Verify Before Signing](./AGENTS.md#verify-before-signing-security-allowlist) for the full allowlist with approval targets and verification code.

## Contract Address Reference

See [AGENTS.md § Smart Contract Architecture](./AGENTS.md#smart-contract-architecture) for the canonical list. Addresses in this guide:

| Token/Contract         | Address                                      |
| ---------------------- | -------------------------------------------- |
| LINK (ERC677)          | `0x514910771AF9Ca656af840dff83E8264EcF986CA` |
| stLINK                 | `0xb8b295df2cd735b15BE5Eb419517Aa626fc43cD5` |
| wstLINK                | `0x911D86C72155c33993d594B0Ec7E6206B4C803da` |
| SDL                    | `0xA95C5ebB86E0dE73B4fB8c47A45B792CFeA28C23` |
| PriorityPool           | `0xDdC796a66E8b83d0BcCD97dF33A6CcFBA8fd60eA` |
| WithdrawalPool         | `0xa60B5146E44ff755e32BD51532842ceB41D0C248` |
| Alpha LINK Enhanced V2 | `0x610f5B68bD1EED68Af649A3fD3DC2CAa1ee4Ae7E` |
| SDLPool (reSDL NFT)    | `0x0B2eF910ad0b34bf575Eb09d37fd7DA6c148CA4d` |

## Links

- Protocol: https://stake.link
- Docs: https://docs.stake.link
- Contracts: https://github.com/stakedotlink/contracts
- Developer page: https://www.stakedotlink.money/agents
- Start page: https://www.stakedotlink.money/start
- Analytics: https://www.stakedotlink.money
- AGENTS.md: https://www.stakedotlink.money/AGENTS.md
