From f99730d0bc182f9708176b13187af138706e30d1 Mon Sep 17 00:00:00 2001
From: Youssef
Date: Tue, 12 May 2026 23:24:49 +0100
Subject: [PATCH 1/6] add build-on-base and base-mcp skills
Co-Authored-By: Claude Sonnet 4.6 (1M context)
---
README.md | 9 +
skills/base-mcp/SKILL.md | 53 ++
skills/base-mcp/plugins/morpho.md | 89 ++
skills/base-mcp/references/approval-mode.md | 24 +
skills/base-mcp/references/batch-calls.md | 23 +
skills/base-mcp/references/history.md | 22 +
skills/base-mcp/references/portfolio.md | 21 +
skills/base-mcp/references/send.md | 27 +
skills/base-mcp/references/sign.md | 21 +
skills/base-mcp/references/swap.md | 20 +
skills/base-mcp/references/tokens.md | 24 +
skills/base-mcp/references/wallets.md | 23 +
skills/build-on-base/SKILL.md | 79 ++
.../references/agents/register.md | 174 ++++
.../references/base-account/authentication.md | 234 ++++++
.../references/base-account/capabilities.md | 263 ++++++
.../references/base-account/overview.md | 73 ++
.../references/base-account/payments.md | 225 +++++
.../references/base-account/prolinks.md | 192 +++++
.../references/base-account/sub-accounts.md | 250 ++++++
.../references/base-account/subscriptions.md | 238 ++++++
.../base-account/troubleshooting.md | 146 ++++
.../references/builder-codes/overview.md | 159 ++++
.../references/builder-codes/privy.md | 60 ++
.../references/builder-codes/rpc.md | 117 +++
.../references/builder-codes/smart-wallets.md | 65 ++
.../references/builder-codes/viem.md | 75 ++
.../references/builder-codes/wagmi.md | 96 +++
.../references/deploy-contracts.md | 144 ++++
.../migrations/farcaster-miniapp-to-app.md | 790 ++++++++++++++++++
.../migrations/minikit-to-farcaster/auth.md | 48 ++
.../minikit-to-farcaster/dependencies.md | 54 ++
.../minikit-to-farcaster/examples.md | 202 +++++
.../minikit-to-farcaster/manifest.md | 50 ++
.../minikit-to-farcaster/mapping.md | 452 ++++++++++
.../minikit-to-farcaster/overview.md | 82 ++
.../minikit-to-farcaster/pitfalls.md | 225 +++++
.../minikit-to-farcaster/provider.md | 170 ++++
.../migrations/onchainkit/overview.md | 131 +++
.../migrations/onchainkit/provider.md | 193 +++++
.../migrations/onchainkit/transaction.md | 528 ++++++++++++
.../migrations/onchainkit/troubleshooting.md | 79 ++
.../migrations/onchainkit/wallet.md | 346 ++++++++
skills/build-on-base/references/network.md | 40 +
skills/build-on-base/references/run-node.md | 48 ++
45 files changed, 6384 insertions(+)
create mode 100644 skills/base-mcp/SKILL.md
create mode 100644 skills/base-mcp/plugins/morpho.md
create mode 100644 skills/base-mcp/references/approval-mode.md
create mode 100644 skills/base-mcp/references/batch-calls.md
create mode 100644 skills/base-mcp/references/history.md
create mode 100644 skills/base-mcp/references/portfolio.md
create mode 100644 skills/base-mcp/references/send.md
create mode 100644 skills/base-mcp/references/sign.md
create mode 100644 skills/base-mcp/references/swap.md
create mode 100644 skills/base-mcp/references/tokens.md
create mode 100644 skills/base-mcp/references/wallets.md
create mode 100644 skills/build-on-base/SKILL.md
create mode 100644 skills/build-on-base/references/agents/register.md
create mode 100644 skills/build-on-base/references/base-account/authentication.md
create mode 100644 skills/build-on-base/references/base-account/capabilities.md
create mode 100644 skills/build-on-base/references/base-account/overview.md
create mode 100644 skills/build-on-base/references/base-account/payments.md
create mode 100644 skills/build-on-base/references/base-account/prolinks.md
create mode 100644 skills/build-on-base/references/base-account/sub-accounts.md
create mode 100644 skills/build-on-base/references/base-account/subscriptions.md
create mode 100644 skills/build-on-base/references/base-account/troubleshooting.md
create mode 100644 skills/build-on-base/references/builder-codes/overview.md
create mode 100644 skills/build-on-base/references/builder-codes/privy.md
create mode 100644 skills/build-on-base/references/builder-codes/rpc.md
create mode 100644 skills/build-on-base/references/builder-codes/smart-wallets.md
create mode 100644 skills/build-on-base/references/builder-codes/viem.md
create mode 100644 skills/build-on-base/references/builder-codes/wagmi.md
create mode 100644 skills/build-on-base/references/deploy-contracts.md
create mode 100644 skills/build-on-base/references/migrations/farcaster-miniapp-to-app.md
create mode 100644 skills/build-on-base/references/migrations/minikit-to-farcaster/auth.md
create mode 100644 skills/build-on-base/references/migrations/minikit-to-farcaster/dependencies.md
create mode 100644 skills/build-on-base/references/migrations/minikit-to-farcaster/examples.md
create mode 100644 skills/build-on-base/references/migrations/minikit-to-farcaster/manifest.md
create mode 100644 skills/build-on-base/references/migrations/minikit-to-farcaster/mapping.md
create mode 100644 skills/build-on-base/references/migrations/minikit-to-farcaster/overview.md
create mode 100644 skills/build-on-base/references/migrations/minikit-to-farcaster/pitfalls.md
create mode 100644 skills/build-on-base/references/migrations/minikit-to-farcaster/provider.md
create mode 100644 skills/build-on-base/references/migrations/onchainkit/overview.md
create mode 100644 skills/build-on-base/references/migrations/onchainkit/provider.md
create mode 100644 skills/build-on-base/references/migrations/onchainkit/transaction.md
create mode 100644 skills/build-on-base/references/migrations/onchainkit/troubleshooting.md
create mode 100644 skills/build-on-base/references/migrations/onchainkit/wallet.md
create mode 100644 skills/build-on-base/references/network.md
create mode 100644 skills/build-on-base/references/run-node.md
diff --git a/README.md b/README.md
index d9dcbde..51e50ad 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,15 @@
[](https://github.com/base/base-skills/pulls)
[](https://github.com/base/base-skills/issues)
+## Recommended Skills
+
+Two consolidated skills that cover the most common use cases. Each uses progressive reference loading — the skill loads a single entry point and pulls in detailed references only when needed.
+
+| Skill | Install | Description |
+| ----- | ------- | ----------- |
+| [build-on-base](./skills/build-on-base/SKILL.md) | `npx skills add base/base-skills --skill build-on-base` | Complete Base development playbook: network, contracts, wallet auth, payments, attribution, and migrations. Consolidates all individual skills into one. |
+| [base-mcp](./skills/base-mcp/SKILL.md) | `npx skills add base/base-skills --skill base-mcp` | Base Account MCP server — gives your AI assistant a wallet via mcp.base.org. Tools for sending, swapping, signing, batching calls, and checking balances. Includes Morpho lending plugin. |
+
## Available Skills
| Skill | Description |
diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md
new file mode 100644
index 0000000..067d2e9
--- /dev/null
+++ b/skills/base-mcp/SKILL.md
@@ -0,0 +1,53 @@
+---
+name: base-mcp
+description: >
+ Base Account MCP — gives your AI assistant a wallet via the Base Account MCP server (mcp.base.org).
+ Tools: get_wallets (list wallets), get_portfolio (balances, any address), send (ETH/ERC-20 transfers),
+ swap (token swaps via Coinbase), sign (EIP-712/personal_sign), send_calls (EIP-5792 batch),
+ get_transaction_history (paginated tx history), get_request_status (poll approval), search_tokens (token lookup).
+ Approval mode: send/swap/sign/send_calls require user approval at keys.coinbase.com; response includes approvalUrl + requestId.
+ Plugins: Morpho lending protocol available via plugins/morpho.md.
+---
+
+# Base Account MCP
+
+The Base Account MCP server gives your AI assistant direct access to the user's Base Account (smart wallet) on Base. Once connected at mcp.base.org, 9 tools are available with no additional setup.
+
+## Connection
+
+Server URL: `https://mcp.base.org`
+Auth: OAuth via Coinbase Base Account (user must have a Coinbase account)
+
+## Tool Routing
+
+Read this table first. For the current task, load ONLY the matching reference file — do not preload all references.
+
+| Task | Tool | Reference |
+|------|------|-----------|
+| List wallets / check delegation | `get_wallets` | [references/wallets.md](references/wallets.md) |
+| Check balance / portfolio / token lookup | `get_portfolio`, `search_tokens` | [references/portfolio.md](references/portfolio.md) |
+| Send ETH or ERC-20 | `send` | [references/send.md](references/send.md) |
+| Swap tokens | `swap` | [references/swap.md](references/swap.md) |
+| Sign a message or typed data | `sign` | [references/sign.md](references/sign.md) |
+| Batch contract calls | `send_calls` | [references/batch-calls.md](references/batch-calls.md) |
+| View transaction history | `get_transaction_history` | [references/history.md](references/history.md) |
+| Check pending approval status | `get_request_status` | [references/approval-mode.md](references/approval-mode.md) |
+| Resolve token by symbol | `search_tokens` | [references/tokens.md](references/tokens.md) |
+
+## Approval Mode
+
+All write tools (send, swap, sign, send_calls) operate in approval mode: the transaction is submitted to keys.coinbase.com and the response includes an `approvalUrl` the user must open and a `requestId` for polling. After the user approves, call `get_request_status` with the `requestId` to confirm completion. Load [references/approval-mode.md](references/approval-mode.md) for full details.
+
+## Plugins
+
+Additional protocol capabilities via plugin MCPs:
+
+| Plugin | Protocol | Reference |
+|--------|---------|-----------|
+| Morpho | Lending / vaults on Base | [plugins/morpho.md](plugins/morpho.md) |
+
+## Installation
+
+```bash
+npx skills add base/base-skills --skill base-mcp
+```
diff --git a/skills/base-mcp/plugins/morpho.md b/skills/base-mcp/plugins/morpho.md
new file mode 100644
index 0000000..8e18314
--- /dev/null
+++ b/skills/base-mcp/plugins/morpho.md
@@ -0,0 +1,89 @@
+# Morpho Plugin
+
+Morpho is a lending protocol on Base. The Morpho MCP server prepares lending operations (deposit, borrow, withdraw, repay, supply collateral) which are then executed via Base Account MCP's `send_calls`.
+
+## MCP Server
+
+URL: `https://mcp.morpho.org/`
+
+## Installation (alongside Base Account MCP)
+
+Add both servers to your MCP config:
+
+```json
+{
+ "mcpServers": {
+ "base-account": { "url": "https://mcp.base.org" },
+ "morpho": { "url": "https://mcp.morpho.org/" }
+ }
+}
+```
+
+Claude Code: `claude mcp add morpho --transport http https://mcp.morpho.org/`
+
+## Morpho Tools (17 total)
+
+### Read
+- `morpho_health_check` — server connectivity
+- `morpho_get_supported_chains` — supported chains
+- `morpho_query_vaults` — list vaults with filtering/sorting
+- `morpho_get_vault` — details for a specific vault
+- `morpho_query_markets` — list markets with filtering
+- `morpho_get_market` — details for a specific market
+- `morpho_get_positions` — all positions for an address (all vaults + markets)
+- `morpho_get_token_balance` — token balance and approval state
+
+### Write (prepare_ returns unsigned calls for send_calls)
+- `morpho_prepare_deposit` — prepare vault deposit with approvals
+- `morpho_prepare_withdraw` — prepare vault withdrawal (supports max)
+- `morpho_prepare_supply` — prepare market supply with approvals
+- `morpho_prepare_borrow` — prepare market borrow with health check
+- `morpho_prepare_repay` — prepare market repay (supports max)
+- `morpho_prepare_supply_collateral` — supply collateral to market
+- `morpho_prepare_withdraw_collateral` — withdraw collateral with health check
+
+### Simulate
+- `morpho_simulate_transactions` — simulate with post-state analysis
+
+## Orchestration Pattern
+
+Morpho `prepare_*` tools return unsigned call data. Pass the result to Base Account MCP's `send_calls` to execute.
+
+```
+morpho_prepare_deposit(vaultAddress, amount) → { calls: [...], chainId }
+↓
+send_calls(chainId, calls) → approvalUrl + requestId
+↓
+User approves at keys.coinbase.com
+↓
+get_request_status(requestId) → confirmed
+```
+
+## Example Prompts
+
+```
+Find the best USDC vault on Base by APY and deposit 100 USDC
+```
+1. `morpho_query_vaults` (filter by USDC, sort by APY)
+2. `morpho_prepare_deposit` (selected vault, 100 USDC)
+3. `send_calls` (chainId + calls from prepare_deposit)
+4. Direct user to approvalUrl, poll get_request_status
+
+```
+Show all my Morpho positions on Base
+```
+1. `get_wallets` (get user's address)
+2. `morpho_get_positions` (user's address)
+
+```
+Check if my Morpho borrow position is healthy
+```
+1. `get_wallets` (get address)
+2. `morpho_get_positions` (address)
+3. Report health factor from position data
+
+## Important Notes
+
+- Morpho `prepare_*` tools simulate before returning — review simulation output before calling `send_calls`
+- Always use `morpho_simulate_transactions` for novel or large operations
+- Morpho operates on Base mainnet; check `morpho_get_supported_chains` for current list
diff --git a/skills/base-mcp/references/approval-mode.md b/skills/base-mcp/references/approval-mode.md
new file mode 100644
index 0000000..7bcfcba
--- /dev/null
+++ b/skills/base-mcp/references/approval-mode.md
@@ -0,0 +1,24 @@
+# Approval Mode
+
+All write tools (send, swap, sign, send_calls) operate in approval mode. The user must manually approve every transaction at keys.coinbase.com.
+
+## Flow
+
+1. **Call the write tool** (send, swap, sign, or send_calls)
+2. **Response includes**:
+ - `approvalUrl` — URL the user must open to approve
+ - `requestId` — ID to poll for completion
+3. **Direct the user** to open `approvalUrl` immediately. Say: "Please open this link to approve the transaction: [approvalUrl]"
+4. **After user confirms they approved**, call `get_request_status` with the `requestId`
+5. **Only report success** when `get_request_status` returns a completed/confirmed status
+
+## get_request_status parameters
+- `requestId` — the ID from the write tool response (required)
+
+## Common mistakes
+- Do NOT report success before calling `get_request_status` — the user may not have approved yet
+- Do NOT skip showing the `approvalUrl` — the transaction cannot complete without user action
+- Do NOT poll `get_request_status` in a tight loop — call once after user confirms they approved
+
+## When approval is NOT needed
+Agent wallets marked `inSession: true` (from `get_wallets`) can transact without approval in M2 mode. The `agentWalletId` parameter on send/swap enables this.
diff --git a/skills/base-mcp/references/batch-calls.md b/skills/base-mcp/references/batch-calls.md
new file mode 100644
index 0000000..0f52cfb
--- /dev/null
+++ b/skills/base-mcp/references/batch-calls.md
@@ -0,0 +1,23 @@
+# send_calls
+
+Submit a batch of EIP-5792 wallet_sendCalls for user approval. Use for arbitrary contract interactions, multi-step transactions, or batched operations.
+
+## When to use
+- Protocol interactions not covered by send/swap (e.g. DeFi, NFT mints, approvals)
+- Batching multiple operations into one user approval
+- Morpho plugin: Morpho prepares `prepare_*` calls → pass the raw calls array to `send_calls`
+
+## Required parameters
+- `chainId` — hex chain ID with 0x prefix (`0x2105` for Base mainnet, `0x14a34` for Base Sepolia)
+- `calls` — array of call objects, each with:
+ - `to` — target address (0x-prefixed, required)
+ - `value` — hex ETH in wei (e.g. `0x0`), optional
+ - `data` — calldata hex (e.g. `0xa9059cbb...`), optional
+
+## Approval mode flow
+Same as send: get `approvalUrl` + `requestId`, direct user to URL, poll `get_request_status`.
+
+## Common use case with Morpho plugin
+1. Morpho `prepare_deposit` (or other prepare_* tool) returns `calls` array
+2. Pass that array directly to `send_calls` with the appropriate `chainId`
+3. Direct user to `approvalUrl` for signing
diff --git a/skills/base-mcp/references/history.md b/skills/base-mcp/references/history.md
new file mode 100644
index 0000000..bbe351d
--- /dev/null
+++ b/skills/base-mcp/references/history.md
@@ -0,0 +1,22 @@
+# get_transaction_history
+
+Returns paginated transaction history for any wallet address in reverse chronological order. Onchain data is public — any address can be queried.
+
+## When to use
+- "Show my recent transactions", "What did I last do?", "Show my USDC sends"
+- Investigating past activity for any address
+
+## Parameters
+- `address` — optional; defaults to session's agent wallet
+- `chain` — optional: `base` or `ethereum` (defaults to base)
+- `asset` — optional symbol filter (e.g. `USDC`, `ETH`)
+- `limit` — 1–200, defaults to 50
+- `cursor` — pagination cursor from previous response's `nextCursor`
+
+## Return fields (per transaction)
+- Transfer details, type classification, fees, USD values at time of transaction
+- `hasMore` — whether more pages exist; continue paginating while `true`
+
+## Key patterns
+- Date range filtering is not supported — paginate to find transactions in a specific period
+- Use `asset` filter to narrow results to a specific token
diff --git a/skills/base-mcp/references/portfolio.md b/skills/base-mcp/references/portfolio.md
new file mode 100644
index 0000000..20899b0
--- /dev/null
+++ b/skills/base-mcp/references/portfolio.md
@@ -0,0 +1,21 @@
+# get_portfolio
+
+Returns portfolio value and per-asset breakdown for any wallet address. Onchain data is public — any address can be queried.
+
+## When to use
+- "What's my balance?", "How much USDC do I have?", "Show me my portfolio"
+- Querying any wallet address's holdings (not just the user's)
+
+## Parameters
+- `address` — optional; defaults to session's agent wallet
+- `chain` — optional filter: `base` or `ethereum`
+- `query` — optional search filter (e.g. "USDC", "ETH")
+- `limit` — max assets to return (default 20)
+- `offset` — pagination offset
+- `includePnl` — include unrealized/realized P&L (default false)
+
+## Key patterns
+- For "my balance" → call without address to get the session wallet
+- For "balance of 0x..." → pass the address parameter
+- Use `query` to filter to a specific token before displaying
+- For tokens not found by `get_portfolio`, use `search_tokens` first to resolve the contract address
diff --git a/skills/base-mcp/references/send.md b/skills/base-mcp/references/send.md
new file mode 100644
index 0000000..8c2548f
--- /dev/null
+++ b/skills/base-mcp/references/send.md
@@ -0,0 +1,27 @@
+# send
+
+Send native ETH or any ERC-20 token to an address. Operates in approval mode: the response includes an `approvalUrl` and `requestId`.
+
+## When to use
+- "Send X to Y", "Transfer X USDC to...", "Pay X ETH to..."
+
+## Required parameters
+- `recipient` — 0x address, ENS name, basename (e.g. `vitalik.eth`), cb.id name, or wallet username
+- `amount` — human-readable decimal (e.g. "1.5")
+- `asset` — symbol (`ETH`, `USDC`) or ERC-20 contract address
+- `chain` — `base` or `base-sepolia`
+
+## Optional parameters
+- `decimals` — required when `asset` is a contract address (not a symbol)
+- `agentWalletId` — scope to a specific agent wallet (M2 mode only)
+
+## Approval mode flow
+1. Call `send` → get `approvalUrl` + `requestId`
+2. Show the user: "Please approve this transaction: [approvalUrl]"
+3. After user confirms, call `get_request_status` with `requestId`
+4. Only report success when status is confirmed
+
+## Key patterns
+- For unknown tokens, call `search_tokens` first to get the contract address and decimals
+- Never report success before `get_request_status` confirms completion
+- Use basenames/ENS for recipient when provided — no need to resolve first
diff --git a/skills/base-mcp/references/sign.md b/skills/base-mcp/references/sign.md
new file mode 100644
index 0000000..76ce6d1
--- /dev/null
+++ b/skills/base-mcp/references/sign.md
@@ -0,0 +1,21 @@
+# sign
+
+Request a user-approved signature from the Base Account. Supports EIP-712 typed data and personal_sign. Operates in approval mode.
+
+## When to use
+- "Sign this message", "Sign this typed data", agent needs a signature for authentication
+
+## Required parameters
+- `type` — `0x01` for EIP-712 typed data, `0x45` for personal_sign
+- `data`:
+ - For `0x01`: EIP-712 TypedData object with `primaryType`, `types`, `domain`, `message`
+ - For `0x45`: object with a `message` string field
+
+## Approval mode flow
+1. Call `sign` → get `approvalUrl` + `requestId`
+2. Direct user to `approvalUrl`
+3. Poll `get_request_status` to retrieve the signature after approval
+
+## Key patterns
+- Use `0x45` for simple text messages (e.g. SIWE, auth challenges)
+- Use `0x01` for structured typed data (e.g. permit signatures, EIP-712 auth)
diff --git a/skills/base-mcp/references/swap.md b/skills/base-mcp/references/swap.md
new file mode 100644
index 0000000..f79201d
--- /dev/null
+++ b/skills/base-mcp/references/swap.md
@@ -0,0 +1,20 @@
+# swap
+
+Swap between two tokens via the Coinbase swap service. Only supported on mainnet chains (not testnets). Operates in approval mode.
+
+## When to use
+- "Swap X for Y", "Buy X ETH with USDC", "Trade X to Y"
+
+## Required parameters
+- `fromAsset` — symbol (ETH, USDC) or contract address
+- `toAsset` — symbol or contract address
+- `amount` — human-readable decimal amount of `fromAsset`
+- `chain` — target chain (e.g. `base`); testnets not supported
+
+## Approval mode flow
+Same as send: get `approvalUrl` + `requestId`, direct user to URL, poll `get_request_status`.
+
+## Key patterns
+- For unknown tokens, call `search_tokens` first to resolve contract address
+- Testnets are not supported — if user requests a testnet swap, explain this
+- Never report success before `get_request_status` confirms completion
diff --git a/skills/base-mcp/references/tokens.md b/skills/base-mcp/references/tokens.md
new file mode 100644
index 0000000..3ec0397
--- /dev/null
+++ b/skills/base-mcp/references/tokens.md
@@ -0,0 +1,24 @@
+# search_tokens
+
+Search for token metadata by symbol or name. Returns contract address, decimals, and chain info needed to use a token with send/swap.
+
+## When to use
+- Before calling `send` with a non-standard token (not ETH or USDC) — need contract address + decimals
+- User references a token by name/symbol and you need to resolve it
+- Verifying a token exists on a specific chain
+
+## Parameters
+- `query` — required; token symbol or name (e.g. `USDC`, `uniswap`, `WETH`)
+- `chain` — optional; `base` or `base-sepolia`
+
+## Return fields (per result)
+- `name`, `symbol` — display info
+- `address` — ERC-20 contract address
+- `decimals` — needed when passing a contract address to send
+- `imageUrl` — token logo
+- `chain` — which chain this token is on
+
+## Key patterns
+- Always pass the returned `address` AND `decimals` to `send` when using a contract address
+- For common tokens (ETH, USDC), you can pass the symbol directly to send/swap — no lookup needed
+- If multiple results, prefer the one on `base` mainnet unless user specified otherwise
diff --git a/skills/base-mcp/references/wallets.md b/skills/base-mcp/references/wallets.md
new file mode 100644
index 0000000..5cdad29
--- /dev/null
+++ b/skills/base-mcp/references/wallets.md
@@ -0,0 +1,23 @@
+# get_wallets
+
+Returns all wallets in the user's wallet group: the Base Account (primary) plus any agent wallets.
+
+## When to use
+- User asks "show me my wallets", "what wallets do I have", "which wallet is active"
+- You need to know if an agent wallet is authorized before a transactional call
+
+## Parameters
+None.
+
+## Return fields (per wallet)
+- `id` — wallet ID
+- `type` — `base-account` or `agent-wallet`
+- `address` — 0x address
+- `inSession` — boolean; only `true` wallets can be used with transactional tools
+- `delegationStatus` — whether the agent wallet has delegated authority from the Base Account
+- `spendPolicy` — summary of spend limits (agent wallets only)
+
+## Key patterns
+- If no wallet is `inSession: true`, all transactional tools will use approval mode (keys.coinbase.com)
+- Agent wallets with `inSession: true` can transact without manual approval (M2 mode)
+- Always check `inSession` before deciding whether approval will be required
diff --git a/skills/build-on-base/SKILL.md b/skills/build-on-base/SKILL.md
new file mode 100644
index 0000000..5ffe5d7
--- /dev/null
+++ b/skills/build-on-base/SKILL.md
@@ -0,0 +1,79 @@
+---
+name: build-on-base
+description: >
+ Complete Base development playbook. Covers: (1) Network — Base RPC URLs, chain IDs (8453/84532),
+ explorer config, testnet setup, connect to Base, Base Sepolia; (2) Contracts — Foundry deployment,
+ forge create, BaseScan verification, CDP faucet, testnet ETH, deploy contract to Base;
+ (3) Builder Codes — ERC-8021 attribution suffix, referral fees, dataSuffix for Wagmi/Viem/Privy/
+ ethers.js/window.ethereum, transaction attribution, earn referral fees, append builder code;
+ (4) Base Account SDK — Sign in with Base (SIWB), Base Pay, USDC payments, paymasters, gas
+ sponsorship, sub-accounts, spend permissions, prolinks, batch transactions, smart wallet,
+ payment link, recurring subscription; (5) Agent registration — trading bots, AI agents, automated
+ senders, ERC-8021 attribution wiring, base.dev API, register agent, builder code registration;
+ (6) Node operation — run Base node, Reth setup, hardware requirements, self-hosted RPC, sync;
+ (7) Migrations — migrate OnchainKit, OnchainKitProvider to WagmiProvider, wagmi migration,
+ remove onchainkit dependency, MiniKit to Farcaster SDK, convert miniapp, Farcaster miniapp to
+ regular app, convert Farcaster miniapp.
+---
+
+# Base Development
+
+Complete playbook for building on Base L2 — network setup, smart contracts, wallet auth, payments,
+developer tool attribution, and framework migrations.
+
+## Default Stack
+
+| Layer | Default |
+|-------|---------|
+| Network | Base Mainnet (8453) / Base Sepolia testnet (84532) |
+| Contracts | Foundry (`forge create` + BaseScan verification) |
+| Wallet auth | Base Account SDK (`@base-org/account`) |
+| Payments | Base Pay — USDC, gasless, settles in <2s |
+| Transactions | wagmi + viem |
+| Attribution | Builder Codes — ERC-8021 via `ox/erc8021` |
+| RPC (prod) | Dedicated node provider or self-hosted Reth |
+
+## Safety Guardrails
+
+- **Never commit private keys** — use `cast wallet import` for Foundry keystores
+- **Never expose RPC API keys or CDP credentials client-side** — proxy through backend
+- **Never skip server-side payment verification** — always call `getPaymentStatus()` server-side and verify `sender`, `amount`, `recipient`; track processed tx IDs to prevent replay attacks
+- **Never send transactions without Builder Code attribution** — silent data loss, no errors, no warnings
+- **Validate all user-provided shell inputs** before constructing forge/cast commands (no spaces, semicolons, pipes)
+- **COOP headers for Base Account popups** — use `same-origin-allow-popups`, not `same-origin`
+
+## Task Routing
+
+Read the reference for your task:
+
+| Task | When to Use | Reference |
+|------|-------------|-----------|
+| **Network config** | RPC URLs, chain IDs, explorer links, testnet setup | [references/network.md](references/network.md) |
+| **Deploy contracts** | Foundry deployment, BaseScan verification, faucet | [references/deploy-contracts.md](references/deploy-contracts.md) |
+| **Run a Base node** | Self-hosted RPC, Reth, hardware requirements | [references/run-node.md](references/run-node.md) |
+| **Builder Codes** | Add ERC-8021 attribution to transactions | [references/builder-codes/overview.md](references/builder-codes/overview.md) |
+| **Base Account SDK** | SIWB, Base Pay, subscriptions, sub-accounts | [references/base-account/overview.md](references/base-account/overview.md) |
+| **Register AI agent/bot** | Register wallet, get builder code, wire attribution | [references/agents/register.md](references/agents/register.md) |
+| **Migrate from OnchainKit** | OnchainKitProvider → wagmi, wallet/tx components | [references/migrations/onchainkit/overview.md](references/migrations/onchainkit/overview.md) |
+| **MiniKit → Farcaster SDK** | `@coinbase/onchainkit/minikit` → `@farcaster/miniapp-sdk` | [references/migrations/minikit-to-farcaster/overview.md](references/migrations/minikit-to-farcaster/overview.md) |
+| **Farcaster miniapp → regular app** | Remove Mini App host coupling, convert to Base/web app | [references/migrations/farcaster-miniapp-to-app.md](references/migrations/farcaster-miniapp-to-app.md) |
+
+## Operating Procedure
+
+1. **Classify the task** using the table above
+2. **Read the relevant reference** before implementing
+3. **Confirm the framework** with the user when multiple options exist (e.g., Privy vs wagmi for Builder Codes)
+4. **Implement** with explicit chain ID, security requirements, and all required validations
+5. **Deliver** diffs, install commands, and any manual steps (env vars, API key setup, wallet registration)
+
+## For Edge Cases and Latest API Changes
+
+- **AI-optimized docs**: [docs.base.org/llms.txt](https://docs.base.org/llms.txt)
+- **Base Account reference**: [docs.base.org/base-account](https://docs.base.org/base-account)
+- **Base chain docs**: [docs.base.org](https://docs.base.org)
+
+## Installation
+
+```bash
+npx skills add base/base-skills --skill build-on-base
+```
diff --git a/skills/build-on-base/references/agents/register.md b/skills/build-on-base/references/agents/register.md
new file mode 100644
index 0000000..2bf11d1
--- /dev/null
+++ b/skills/build-on-base/references/agents/register.md
@@ -0,0 +1,174 @@
+# Base Builder Code Registration
+
+This reference registers an agent with Base and shows how to attach builder code attribution to transactions. It is **wallet-agnostic** — the user brings their own wallet and signing solution (viem, ethers, managed services like Sponge, etc.). This reference only handles registration and attribution.
+
+## Check if already registered
+
+Before doing anything, check whether registration has already happened:
+
+1. Look for a `builderCode.ts` file in the project (check `src/constants/builderCode.ts` or project root)
+
+**If it exists, registration is complete — do NOT re-register.** Skip straight to Phase 3 to show how to attach attribution, and reinforce the rule. Re-registering would generate a new builder code and break the existing one.
+
+**If it's missing**, proceed with the full registration flow below.
+
+---
+
+## Phase 1 — Wallet
+
+Every agent needs a wallet to sign transactions. Ask the user before doing anything else.
+
+1. **Ask: "Do you have a wallet? If yes, share your wallet address."**
+2. **If yes** — take the wallet address they provide and move to Phase 2.
+3. **If no** — direct them to the Base wallet setup guide: https://docs.base.org/ai-agents/guides/wallet-setup — do not proceed until they have a wallet and can provide their address.
+
+---
+
+## Phase 2 — Registration
+
+Register the wallet with the Base builder code API. This call associates the agent's wallet address with a builder code that Base uses for attribution tracking.
+
+Use the bundled `skill/scripts/register.sh`. It handles errors and extracts the builder code from the response:
+
+```bash
+BUILDER_CODE=$(bash skill/scripts/register.sh "")
+```
+
+Or call the API directly:
+
+```bash
+curl -X POST https://api.base.dev/v1/agents/builder-codes \
+ -H "Content-Type: application/json" \
+ -d '{"wallet_address": ""}'
+```
+
+The API returns a response like:
+
+```json
+{
+ "builder_code": "bc_a1b2c3d4",
+ "wallet_address": "0x...",
+ "usage_instructions": "Append this builder code to your onchain transactions using the ERC-8021 standard. See: https://docs.base.org/base-chain/quickstart/builder-codes"
+}
+```
+
+Extract the `builder_code` value from the response and write it to a constants file:
+
+```typescript
+// src/constants/builderCode.ts
+export const BUILDER_CODE = "bc_a1b2c3d4"
+```
+
+Use `src/constants/builderCode.ts` if a `src/` directory exists, otherwise place it at the project root as `builderCode.ts`.
+
+If `builderCode.ts` already exists, do not call this API — the agent is already registered.
+
+---
+
+## Phase 3 — Attribution Setup & Documentation
+
+The builder code from Phase 2 (the `bc_...` value now in `builderCode.ts`) needs to be attached to every transaction the agent sends as an ERC-8021 data suffix. This phase wires that in and writes an `AGENT_README.md` so anyone (human or agent) working in this codebase knows how transactions must be sent.
+
+First, install the attribution utility if not already present:
+
+```bash
+npm i ox
+```
+
+Convert the builder code into a data suffix. Import `BUILDER_CODE` from the constants file written in Phase 2 — this is not generating a new code, it is encoding the existing one into the ERC-8021 byte format:
+
+```typescript
+import { Attribution } from "ox/erc8021"
+import { BUILDER_CODE } from "./constants/builderCode"
+
+// BUILDER_CODE is the builder_code value from the Phase 2 API response (e.g. "bc_a1b2c3d4")
+const DATA_SUFFIX = Attribution.toDataSuffix({
+ codes: [BUILDER_CODE],
+})
+```
+
+### Wiring attribution into the transaction flow
+
+How you attach the suffix depends on the signing setup. Ask the user which they use, then follow the matching option:
+
+**Option A: viem (self-custodied wallet)**
+
+Add `dataSuffix` to the wallet client — every transaction automatically carries it:
+
+```typescript
+import { createWalletClient, http } from "viem"
+import { base } from "viem/chains"
+import { privateKeyToAccount } from "viem/accounts"
+import { Attribution } from "ox/erc8021"
+import { BUILDER_CODE } from "./constants/builderCode"
+
+const DATA_SUFFIX = Attribution.toDataSuffix({
+ codes: [BUILDER_CODE],
+})
+
+const account = privateKeyToAccount(process.env.PRIVATE_KEY! as `0x${string}`)
+
+export const walletClient = createWalletClient({
+ account,
+ chain: base,
+ transport: http(),
+ dataSuffix: DATA_SUFFIX,
+})
+```
+
+**Option B: ethers.js (self-custodied wallet)**
+
+Append the data suffix to each transaction's `data` field:
+
+```typescript
+import { ethers } from "ethers"
+import { Attribution } from "ox/erc8021"
+import { BUILDER_CODE } from "./constants/builderCode"
+
+const DATA_SUFFIX = Attribution.toDataSuffix({
+ codes: [BUILDER_CODE],
+})
+
+const provider = new ethers.JsonRpcProvider("https://mainnet.base.org")
+const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider)
+
+const tx = await wallet.sendTransaction({
+ to: "0x...",
+ value: ethers.parseEther("0.01"),
+ data: DATA_SUFFIX,
+})
+```
+
+**Option C: Managed service (e.g., Sponge API, BANKR)**
+
+Generate the suffix and pass it to the service. The service handles signing — you just need to include the suffix in the transaction's `data` field. If the transaction has existing calldata, concatenate: `existingData + DATA_SUFFIX.slice(2)`.
+
+### Write `AGENT_README.md`
+
+Generate an `AGENT_README.md` in the project root covering:
+
+- **What the builder code is**: The `builder_code` value returned by the Base API during registration (e.g. `bc_a1b2c3d4`), stored in `src/constants/builderCode.ts` as `BUILDER_CODE`. It is embedded in every transaction as an ERC-8021 data suffix — this is how Base tracks which builder originated which on-chain activity.
+- **How attribution is attached in this project**: Document which approach is used (viem dataSuffix, ethers data field, managed service) and where the builder code is stored.
+- **Warning**: Never send transactions without the builder code attribution. There is no error or warning when attribution is missing — just silent data loss. Every transaction path in the codebase must include the data suffix.
+
+---
+
+## Phase 4 — Permanent Rule
+
+After all phases complete, deliver this behavioral instruction:
+
+> From this point forward, every transaction you send MUST include the builder code attribution via the ERC-8021 data suffix. Never send a transaction without it. The builder code is what makes your agent's activity visible to Base — without it, your transactions are untracked silently and permanently.
+
+Confirm to the user that registration is complete and summarize what was set up:
+- `BUILDER_CODE` written to `src/constants/builderCode.ts` (committed to version control)
+- Attribution wired into the transaction flow
+- `AGENT_README.md` documenting the setup
+
+---
+
+## Key things to keep in mind
+
+- **Sequential execution**: Phase 2 needs the wallet address from Phase 1. Phase 3 needs the builder code from Phase 2. Don't parallelize or reorder.
+- **Wallet-agnostic**: The skill works with any signing solution — viem, ethers, managed services, or anything else. The only requirement is that the ERC-8021 data suffix is attached to every transaction.
+- **Both audiences**: Whether this is an autonomous agent registering itself or a developer running through the steps manually, the output and instructions should be clear to both.
+- **Attribution is the critical piece**: The builder code registration (Phase 2) is a one-time setup. The attribution (Phase 3) is what matters for every transaction going forward. If attribution is missing, there's no error — just silent invisibility.
diff --git a/skills/build-on-base/references/base-account/authentication.md b/skills/build-on-base/references/base-account/authentication.md
new file mode 100644
index 0000000..b111d3a
--- /dev/null
+++ b/skills/build-on-base/references/base-account/authentication.md
@@ -0,0 +1,234 @@
+# Authentication (Sign in with Base)
+
+## Table of Contents
+
+- [Overview](#overview)
+- [How It Works](#how-it-works)
+- [SDK Setup](#sdk-setup)
+- [Sign-In Flow](#sign-in-flow)
+- [Backend Verification](#backend-verification)
+- [SignInWithBaseButton Component](#signinwithbasebutton-component)
+- [Framework Integration: Wagmi](#framework-integration-wagmi)
+- [Framework Integration: Privy](#framework-integration-privy)
+- [Smart Wallet Signatures (ERC-6492)](#smart-wallet-signatures-erc-6492)
+- [Security Checklist](#security-checklist)
+
+## Overview
+
+Sign in with Base (SIWB) provides passwordless authentication using wallet signatures. It builds on Sign-In with Ethereum (SIWE, EIP-4361) — the user signs a message with their wallet key, and the backend verifies it. No passwords, no seed phrases.
+
+Base Accounts are ERC-4337 smart wallets. Unlike traditional wallets (EOAs), the user's key is a passkey — the wallet contract verifies signatures via `isValidSignature` (EIP-1271). Viem handles this automatically.
+
+## How It Works
+
+1. Generate a nonce **before** the user clicks sign-in (avoids popup blockers)
+2. Call `wallet_connect` with the `signInWithEthereum` capability
+3. User approves in the Base Account popup (`keys.coinbase.com`)
+4. SDK returns `{ address, message, signature }`
+5. Send `message` + `signature` to your backend
+6. Backend verifies with viem and creates a session/JWT
+
+## SDK Setup
+
+```bash
+npm install @base-org/account @base-org/account-ui
+```
+
+```typescript
+import { createBaseAccountSDK } from '@base-org/account';
+
+const sdk = createBaseAccountSDK({
+ appName: 'My App',
+ appLogoUrl: 'https://example.com/logo.png',
+ appChainIds: [8453],
+});
+
+const provider = sdk.getProvider();
+```
+
+`createBaseAccountSDK` parameters:
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `appName` | `string` | No | App name shown in wallet UI (default: `"App"`) |
+| `appLogoUrl` | `string` | No | Logo URL for wallet UI |
+| `appChainIds` | `number[]` | No | Supported chain IDs |
+| `paymasterUrls` | `Record` | No | Chain ID to paymaster URL mapping |
+
+## Sign-In Flow
+
+```typescript
+const nonce = crypto.randomUUID().replace(/-/g, '');
+
+const { accounts } = await provider.request({
+ method: 'wallet_connect',
+ params: [{
+ version: '1',
+ capabilities: {
+ signInWithEthereum: {
+ nonce,
+ chainId: '0x2105', // Base Mainnet (8453)
+ },
+ },
+ }],
+});
+
+const { address } = accounts[0];
+const { message, signature } = accounts[0].capabilities.signInWithEthereum;
+```
+
+`signInWithEthereum` capability parameters:
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `nonce` | `string` | Yes | Unique random string per auth attempt |
+| `chainId` | `string` | Yes | Hex chain ID (`"0x2105"` = Base Mainnet 8453) |
+
+Response shape:
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `accounts[0].address` | `string` | User's wallet address |
+| `accounts[0].capabilities.signInWithEthereum.message` | `string` | SIWE-formatted message |
+| `accounts[0].capabilities.signInWithEthereum.signature` | `string` | Cryptographic signature |
+
+### Fallback for Non-Base Wallets
+
+Not every wallet supports `wallet_connect`. Fall back to `eth_requestAccounts` + `personal_sign`:
+
+```typescript
+try {
+ const { accounts } = await provider.request({
+ method: 'wallet_connect',
+ params: [{ version: '1', capabilities: { signInWithEthereum: { nonce, chainId: '0x2105' } } }],
+ });
+ // use accounts[0].capabilities.signInWithEthereum
+} catch (err) {
+ if (err.code === 4100) {
+ const [address] = await provider.request({ method: 'eth_requestAccounts' });
+ const signature = await provider.request({
+ method: 'personal_sign',
+ params: [siweMessage, address],
+ });
+ }
+}
+```
+
+## Backend Verification
+
+Use viem to verify the signature. It handles both EOA and smart wallet (EIP-1271/ERC-6492) signatures automatically.
+
+```typescript
+import { createPublicClient, http } from 'viem';
+import { base } from 'viem/chains';
+
+const client = createPublicClient({ chain: base, transport: http() });
+
+const valid = await client.verifyMessage({
+ address,
+ message,
+ signature,
+});
+```
+
+### Full Express Server Example
+
+```typescript
+import express from 'express';
+import { createPublicClient, http } from 'viem';
+import { base } from 'viem/chains';
+
+const app = express();
+const client = createPublicClient({ chain: base, transport: http() });
+const usedNonces = new Set();
+
+app.get('/auth/nonce', (req, res) => {
+ const nonce = crypto.randomUUID().replace(/-/g, '');
+ res.json({ nonce });
+});
+
+app.post('/auth/verify', async (req, res) => {
+ const { address, message, signature } = req.body;
+ const nonceMatch = message.match(/Nonce: (\w+)/);
+ if (!nonceMatch || usedNonces.has(nonceMatch[1])) {
+ return res.status(401).json({ error: 'Invalid or reused nonce' });
+ }
+
+ const valid = await client.verifyMessage({ address, message, signature });
+ if (!valid) return res.status(401).json({ error: 'Invalid signature' });
+
+ usedNonces.add(nonceMatch[1]);
+ // Create session/JWT here
+ res.json({ success: true, address });
+});
+```
+
+## SignInWithBaseButton Component
+
+Pre-built React button from `@base-org/account-ui`.
+
+```tsx
+import { SignInWithBaseButton } from '@base-org/account-ui/react';
+
+
+```
+
+| Prop | Type | Values | Default |
+|------|------|--------|---------|
+| `align` | `string` | `'left'`, `'center'`, `'right'` | `'center'` |
+| `variant` | `string` | `'solid'`, `'transparent'` | `'solid'` |
+| `colorScheme` | `string` | `'light'`, `'dark'`, `'system'` | `'light'` |
+| `size` | `string` | `'small'`, `'medium'`, `'large'` | `'medium'` |
+| `disabled` | `boolean` | — | `false` |
+| `onClick` | `() => void` | — | — |
+| `onSignInResult` | `(result) => void` | — | — |
+
+Follow the [Brand Guidelines](https://docs.base.org/base-account/reference/ui-elements/brand-guidelines): use Base blue (`#0000FF`) on light backgrounds, all-white lockup on dark backgrounds. Do not modify the Base Square color or corner radius.
+
+## Framework Integration: Wagmi
+
+```typescript
+import { createConfig, http } from 'wagmi';
+import { base } from 'wagmi/chains';
+import { createBaseAccountSDK } from '@base-org/account';
+import { custom } from 'viem';
+
+const sdk = createBaseAccountSDK({
+ appName: 'My App',
+ appLogoUrl: 'https://example.com/logo.png',
+ appChainIds: [8453],
+});
+
+const config = createConfig({
+ chains: [base],
+ transports: {
+ [base.id]: custom(sdk.getProvider()),
+ },
+});
+```
+
+Then use wagmi hooks (`useConnect`, `useAccount`, `useSignMessage`) as usual.
+
+## Framework Integration: Privy
+
+Privy has day-1 Base Account support. Configure it as a wallet connector — see [Privy docs](https://docs.privy.io/) for the latest integration guide. Base Account appears as a wallet option in the Privy modal.
+
+## Smart Wallet Signatures (ERC-6492)
+
+Base Accounts may not be deployed onchain until the user's first transaction. Signatures from undeployed wallets include an ERC-6492 wrapper that lets verifiers deploy the contract in a simulation to validate the signature.
+
+**You don't need to do anything special** — viem's `verifyMessage` and `verifyTypedData` handle ERC-6492 automatically. Just make sure you're using viem for verification.
+
+## Security Checklist
+
+- Generate nonces **before** the user clicks sign-in (avoids popup blockers)
+- Track used nonces server-side — reject any reused nonce
+- Verify signatures on your backend, never trust the frontend alone
+- Use `Cross-Origin-Opener-Policy: same-origin-allow-popups` (NOT `same-origin`, which breaks the popup)
+- Set appropriate session/JWT expiry times
+- Include `chainId` in verification to prevent cross-chain replay
diff --git a/skills/build-on-base/references/base-account/capabilities.md b/skills/build-on-base/references/base-account/capabilities.md
new file mode 100644
index 0000000..0b26b21
--- /dev/null
+++ b/skills/build-on-base/references/base-account/capabilities.md
@@ -0,0 +1,263 @@
+# Capabilities & Batch Transactions
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Discovering Capabilities](#discovering-capabilities)
+- [wallet_sendCalls](#wallet_sendcalls)
+- [wallet_getCallsStatus](#wallet_getcallsstatus)
+- [Capability: paymasterService](#capability-paymasterservice)
+- [Capability: auxiliaryFunds](#capability-auxiliaryfunds)
+- [Capability: atomic](#capability-atomic)
+- [Capability: flowControl](#capability-flowcontrol)
+- [Capability: dataCallback](#capability-datacallback)
+- [Capability: dataSuffix (Attribution)](#capability-datasuffix-attribution)
+
+## Overview
+
+Capabilities are chain-specific feature flags that describe what a wallet supports. They're discovered via `wallet_getCapabilities` and used in `wallet_connect` and `wallet_sendCalls` calls.
+
+Base Account (a smart wallet) supports capabilities that traditional wallets (EOAs) cannot: atomic batching, gas sponsorship, auxiliary funds, etc.
+
+## Discovering Capabilities
+
+```typescript
+const capabilities = await provider.request({
+ method: 'wallet_getCapabilities',
+ params: [userAddress],
+});
+
+const baseCapabilities = capabilities['0x2105']; // Base Mainnet
+```
+
+Response structure (keyed by hex chain ID):
+
+```typescript
+{
+ "0x2105": {
+ auxiliaryFunds: { supported: true },
+ atomic: { supported: "supported" },
+ paymasterService: { supported: true },
+ flowControl: { supported: false },
+ datacallback: { supported: false },
+ }
+}
+```
+
+Use this to conditionally enable features:
+
+```typescript
+const hasPaymaster = !!baseCapabilities.paymasterService?.supported;
+const hasAuxFunds = baseCapabilities.auxiliaryFunds?.supported || false;
+const hasAtomicBatch = baseCapabilities.atomic?.supported === 'supported';
+```
+
+## wallet_sendCalls
+
+**Spec: EIP-5792.** Submits a batch of calls to the wallet for execution.
+
+```typescript
+const { batchId } = await provider.request({
+ method: 'wallet_sendCalls',
+ params: [{
+ version: '2.0.0',
+ from: userAddress,
+ chainId: '0x2105',
+ atomicRequired: true,
+ calls: [
+ { to: '0xTokenAddress', data: '0xapproveCalldata', value: '0x0' },
+ { to: '0xDexAddress', data: '0xswapCalldata', value: '0x0' },
+ ],
+ capabilities: {
+ paymasterService: { url: 'https://your-paymaster.xyz' },
+ },
+ }],
+});
+```
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `version` | `string` | Yes | Must be `"2.0.0"` |
+| `from` | `string` | Yes | Sender address |
+| `chainId` | `string` | Yes | Hex chain ID |
+| `atomicRequired` | `boolean` | Yes | Require all-or-nothing execution |
+| `calls` | `Call[]` | Yes | Array of `{ to, value, data? }` |
+| `capabilities` | `object` | No | Capability config |
+
+Returns: `{ batchId, status }`
+
+Error codes:
+
+| Code | Meaning |
+|------|---------|
+| `4001` | User rejected |
+| `5700` | Missing required capability |
+| `5720` | Duplicate batch ID |
+| `5740` | Batch too large |
+
+## wallet_getCallsStatus
+
+Check the status of a batch submitted via `wallet_sendCalls`.
+
+```typescript
+const result = await provider.request({
+ method: 'wallet_getCallsStatus',
+ params: [batchId],
+});
+```
+
+Status codes:
+
+| Code | Meaning |
+|------|---------|
+| `100` | Pending — received, not yet onchain |
+| `200` | Success — included onchain, no reverts |
+| `400` | Offchain failure — wallet will not retry |
+| `500` | Chain failure — batch reverted |
+| `600` | Partial failure — some changes may be onchain |
+
+Returns: `{ version, chainId, id, status, atomic, receipts, capabilities }`
+
+Polling pattern:
+
+```typescript
+async function waitForBatch(batchId: string) {
+ while (true) {
+ const { status, receipts } = await provider.request({
+ method: 'wallet_getCallsStatus',
+ params: [batchId],
+ });
+ if (status !== 100) return { status, receipts };
+ await new Promise(r => setTimeout(r, 1000));
+ }
+}
+```
+
+## Capability: paymasterService
+
+**Spec: ERC-7677.** Sponsors gas fees so users transact for free.
+
+```typescript
+capabilities: {
+ paymasterService: {
+ url: 'https://your-paymaster-service.xyz',
+ },
+}
+```
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `url` | `string` | Yes | HTTPS URL of an ERC-7677-compliant paymaster |
+
+The paymaster service must implement:
+- `pm_getPaymasterStubData` — for gas estimation
+- `pm_getPaymasterData` — for actual UserOp paymaster data
+
+Get a paymaster URL from [Coinbase Developer Platform](https://portal.cdp.coinbase.com). See also the [Base Gasless Campaign](https://docs.base.org/base-account/more/base-gasless-campaign) for gas credits.
+
+Best practice: handle failures gracefully with a fallback to regular (user-pays-gas) transactions.
+
+## Capability: auxiliaryFunds
+
+**Spec: EIP-5792.** Indicates the wallet has access to funds beyond the visible onchain balance (MagicSpend — use Coinbase balances onchain).
+
+No configuration parameters — it's a support flag only.
+
+When `auxiliaryFunds.supported === true`:
+- **Do not** block transactions based on visible onchain balance
+- **Do not** show "insufficient funds" warnings based on balance checks
+- Let the wallet handle funding — it can pull from the user's Coinbase account
+
+```typescript
+if (baseCapabilities.auxiliaryFunds?.supported) {
+ // Skip balance check, let wallet handle it
+} else {
+ // Traditional balance check
+ const balance = await client.getBalance({ address: userAddress });
+ if (balance < requiredAmount) showInsufficientFundsWarning();
+}
+```
+
+## Capability: atomic
+
+**Spec: EIP-5792.** Ensures batched calls execute atomically — all succeed or all revert.
+
+Support values (string, not boolean):
+
+| Value | Meaning |
+|-------|---------|
+| `"supported"` | Wallet executes atomically |
+| `"ready"` | Wallet can upgrade to atomic via EIP-7702 |
+| `"unsupported"` | No atomicity guarantees |
+
+Set `atomicRequired: true` in `wallet_sendCalls` to enforce atomic execution. If the wallet doesn't support it, the call fails with error `5700`.
+
+Use cases: approve + swap, mint + pay, any multi-step flow requiring all-or-nothing.
+
+## Capability: flowControl
+
+**Spec: ERC-7867 (proposed, not finalized).** Controls behavior when individual calls in a batch fail.
+
+```typescript
+calls: [{
+ to: '0x...',
+ data: '0x...',
+ flowControl: {
+ onFailure: 'continue',
+ fallbackCall: { to: '0xFallback', data: '0x...' },
+ },
+}]
+```
+
+| Parameter | Type | Values | Description |
+|-----------|------|--------|-------------|
+| `onFailure` | `string` | `'continue'`, `'stop'`, `'retry'` | What to do when this call reverts |
+| `fallbackCall` | `object` | `{ to, value?, data? }` | Optional alternative call to execute on failure |
+
+**Note:** This spec is actively being developed. Check the latest docs before using.
+
+## Capability: dataCallback
+
+Collects user profile information (email, phone, address) during transaction flows. Same mechanism as `payerInfo` in `pay()` but for `wallet_sendCalls`.
+
+```typescript
+capabilities: {
+ dataCallback: {
+ requests: [
+ { type: 'email' },
+ { type: 'name', optional: true },
+ ],
+ callbackURL: 'https://your-api.com/validate',
+ },
+}
+```
+
+Request types: `'email'`, `'phoneNumber'`, `'physicalAddress'`, `'name'`
+
+The `callbackURL` receives a POST with user data before the transaction. Respond with `{ request: requestData }` to accept or `{ errors: { email: 'Invalid' } }` to reject.
+
+## Capability: dataSuffix (Attribution)
+
+**Spec: ERC-8021.** Appends arbitrary bytes to transaction calldata for attribution tracking. Used primarily with **Builder Codes** for tracking which app generated a transaction.
+
+```typescript
+import { Attribution } from 'ox/erc8021';
+
+const builderCodeSuffix = Attribution.toDataSuffix({
+ codes: ['bc_foobar'], // Register at base.dev
+});
+
+capabilities: {
+ dataSuffix: {
+ value: builderCodeSuffix,
+ optional: true,
+ },
+}
+```
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `value` | `0x${string}` | Yes | Hex bytes to append to calldata |
+| `optional` | `boolean` | No | If `true`, wallet may ignore if unsupported |
+
+Best practice: use `optional: true` if your app functions without attribution. Register for a Builder Code at [base.dev](https://base.dev). Keep suffixes small — larger means more gas.
diff --git a/skills/build-on-base/references/base-account/overview.md b/skills/build-on-base/references/base-account/overview.md
new file mode 100644
index 0000000..fceca8a
--- /dev/null
+++ b/skills/build-on-base/references/base-account/overview.md
@@ -0,0 +1,73 @@
+# Building with Base Account
+
+Base Account is an ERC-4337 smart wallet providing universal sign-on, one-tap USDC payments, and multi-chain support (Base, Arbitrum, Optimism, Zora, Polygon, BNB, Avalanche, Lordchain, Ethereum Mainnet).
+
+## Quick Start
+
+```bash
+npm install @base-org/account @base-org/account-ui
+```
+
+```typescript
+import { createBaseAccountSDK } from '@base-org/account';
+
+const sdk = createBaseAccountSDK({
+ appName: 'My App',
+ appLogoUrl: 'https://example.com/logo.png',
+ appChainIds: [8453], // Base Mainnet
+});
+
+const provider = sdk.getProvider();
+```
+
+## Feature References
+
+Read the reference for the feature you're implementing:
+
+| Feature | Reference | When to Read |
+|---------|-----------|-------------|
+| Sign in with Base | [authentication.md](authentication.md) | Wallet auth, SIWE, backend verification, SignInWithBaseButton, Wagmi/Privy setup |
+| Base Pay | [payments.md](payments.md) | One-tap USDC payments, payerInfo, server-side verification, BasePayButton |
+| Subscriptions | [subscriptions.md](subscriptions.md) | Recurring charges, spend permissions, CDP wallet setup, charge/revoke lifecycle |
+| Sub Accounts | [sub-accounts.md](sub-accounts.md) | App-specific embedded wallets, key generation, funding |
+| Capabilities | [capabilities.md](capabilities.md) | Batch transactions, gas sponsorship (paymasters), atomic execution, auxiliaryFunds, attribution |
+| Prolinks | [prolinks.md](prolinks.md) | Shareable payment links, QR codes, encoded transaction URLs |
+| Troubleshooting | [troubleshooting.md](troubleshooting.md) | Popup issues, gas usage, unsupported calls, migration, doc links |
+
+## Critical Requirements
+
+### Security
+
+- **Track transaction IDs** to prevent replay attacks
+- **Verify sender matches authenticated user** to prevent impersonation
+- **Use a proxy** to protect Paymaster URLs from frontend exposure
+- **Paymaster providers must be ERC-7677-compliant**
+- **Never expose CDP credentials client-side** (subscription backend only)
+
+### Popup Handling
+
+- Generate nonces **before** user clicks "Sign in" to avoid popup blockers
+- Use `Cross-Origin-Opener-Policy: same-origin-allow-popups`
+- `same-origin` breaks the Base Account popup
+
+### Base Pay
+
+- Base Pay works independently from SIWB — no auth required for `pay()`
+- `testnet` param in `getPaymentStatus()` must match `pay()` call
+- Never disable actions based on onchain balance alone — check `auxiliaryFunds` capability
+
+### Sub Accounts
+
+- Call `wallet_addSubAccount` each session before use
+- Ownership changes expected on new devices/browsers
+- Only Coinbase Smart Wallet contracts supported for import
+
+### Smart Wallets
+
+- ERC-6492 wrapper enables signature verification before wallet deployment
+- Viem's `verifyMessage`/`verifyTypedData` handle this automatically
+
+## For Edge Cases and Latest API Changes
+
+- **AI-optimized docs**: [docs.base.org/llms.txt](https://docs.base.org/llms.txt)
+- **Full reference**: [docs.base.org/base-account](https://docs.base.org/base-account)
diff --git a/skills/build-on-base/references/base-account/payments.md b/skills/build-on-base/references/base-account/payments.md
new file mode 100644
index 0000000..afd9287
--- /dev/null
+++ b/skills/build-on-base/references/base-account/payments.md
@@ -0,0 +1,225 @@
+# Payments (Base Pay)
+
+## Table of Contents
+
+- [Overview](#overview)
+- [One-Time Payments](#one-time-payments)
+- [Checking Payment Status](#checking-payment-status)
+- [Collecting User Info (payerInfo)](#collecting-user-info-payerinfo)
+- [Server-Side Verification](#server-side-verification)
+- [Server-Side User Info Validation](#server-side-user-info-validation)
+- [BasePayButton Component](#basepaybutton-component)
+- [Framework Integration: Wagmi](#framework-integration-wagmi)
+- [Testing](#testing)
+- [Security Checklist](#security-checklist)
+
+## Overview
+
+Base Pay enables one-tap USDC payments on Base. Key facts:
+
+- Currency is USDC (a digital dollar stablecoin), not ETH
+- Gas is sponsored automatically — users don't pay gas fees
+- Settles in under 2 seconds on Base
+- No chargebacks, no FX fees, no merchant fees
+- **Base Pay works independently from Sign in with Base** — no authentication required to call `pay()`
+- Users can pay from their Base Account or Coinbase account
+
+## One-Time Payments
+
+### `pay()`
+
+```typescript
+import { pay } from '@base-org/account';
+
+const payment = await pay({
+ amount: '10.50',
+ to: '0xRecipientAddress',
+ testnet: false,
+});
+```
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `amount` | `string` | Yes | USDC amount (e.g., `"10.50"`) |
+| `to` | `string` | Yes | Recipient Ethereum address (`0x...`) |
+| `testnet` | `boolean` | No | Use Base Sepolia testnet (default: `false`) |
+| `payerInfo` | `object` | No | Collect user info during payment — see [payerInfo section](#collecting-user-info-payerinfo) |
+
+Returns `PayResult`:
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `id` | `string` | Transaction hash |
+| `amount` | `string` | Amount sent |
+| `to` | `string` | Recipient address |
+| `payerInfoResponses` | `object` | Collected user info (if `payerInfo` was provided) |
+
+## Checking Payment Status
+
+### `getPaymentStatus()`
+
+```typescript
+import { getPaymentStatus } from '@base-org/account';
+
+const status = await getPaymentStatus({
+ id: payment.id,
+ testnet: false,
+});
+```
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `id` | `string` | Yes | Transaction hash from `pay()` |
+| `testnet` | `boolean` | No | **Must match** the `testnet` value used in `pay()` |
+
+Returns `PaymentStatus`:
+
+| Field | Type | Present When |
+|-------|------|-------------|
+| `status` | `"completed" \| "pending" \| "failed" \| "not_found"` | Always |
+| `id` | `string` | Always |
+| `message` | `string` | Always |
+| `sender` | `string` | `pending`, `completed`, `failed` |
+| `amount` | `string` | `completed` |
+| `recipient` | `string` | `completed` |
+| `error` | `object` | `failed` |
+
+## Collecting User Info (payerInfo)
+
+Request user information (email, name, phone, address) during the payment flow.
+
+```typescript
+const payment = await pay({
+ amount: '25.00',
+ to: '0xRecipient',
+ payerInfo: {
+ requests: [
+ { type: 'email' },
+ { type: 'phoneNumber', optional: true },
+ { type: 'physicalAddress', optional: true },
+ ],
+ callbackURL: 'https://your-api.com/validate',
+ },
+});
+```
+
+Supported `payerInfo` request types:
+
+| Type | Response Shape |
+|------|---------------|
+| `email` | `string` |
+| `name` | `{ firstName: string, familyName: string }` |
+| `phoneNumber` | `{ number: string, country: string }` |
+| `physicalAddress` | `{ address1, address2?, city, state, postalCode, country, name: { firstName, familyName } }` |
+| `onchainAddress` | `string` |
+
+Fields are **required by default**. Set `optional: true` to avoid aborting the payment if the user declines to share.
+
+## Server-Side Verification
+
+Never trust frontend payment confirmations alone. Always verify on your backend.
+
+```typescript
+import { getPaymentStatus } from '@base-org/account';
+
+async function verifyPayment(txId: string, expectedAmount: string, expectedRecipient: string, authenticatedUser: string) {
+ // 1. Check if already processed (dedup by txId)
+ if (await isProcessed(txId)) throw new Error('Already processed');
+
+ // 2. Verify payment status
+ const { status, sender, amount, recipient } = await getPaymentStatus({ id: txId });
+ if (status !== 'completed') throw new Error(`Payment not completed: ${status}`);
+
+ // 3. Verify sender matches authenticated user (prevents impersonation)
+ if (sender.toLowerCase() !== authenticatedUser.toLowerCase()) {
+ throw new Error('Sender mismatch');
+ }
+
+ // 4. Validate amount and recipient
+ if (amount !== expectedAmount || recipient.toLowerCase() !== expectedRecipient.toLowerCase()) {
+ throw new Error('Payment details mismatch');
+ }
+
+ // 5. Mark processed BEFORE fulfilling
+ await markProcessed(txId);
+ await fulfillOrder(txId);
+}
+```
+
+Key threats this prevents:
+- **Replay attacks**: Track processed transaction IDs with unique constraints
+- **Impersonation**: Verify `sender` matches the authenticated user
+- **Amount tampering**: Validate `amount` and `recipient` server-side
+
+## Server-Side User Info Validation
+
+When you provide a `callbackURL` in `payerInfo`, your endpoint receives the user's data **before** the transaction is submitted. You can validate and accept or reject.
+
+```typescript
+// POST handler at your callbackURL
+app.post('/validate', (req, res) => {
+ const { requestData } = req.body;
+ const info = requestData.capabilities.dataCallback.requestedInfo;
+
+ // Reject with errors (shown to user)
+ if (!isValidEmail(info.email)) {
+ return res.json({ errors: { email: 'Invalid email address' } });
+ }
+
+ // Accept — return the original request data
+ return res.json({ request: requestData });
+});
+```
+
+## BasePayButton Component
+
+Pre-built React button from `@base-org/account-ui`.
+
+```tsx
+import { BasePayButton } from '@base-org/account-ui/react';
+
+
+```
+
+| Prop | Type | Values | Default |
+|------|------|--------|---------|
+| `colorScheme` | `string` | `'light'`, `'dark'`, `'system'` | `'light'` |
+| `size` | `string` | `'small'`, `'medium'`, `'large'` | `'medium'` |
+| `variant` | `string` | `'solid'`, `'outline'` | `'solid'` |
+| `disabled` | `boolean` | — | `false` |
+| `onClick` | `() => void` | — | — |
+| `onPaymentResult` | `(result) => void` | — | — |
+
+Follow the [Brand Guidelines](https://docs.base.org/base-account/reference/ui-elements/brand-guidelines): always use the combination mark (never plain text "Base Pay"), pad the button with at least 1x height on all sides.
+
+## Framework Integration: Wagmi
+
+`pay()` and `getPaymentStatus()` are standalone functions — they don't require a provider or wagmi config. Call them directly:
+
+```typescript
+import { pay, getPaymentStatus } from '@base-org/account';
+
+const { id } = await pay({ amount: '5.00', to: '0x...', testnet: true });
+const status = await getPaymentStatus({ id, testnet: true });
+```
+
+If you're also using SIWB with wagmi, the `pay()` function still works independently alongside the wagmi provider setup.
+
+## Testing
+
+- Use `testnet: true` in both `pay()` and `getPaymentStatus()`
+- Test on Base Sepolia (chain ID 84532)
+- Get test USDC from the [Circle Faucet](https://faucet.circle.com/) on Base Sepolia
+
+## Security Checklist
+
+- Always verify payments server-side with `getPaymentStatus()`
+- Track processed transaction IDs in a database with unique constraints
+- Verify `sender` matches your authenticated user
+- Validate `amount` and `recipient` match the expected order
+- `testnet` param must match between `pay()` and `getPaymentStatus()`
+- Never disable payment buttons based on onchain balance alone — check `auxiliaryFunds` capability (users may have Coinbase balances available via MagicSpend)
diff --git a/skills/build-on-base/references/base-account/prolinks.md b/skills/build-on-base/references/base-account/prolinks.md
new file mode 100644
index 0000000..0b74411
--- /dev/null
+++ b/skills/build-on-base/references/base-account/prolinks.md
@@ -0,0 +1,192 @@
+# Prolinks (Shareable Payment Links)
+
+## Table of Contents
+
+- [Overview](#overview)
+- [encodeProlink](#encodeprolink)
+- [decodeProlink](#decodeprolink)
+- [createProlinkUrl](#createprolinkurl)
+- [Common Patterns](#common-patterns)
+
+## Overview
+
+Prolinks encode transaction requests (JSON-RPC) into compressed, URL-safe strings that can be shared as links. When a user opens a prolink URL, their Base Account app decodes and executes the request.
+
+Use cases: shareable payment requests, pre-filled transaction links, QR codes for onchain actions.
+
+The encoding is optimized per method type (`wallet_sendCalls`, `wallet_sign`, generic JSON-RPC) and uses gzip compression for payloads >= 1KB (50-80% size reduction).
+
+## encodeProlink
+
+Encodes a JSON-RPC request into a compressed, base64url-encoded prolink payload.
+
+```typescript
+import { encodeProlink } from '@base-org/account';
+
+const prolink = await encodeProlink({
+ method: 'wallet_sendCalls',
+ params: {
+ version: '2.0.0',
+ chainId: '0x2105',
+ calls: [{
+ to: '0xUSDCAddress',
+ data: '0xtransferCalldata',
+ value: '0x0',
+ }],
+ },
+});
+```
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `method` | `string` | Yes | JSON-RPC method (`wallet_sendCalls`, `wallet_sign`, or any) |
+| `params` | `unknown` | Yes | Method parameters |
+| `chainId` | `number` | No | Required for generic methods; auto-extracted for `wallet_sendCalls`/`wallet_sign` |
+| `capabilities` | `Record` | No | Wallet capabilities (e.g., `dataCallback`) |
+
+Returns: `Promise` — base64url-encoded prolink payload.
+
+### Examples
+
+**ERC-20 Transfer (USDC):**
+
+```typescript
+const prolink = await encodeProlink({
+ method: 'wallet_sendCalls',
+ params: {
+ version: '2.0.0',
+ chainId: '0x2105',
+ calls: [{
+ to: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
+ data: '0xa9059cbb000000000000000000000000RECIPIENT0000000000000000000000000000000000000000000000000000000000989680', // transfer(address,uint256)
+ value: '0x0',
+ }],
+ },
+});
+```
+
+**With Capabilities (dataCallback):**
+
+```typescript
+const prolink = await encodeProlink({
+ method: 'wallet_sendCalls',
+ params: { /* ... */ },
+ capabilities: {
+ dataCallback: {
+ callbackURL: 'https://your-api.com/callback',
+ events: ['initiated', 'postSign'],
+ },
+ },
+});
+```
+
+**Batch Calls (approve + swap):**
+
+```typescript
+const prolink = await encodeProlink({
+ method: 'wallet_sendCalls',
+ params: {
+ version: '2.0.0',
+ chainId: '0x2105',
+ calls: [
+ { to: '0xToken', data: '0xapproveData', value: '0x0' },
+ { to: '0xDex', data: '0xswapData', value: '0x0' },
+ ],
+ },
+});
+```
+
+## decodeProlink
+
+Decodes a prolink payload back into a JSON-RPC request.
+
+```typescript
+import { decodeProlink } from '@base-org/account';
+
+const decoded = await decodeProlink(payload);
+// decoded.method → 'wallet_sendCalls'
+// decoded.params → { version, chainId, calls }
+// decoded.chainId → number | undefined
+// decoded.capabilities → Record | undefined
+```
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `payload` | `string` | Yes | Base64url-encoded prolink payload |
+
+Returns `ProlinkDecoded`:
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `method` | `string` | JSON-RPC method name |
+| `params` | `unknown` | Method parameters |
+| `chainId` | `number \| undefined` | Target chain ID |
+| `capabilities` | `Record \| undefined` | Wallet capabilities |
+
+### Validation Before Execution
+
+Always validate decoded prolinks before executing:
+
+```typescript
+const decoded = await decodeProlink(payload);
+
+if (decoded.chainId !== 8453) throw new Error('Wrong chain');
+if (decoded.method !== 'wallet_sendCalls') throw new Error('Unexpected method');
+
+const { calls } = decoded.params;
+const allowedContracts = ['0xUSDC...', '0xDex...'];
+for (const call of calls) {
+ if (!allowedContracts.includes(call.to)) {
+ throw new Error(`Untrusted contract: ${call.to}`);
+ }
+}
+
+await provider.request({ method: decoded.method, params: [decoded.params] });
+```
+
+## createProlinkUrl
+
+Creates a complete URL with the prolink as a query parameter.
+
+```typescript
+import { createProlinkUrl } from '@base-org/account';
+
+const url = createProlinkUrl(prolink, 'https://yourapp.com/pay');
+// https://yourapp.com/pay?prolink=
+```
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `prolink` | `string` | Yes | Base64url-encoded prolink from `encodeProlink` |
+| `url` | `string` | Yes | Base URL (default: `https://base.app/base-pay`) |
+| `additionalQueryParams` | `Record` | No | Extra query parameters |
+
+Returns: Complete URL string.
+
+## Common Patterns
+
+### Payment Request Link
+
+```typescript
+const prolink = await encodeProlink({
+ method: 'wallet_sendCalls',
+ params: {
+ version: '2.0.0',
+ chainId: '0x2105',
+ calls: [{ to: recipientAddress, data: transferCalldata, value: '0x0' }],
+ },
+});
+const paymentUrl = createProlinkUrl(prolink);
+// Share this URL or render as QR code
+```
+
+### Extract and Display Transaction Preview
+
+```typescript
+const decoded = await decodeProlink(payload);
+const { calls } = decoded.params;
+
+const preview = calls.map((call, i) =>
+ `Call ${i + 1}: to=${call.to}, value=${call.value}`
+).join('\n');
+```
diff --git a/skills/build-on-base/references/base-account/sub-accounts.md b/skills/build-on-base/references/base-account/sub-accounts.md
new file mode 100644
index 0000000..9e52765
--- /dev/null
+++ b/skills/build-on-base/references/base-account/sub-accounts.md
@@ -0,0 +1,250 @@
+# Sub Accounts
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Key Concepts](#key-concepts)
+- [SDK Configuration](#sdk-configuration)
+- [Key Management](#key-management)
+- [Creating Sub Accounts](#creating-sub-accounts)
+- [Retrieving Sub Accounts](#retrieving-sub-accounts)
+- [Adding Owners](#adding-owners)
+- [wallet_addSubAccount RPC](#wallet_addsubaccount-rpc)
+- [wallet_getSubAccounts RPC](#wallet_getsubaccounts-rpc)
+- [Funding Sub Accounts](#funding-sub-accounts)
+- [Session Management](#session-management)
+
+## Overview
+
+Sub accounts are app-specific embedded wallets created under a user's Base Account. They let your app perform transactions on behalf of the user without requiring approval popups for every action — useful for gaming, DeFi automation, or any UX that needs low-friction transactions.
+
+Each sub account is a separate smart wallet owned by the parent Base Account.
+
+## Key Concepts
+
+- Sub accounts are **app-scoped** — each app gets its own sub account(s)
+- The parent Base Account is the **owner** of each sub account
+- Sub accounts can be funded via **spend permissions** or **manual transfers**
+- Ownership may change across devices/browsers — always call `wallet_addSubAccount` each session
+- Only **Coinbase Smart Wallet** contracts are supported for importing existing sub accounts
+
+## SDK Configuration
+
+Configure sub accounts when creating the SDK:
+
+```typescript
+import { createBaseAccountSDK, getCryptoKeyAccount } from '@base-org/account';
+
+const sdk = createBaseAccountSDK({
+ appName: 'My App',
+ appLogoUrl: 'https://example.com/logo.png',
+ appChainIds: [8453],
+ subAccounts: {
+ creation: 'on-connect',
+ defaultAccount: 'sub',
+ funding: 'spend-permissions',
+ toOwnerAccount: async () => {
+ const { account } = await getCryptoKeyAccount();
+ return { account };
+ },
+ },
+});
+```
+
+`SubAccountOptions`:
+
+| Property | Type | Values | Description |
+|----------|------|--------|-------------|
+| `creation` | `string` | `'on-connect'`, `'manual'` | When to create sub accounts |
+| `defaultAccount` | `string` | `'sub'`, `'universal'` | Which account is default (first in accounts array) |
+| `funding` | `string` | `'spend-permissions'`, `'manual'` | How sub accounts are funded |
+| `toOwnerAccount` | `function` | — | Returns `{ account: LocalAccount \| WebAuthnAccount \| null }` |
+
+## Key Management
+
+Sub accounts require a key pair for signing. The SDK provides utilities for P256 key management.
+
+### `generateKeyPair()`
+
+```typescript
+import { generateKeyPair } from '@base-org/account';
+
+const keyPair = await generateKeyPair();
+// keyPair.publicKey → hex string
+// keyPair.privateKey → hex string
+```
+
+### `getKeypair()`
+
+Retrieves an existing key pair from secure storage (returns `null` if none).
+
+```typescript
+import { getKeypair } from '@base-org/account';
+
+let keyPair = await getKeypair();
+if (!keyPair) {
+ keyPair = await generateKeyPair();
+}
+```
+
+### `getCryptoKeyAccount()`
+
+Gets the current crypto key account info.
+
+```typescript
+import { getCryptoKeyAccount } from '@base-org/account';
+
+const { account } = await getCryptoKeyAccount();
+// account.publicKey → hex string
+// account.type → 'webauthn' | 'local'
+// account.address → (for LocalAccount only)
+```
+
+Returns `{ account }` where `account` is one of:
+- `WebAuthnAccount`: `{ publicKey, type: 'webauthn' }`
+- `LocalAccount`: `{ address, publicKey, type: 'local' }`
+- `null`: No account available
+
+## Creating Sub Accounts
+
+### Via SDK Helper
+
+```typescript
+const subAccount = await sdk.subAccount.create({
+ type: 'webauthn-p256',
+ publicKey: keyPair.publicKey,
+});
+// subAccount.address → the sub account address
+```
+
+### Via RPC (wallet_addSubAccount)
+
+```typescript
+const result = await provider.request({
+ method: 'wallet_addSubAccount',
+ params: [{
+ account: {
+ type: 'create',
+ keys: [{
+ type: 'webauthn-p256',
+ publicKey: keyPair.publicKey,
+ }],
+ },
+ }],
+});
+// result.address → the sub account address
+```
+
+Key types for the `keys` array:
+
+| Type | Description |
+|------|-------------|
+| `'address'` | Raw Ethereum address |
+| `'p256'` | P256 public key |
+| `'webcrypto-p256'` | WebCrypto P256 key |
+| `'webauthn-p256'` | WebAuthn P256 key (recommended) |
+
+## Retrieving Sub Accounts
+
+```typescript
+const subAccount = await sdk.subAccount.get();
+// Returns the current sub account or null
+```
+
+Or via RPC:
+
+```typescript
+const subAccounts = await provider.request({
+ method: 'wallet_getSubAccounts',
+});
+// Array of { address, factory?, factoryData? }
+```
+
+## Adding Owners
+
+```typescript
+await sdk.subAccount.addOwner({
+ address: newOwnerAddress,
+ chainId: 8453,
+});
+```
+
+## wallet_addSubAccount RPC
+
+Two modes of operation:
+
+### Create a New Sub Account
+
+```typescript
+await provider.request({
+ method: 'wallet_addSubAccount',
+ params: [{
+ account: {
+ type: 'create',
+ keys: [{ type: 'webauthn-p256', publicKey: '0x...' }],
+ },
+ }],
+});
+```
+
+### Import an Existing Deployed Account
+
+```typescript
+await provider.request({
+ method: 'wallet_addSubAccount',
+ params: [{
+ account: {
+ type: 'deployed',
+ address: '0xExistingSubAccount',
+ chainId: 8453,
+ },
+ }],
+});
+```
+
+Returns: `{ address, factory?, factoryData? }`
+
+## wallet_getSubAccounts RPC
+
+```typescript
+const accounts = await provider.request({
+ method: 'wallet_getSubAccounts',
+});
+```
+
+Returns an array of sub account objects.
+
+## Funding Sub Accounts
+
+Two strategies:
+
+### Spend Permissions (Recommended)
+
+Set `funding: 'spend-permissions'` in SDK config. The parent Base Account grants a spend permission to the sub account, which can then spend tokens within the allowed limit.
+
+### Manual
+
+Set `funding: 'manual'`. You transfer tokens directly to the sub account address.
+
+## Session Management
+
+**Call `wallet_addSubAccount` at the start of each session** before using the sub account. This is necessary because:
+
+- Ownership may change when users switch devices or browsers
+- The sub account needs to be re-registered with the current session
+- Without this call, sub account operations may fail silently
+
+```typescript
+async function initSession() {
+ const keyPair = await getKeypair() || await generateKeyPair();
+ await provider.request({
+ method: 'wallet_addSubAccount',
+ params: [{
+ account: {
+ type: 'create',
+ keys: [{ type: 'webauthn-p256', publicKey: keyPair.publicKey }],
+ },
+ }],
+ });
+}
+```
diff --git a/skills/build-on-base/references/base-account/subscriptions.md b/skills/build-on-base/references/base-account/subscriptions.md
new file mode 100644
index 0000000..4d56c9f
--- /dev/null
+++ b/skills/build-on-base/references/base-account/subscriptions.md
@@ -0,0 +1,238 @@
+# Subscriptions (Recurring Payments)
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Architecture](#architecture)
+- [Backend Setup: CDP Owner Wallet](#backend-setup-cdp-owner-wallet)
+- [Frontend: Create a Subscription](#frontend-create-a-subscription)
+- [Backend: Check Subscription Status](#backend-check-subscription-status)
+- [Backend: Charge a Subscription](#backend-charge-a-subscription)
+- [Backend: Cancel a Subscription](#backend-cancel-a-subscription)
+- [Advanced: Manual Execution](#advanced-manual-execution)
+- [Fund Routing Patterns](#fund-routing-patterns)
+- [Testing](#testing)
+
+## Overview
+
+Recurring payments use **Spend Permissions** — an onchain primitive that lets a user grant revocable spending rights to your app. The user approves once, and your backend charges periodically without further user interaction.
+
+Key properties:
+- Spending limit auto-resets each period (no rollover between periods)
+- User can cancel anytime from their wallet
+- USDC only (on Base Mainnet and Base Sepolia)
+- Requires both client-side (subscribe) and server-side (charge/revoke) code
+
+## Architecture
+
+```
+Client (browser) Server (Node.js)
+───────────────── ────────────────
+subscribe() ──────────────────────> Store subscription ID
+ ↓
+ getStatus() → check if chargeable
+ ↓
+ charge() → execute periodic charge
+ ↓
+ revoke() → cancel when needed
+```
+
+The server uses a **CDP (Coinbase Developer Platform) smart wallet** to act as the subscription owner (the entity authorized to spend).
+
+## Backend Setup: CDP Owner Wallet
+
+### Environment Variables
+
+```bash
+CDP_API_KEY_ID=your-api-key-id
+CDP_API_KEY_SECRET=your-api-key-secret
+CDP_WALLET_SECRET=your-wallet-secret
+PAYMASTER_URL=https://your-paymaster.xyz # optional, for gasless transactions
+```
+
+Get these from [Coinbase Developer Platform](https://portal.cdp.coinbase.com).
+
+### Create or Retrieve the Owner Wallet
+
+```typescript
+import { base } from '@base-org/account/node';
+
+const wallet = await base.subscription.getOrCreateSubscriptionOwnerWallet({
+ walletName: 'my-app-subscriptions',
+});
+// wallet.address → share this with the frontend as subscriptionOwner
+// wallet.walletName → must match across charge() and revoke() calls
+```
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `walletName` | `string` | No | Wallet identifier (default: `"subscription owner"`) |
+| `cdpApiKeyId` | `string` | No | Falls back to `CDP_API_KEY_ID` env var |
+| `cdpApiKeySecret` | `string` | No | Falls back to `CDP_API_KEY_SECRET` env var |
+| `cdpWalletSecret` | `string` | No | Falls back to `CDP_WALLET_SECRET` env var |
+
+Returns: `{ address, walletName, eoaAddress }`
+
+This is **idempotent** — the same `walletName` always returns the same wallet. The `address` is the CDP smart wallet address (safe to share publicly as `subscriptionOwner`).
+
+**Never expose CDP credentials client-side.** Only the wallet `address` is public.
+
+## Frontend: Create a Subscription
+
+```typescript
+import { base } from '@base-org/account';
+
+const subscription = await base.subscription.subscribe({
+ recurringCharge: '9.99',
+ subscriptionOwner: '0xYourCDPWalletAddress',
+ periodInDays: 30,
+ testnet: false,
+});
+// subscription.id → store this as the subscription identifier
+```
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `recurringCharge` | `string` | Yes | USDC amount per period (max 6 decimals) |
+| `subscriptionOwner` | `string` | Yes | Your CDP wallet address |
+| `periodInDays` | `number` | No | Charge period in days (default: `30`) |
+| `testnet` | `boolean` | No | Use testnet (default: `false`) |
+| `requireBalance` | `boolean` | No | Check payer balance first (default: `true`) |
+
+Returns `SubscriptionResult`:
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `id` | `string` | Permission hash (subscription identifier) |
+| `subscriptionOwner` | `string` | Your app's wallet address |
+| `subscriptionPayer` | `string` | The user's wallet address |
+| `recurringCharge` | `string` | Amount in USD |
+| `periodInDays` | `number` | Period length |
+
+## Backend: Check Subscription Status
+
+```typescript
+import { base } from '@base-org/account';
+
+const status = await base.subscription.getStatus({
+ id: subscriptionId,
+ testnet: false,
+});
+```
+
+| Parameter | Type | Required |
+|-----------|------|----------|
+| `id` | `string` | Yes |
+| `testnet` | `boolean` | No |
+
+Returns `SubscriptionStatus`:
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `isSubscribed` | `boolean` | Whether subscription is active |
+| `recurringCharge` | `string` | Charge amount |
+| `remainingChargeInPeriod` | `string` | How much can still be charged this period |
+| `currentPeriodStart` | `Date` | — |
+| `nextPeriodStart` | `Date` | — |
+| `periodInDays` | `number` | — |
+
+Check before charging:
+
+```typescript
+const status = await base.subscription.getStatus({ id: subscriptionId });
+if (status.isSubscribed && parseFloat(status.remainingChargeInPeriod!) > 0) {
+ // safe to charge
+}
+```
+
+## Backend: Charge a Subscription
+
+```typescript
+import { base } from '@base-org/account/node';
+
+const result = await base.subscription.charge({
+ id: subscriptionId,
+ amount: 'max-remaining-charge',
+ paymasterUrl: process.env.PAYMASTER_URL,
+ testnet: false,
+});
+```
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `id` | `string` | Yes | Subscription ID |
+| `amount` | `string \| 'max-remaining-charge'` | Yes | USDC amount or `'max-remaining-charge'` |
+| `paymasterUrl` | `string` | No | For gasless transactions |
+| `recipient` | `string` | No | Send USDC to a different address (default: stays in CDP wallet) |
+| `testnet` | `boolean` | No | Default: `false` |
+| `walletName` | `string` | No | Must match the wallet used in setup |
+
+Returns: `{ success, id, subscriptionId, amount, subscriptionOwner, recipient }`
+
+`charge()` handles all transaction details: gas estimation, nonce management, and signing.
+
+## Backend: Cancel a Subscription
+
+```typescript
+import { base } from '@base-org/account/node';
+
+const result = await base.subscription.revoke({
+ id: subscriptionId,
+ paymasterUrl: process.env.PAYMASTER_URL,
+ testnet: false,
+});
+```
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `id` | `string` | Yes | Subscription ID |
+| `paymasterUrl` | `string` | No | For gasless transactions |
+| `testnet` | `boolean` | No | Default: `false` |
+| `walletName` | `string` | No | Must match the wallet used in setup |
+
+Returns: `{ success, id, subscriptionId, subscriptionOwner }`
+
+Revoking is **permanent**. The user would need to create a new subscription.
+
+## Advanced: Manual Execution
+
+For custom wallet infrastructure (not using CDP wallets), use `prepareCharge` and `prepareRevoke` to get raw call data.
+
+### `prepareCharge()`
+
+```typescript
+import { base } from '@base-org/account';
+
+const calls = await base.subscription.prepareCharge({
+ id: subscriptionId,
+ amount: 'max-remaining-charge',
+ testnet: false,
+});
+// calls → Array<{ to, data, value: '0x0' }>
+// Execute via wallet_sendCalls or eth_sendTransaction
+```
+
+### `prepareRevoke()`
+
+```typescript
+const call = await base.subscription.prepareRevoke({
+ id: subscriptionId,
+ testnet: false,
+});
+// call → { to, data, value: '0x0' }
+```
+
+## Fund Routing Patterns
+
+| Pattern | How | When |
+|---------|-----|------|
+| Default | Omit `recipient` | USDC stays in CDP wallet |
+| Treasury | `recipient: '0xTreasury'` | Auto-transfer to treasury |
+| Dynamic | Set `recipient` per charge | Route to different addresses based on plan type |
+
+## Testing
+
+- Use `testnet: true` in all calls (`subscribe`, `getStatus`, `charge`, `revoke`)
+- Use `periodInDays: 1` for faster testing cycles
+- Test on Base Sepolia (chain ID 84532)
+- Get test USDC from the [Circle Faucet](https://faucet.circle.com/)
diff --git a/skills/build-on-base/references/base-account/troubleshooting.md b/skills/build-on-base/references/base-account/troubleshooting.md
new file mode 100644
index 0000000..db090e8
--- /dev/null
+++ b/skills/build-on-base/references/base-account/troubleshooting.md
@@ -0,0 +1,146 @@
+# Troubleshooting
+
+## Table of Contents
+
+- [Quick Fixes](#quick-fixes)
+- [Popup Issues](#popup-issues)
+- [Gas Usage](#gas-usage)
+- [Unsupported Operations](#unsupported-operations)
+- [Wallet Library Compatibility](#wallet-library-compatibility)
+- [Migration from Coinbase Wallet SDK](#migration-from-coinbase-wallet-sdk)
+- [Transaction Simulation Debugging](#transaction-simulation-debugging)
+- [When to Consult the Docs](#when-to-consult-the-docs)
+
+## Quick Fixes
+
+| Issue | Solution |
+|-------|----------|
+| Peer dependency error during install | Use `--legacy-peer-deps` flag |
+| Popup shows infinite spinner | Set COOP header to `same-origin-allow-popups` (not `same-origin`) |
+| Signature verification fails pre-deploy | Use viem — it handles ERC-6492 automatically |
+| `wallet_connect` throws `4100` | Wallet doesn't support it; fall back to `eth_requestAccounts` + `personal_sign` |
+| Payment status returns `not_found` | Ensure `testnet` param in `getPaymentStatus()` matches `pay()` |
+| Sub account operations fail | Call `wallet_addSubAccount` at the start of each session |
+| Balance appears insufficient | Check `auxiliaryFunds` capability — user may have Coinbase balances available |
+
+## Popup Issues
+
+Base Account uses a popup window at `keys.coinbase.com` for user approvals.
+
+### Cross-Origin-Opener-Policy (COOP)
+
+| COOP Value | Works? |
+|------------|--------|
+| `unsafe-none` (browser default) | Yes |
+| `same-origin-allow-popups` | Yes (recommended) |
+| `same-origin` | **No** — breaks the popup entirely |
+
+If using `same-origin`, the popup either errors or shows an infinite spinner. Switch to `same-origin-allow-popups`.
+
+### Popup Blockers
+
+Browsers block popups unless triggered by a direct user click. To avoid blocking:
+
+- Generate nonces and do any async work **before** the user clicks the sign-in button
+- Keep zero or minimal logic between the button click handler and the SDK call
+- Test across all target browsers — popup blocking behavior varies
+
+### Popup "Linger" Behavior
+
+After responding to a request, the popup stays open for **200ms** before closing. If a second SDK request arrives within that window, it's handled in the same popup (no new popup needed).
+
+If the second request arrives **after** 200ms (popup already closed), the browser will block the new programmatic popup. Design flows to either:
+- Chain requests quickly (< 200ms gap)
+- Require a new user click for the second request
+
+## Gas Usage
+
+Base Accounts use more gas than traditional Ethereum accounts (EOAs) because they're smart contracts processed through ERC-4337 bundling.
+
+| Operation | EOA | Base Account |
+|-----------|-----|-------------|
+| Native token transfer | ~21,000 gas | ~100,000 gas |
+| ERC-20 token transfer | ~65,000 gas | ~150,000 gas |
+| First-time deployment | N/A | ~300,000+ gas (one-time) |
+
+On L2 networks like Base, the cost difference is typically just a few cents. Use a paymaster to sponsor gas entirely (see [capabilities reference](capabilities.md#capability-paymasterservice)).
+
+## Unsupported Operations
+
+Base Account is an ERC-4337 smart wallet. Some operations behave differently:
+
+### Self-Calls
+
+Apps **cannot** make calls to the user's own Base Account address. This is a security measure to prevent changing owners, upgrading the account, or other admin operations.
+
+### CREATE Opcode
+
+Not supported due to ERC-4337 limitations. Workarounds:
+- Use a **factory contract** that deploys on behalf of the user
+- Use the `CREATE2` opcode instead
+
+### Solidity `transfer` Function
+
+Base Account wallets **cannot receive ETH** via Solidity's built-in `transfer` function because it only forwards 2,300 gas — insufficient for smart contract `receive`/`fallback` functions.
+
+Use `call` instead:
+
+```solidity
+// Won't work with Base Account
+payable(baseAccountAddress).transfer(amount);
+
+// Use this instead
+(bool success, ) = payable(baseAccountAddress).call{value: amount}("");
+require(success, "Transfer failed");
+```
+
+Known affected contract: **WETH9 on Base** (`0x4200000000000000000000000000000000000006`) — Base Accounts cannot directly unwrap ETH from it.
+
+## Wallet Library Compatibility
+
+These wallet aggregation libraries have day-1 Base Account support:
+
+| Library | Supported |
+|---------|-----------|
+| Dynamic | Yes |
+| Privy | Yes |
+| ThirdWeb | Yes |
+| ConnectKit | Yes |
+| Web3Modal / Reown | Yes |
+| Web3-Onboard | Yes |
+| RainbowKit | Yes |
+
+## Migration from Coinbase Wallet SDK
+
+The Coinbase Wallet app is transitioning to the Base app. To migrate:
+
+1. **Don't immediately replace** the existing "Coinbase Wallet" button
+2. **Add** a "Sign in with Base" button as a new option alongside it
+3. Over time, existing Coinbase Wallet users will be migrated to Base Accounts
+
+Code change:
+
+```typescript
+// New: add Base Account SDK
+import { createBaseAccountSDK } from '@base-org/account';
+const baseAccount = createBaseAccountSDK({ appName: 'My App' });
+```
+
+As of Coinbase Wallet SDK v4.0, users without the extension see a popup with options (mobile WalletLink or passkey-powered Smart Wallet). To avoid any popup window, use Coinbase Wallet SDK version < 4.0.
+
+## Transaction Simulation Debugging
+
+Hidden feature in the Base Account popup: click the transaction simulation area **five times** to copy the simulation request/response data to your clipboard. Paste into a text editor to inspect.
+
+## When to Consult the Docs
+
+This skill covers the most common patterns. For edge cases, advanced configurations, or the latest API changes, consult:
+
+- **AI-optimized docs**: [docs.base.org/llms.txt](https://docs.base.org/llms.txt) — feed this to your agent for comprehensive context
+- **Base Account reference**: [docs.base.org/base-account](https://docs.base.org/base-account) — full API reference, all RPC methods, all capabilities
+- **Base Account SDK source**: [github.com/base/account-sdk](https://github.com/base/account-sdk)
+- **Smart Wallet contracts**: [github.com/coinbase/smart-wallet](https://github.com/coinbase/smart-wallet)
+- **Spend Permissions contracts**: [github.com/coinbase/spend-permissions](https://github.com/coinbase/spend-permissions)
+- **Coinbase Developer Platform**: [portal.cdp.coinbase.com](https://portal.cdp.coinbase.com) — paymaster setup, API keys, wallet management
+
+For standard Ethereum RPC methods (`eth_getBalance`, `eth_sendTransaction`, `eth_getTransactionReceipt`, etc.), Base Account's provider supports all standard methods. See the [provider RPC methods reference](https://docs.base.org/base-account/reference/core/provider-rpc-methods/sdk-overview) for the full list.
diff --git a/skills/build-on-base/references/builder-codes/overview.md b/skills/build-on-base/references/builder-codes/overview.md
new file mode 100644
index 0000000..49d12bc
--- /dev/null
+++ b/skills/build-on-base/references/builder-codes/overview.md
@@ -0,0 +1,159 @@
+# Adding Builder Codes
+
+Integrate [Base Builder Codes](https://base.dev) into an onchain application. Builder Codes append an ERC-8021 attribution suffix to transaction calldata so Base can attribute activity to your app and you can earn referral fees. No smart contract changes required.
+
+## When to Use
+
+Use when a developer asks to:
+
+- "Add builder codes to my application"
+- "How do I append a builder code to my transactions?"
+- "I want to earn referral fees on Base transactions"
+- "Integrate builder codes"
+- Set up transaction attribution on Base
+
+## Prerequisites
+
+- A Builder Code from [base.dev](https://base.dev) > Settings > Builder Codes
+- The `ox` library for generating ERC-8021 suffixes: `npm install ox`
+
+## Integration Workflow
+
+Copy this checklist and track progress:
+
+```
+Builder Codes Integration:
+- [ ] Step 1: Detect framework (Required First Step)
+- [ ] Step 2: Install dependencies
+- [ ] Step 3: Generate the dataSuffix constant
+- [ ] Step 4: Apply attribution (framework-specific)
+- [ ] Step 5: Verify attribution is working
+```
+
+## Framework Detection (Required First Step)
+
+Before implementing, determine the framework in use.
+
+### 1. Read package.json and scan source files
+
+```bash
+# Check for framework dependencies
+grep -E "wagmi|@privy-io/react-auth|viem|ethers" package.json
+
+# Check for smart wallet / account abstraction usage
+grep -rn "useSendCalls\|sendCalls\|ERC-4337\|useSmartWallets" src/
+
+# Check for EOA transaction patterns
+grep -rn "useSendTransaction\|sendTransaction\|writeContract\|useWriteContract" src/
+
+# Check Privy version if present
+grep "@privy-io/react-auth" package.json
+```
+
+### 2. Classify into one framework
+
+| Framework | Detection Signal |
+|-----------|-----------------|
+| `privy` | `@privy-io/react-auth` in package.json or imports |
+| `wagmi` | `wagmi` in package.json or imports (without Privy) |
+| `viem` | `viem` in package.json, no React framework |
+| `rpc` | `ethers`, `window.ethereum`, or no Web3 library detected |
+
+Priority order if multiple are detected: **Privy > Wagmi > Viem > Standard RPC**
+
+### 3. Confirm with user
+
+Before proceeding, confirm the detected framework:
+
+> "I detected you are using [Framework]. I'll implement builder codes using the [Framework] approach — does that sound right?"
+
+Wait for user confirmation before implementing.
+
+### Implementation Path
+
+- **Privy** (`@privy-io/react-auth` v3.13.0+) → See [privy.md](privy.md)
+- **Wagmi** (without Privy) → See [wagmi.md](wagmi.md)
+- **Viem only** (no React framework) → See [viem.md](viem.md)
+- **Standard RPC** (ethers.js or raw `window.ethereum`) → See [rpc.md](rpc.md)
+
+### Step 2: Install dependencies
+
+```bash
+npm install ox
+```
+
+Requires `viem >= 2.45.0` for Wagmi/Viem paths. Privy requires `@privy-io/react-auth >= 3.13.0`.
+
+### Step 3: Generate the dataSuffix constant
+
+Create a shared constant (e.g., `src/lib/attribution.ts` or `src/constants/builderCode.ts`):
+
+```typescript
+import { Attribution } from "ox/erc8021";
+
+export const DATA_SUFFIX = Attribution.toDataSuffix({
+ codes: ["YOUR-BUILDER-CODE"], // Replace with your code from base.dev
+});
+```
+
+### Step 4: Apply attribution
+
+Follow the framework-specific guide:
+
+#### Privy Implementation
+
+See [privy.md](privy.md) — plugin-based, one config change required.
+
+#### Wagmi Implementation
+
+See [wagmi.md](wagmi.md) — add `dataSuffix` to Wagmi client config.
+
+#### Viem Implementation
+
+See [viem.md](viem.md) — add `dataSuffix` to wallet client.
+
+#### Standard RPC Implementation
+
+See [rpc.md](rpc.md) — append `DATA_SUFFIX` to transaction data for ethers.js or raw `window.ethereum`.
+
+**Preferred approach**: Configure at the **client level** so all transactions are automatically attributed. Only use the per-transaction approach if you need conditional attribution.
+
+For Smart Wallets (EIP-5792 `sendCalls`): See [smart-wallets.md](smart-wallets.md) — pass via `capabilities`.
+
+### Step 5: Verify attribution
+
+1. **base.dev**: Check Onchain > Total Transactions for attribution counts
+2. **Block explorer**: Find tx hash, view input data, confirm last 16 bytes are `8021` repeating
+3. **Validation tool**: Use [builder-code-checker.vercel.app](https://builder-code-checker.vercel.app/)
+
+## Key Facts
+
+- Builder Codes are ERC-721 NFTs minted on Base
+- The suffix is appended to calldata; smart contracts ignore it (no upgrades needed)
+- Gas cost is negligible: 16 gas per non-zero byte
+- Analytics on base.dev currently support Smart Account (AA) transactions; EOA support is coming (attribution data is preserved)
+- The `dataSuffix` plugin in Privy appends to **all chains**, not just Base. If chain-specific behavior is needed, contact Privy
+- Privy's `dataSuffix` plugin is NOT yet supported with `@privy-io/wagmi` adapter
+
+## Finding Transaction Call Sites
+
+When retrofitting an existing project, search for these patterns:
+
+```bash
+# React hooks (Wagmi)
+grep -rn "useSendTransaction\|useSendCalls\|useWriteContract\|useContractWrite" src/
+
+# Viem client calls
+grep -rn "sendTransaction\|writeContract\|sendRawTransaction" src/
+
+# Privy embedded wallet calls
+grep -rn "sendTransaction\|signTransaction" src/
+
+# ethers.js
+grep -rn "signer\.sendTransaction\|contract\.connect" src/
+
+# Raw window.ethereum
+grep -rn "window\.ethereum\|eth_sendTransaction" src/
+```
+
+For client-level integration (Wagmi/Viem/Privy), you typically only need to modify the config file — individual transaction call sites remain unchanged.
diff --git a/skills/build-on-base/references/builder-codes/privy.md b/skills/build-on-base/references/builder-codes/privy.md
new file mode 100644
index 0000000..75a40c6
--- /dev/null
+++ b/skills/build-on-base/references/builder-codes/privy.md
@@ -0,0 +1,60 @@
+# Privy Integration
+
+Privy provides a `dataSuffix` plugin that automatically appends your Builder Code to **all** transactions, including EOA and ERC-4337 smart wallet user operations.
+
+## Requirements
+
+- `@privy-io/react-auth` >= v3.13.0
+- `ox` library installed
+
+## Setup
+
+Import the `dataSuffix` plugin and configure it in your `PrivyProvider`:
+
+```tsx
+import { PrivyProvider, dataSuffix } from "@privy-io/react-auth";
+import { Attribution } from "ox/erc8021";
+
+const ERC_8021_ATTRIBUTION_SUFFIX = Attribution.toDataSuffix({
+ codes: ["YOUR-BUILDER-CODE"],
+});
+
+function App() {
+ return (
+
+ {/* your app */}
+
+ );
+}
+```
+
+Once configured, **no changes** to individual transaction calls are needed.
+
+## How It Appends
+
+| Transaction Type | Where Suffix Goes |
+|---|---|
+| EOA transactions | `transaction.data` field |
+| Smart wallets (ERC-4337) | `userOp.callData` field |
+
+## Limitations
+
+- Appends suffix on **all chains**, not just Base. Contact Privy for chain-specific behavior.
+- NOT yet supported with the `@privy-io/wagmi` adapter. Use the native Privy provider instead.
+- If your project uses `@privy-io/wagmi`, you must either switch to the native Privy transaction flow or use the Wagmi client-level approach from [wagmi.md](wagmi.md).
+
+## Upgrading Privy
+
+If the project is on an older version:
+
+```bash
+npm install @privy-io/react-auth@latest
+```
+
+Verify version >= 3.13.0 before using the `dataSuffix` plugin.
diff --git a/skills/build-on-base/references/builder-codes/rpc.md b/skills/build-on-base/references/builder-codes/rpc.md
new file mode 100644
index 0000000..dd5a9d2
--- /dev/null
+++ b/skills/build-on-base/references/builder-codes/rpc.md
@@ -0,0 +1,117 @@
+# Standard Ethereum RPC / ethers.js Integration
+
+For projects using raw `window.ethereum`, `ethers.js`, or any standard EIP-1193 provider without a higher-level framework.
+
+## Requirements
+
+- `ox` library installed: `npm install ox`
+
+## Generate the dataSuffix
+
+Create a shared constant:
+
+```typescript
+import { Attribution } from "ox/erc8021";
+
+export const DATA_SUFFIX = Attribution.toDataSuffix({
+ codes: ["YOUR-BUILDER-CODE"],
+});
+```
+
+## ethers.js Integration
+
+### v6 (Recommended)
+
+```typescript
+import { ethers } from "ethers";
+import { DATA_SUFFIX } from "./attribution";
+
+const provider = new ethers.BrowserProvider(window.ethereum);
+const signer = await provider.getSigner();
+
+// Simple ETH transfer
+const tx = await signer.sendTransaction({
+ to: "0x...",
+ value: ethers.parseEther("0.01"),
+ data: DATA_SUFFIX,
+});
+```
+
+### Appending to existing calldata (contract calls)
+
+If the transaction already has `data`, concatenate the suffix after it:
+
+```typescript
+import { ethers } from "ethers";
+import { DATA_SUFFIX } from "./attribution";
+
+function withAttribution(data: string): string {
+ // data is a hex string starting with '0x'
+ return data + DATA_SUFFIX.slice(2); // strip '0x' from suffix before concatenating
+}
+
+const iface = new ethers.Interface(ABI);
+const calldata = iface.encodeFunctionData("transfer", [recipient, amount]);
+
+const tx = await signer.sendTransaction({
+ to: contractAddress,
+ data: withAttribution(calldata),
+});
+```
+
+### ethers v5
+
+```typescript
+import { ethers } from "ethers";
+import { DATA_SUFFIX } from "./attribution";
+
+const provider = new ethers.providers.Web3Provider(window.ethereum);
+const signer = provider.getSigner();
+
+const tx = await signer.sendTransaction({
+ to: "0x...",
+ value: ethers.utils.parseEther("0.01"),
+ data: DATA_SUFFIX,
+});
+```
+
+## Raw window.ethereum (EIP-1193)
+
+### Simple ETH transfer
+
+```typescript
+import { DATA_SUFFIX } from "./attribution";
+
+const accounts = await window.ethereum.request({ method: "eth_accounts" });
+
+const txHash = await window.ethereum.request({
+ method: "eth_sendTransaction",
+ params: [{
+ from: accounts[0],
+ to: "0x...",
+ value: "0x" + BigInt("10000000000000000").toString(16), // 0.01 ETH in wei hex
+ data: DATA_SUFFIX,
+ }],
+});
+```
+
+### With existing calldata
+
+```typescript
+import { DATA_SUFFIX } from "./attribution";
+
+const existingData = "0xabcdef..."; // your ABI-encoded contract call
+
+const txHash = await window.ethereum.request({
+ method: "eth_sendTransaction",
+ params: [{
+ from: accounts[0],
+ to: contractAddress,
+ data: existingData + DATA_SUFFIX.slice(2), // append without '0x' prefix
+ }],
+});
+```
+
+## How It Works
+
+`DATA_SUFFIX` is appended to the transaction's `data` field. Smart contracts process only the calldata they expect (ABI-encoded function selector + parameters) and ignore trailing bytes. Base's indexer reads the suffix to attribute the transaction to your builder code.
diff --git a/skills/build-on-base/references/builder-codes/smart-wallets.md b/skills/build-on-base/references/builder-codes/smart-wallets.md
new file mode 100644
index 0000000..8b0df56
--- /dev/null
+++ b/skills/build-on-base/references/builder-codes/smart-wallets.md
@@ -0,0 +1,65 @@
+# Smart Wallets (EIP-5792 / ERC-4337)
+
+For smart wallet transactions using `sendCalls` (EIP-5792), pass the `dataSuffix` via the `capabilities` object.
+
+## Wagmi useSendCalls
+
+```tsx
+import { useSendCalls } from "wagmi";
+import { parseEther } from "viem";
+import { Attribution } from "ox/erc8021";
+
+const DATA_SUFFIX = Attribution.toDataSuffix({
+ codes: ["YOUR-BUILDER-CODE"],
+});
+
+function App() {
+ const { sendCalls } = useSendCalls();
+
+ return (
+
+ );
+}
+```
+
+## Where the Suffix Goes
+
+| Wallet Type | Appended To |
+|---|---|
+| EOA (`sendTransaction`) | `transaction.data` |
+| Smart Wallet (`sendCalls`) | `userOp.callData` (not individual call data) |
+
+**Important**: For ERC-4337 user operations, the suffix is appended to the outer `callData` field of the UserOperation, not to individual call data within batched calls.
+
+## Wallet Support
+
+The connected wallet must support the `dataSuffix` capability via ERC-5792 `wallet_sendCalls`. Setting `optional: true` means the transaction proceeds even if the wallet doesn't support it.
+
+Currently supported by: Base Smart Wallet, Coinbase Wallet, and other ERC-5792 compliant wallets.
+
+## Client-Level Alternative
+
+If using Wagmi with `dataSuffix` in the config (see [wagmi.md](wagmi.md)), `useSendCalls` transactions are also attributed automatically without needing to pass `capabilities`.
+
+## Privy Smart Wallets
+
+If using Privy's embedded smart wallets, the `dataSuffix` plugin handles everything automatically. See [privy.md](privy.md). No need to manually pass capabilities.
diff --git a/skills/build-on-base/references/builder-codes/viem.md b/skills/build-on-base/references/builder-codes/viem.md
new file mode 100644
index 0000000..ea30e58
--- /dev/null
+++ b/skills/build-on-base/references/builder-codes/viem.md
@@ -0,0 +1,75 @@
+# Viem Integration
+
+Configure `dataSuffix` on your wallet client to automatically append your Builder Code to all transactions.
+
+## Requirements
+
+- `viem >= 2.45.0`
+- `ox` library installed
+
+## Client-Level Setup
+
+```typescript
+// client.ts
+import { createWalletClient, http } from "viem";
+import { base } from "viem/chains";
+import { Attribution } from "ox/erc8021";
+
+const DATA_SUFFIX = Attribution.toDataSuffix({
+ codes: ["YOUR-BUILDER-CODE"],
+});
+
+export const walletClient = createWalletClient({
+ chain: base,
+ transport: http(),
+ dataSuffix: DATA_SUFFIX,
+});
+```
+
+All transactions through this client are automatically attributed:
+
+```typescript
+import { parseEther } from "viem";
+import { walletClient } from "./client";
+
+const hash = await walletClient.sendTransaction({
+ to: "0x...",
+ value: parseEther("0.01"),
+});
+```
+
+## Per-Transaction Override
+
+```typescript
+const hash = await walletClient.sendTransaction({
+ to: "0x...",
+ value: parseEther("0.01"),
+ dataSuffix: DATA_SUFFIX,
+});
+```
+
+## Server-Side / Agent Usage
+
+For backend agents or bots using viem directly with a private key:
+
+```typescript
+import { createWalletClient, http } from "viem";
+import { privateKeyToAccount } from "viem/accounts";
+import { base } from "viem/chains";
+import { Attribution } from "ox/erc8021";
+
+const DATA_SUFFIX = Attribution.toDataSuffix({
+ codes: ["YOUR-BUILDER-CODE"],
+});
+
+const account = privateKeyToAccount("0x...");
+
+const walletClient = createWalletClient({
+ account,
+ chain: base,
+ transport: http(),
+ dataSuffix: DATA_SUFFIX,
+});
+```
+
+This is the typical pattern for AI agent wallets that transact on behalf of users.
diff --git a/skills/build-on-base/references/builder-codes/wagmi.md b/skills/build-on-base/references/builder-codes/wagmi.md
new file mode 100644
index 0000000..897db2b
--- /dev/null
+++ b/skills/build-on-base/references/builder-codes/wagmi.md
@@ -0,0 +1,96 @@
+# Wagmi Integration
+
+Configure `dataSuffix` at the Wagmi client level to automatically append your Builder Code to all transactions.
+
+## Requirements
+
+- `wagmi` with `viem >= 2.45.0`
+- `ox` library installed
+
+## Client-Level Setup (Recommended)
+
+Add `dataSuffix` to your Wagmi config. All transactions via `useSendTransaction`, `useWriteContract`, and `useSendCalls` will automatically include attribution.
+
+```typescript
+// config.ts
+import { createConfig, http } from "wagmi";
+import { base } from "wagmi/chains";
+import { Attribution } from "ox/erc8021";
+
+const DATA_SUFFIX = Attribution.toDataSuffix({
+ codes: ["YOUR-BUILDER-CODE"],
+});
+
+export const config = createConfig({
+ chains: [base],
+ transports: {
+ [base.id]: http(),
+ },
+ dataSuffix: DATA_SUFFIX,
+});
+```
+
+With this in place, hooks work unchanged:
+
+```tsx
+import { useSendTransaction } from "wagmi";
+import { parseEther } from "viem";
+
+function SendButton() {
+ const { sendTransaction } = useSendTransaction();
+ return (
+
+ );
+}
+```
+
+## Per-Transaction Override (If Needed)
+
+For conditional attribution, pass `dataSuffix` directly on individual calls:
+
+### useSendTransaction
+
+```tsx
+sendTransaction({
+ to: "0x...",
+ value: parseEther("0.01"),
+ dataSuffix: DATA_SUFFIX,
+});
+```
+
+### useSendCalls (EIP-5792 / Smart Wallets)
+
+```tsx
+sendCalls({
+ calls: [{ to: "0x...", value: parseEther("1") }],
+ capabilities: {
+ dataSuffix: {
+ value: DATA_SUFFIX,
+ optional: true,
+ },
+ },
+});
+```
+
+See [smart-wallets.md](smart-wallets.md) for more on `useSendCalls` and EIP-5792.
+
+## Multi-Chain Configs
+
+If your config includes multiple chains, `dataSuffix` applies to all of them. This is fine — only Base's indexer reads the suffix.
+
+```typescript
+export const config = createConfig({
+ chains: [base, mainnet, optimism],
+ transports: {
+ [base.id]: http(),
+ [mainnet.id]: http(),
+ [optimism.id]: http(),
+ },
+ dataSuffix: DATA_SUFFIX,
+});
+```
diff --git a/skills/build-on-base/references/deploy-contracts.md b/skills/build-on-base/references/deploy-contracts.md
new file mode 100644
index 0000000..8d9c2eb
--- /dev/null
+++ b/skills/build-on-base/references/deploy-contracts.md
@@ -0,0 +1,144 @@
+# Deploying Contracts on Base
+
+## Prerequisites
+
+1. Configure RPC endpoint (testnet: `sepolia.base.org`, mainnet: `mainnet.base.org`)
+2. Store private keys in Foundry's encrypted keystore — **never commit keys**
+3. [Obtain testnet ETH](#obtaining-testnet-eth-via-cdp-faucet) from CDP faucet (testnet only)
+4. [Get a BaseScan API key](#obtaining-a-basescan-api-key) for contract verification
+
+## Security
+
+- **Never commit private keys** to version control — use Foundry's encrypted keystore (`cast wallet import`)
+- **Never hardcode API keys** in source files — use environment variables or `foundry.toml` with `${ENV_VAR}` references
+- **Never expose `.env` files** — add `.env` to `.gitignore`
+- **Use production RPC providers** (not public endpoints) for mainnet deployments to avoid rate limits and data leaks
+- **Verify contracts on BaseScan** to enable public audit of deployed code
+
+## Input Validation
+
+Before constructing shell commands, validate all user-provided values:
+
+- **contract-path**: Must match `^[a-zA-Z0-9_/.-]+\.sol:[a-zA-Z0-9_]+$`. Reject paths with spaces, semicolons, pipes, or backticks.
+- **rpc-url**: Must be a valid HTTPS URL (`^https://[^\s;|&]+$`). Reject non-HTTPS or malformed URLs.
+- **keystore-account**: Must be alphanumeric with hyphens/underscores (`^[a-zA-Z0-9_-]+$`).
+- **etherscan-api-key**: Must be alphanumeric (`^[a-zA-Z0-9]+$`).
+
+Do not pass unvalidated user input into shell commands.
+
+## Obtaining Testnet ETH via CDP Faucet
+
+Testnet ETH is required to pay gas on Base Sepolia. Use the [CDP Faucet](https://portal.cdp.coinbase.com/products/faucet) to claim it. Supported tokens: ETH, USDC, EURC, cbBTC. ETH claims are capped at 0.0001 ETH per claim, 1000 claims per 24 hours.
+
+### Option A: CDP Portal UI (recommended for quick setup)
+
+> **Agent behavior:** If you have browser access, navigate to the portal and claim directly. Otherwise, ask the user to complete these steps and provide the funded wallet address.
+
+1. Sign in to [CDP Portal](https://portal.cdp.coinbase.com/signin) (create an account at [portal.cdp.coinbase.com/create-account](https://portal.cdp.coinbase.com/create-account) if needed)
+2. Go to [Faucets](https://portal.cdp.coinbase.com/products/faucet)
+3. Select **Base Sepolia** network
+4. Select **ETH** token
+5. Enter the wallet address and click **Claim**
+6. Verify on [sepolia.basescan.org](https://sepolia.basescan.org) that the funds arrived
+
+### Option B: Programmatic via CDP SDK
+
+Requires a [CDP API key](https://portal.cdp.coinbase.com/projects/api-keys) and [Wallet Secret](https://portal.cdp.coinbase.com/products/server-wallets).
+
+```bash
+npm install @coinbase/cdp-sdk dotenv
+```
+
+```typescript
+import { CdpClient } from "@coinbase/cdp-sdk";
+import dotenv from "dotenv";
+dotenv.config();
+
+const cdp = new CdpClient();
+const account = await cdp.evm.createAccount();
+
+const faucetResponse = await cdp.evm.requestFaucet({
+ address: account.address,
+ network: "base-sepolia",
+ token: "eth",
+});
+
+console.log(`Funded: https://sepolia.basescan.org/tx/${faucetResponse.transactionHash}`);
+```
+
+Environment variables needed in `.env`:
+
+```
+CDP_API_KEY_ID=your-api-key-id
+CDP_API_KEY_SECRET=your-api-key-secret
+CDP_WALLET_SECRET=your-wallet-secret
+```
+
+To fund an **existing** wallet instead of creating a new one, pass its address directly to `requestFaucet`.
+
+## Obtaining a BaseScan API Key
+
+A BaseScan API key is required for the `--verify` flag to auto-verify contracts on BaseScan. BaseScan uses the same account system as Etherscan.
+
+> **Agent behavior:** If you have browser access, navigate to the BaseScan site and create the key. Otherwise, ask the user to complete these steps and provide the API key.
+
+1. Go to [basescan.org/myapikey](https://basescan.org/apidashboard) (or [etherscan.io/myapikey](https://etherscan.io/apidashboard) — same account works)
+2. Sign in or create a free account
+3. Click **Add** to create a new API key
+4. Copy the key and set it in your environment:
+
+```bash
+export ETHERSCAN_API_KEY=your-basescan-api-key
+```
+
+Alternatively, pass it directly to forge:
+
+```bash
+forge create ... --etherscan-api-key
+```
+
+Or add it to `foundry.toml`:
+
+```toml
+[etherscan]
+base-sepolia = { key = "${ETHERSCAN_API_KEY}", url = "https://api-sepolia.basescan.org/api" }
+base = { key = "${ETHERSCAN_API_KEY}", url = "https://api.basescan.org/api" }
+```
+
+## Deployment Commands
+
+### Testnet
+
+```bash
+forge create src/MyContract.sol:MyContract \
+ --rpc-url https://sepolia.base.org \
+ --account \
+ --verify \
+ --etherscan-api-key $ETHERSCAN_API_KEY
+```
+
+### Mainnet
+
+```bash
+forge create src/MyContract.sol:MyContract \
+ --rpc-url https://mainnet.base.org \
+ --account \
+ --verify \
+ --etherscan-api-key $ETHERSCAN_API_KEY
+```
+
+## Key Notes
+
+- Contract format: `:`
+- `--verify` flag auto-verifies on BaseScan (requires API key)
+- Explorers: basescan.org (mainnet), sepolia.basescan.org (testnet)
+- CDP Faucet docs: [docs.cdp.coinbase.com/faucets](https://docs.cdp.coinbase.com/faucets/introduction/quickstart)
+
+## Common Issues
+
+| Error | Cause |
+|-------|-------|
+| `nonce has already been used` | Node sync incomplete |
+| Transaction fails | Insufficient ETH for gas — [claim from faucet](#obtaining-testnet-eth-via-cdp-faucet) |
+| Verification fails | Wrong RPC endpoint for target network |
+| Verification 403/unauthorized | Missing or invalid BaseScan API key |
diff --git a/skills/build-on-base/references/migrations/farcaster-miniapp-to-app.md b/skills/build-on-base/references/migrations/farcaster-miniapp-to-app.md
new file mode 100644
index 0000000..9aeb2ac
--- /dev/null
+++ b/skills/build-on-base/references/migrations/farcaster-miniapp-to-app.md
@@ -0,0 +1,790 @@
+# Convert Farcaster Miniapp to Base App
+
+Convert a Farcaster miniapp into a normal app on Base. The default outcome is a regular web app that works in the Base app browser and on the open web, with Farcaster Mini App host coupling removed.
+
+If some Farcaster functionality truly needs to survive, keep it separate from the main app surface. Prefer read-only data first. Only preserve Mini App-specific behavior when the user explicitly insists, and isolate it behind a dedicated route or page rather than carrying it through the whole app.
+
+## Core Principle
+
+Always separate these decisions:
+
+1. Remove Mini App host/runtime coupling.
+2. Decide whether any Farcaster-facing functionality should remain.
+3. If something remains, keep it isolated from the main app and avoid introducing new vendor dependencies by default.
+
+Do **not** automatically turn "keep some Farcaster functionality" into "migrate to Neynar." If a project already uses Neynar and the user wants to keep an isolated Farcaster-only area, you may preserve that existing integration. Do not introduce new Neynar adoption as the default recommendation.
+
+## Quick Start
+
+Follow these five phases sequentially:
+
+0. **Discovery** — Quick scan + quiz to choose a path
+1. **Analysis** — Detailed read-only analysis scoped to the chosen path
+2. **Conversion** — Remove Mini App SDK patterns and isolate any intentionally preserved Farcaster surface
+3. **Cleanup** — Remove dead code, env vars, and dependencies
+4. **Verification** — Type check, build, and summarize
+
+## Conversion Paths
+
+The quiz should route the user into one of two paths:
+
+| Path | Name | Who it's for | What happens |
+|------|------|-------------|-------------|
+| **A** | Regular App Default | Most projects | Strip Farcaster Mini App coupling and become a normal Base/web app |
+| **B** | Isolated Farcaster Surface | The app still needs a small Farcaster-specific area | Convert the main app into a normal app, then keep only a separate Farcaster route/page for the remaining functionality |
+
+`Path B` is still biased toward removing complexity:
+- Prefer **read-only** Farcaster data.
+- Avoid preserving Mini App host/runtime behavior unless the user explicitly asks for it.
+- Keep any preserved Farcaster logic out of the main app shell, shared providers, and primary auth flow.
+
+---
+
+## Phase 0: Discovery
+
+### 0A. Quick Scan (automated, no user interaction)
+
+Run a lightweight scan before asking questions. Produce an internal tally:
+
+1. **Detect framework** from `package.json` (`next`, `vite`, `react-scripts`, `@remix-run/*`)
+2. **Count Farcaster packages** in `dependencies` and `devDependencies`
+3. **Grep source files** (`.ts`, `.tsx`, `.js`, `.jsx`) for:
+ - `sdk.actions.*` calls (count total)
+ - `sdk.quickAuth` usage (yes/no)
+ - `sdk.context` usage (yes/no)
+ - `.well-known/farcaster.json` (yes/no)
+ - `farcasterMiniApp` / `miniAppConnector` connector (yes/no)
+ - Total files with any `@farcaster/` import
+ - `@neynar/` imports or `api.neynar.com` fetch calls (yes/no)
+4. **Identify the blast radius**:
+ - Are Farcaster references spread across the main app shell?
+ - Are they already mostly confined to a route like `app/farcaster/`, `pages/farcaster/`, or a small set of components?
+ - Are there obvious host-only features such as haptics, notifications, `openMiniApp`, or `sdk.context.client`?
+
+Use this tally to inform quiz suggestions. Do **not** dump raw scan output to the user before asking the quiz.
+
+### 0B. Interactive Quiz
+
+Ask these questions one at a time. Use the quick scan results to suggest the most likely answer.
+
+**Q1** (always ask):
+
+> Based on my scan, your project has [X] files using the Farcaster SDK with [summary of what is used].
+>
+> Which outcome do you want?
+> - **(a) Regular app everywhere** — remove Farcaster-specific behavior and just keep a normal Base/web app
+> - **(b) Regular app first, plus a separate Farcaster area** — keep the main app clean, but preserve a small isolated route/page if really needed
+
+**Q2** (always ask):
+
+> How deeply is the Mini App SDK used today?
+> - **(a) Minimal** — mostly `sdk.actions.ready()` and a few helpers
+> - **(b) Moderate** — some `context`, `openUrl`, profile links, or conditional `isInMiniApp` logic
+> - **(c) Heavy** — auth, wallet connector, notifications, compose flows, or host-specific behavior
+
+**Q3** (ask if Q1 = b):
+
+> What is the smallest Farcaster feature set you actually need to preserve?
+> - **(a) Read-only only** — profile or cast display, links out to Farcaster profiles, maybe a small social page
+> - **(b) Some Farcaster-specific interactions** — there is a separate page/path that still needs more than read-only behavior
+> - **(c) Not sure** — analyze what is isolated already and recommend the smallest keep-surface possible
+
+**Q4** (ask if Q1 = b and there is existing isolated Farcaster code or existing Neynar usage):
+
+> Does the project already have an isolated Farcaster-only route/page or integration that you want to keep as-is if possible?
+> - **(a) Yes** — preserve only that isolated surface
+> - **(b) No** — prefer removing it unless there is a very strong reason to keep it
+
+**Q5** (ask if quick auth or other Mini App auth is present):
+
+> After conversion, what should the main app use for authentication?
+> - **(a) SIWE** — wallet-based auth for the regular app
+> - **(b) Existing non-Farcaster auth** — keep whatever normal web auth already exists
+> - **(c) No auth** — remove auth entirely
+
+### 0C. Path Selection
+
+Map answers to a path:
+
+| Desired outcome | Typical result |
+|-----------------|----------------|
+| `Q1 = regular app everywhere` | **Path A** — Regular App Default |
+| `Q1 = regular app first, plus separate Farcaster area` | **Path B** — Isolated Farcaster Surface |
+
+Then tighten the recommendation:
+
+- If the user chose `Path B`, prefer **read-only preservation** unless they explicitly require something else.
+- If the scan shows heavy host/runtime coupling but the user wants `Path A`, warn them that some features will be deleted rather than recreated.
+- If the project already uses Neynar, only keep it if it remains inside the isolated Farcaster surface. Do not expand it into the main app.
+
+Announce the chosen path:
+
+> Based on your answers, I'll use **Path [X]: [Name]**. This will [one-sentence description]. I'll now do a detailed analysis of your project.
+
+Record the quiz answers internally. They guide whether the agent should:
+- fully remove Farcaster features
+- preserve only a read-only isolated surface
+- quarantine any unavoidable Farcaster-specific logic to a dedicated route/page
+
+**Proceed to Phase 1.**
+
+---
+
+## Phase 1: Analysis (Read-Only)
+
+### 1A. Detect Framework
+
+Read `package.json`:
+- `next` → Next.js
+- `vite` → Vite
+- `react-scripts` → Create React App
+- `@remix-run/*` → Remix
+
+### 1B. Scan for Farcaster Dependencies
+
+List all packages matching:
+- `@farcaster/miniapp-sdk`, `@farcaster/miniapp-core`, `@farcaster/miniapp-wagmi-connector`
+- `@farcaster/frame-sdk`, `@farcaster/frame-wagmi-connector`
+- `@farcaster/quick-auth`, `@farcaster/auth-kit`
+- `@neynar/*` (compatibility only; do not assume it stays)
+
+### 1C. Grep for Farcaster Code
+
+Search source files (`.ts`, `.tsx`, `.js`, `.jsx`) for:
+
+**SDK imports:**
+```
+@farcaster/miniapp-sdk
+@farcaster/miniapp-core
+@farcaster/miniapp-wagmi-connector
+@farcaster/frame-sdk
+@farcaster/frame-wagmi-connector
+@farcaster/quick-auth
+@farcaster/auth-kit
+@neynar/
+```
+
+**SDK calls:**
+```
+sdk.actions.ready
+sdk.actions.openUrl
+sdk.actions.close
+sdk.actions.composeCast
+sdk.actions.addMiniApp
+sdk.actions.requestWalletAddress
+sdk.actions.viewProfile
+sdk.actions.viewToken
+sdk.actions.sendToken
+sdk.actions.swapToken
+sdk.actions.signIn
+sdk.actions.setPrimaryButton
+sdk.actions.openMiniApp
+sdk.quickAuth
+sdk.context
+sdk.isInMiniApp
+sdk.getCapabilities
+sdk.haptics
+sdk.back
+sdk.wallet
+```
+
+**Connectors & providers:**
+```
+farcasterMiniApp()
+miniAppConnector()
+farcasterFrame()
+MiniAppProvider
+MiniAppContext
+useMiniApp
+useMiniAppContext
+```
+
+**Manifest & meta:**
+```
+.well-known/farcaster.json
+fc:miniapp
+fc:frame
+```
+
+**Environment variables:**
+```
+NEYNAR_API_KEY
+NEXT_PUBLIC_NEYNAR_CLIENT_ID
+FARCASTER_
+FC_
+```
+
+### 1D. Check Existing Web3 Setup
+
+Look for:
+- `coinbaseWallet` connector in wagmi config
+- SIWE / `siwe` package usage
+- `connectkit`, `rainbowkit`, or `@coinbase/onchainkit`
+- Existing wallet connection UI
+
+### 1E. Check Separation Boundaries
+
+Map where Farcaster logic currently lives:
+
+- Root providers or app shell
+- Shared hooks or auth middleware
+- One-off components
+- Dedicated routes/pages like `app/farcaster/*`
+- Server routes used only by Farcaster functionality
+
+Mark each location with one action:
+- **remove**
+- **stub**
+- **move behind isolated route/page**
+- **keep only as read-only**
+
+### 1F. Report Findings
+
+Create a path-scoped summary.
+
+**All paths include:**
+```
+## Conversion Analysis — Path [X]: [Name]
+
+**Framework:** [detected]
+**Farcaster packages:** [list]
+**Files with Farcaster code:** [count]
+
+### Wagmi Connector
+- File: [path]
+- Current connector: [farcasterMiniApp / miniAppConnector / farcasterFrame / none]
+- Other connectors: [list]
+- Action: [replace with coinbaseWallet / leave existing wallet setup / remove only]
+
+### MiniApp Provider
+- File: [path]
+- Pattern: [simple / complex]
+- Consumers: [files importing from this]
+- Action: [stub / remove / isolate]
+
+### SDK Action Calls
+[list each: file, what it does, action]
+
+### Manifest & Meta
+- Manifest route: [path or N/A]
+- Meta tags: [file or N/A]
+```
+
+**Path A additionally includes:**
+```
+### Main App Outcome
+- Action: remove Farcaster-specific UI and flows from the main app entirely
+
+### Authentication
+- Quick Auth used: [yes/no, file]
+- Action: replace with SIWE / keep existing non-Farcaster auth / remove
+
+### Existing Neynar Usage
+- Package or files: [list or N/A]
+- Action: remove entirely unless the user later re-scopes to Path B
+
+### Environment Variables
+[list all FC/Neynar vars that will be removed]
+```
+
+**Path B additionally includes:**
+```
+### Main App Outcome
+- Action: convert the main app into a normal web app first
+
+### Isolated Farcaster Surface
+- Route/page to keep: [path or proposed path]
+- Scope: [read-only / mixed / host-specific]
+- Recommended target scope: [prefer read-only / quarantine existing behavior / remove]
+
+### Authentication
+- Quick Auth used: [yes/no, file]
+- Main app action: replace with SIWE / keep existing non-Farcaster auth / remove
+- Isolated Farcaster surface action: [remove auth coupling / preserve existing isolated flow only if explicitly requested]
+
+### Existing Neynar Usage
+- Package or files: [list or N/A]
+- Action: [remove / keep only inside isolated surface]
+
+### Environment Variables
+- Remove from main app: [FC_*, FARCASTER_*, etc.]
+- Keep only if isolated surface truly still needs them: [NEYNAR_API_KEY, etc. or N/A]
+```
+
+**All paths end with:**
+```
+### Potential Issues
+- [ ] FID used as database primary key
+- [ ] Farcaster colors in tailwind config
+- [ ] `isInMiniApp` branches with unique else logic
+- [ ] Components only meaningful inside Farcaster
+- [ ] Farcaster code mixed into shared providers or root layout
+```
+
+Ask:
+
+> Does this analysis look correct? Ready to proceed with conversion?
+
+**STOP and wait for user confirmation before Phase 2.**
+
+---
+
+## Phase 2: Conversion
+
+Steps are organized by feature area. Each step notes which paths it applies to and what to do differently for `Path B`.
+
+### 2A. Wagmi Connector (All Paths)
+
+Find the wagmi config file (`lib/wagmi.ts`, `config/wagmi.ts`, `providers/wagmi-provider.tsx`, etc.):
+
+1. Remove import of `farcasterMiniApp` or `miniAppConnector` from `@farcaster/miniapp-wagmi-connector`
+2. Remove the `farcasterMiniApp()` / `miniAppConnector()` call from the `connectors` array
+3. If no wallet connector remains, add:
+ ```typescript
+ import { coinbaseWallet } from "wagmi/connectors";
+
+ coinbaseWallet({ appName: "" })
+ ```
+4. If `coinbaseWallet` already exists, leave it as-is
+5. Clean up empty lines and stale imports
+
+Skip this step if wagmi is not in the project.
+
+### 2B. MiniApp Provider / Context (All Paths)
+
+If the app has a shared Mini App provider, remove host/runtime assumptions from the main app.
+
+**Pattern A: simple provider**
+
+Replace with a safe stub:
+
+```tsx
+'use client'
+
+import React, { createContext, useContext, useMemo } from "react";
+
+interface MiniAppContextType {
+ context: undefined;
+ ready: boolean;
+ isInMiniApp: boolean;
+}
+
+const MiniAppContext = createContext(undefined);
+
+export function useMiniAppContext() {
+ const context = useContext(MiniAppContext);
+ if (context === undefined) {
+ throw new Error("useMiniAppContext must be used within a MiniAppProvider");
+ }
+ return context;
+}
+
+export default function MiniAppProvider({ children }: { children: React.ReactNode }) {
+ const value = useMemo(
+ () => ({
+ context: undefined,
+ ready: true,
+ isInMiniApp: false,
+ }),
+ []
+ );
+
+ return {children};
+}
+```
+
+Preserve export style and hook names so consumers do not break.
+
+**Pattern B: complex provider**
+
+- If many consumers depend on it, stub it first.
+- If only a few files use it, remove it and update those imports directly.
+- In `Path B`, do not let the isolated Farcaster surface keep this provider wired through the root app shell. If needed, make it local to the isolated route only.
+
+### 2C. Authentication
+
+The main app should use normal web auth, not Mini App auth.
+
+**Default rule for both paths:**
+- If `sdk.quickAuth.getToken()` is used, replace it with SIWE or remove it.
+- If a normal non-Farcaster auth system already exists, prefer that over adding new auth.
+- Do not introduce new Farcaster or Neynar auth as the default conversion target.
+
+#### SIWE Replacement Pattern
+
+**Client-side** (e.g. `useSignIn.ts`):
+- Remove `import sdk from "@farcaster/miniapp-sdk"`
+- Remove `sdk.quickAuth.getToken()`
+- Replace with:
+ 1. Get wallet address from `useAccount()` (wagmi)
+ 2. Create a SIWE message with `siwe`
+ 3. Sign with `useSignMessage()` (wagmi)
+ 4. Send signature + message to the backend for verification
+
+**Server-side** (e.g. `app/api/auth/sign-in/route.ts`):
+- Remove `@farcaster/quick-auth` verification
+- Replace with SIWE verification:
+ 1. Parse the SIWE message
+ 2. Verify the signature with `siwe` or `viem`
+ 3. Use recovered wallet address as the normal app identity
+
+**If FID is used as a database primary key:**
+- Do not auto-change schema
+- Add a TODO comment for later migration
+- Warn in Phase 4 summary
+
+**Path B note:**
+- If the isolated Farcaster surface already has its own auth or integration flow and the user explicitly wants to keep it, quarantine it there only.
+- Do not let that flow remain the default app-wide auth system.
+
+### 2D. SDK Action Calls
+
+#### 2D-1. Main replacements for both paths
+
+| Original | Replacement |
+|----------|-------------|
+| `sdk.actions.ready()` | Remove entirely |
+| `sdk.actions.openUrl(url)` | `window.open(url, "_blank")` |
+| `sdk.actions.close()` | `window.close()` or remove |
+| `sdk.actions.composeCast(...)` | Remove from the main app |
+| `sdk.actions.addMiniApp()` | Remove call and UI |
+| `sdk.actions.requestWalletAddress()` | Remove; wagmi handles wallet access |
+| `sdk.actions.viewProfile(fid)` | `window.open(\`https://warpcast.com/~/profiles/${fid}\`, "_blank")` |
+| `sdk.actions.viewToken(opts)` | `window.open(\`https://basescan.org/token/${opts.token}\`, "_blank")` |
+| `sdk.actions.sendToken(opts)` | Replace with wagmi flow if the feature matters, otherwise remove |
+| `sdk.actions.swapToken(opts)` | Remove call and UI unless there is a real app-specific swap feature outside Farcaster |
+| `sdk.actions.signIn(...)` | Remove; auth handled by normal web auth |
+| `sdk.actions.setPrimaryButton(...)` | Remove entirely |
+| `sdk.actions.openMiniApp(...)` | Remove from the main app |
+| `sdk.isInMiniApp()` | Remove conditional and keep the non-Farcaster branch |
+| `sdk.context` | Remove from the main app |
+| `sdk.getCapabilities()` | Remove or replace with `async () => []` |
+| `sdk.haptics.*` | Remove entirely |
+| `sdk.back.*` | Remove entirely |
+| `sdk.wallet.*` | Remove; wagmi handles wallet access |
+
+For conditional `isInMiniApp` branches:
+
+```tsx
+// BEFORE
+if (isInMiniApp) {
+ sdk.actions.openUrl(url);
+} else {
+ window.open(url, "_blank");
+}
+
+// AFTER
+window.open(url, "_blank");
+```
+
+Always keep the normal web branch.
+
+#### 2D-2. Path B overrides
+
+`Path B` does **not** mean "recreate everything." It means "keep the main app clean and preserve the smallest separate Farcaster surface possible."
+
+- `sdk.context`
+ - Remove from the main app
+ - For the isolated surface, prefer replacing it with read-only fetched data or explicit route params
+ - Remove `context.location`, `context.client`, safe area, and other host-derived assumptions unless the user explicitly insists on preserving a host-specific page
+
+- `sdk.actions.composeCast(...)`
+ - Remove from the main app
+ - If the user only needs read-only, delete it entirely
+ - If they insist on preserving it, keep it isolated behind a dedicated page/path and flag it as a manual follow-up rather than rebuilding it by default
+
+- `sdk.actions.openMiniApp(...)`
+ - Remove from the main app
+ - Only keep it in an isolated route if the user explicitly wants a Farcaster-only surface
+
+- notifications / haptics / host buttons
+ - Remove from the main app
+ - Preserve only if the isolated route truly depends on them and the user has explicitly opted into that complexity
+
+### 2E. Optional Read-Only Farcaster Data (Path B only)
+
+If the user wants an isolated Farcaster surface, prefer **read-only** data first.
+
+**Create `lib/farcaster-readonly.ts`** (or equivalent) only if the app needs it:
+
+```typescript
+const HUB_URL = "https://hub.farcaster.xyz";
+
+export async function getUserData(fid: number) {
+ const res = await fetch(`${HUB_URL}/v1/userDataByFid?fid=${fid}`);
+ if (!res.ok) throw new Error(`Hub user data fetch failed: ${res.status}`);
+ return res.json();
+}
+
+export async function getCastsByFid(fid: number, pageSize = 25) {
+ const res = await fetch(`${HUB_URL}/v1/castsByFid?fid=${fid}&pageSize=${pageSize}`);
+ if (!res.ok) throw new Error(`Hub casts fetch failed: ${res.status}`);
+ return res.json();
+}
+```
+
+Then:
+- Keep these calls inside the isolated route/page only
+- Do not thread Farcaster data requirements through the main app shell
+- If the project already has a small isolated Neynar-based read-only integration, you may keep it only if removing it would create more churn than it saves
+- Do not add new Neynar packages for this by default
+
+### 2F. Manifest Route (All Paths)
+
+Delete `.well-known/farcaster.json` route:
+- `app/.well-known/farcaster.json/route.ts`
+- `public/.well-known/farcaster.json`
+- `api/farcaster-manifest.ts` or similar helpers
+
+If the `.well-known` directory becomes empty, delete it.
+
+### 2G. Meta Tags (All Paths)
+
+In root layout or metadata files, remove:
+- `` tags with `property="fc:miniapp*"` or `property="fc:frame*"`
+- `Metadata.other` entries that only exist for Farcaster tags
+- `generateMetadata` logic that only produces Mini App metadata
+
+### 2H. Dependencies
+
+**All paths — remove:**
+- `@farcaster/miniapp-sdk`, `@farcaster/miniapp-wagmi-connector`, `@farcaster/miniapp-core`
+- `@farcaster/frame-sdk`, `@farcaster/frame-wagmi-connector`
+- `@farcaster/quick-auth`, `@farcaster/auth-kit`
+
+**Path A — also remove:**
+- `@neynar/nodejs-sdk`, `@neynar/react`
+- any other Neynar packages or helpers
+
+**Path B — remove by default:**
+- `@neynar/nodejs-sdk`, `@neynar/react`, and Neynar helpers unless they remain inside the intentionally isolated Farcaster surface
+
+**All paths — add only if truly needed:**
+- `siwe` if SIWE auth is introduced and not already present
+
+Do not add new Neynar packages as part of the default conversion.
+
+### 2I. Farcaster-Specific Routes & Components
+
+**Path A:**
+- Delete `app/farcaster/`, `pages/farcaster/`, and Farcaster-only components entirely
+- Delete Farcaster-only API routes such as `/api/farcaster/*` and `/api/neynar/*`
+- Remove any navigation links that point to deleted routes
+
+**Path B:**
+- Keep the main app route tree clean
+- Move preserved Farcaster UI behind one dedicated route/page if it is not already isolated
+- Prefer names like `app/farcaster/` or `app/social/` over spreading Farcaster logic throughout generic shared pages
+- Remove any component that has no purpose outside that isolated surface
+- Keep any remaining Neynar usage, if any, confined to that isolated route/page and its server helpers only
+
+---
+
+## Phase 3: Cleanup
+
+### 3A. Update `package.json`
+
+**All paths — remove Mini App packages:**
+- `@farcaster/miniapp-sdk`, `@farcaster/miniapp-wagmi-connector`, `@farcaster/miniapp-core`
+- `@farcaster/frame-sdk`, `@farcaster/frame-wagmi-connector`
+- `@farcaster/quick-auth`, `@farcaster/auth-kit`
+
+**Path A — also remove:**
+- all `@neynar/*` packages
+
+**Path B — remove unless still isolated and intentionally preserved:**
+- `@neynar/*`
+
+**All paths — add if introduced:**
+- `siwe`
+
+### 3B. Environment Variables
+
+**Path A — remove from all `.env*` files:**
+- `NEYNAR_API_KEY`
+- `NEXT_PUBLIC_NEYNAR_CLIENT_ID`
+- `FARCASTER_*`
+- `FC_*`
+- `NEXT_PUBLIC_FC_*`
+- `NEXT_PUBLIC_FARCASTER_*`
+
+**Path B — remove from the main app by default:**
+- `FARCASTER_*`
+- `FC_*`
+- `NEXT_PUBLIC_FC_*`
+- `NEXT_PUBLIC_FARCASTER_*`
+
+Only keep `NEYNAR_*` vars if the isolated surface explicitly still depends on existing Neynar integration.
+
+Also update env validation schemas (`env.ts`, `env.mjs`, zod schemas, etc.).
+
+### 3C. Dead Code Cleanup
+
+- Remove unused imports from modified files
+- Remove unused Farcaster types and helper functions
+- Remove empty import statements
+- Remove dead hooks or API wrappers that only existed for the Mini App SDK
+
+### 3D. Tailwind Colors
+
+If `tailwind.config.ts` or `tailwind.config.js` includes Farcaster brand colors such as `farcaster: "#8B5CF6"`, remove them unless that branding is intentionally kept inside an isolated Farcaster-only surface.
+
+### 3E. Install Dependencies
+
+Tell the user:
+
+```bash
+npm install
+```
+
+---
+
+## Phase 4: Verification
+
+### 4A. Search for Remaining References
+
+**All paths — search for:**
+```
+@farcaster
+farcasterMiniApp
+miniAppConnector
+sdk.actions
+sdk.quickAuth
+sdk.context
+fc:miniapp
+fc:frame
+```
+
+**Path A — also search for:**
+```
+@neynar
+NEYNAR_API_KEY
+NEXT_PUBLIC_NEYNAR_CLIENT_ID
+api.neynar.com
+```
+
+**Path B — if Neynar was intentionally preserved:**
+- verify that remaining `@neynar` imports and env vars exist only inside the isolated Farcaster surface and its server helpers
+
+Report any source matches, ignoring `node_modules`, lock files, and git history.
+
+### 4B. Type Check
+
+```bash
+npx tsc --noEmit
+```
+
+Report and fix type errors.
+
+### 4C. Build Check
+
+```bash
+npm run build
+```
+
+Report and fix build errors.
+
+### 4D. Conversion Summary
+
+```
+## Conversion Complete — Path [X]: [Name]
+
+**Files modified:** [count]
+**Files deleted:** [count]
+**Files created:** [count, if any]
+**Packages removed:** [list]
+**Packages added:** [list, if any]
+
+### What was done
+- [x] Removed Farcaster Mini App wagmi connector
+- [x] Stubbed or removed Mini App provider/context
+- [x] Replaced Mini App auth with normal web auth or removed it
+- [x] Removed or replaced SDK action calls
+- [x] Deleted manifest route
+- [x] Removed Farcaster meta tags
+- [x] Cleaned up dependencies and env vars
+```
+
+**Path B summary additionally includes:**
+```
+- [x] Kept the main app as a normal web app
+- [x] Confined remaining Farcaster functionality to a dedicated route/page
+- [x] Preferred read-only Farcaster data where possible
+- [x] Removed Farcaster host/runtime coupling from shared app infrastructure
+```
+
+**All paths end with:**
+```
+### Manual steps
+- [ ] Run `npm install`
+- [ ] Test wallet connection flow
+- [ ] If FID migration is needed, migrate from FID-based identity to wallet address
+- [ ] If Path B preserves a Farcaster-only area, verify it stays isolated from the main app shell
+
+### Verification
+- TypeScript: [pass/fail]
+- Build: [pass/fail]
+- Remaining Farcaster references: [none / list]
+```
+
+---
+
+## Edge Cases
+
+### No wagmi
+
+Skip Phase 2A. Do not introduce wagmi unless the app actually needs wallet connectivity.
+
+### No auth system
+
+Skip Phase 2C. Do not add SIWE unnecessarily.
+
+### `@farcaster/frame-sdk` (older)
+
+Treat identically to `@farcaster/miniapp-sdk`.
+
+### Monorepo
+
+Ask which workspace(s) to convert. Only modify those.
+
+### FID as database primary key
+
+Do not change schema automatically. Flag it and warn in Phase 4.
+
+### Conditional `isInMiniApp` branches
+
+Always keep the normal web branch and remove the Mini App branch.
+
+### Components with no non-Farcaster purpose
+
+Delete them entirely in `Path A`. In `Path B`, keep them only if they live inside the isolated Farcaster route/page.
+
+### Existing Neynar usage
+
+If the project already uses Neynar:
+- remove it in `Path A`
+- keep it only if it remains inside the isolated `Path B` surface
+- do not add more Neynar usage than already exists unless the user explicitly requests it
+
+### Read-only is usually enough
+
+If the user says they want to "keep Farcaster stuff," bias toward:
+- profile links
+- read-only profile or cast display
+- a dedicated social page
+
+Do not assume they want write access, notifications, or host/runtime behavior.
+
+### Quiz ambiguity
+
+If the scan and quiz conflict, point it out and ask the user to confirm the smaller keep-surface first.
+
+---
+
+## Security
+
+- **Validate wallet setup** — ensure `coinbaseWallet` or the chosen wallet connector is configured correctly
+- **FID-based identity** — requires manual database migration if used as a primary key
+- **SIWE verification** — verify signatures server-side before trusting them
+- **Preserved isolated surface** — do not let a Farcaster-only route/page leak host/runtime assumptions into the main app shell
+- **Existing Neynar usage** — keep API keys server-side only, and only if that isolated surface still depends on them
\ No newline at end of file
diff --git a/skills/build-on-base/references/migrations/minikit-to-farcaster/auth.md b/skills/build-on-base/references/migrations/minikit-to-farcaster/auth.md
new file mode 100644
index 0000000..8bb9bc3
--- /dev/null
+++ b/skills/build-on-base/references/migrations/minikit-to-farcaster/auth.md
@@ -0,0 +1,48 @@
+# Quick Auth Migration
+
+`useAuthenticate` → `sdk.quickAuth`
+
+## Client
+
+```typescript
+import { sdk } from '@farcaster/miniapp-sdk';
+
+// Make authenticated request (recommended)
+const res = await sdk.quickAuth.fetch('/api/auth');
+
+// Or get token directly
+const { token } = await sdk.quickAuth.getToken();
+```
+
+## Server (Next.js)
+
+```bash
+npm install @farcaster/quick-auth
+```
+
+```typescript
+// app/api/auth/route.ts
+import { createClient } from '@farcaster/quick-auth';
+import { NextRequest, NextResponse } from 'next/server';
+
+const client = createClient();
+
+export async function GET(request: NextRequest) {
+ const auth = request.headers.get('Authorization');
+ if (!auth?.startsWith('Bearer ')) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ try {
+ const payload = await client.verifyJwt({
+ token: auth.split(' ')[1],
+ domain: 'your-domain.com',
+ });
+ return NextResponse.json({ fid: payload.sub });
+ } catch {
+ return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
+ }
+}
+```
+
+See [Farcaster Quick Auth docs](https://miniapps.farcaster.xyz/docs/sdk/quick-auth).
diff --git a/skills/build-on-base/references/migrations/minikit-to-farcaster/dependencies.md b/skills/build-on-base/references/migrations/minikit-to-farcaster/dependencies.md
new file mode 100644
index 0000000..e897688
--- /dev/null
+++ b/skills/build-on-base/references/migrations/minikit-to-farcaster/dependencies.md
@@ -0,0 +1,54 @@
+# Dependencies
+
+## Requirements
+
+- Node.js 22.11.0+
+- Farcaster SDK 0.2.0+ (breaking changes from 0.1.x)
+
+## Quick Install
+
+```bash
+npm uninstall @coinbase/onchainkit && \
+npm install @farcaster/miniapp-sdk @farcaster/miniapp-wagmi-connector wagmi viem @tanstack/react-query
+```
+
+## package.json
+
+```json
+{
+ "engines": { "node": ">=22.11.0" },
+ "dependencies": {
+ "@farcaster/miniapp-sdk": "^0.3.0",
+ "@farcaster/miniapp-wagmi-connector": "^0.0.15",
+ "@tanstack/react-query": "^5.50.0",
+ "viem": "^2.17.0",
+ "wagmi": "^2.12.0"
+ }
+}
+```
+
+## Check Version
+
+```bash
+npm list @farcaster/miniapp-sdk
+```
+
+## Common Errors
+
+**Peer dependency conflict:**
+```bash
+npm install @farcaster/miniapp-sdk@^0.3.0 @farcaster/miniapp-wagmi-connector@^0.0.15
+```
+
+**Node.js too old:**
+```bash
+nvm install 22 && nvm use 22
+```
+
+## Optional: Server Auth
+
+```bash
+npm install @farcaster/quick-auth
+```
+
+See [AUTH.md](AUTH.md) for server-side token verification.
diff --git a/skills/build-on-base/references/migrations/minikit-to-farcaster/examples.md b/skills/build-on-base/references/migrations/minikit-to-farcaster/examples.md
new file mode 100644
index 0000000..0cb6ffe
--- /dev/null
+++ b/skills/build-on-base/references/migrations/minikit-to-farcaster/examples.md
@@ -0,0 +1,202 @@
+# Conversion Examples
+
+## Contents
+- Social actions
+- User profile
+- App initialization
+- Primary button
+- Sign-in flow
+- Safe area insets
+
+---
+
+## Social Actions
+
+**Before:**
+```typescript
+import { useClose, useOpenUrl, useViewProfile } from '@coinbase/onchainkit/minikit';
+
+function Actions({ fid }) {
+ const close = useClose();
+ const viewProfile = useViewProfile();
+ return (
+ <>
+
+
+ >
+ );
+}
+```
+
+**After:**
+```typescript
+import { sdk } from '@farcaster/miniapp-sdk';
+
+function Actions({ fid }) {
+ return (
+ <>
+
+
+ >
+ );
+}
+```
+
+---
+
+## User Profile
+
+**Before:**
+```typescript
+const { context } = useMiniKit();
+const { fid, username } = context?.user ?? {};
+```
+
+**After:**
+```typescript
+const [user, setUser] = useState(null);
+
+useEffect(() => {
+ const load = async () => {
+ const ctx = await sdk.context;
+ setUser(ctx?.user);
+ };
+ load();
+}, []);
+
+const { fid, username } = user ?? {};
+```
+
+Or use FrameProvider (see [PROVIDER.md](PROVIDER.md)):
+```typescript
+import { useFrameContext } from '@/components/providers/FrameProvider';
+
+const frameContext = useFrameContext();
+const { fid, username } = frameContext?.context?.user ?? {};
+```
+
+---
+
+## App Initialization
+
+**Before:**
+```typescript
+const { setFrameReady, context, isSDKLoaded } = useMiniKit();
+
+useEffect(() => {
+ if (isSDKLoaded) setFrameReady();
+}, [isSDKLoaded]);
+```
+
+**After:**
+```typescript
+const [ready, setReady] = useState(false);
+const [context, setContext] = useState(null);
+
+useEffect(() => {
+ const init = async () => {
+ const inMiniApp = await sdk.isInMiniApp();
+ if (inMiniApp) {
+ const ctx = await sdk.context;
+ setContext(ctx);
+ await sdk.actions.ready();
+ }
+ setReady(true);
+ };
+ init();
+}, []);
+```
+
+---
+
+## Primary Button (Breaking Change)
+
+**Before:**
+```typescript
+usePrimaryButton(
+ { text: `Clicked ${count}`, disabled: false },
+ () => setCount(c => c + 1)
+);
+```
+
+**After (no callback support):**
+```typescript
+useEffect(() => {
+ const setup = async () => {
+ await sdk.actions.setPrimaryButton({
+ text: "Action",
+ disabled: false,
+ hidden: false,
+ loading: false
+ });
+ };
+ setup();
+}, []);
+
+// Use regular React buttons for click handling
+```
+
+---
+
+## Sign-In Flow
+
+**Before:**
+```typescript
+const { signIn } = useAuthenticate();
+const result = await signIn({ nonce });
+if (result === false) { /* failed */ }
+```
+
+**After (Quick Auth):**
+```typescript
+const { token } = await sdk.quickAuth.getToken();
+await fetch('/api/auth', {
+ headers: { Authorization: `Bearer ${token}` }
+});
+```
+
+Or use authenticated fetch:
+```typescript
+const res = await sdk.quickAuth.fetch('/api/auth');
+```
+
+---
+
+## Safe Area Insets
+
+**Before:**
+```typescript
+const { context } = useMiniKit();
+const insets = context?.client?.safeAreaInsets;
+```
+
+**After:**
+```typescript
+const [insets, setInsets] = useState(null);
+
+useEffect(() => {
+ const load = async () => {
+ const ctx = await sdk.context;
+ setInsets(ctx?.client?.safeAreaInsets);
+ };
+ load();
+}, []);
+```
+
+---
+
+## Add Mini App
+
+**Before:**
+```typescript
+const addFrame = useAddFrame();
+const result = await addFrame();
+```
+
+**After:**
+```typescript
+const result = await sdk.actions.addMiniApp();
+if (result) {
+ saveTokenToServer(result.url, result.token);
+}
+```
diff --git a/skills/build-on-base/references/migrations/minikit-to-farcaster/manifest.md b/skills/build-on-base/references/migrations/minikit-to-farcaster/manifest.md
new file mode 100644
index 0000000..f2c5f17
--- /dev/null
+++ b/skills/build-on-base/references/migrations/minikit-to-farcaster/manifest.md
@@ -0,0 +1,50 @@
+# Manifest Migration
+
+Change root key from `frame` to `miniapp` in `/.well-known/farcaster.json`.
+
+## Before
+
+```typescript
+return Response.json({
+ accountAssociation: { ... },
+ frame: {
+ version: "1",
+ name: "My App",
+ ...
+ }
+});
+```
+
+## After
+
+```typescript
+return Response.json({
+ accountAssociation: { ... },
+ miniapp: {
+ version: "1",
+ name: "My App",
+ homeUrl: "https://yourapp.com",
+ iconUrl: "https://yourapp.com/icon.png",
+ splashImageUrl: "https://yourapp.com/splash.png",
+ splashBackgroundColor: "#000000",
+ // Optional
+ subtitle: "Short tagline",
+ description: "Longer description",
+ primaryCategory: "utilities",
+ webhookUrl: "https://yourapp.com/api/webhook",
+ }
+});
+```
+
+## Required Fields
+
+- `version`: Always `"1"`
+- `name`: App name (max 32 chars)
+- `homeUrl`: Main app URL
+- `iconUrl`: 1:1 ratio, min 200x200
+- `splashImageUrl`: 1:1 ratio
+- `splashBackgroundColor`: Hex color
+
+## Categories
+
+`games` | `social` | `finance` | `utilities` | `productivity` | `entertainment` | `news` | `shopping` | `health` | `education`
diff --git a/skills/build-on-base/references/migrations/minikit-to-farcaster/mapping.md b/skills/build-on-base/references/migrations/minikit-to-farcaster/mapping.md
new file mode 100644
index 0000000..a1e24a3
--- /dev/null
+++ b/skills/build-on-base/references/migrations/minikit-to-farcaster/mapping.md
@@ -0,0 +1,452 @@
+# MiniKit to Farcaster SDK Mapping
+
+Complete reference for converting each MiniKit hook to Farcaster SDK calls.
+
+## Table of Contents
+
+- [Import Changes](#import-changes)
+- [useMiniKit](#useminikit)
+- [useClose](#useclose)
+- [useOpenUrl](#useopenurl)
+- [useViewProfile](#useviewprofile)
+- [useViewCast](#useviewcast)
+- [useComposeCast](#usecomposecast)
+- [useAddFrame](#useaddframe)
+- [useAuthenticate](#useauthenticate)
+- [useNotification](#usenotification)
+
+---
+
+## Import Changes
+
+### Before (MiniKit)
+```typescript
+import {
+ useMiniKit,
+ useClose,
+ useOpenUrl,
+ useViewProfile,
+ useViewCast,
+ useComposeCast,
+ useAddFrame,
+ useAuthenticate
+} from '@coinbase/onchainkit/minikit';
+```
+
+### After (Farcaster SDK)
+```typescript
+import { sdk } from '@farcaster/miniapp-sdk';
+```
+
+**Note**: All hooks become direct SDK method calls. No React hooks needed.
+
+---
+
+## useMiniKit
+
+The main hook that provides context and ready signal.
+
+### Before (MiniKit)
+```typescript
+import { useMiniKit } from '@coinbase/onchainkit/minikit';
+
+function App() {
+ const { setFrameReady, isFrameReady, context } = useMiniKit();
+
+ useEffect(() => {
+ if (!isFrameReady) {
+ setFrameReady();
+ }
+ }, [setFrameReady, isFrameReady]);
+
+ // Access user info
+ const userFid = context?.user?.fid;
+ const username = context?.user?.username;
+
+ return
;
+}
+```
+
+---
+
+## Remove Old Imports
+
+```typescript
+// Delete these
+import { MiniKitProvider } from '@coinbase/onchainkit/minikit';
+import '@coinbase/onchainkit/styles.css';
+
+// Delete from .env
+NEXT_PUBLIC_ONCHAINKIT_API_KEY=xxx
+```
diff --git a/skills/build-on-base/references/migrations/onchainkit/overview.md b/skills/build-on-base/references/migrations/onchainkit/overview.md
new file mode 100644
index 0000000..ed59077
--- /dev/null
+++ b/skills/build-on-base/references/migrations/onchainkit/overview.md
@@ -0,0 +1,131 @@
+# OnchainKit Migration
+
+Migrate apps from `@coinbase/onchainkit` to standalone `wagmi`/`viem` components with zero OnchainKit dependency.
+
+## Before Starting
+
+Create a `mistakes.md` file in the project root:
+
+```markdown
+# Migration Mistakes & Learnings
+
+Track errors, fixes, and lessons learned during OnchainKit migration.
+
+## Errors
+
+## Lessons Learned
+```
+
+Append every error, fix, and lesson to this file throughout the migration. This file improves the skill over time.
+
+## Migration Workflow
+
+Migrations MUST happen in this order. Do not skip steps.
+
+### Step 1: Detection
+
+Scan the project to understand current OnchainKit usage:
+
+1. Search all files for `@coinbase/onchainkit` imports
+2. Identify which components are used:
+ - **Provider**: `OnchainKitProvider` (always present if using OnchainKit)
+ - **Wallet**: `Wallet`, `ConnectWallet`, `WalletDropdown`, `WalletModal`, `Connected`
+ - **Transaction**: `Transaction`, `TransactionButton`, `TransactionStatus`
+ - **Other**: `Identity`, `Avatar`, `Name`, `Address`, `Swap`, `Checkout`, etc.
+3. Check `package.json` for existing `wagmi`, `viem`, `@tanstack/react-query` dependencies
+4. Identify the project's styling approach (Tailwind, CSS Modules, styled-components, etc.)
+5. Report findings to the user before proceeding
+
+### Step 2: Provider Migration (always first)
+
+Read [provider.md](provider.md) for detailed instructions and code.
+
+Summary:
+1. Ensure `wagmi`, `viem`, and `@tanstack/react-query` are installed
+2. Create `wagmi-config.ts` with chain and connector configuration
+3. Replace `OnchainKitProvider` with `WagmiProvider` + `QueryClientProvider`
+4. Remove `@coinbase/onchainkit/styles.css` import
+5. Remove any `SafeArea` or MiniKit imports from layout files
+
+**Validation gate**: Run `npm run build` (or the project's build command). Must pass before continuing. If it fails, fix errors and document them in `mistakes.md`.
+
+### Step 3: Wallet Migration (after provider)
+
+Read [wallet.md](wallet.md) for detailed instructions and code.
+
+Summary:
+1. Create a `WalletConnect` component using wagmi hooks (`useAccount`, `useConnect`, `useDisconnect`)
+2. Component includes a modal with wallet options: Base Account, Coinbase Wallet, MetaMask
+3. Shows truncated address + disconnect button when connected
+4. Replace all OnchainKit wallet imports and component usage
+
+**Validation gate**: Run `npm run build`. Must pass before continuing. Document any errors in `mistakes.md`.
+
+### Step 4: Transaction Migration (after wallet)
+
+Read [transaction.md](transaction.md) for detailed instructions and code.
+
+Summary:
+1. Check the `chainId` prop on existing `` components -- add any missing chains to `wagmi-config.ts`
+2. Create a `TransactionForm` component using wagmi hooks (`useWriteContract`, `useWaitForTransactionReceipt`, `useSwitchChain`)
+3. Component handles the full lifecycle: idle, pending wallet confirmation, confirming on-chain, success, error
+4. Replace all OnchainKit transaction imports (`Transaction`, `TransactionButton`, `TransactionStatus`, `TransactionSponsor`, etc.)
+5. Update the `calls` array format -- use `address`, `abi`, `functionName`, `args` with proper `as const` typing
+6. Map `onStatus` callback to the new lifecycle status names (init, pending, confirmed, success, error)
+
+**Validation gate**: Run `npm run build`. Must pass before continuing. Document any errors in `mistakes.md`.
+
+### Step 5: Cleanup
+
+1. Search for any remaining `@coinbase/onchainkit` imports -- there should be none
+2. Optionally remove `@coinbase/onchainkit` from `package.json` dependencies
+3. Run final `npm run build` to verify everything works
+4. Update `mistakes.md` with final lessons learned
+
+## Troubleshooting
+
+See [troubleshooting.md](troubleshooting.md) for common build and runtime errors.
+
+### Build fails after provider migration
+- **Missing dependencies**: Ensure `wagmi`, `viem`, `@tanstack/react-query` are installed
+- **Type errors with wagmi config**: Check that `chains` array has at least one chain and `transports` has a matching entry
+- **"use client" missing**: Both the provider and wallet components must have `"use client"` directive
+
+### MetaMask SDK react-native warning
+- The warning `Can't resolve '@react-native-async-storage/async-storage'` is harmless in web builds. It comes from MetaMask SDK and does not affect functionality.
+
+### Wallet won't connect
+- Verify the wagmi config has the correct connectors configured
+- Check that `WagmiProvider` wraps the component tree before any wallet hooks are used
+- Ensure `QueryClientProvider` is inside `WagmiProvider`
+
+### Transaction receipt stuck in pending
+- **Symptom**: Transaction hash appears, tx confirms on-chain, but UI stays stuck on "Transaction in progress..." forever
+- **Cause**: `useWaitForTransactionReceipt` has no RPC to poll because the transaction's chain is missing from the wagmi config's `chains` + `transports`
+- **Fix**: (1) Add the target chain to `wagmi-config.ts` chains array AND transports object. (2) Always pass `chainId` to `useWaitForTransactionReceipt({ hash, chainId })` so it polls the correct chain
+
+### Transaction targets wrong chain
+- The `TransactionForm` auto-switches chains, but the target chain must exist in the wagmi config's `chains` array and `transports` object
+- Common: add `baseSepolia` for testnet transactions (chainId 84532)
+
+### Next.js page export restrictions
+- Next.js only allows specific named exports from page files. Exporting contract call arrays or ABI constants from a page file will cause a build error
+- Fix: make them non-exported `const` declarations or move them to a separate module
+
+### ABI type errors after transaction migration
+- When defining ABIs inline, use `as const` on the array for proper type inference
+- Mark individual fields like `type: 'function' as const` and `stateMutability: 'nonpayable' as const`
+
+### Existing wagmi setup detected
+- If the project already wraps with `WagmiProvider`, do NOT add another one
+- Instead, just update the existing wagmi config to include the needed connectors
+- OnchainKit's provider detects and defers to existing wagmi providers -- the standalone setup should too
+
+## Important Notes
+
+- Always use `wagmi` and `viem` directly. Never import from `@coinbase/onchainkit`.
+- The `baseAccount` connector comes from `wagmi/connectors`, not from a separate package.
+- `wagmi-config.ts` must include every chain the app transacts on. If the original OnchainKit `` used a specific chain, that chain must be in both `chains` and `transports`. Without it, `useWaitForTransactionReceipt` will hang forever.
+- If the project uses Tailwind, use Tailwind classes for the components. If not, adapt to inline styles or the project's existing styling approach (e.g., CSS Modules).
+- Do not export contract call arrays, ABI constants, or other non-page values from Next.js page files. Use non-exported constants or a separate module.
+- Inspect the OnchainKit source code in `node_modules/@coinbase/onchainkit/src/` if you need to understand how a specific component works internally.
diff --git a/skills/build-on-base/references/migrations/onchainkit/provider.md b/skills/build-on-base/references/migrations/onchainkit/provider.md
new file mode 100644
index 0000000..0fc7adf
--- /dev/null
+++ b/skills/build-on-base/references/migrations/onchainkit/provider.md
@@ -0,0 +1,193 @@
+# Provider Migration: OnchainKitProvider to WagmiProvider
+
+Replace `OnchainKitProvider` from `@coinbase/onchainkit` with direct `WagmiProvider` + `QueryClientProvider` setup.
+
+## What OnchainKitProvider Does Internally
+
+OnchainKitProvider is a wrapper that:
+1. Creates a wagmi config with `base` + `baseSepolia` chains
+2. Uses `cookieStorage` for persistence and `ssr: true`
+3. Default connector: `baseAccount()` from `wagmi/connectors`
+4. Sets up CDP RPC URLs if `apiKey` is provided
+5. Creates a default `QueryClient` from `@tanstack/react-query`
+6. Applies theme/appearance settings via CSS custom properties
+
+The provider detects if `WagmiProvider` or `QueryClientProvider` already exist in the React tree and skips creating them if so.
+
+## Prerequisites
+
+Ensure these packages are installed. They are likely already present since OnchainKit depends on them:
+
+```bash
+npm install wagmi viem @tanstack/react-query
+```
+
+If the project uses Tailwind CSS and it's not yet installed:
+
+```bash
+npm install tailwindcss @tailwindcss/postcss postcss
+```
+
+## Step-by-Step Migration
+
+### 1. Create wagmi-config.ts
+
+Create a new file for the wagmi configuration. Place it alongside the existing provider file (typically `app/wagmi-config.ts`).
+
+```typescript
+import { http, cookieStorage, createConfig, createStorage } from "wagmi";
+import { base } from "wagmi/chains";
+import { coinbaseWallet, metaMask } from "wagmi/connectors";
+
+export const wagmiConfig = createConfig({
+ chains: [base],
+ connectors: [
+ coinbaseWallet({ appName: "My App", preference: "all" }),
+ metaMask(),
+ ],
+ storage: createStorage({ storage: cookieStorage }),
+ ssr: true,
+ transports: {
+ [base.id]: http(),
+ },
+});
+```
+
+**Adapt based on the existing OnchainKitProvider config:**
+- If `chain` prop was set to something other than `base`, use that chain instead
+- If `apiKey` was set, you can use CDP RPC URLs: `http(\`https://api.developer.coinbase.com/rpc/v1/base/${apiKey}\`)`
+- If `config.wallet.preference` was `"smartWalletOnly"`, adjust the coinbaseWallet connector accordingly
+- Add additional chains to the `chains` array and `transports` object as needed
+
+### 2. Rewrite the Provider Component
+
+Replace the existing provider file (typically `rootProvider.tsx` or `providers.tsx`).
+
+**Before (OnchainKit):**
+```typescript
+"use client";
+import { ReactNode } from "react";
+import { base } from "wagmi/chains";
+import { OnchainKitProvider } from "@coinbase/onchainkit";
+import "@coinbase/onchainkit/styles.css";
+
+export function RootProvider({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+```
+
+**After (wagmi/viem):**
+```typescript
+"use client";
+import { type ReactNode, useState } from "react";
+import { WagmiProvider } from "wagmi";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { wagmiConfig } from "./wagmi-config";
+
+export function RootProvider({ children }: { children: ReactNode }) {
+ const [queryClient] = useState(() => new QueryClient());
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+```
+
+Key changes:
+- Remove `@coinbase/onchainkit` import
+- Remove `@coinbase/onchainkit/styles.css` import
+- `QueryClient` is created with `useState` to avoid re-creation on re-renders
+- `WagmiProvider` must wrap `QueryClientProvider`
+
+### 3. Update Layout File
+
+Remove any OnchainKit-specific imports from the layout:
+
+- Remove `SafeArea` from `@coinbase/onchainkit/minikit`
+- Remove `minikitConfig` imports
+- Remove MiniKit-related metadata generation
+- Move `` inside `` (wagmi provider must be a client component, so it should wrap the content, not the `` tag)
+
+**Before:**
+```typescript
+import { SafeArea } from "@coinbase/onchainkit/minikit";
+
+export default function RootLayout({ children }) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
+```
+
+**After:**
+```typescript
+export default function RootLayout({ children }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+```
+
+### 4. Verify
+
+Run the build command:
+```bash
+npm run build
+```
+
+Expected: Build succeeds. The MetaMask SDK warning about `@react-native-async-storage/async-storage` is harmless and can be ignored.
+
+## Edge Cases
+
+### Project already has a WagmiProvider
+If the project wraps with its own `WagmiProvider` outside of OnchainKit, simply remove the `OnchainKitProvider` wrapper. Update the existing wagmi config to include any connectors that were configured via OnchainKit.
+
+### Project uses CDP API key for RPC
+If the existing setup relied on `apiKey` for RPC access, add the CDP RPC URL to the transport:
+
+```typescript
+transports: {
+ [base.id]: http(`https://api.developer.coinbase.com/rpc/v1/base/${process.env.NEXT_PUBLIC_ONCHAINKIT_API_KEY}`),
+},
+```
+
+### Project uses multiple chains
+Add all needed chains to both the `chains` array and `transports` object:
+
+```typescript
+import { base, baseSepolia } from "wagmi/chains";
+
+export const wagmiConfig = createConfig({
+ chains: [base, baseSepolia],
+ transports: {
+ [base.id]: http(),
+ [baseSepolia.id]: http(),
+ },
+ // ...rest
+});
+```
diff --git a/skills/build-on-base/references/migrations/onchainkit/transaction.md b/skills/build-on-base/references/migrations/onchainkit/transaction.md
new file mode 100644
index 0000000..86c2089
--- /dev/null
+++ b/skills/build-on-base/references/migrations/onchainkit/transaction.md
@@ -0,0 +1,528 @@
+# Transaction Migration: OnchainKit Transaction to wagmi
+
+Replace OnchainKit's `Transaction`, `TransactionButton`, `TransactionStatus`, `TransactionSponsor`, and related components with a standalone `TransactionForm` component built on wagmi hooks.
+
+## What OnchainKit's Transaction Components Do
+
+OnchainKit provides a composable transaction system:
+- `` -- container that manages the full transaction lifecycle, accepts `calls`, `chainId`, `onStatus`
+- `` -- submits the transaction, shows status-dependent text (Transact/Confirm/Try again/View transaction)
+- `` -- displays current transaction state with label and action
+- `` -- text label ("Confirm in wallet", "Transaction in progress", "Successful", error message)
+- `` -- link to block explorer or call status viewer
+- `` -- shows "Zero transaction fee" when paymaster is configured
+- `LifecycleStatus` type -- status object with `statusName` and `statusData`
+
+Internally, OnchainKit uses two submission paths:
+- **Smart Wallet (batched):** `useSendCalls` (EIP-5792) for wallets with `atomicBatch` capability
+- **EOA (single):** `useSendTransaction` with `encodeFunctionData` for standard wallets
+
+The replacement component uses `useWriteContract` which handles both EOA and smart wallet scenarios for single contract calls.
+
+## Prerequisites
+
+- Provider migration must be completed first (WagmiProvider + QueryClientProvider in the tree)
+- Tailwind CSS installed (if not, install it or adapt styles)
+- If the transaction targets a chain other than what's in the wagmi config, add that chain to `wagmi-config.ts`
+
+## Important: Chain Configuration
+
+OnchainKit's Transaction accepts a `chainId` prop and handles chain switching. The replacement does too, BUT the target chain must exist in the wagmi config's `chains` array and `transports` object.
+
+For example, if transactions target Base Sepolia (84532):
+
+```typescript
+import { base, baseSepolia } from "wagmi/chains";
+
+export const wagmiConfig = createConfig({
+ chains: [base, baseSepolia],
+ transports: {
+ [base.id]: http(),
+ [baseSepolia.id]: http(),
+ },
+ // ...rest
+});
+```
+
+## The TransactionForm Component
+
+Create `app/components/TransactionForm.tsx` (or wherever components live in the project):
+
+```typescript
+"use client";
+import { useCallback, useEffect, useState } from "react";
+import {
+ useAccount,
+ useWriteContract,
+ useWaitForTransactionReceipt,
+ useSwitchChain,
+} from "wagmi";
+import type { Abi, Address } from "viem";
+
+type ContractCall = {
+ address: Address;
+ abi: Abi;
+ functionName: string;
+ args?: readonly unknown[];
+ value?: bigint;
+};
+
+type LifecycleStatus =
+ | { statusName: "init"; statusData: null }
+ | { statusName: "pending"; statusData: null }
+ | { statusName: "confirmed"; statusData: { transactionHash: string } }
+ | {
+ statusName: "success";
+ statusData: { transactionHash: string; blockNumber: bigint };
+ }
+ | { statusName: "error"; statusData: { message: string } };
+
+type TransactionFormProps = {
+ calls: ContractCall[];
+ chainId?: number;
+ buttonText?: string;
+ onStatus?: (status: LifecycleStatus) => void;
+ disabled?: boolean;
+ className?: string;
+};
+
+export function TransactionForm({
+ calls,
+ chainId,
+ buttonText = "Transact",
+ onStatus,
+ disabled = false,
+ className,
+}: TransactionFormProps) {
+ const { isConnected, chainId: currentChainId } = useAccount();
+ const { switchChainAsync } = useSwitchChain();
+
+ const [status, setStatus] = useState({
+ statusName: "init",
+ statusData: null,
+ });
+
+ const updateStatus = useCallback(
+ (newStatus: LifecycleStatus) => {
+ setStatus(newStatus);
+ onStatus?.(newStatus);
+ },
+ [onStatus]
+ );
+
+ const {
+ writeContract,
+ data: txHash,
+ isPending: isWritePending,
+ reset: resetWrite,
+ } = useWriteContract();
+
+ // CRITICAL: Always pass chainId so wagmi polls the correct chain's RPC.
+ // Without this, if the user's wallet is on a different chain than the
+ // transaction target, wagmi has no transport to poll and the receipt
+ // is never found -- the UI hangs in "pending" forever.
+ const { data: receipt, isLoading: isWaiting } =
+ useWaitForTransactionReceipt({
+ hash: txHash,
+ chainId,
+ });
+
+ useEffect(() => {
+ if (isWritePending) {
+ updateStatus({ statusName: "pending", statusData: null });
+ }
+ }, [isWritePending, updateStatus]);
+
+ useEffect(() => {
+ if (txHash && !receipt) {
+ updateStatus({
+ statusName: "confirmed",
+ statusData: { transactionHash: txHash },
+ });
+ }
+ }, [txHash, receipt, updateStatus]);
+
+ useEffect(() => {
+ if (receipt) {
+ updateStatus({
+ statusName: "success",
+ statusData: {
+ transactionHash: receipt.transactionHash,
+ blockNumber: receipt.blockNumber,
+ },
+ });
+ }
+ }, [receipt, updateStatus]);
+
+ const handleSubmit = useCallback(async () => {
+ if (!isConnected || calls.length === 0) return;
+
+ try {
+ if (chainId && currentChainId !== chainId) {
+ await switchChainAsync({ chainId });
+ }
+
+ const call = calls[0];
+ writeContract(
+ {
+ address: call.address,
+ abi: call.abi,
+ functionName: call.functionName,
+ args: call.args ?? [],
+ value: call.value,
+ chainId,
+ },
+ {
+ onError: (error) => {
+ const isUserRejection =
+ error.message?.includes("User rejected") ||
+ error.message?.includes("User denied") ||
+ error.message?.includes("Request denied");
+ const message = isUserRejection
+ ? "Request denied."
+ : error.message || "Transaction failed";
+ updateStatus({ statusName: "error", statusData: { message } });
+ },
+ }
+ );
+ } catch (error) {
+ const message =
+ error instanceof Error ? error.message : "Transaction failed";
+ updateStatus({ statusName: "error", statusData: { message } });
+ }
+ }, [
+ isConnected,
+ calls,
+ chainId,
+ currentChainId,
+ switchChainAsync,
+ writeContract,
+ updateStatus,
+ ]);
+
+ const handleReset = useCallback(() => {
+ resetWrite();
+ updateStatus({ statusName: "init", statusData: null });
+ }, [resetWrite, updateStatus]);
+
+ const isLoading = isWritePending || isWaiting;
+
+ return (
+
+ );
+}
+```
+
+## Step-by-Step Replacement
+
+### 1. Check Chain Configuration
+
+Look at the `chainId` prop on the existing `` component. If it references a chain not in the wagmi config, add it:
+
+```typescript
+// Common: Base Sepolia for testnet
+import { base, baseSepolia } from "wagmi/chains";
+// Add to wagmi config chains array and transports
+```
+
+### 2. Create the Component File
+
+Copy the `TransactionForm` component code above into the project's components directory.
+
+### 3. Replace OnchainKit Transaction Imports and Usage
+
+**Before (OnchainKit):**
+```typescript
+import {
+ Transaction,
+ TransactionButton,
+ TransactionSponsor,
+ TransactionStatus,
+ TransactionStatusAction,
+ TransactionStatusLabel,
+} from '@coinbase/onchainkit/transaction';
+import type { LifecycleStatus } from '@coinbase/onchainkit/transaction';
+
+const calls = [
+ {
+ address: '0x67c97D1FB8184F038592b2109F854dfb09C77C75',
+ abi: clickContractAbi,
+ functionName: 'click',
+ args: [],
+ }
+];
+
+
+
+
+
+
+
+
+
+```
+
+**After (wagmi):**
+```typescript
+import { TransactionForm } from "./components/TransactionForm";
+import type { Address } from "viem";
+
+const clickContractAddress: Address = '0x67c97D1FB8184F038592b2109F854dfb09C77C75';
+const clickContractAbi = [
+ {
+ type: 'function' as const,
+ name: 'click',
+ inputs: [],
+ outputs: [],
+ stateMutability: 'nonpayable' as const,
+ },
+] as const;
+
+const calls = [
+ {
+ address: clickContractAddress,
+ abi: clickContractAbi,
+ functionName: 'click',
+ args: [],
+ },
+];
+
+
+```
+
+### 4. Handle the onStatus Callback
+
+The OnchainKit `LifecycleStatus` type has these states: `init`, `transactionIdle`, `buildingTransaction`, `transactionPending`, `transactionLegacyExecuted`, `success`, `error`, `reset`.
+
+The replacement uses a simplified set: `init`, `pending`, `confirmed`, `success`, `error`.
+
+**Mapping:**
+
+| OnchainKit Status | Replacement Status |
+|---|---|
+| `init` / `transactionIdle` | `init` |
+| `buildingTransaction` / `transactionPending` | `pending` |
+| `transactionLegacyExecuted` | `confirmed` |
+| `success` | `success` |
+| `error` | `error` |
+
+If the existing `onStatus` callback checks specific OnchainKit status names, update the checks to use the new names.
+
+### 5. Verify
+
+Run `npm run build` and confirm no errors.
+
+## What's Not Covered
+
+### Gas Sponsorship (TransactionSponsor)
+OnchainKit's `TransactionSponsor` uses a paymaster URL to sponsor gas fees. This requires a paymaster service (e.g., Coinbase Developer Platform Paymaster). The replacement component does not include paymaster support. To add it, you would need to use wagmi's `useSendCalls` with the paymaster capability.
+
+### Batched Calls (EIP-5792)
+OnchainKit's Transaction supports batching multiple calls into a single transaction for smart wallets. The replacement uses `useWriteContract` which handles one call at a time. For batched calls, use wagmi's `useSendCalls` hook directly.
+
+### Transaction Toast
+OnchainKit's `TransactionToast` provides toast-style notifications. The replacement shows inline status instead. Add a toast library if toast notifications are needed.
+
+## Common Issues
+
+### Transaction receipt stuck in pending (UI hangs after wallet confirms)
+**This is the most common bug.** The transaction hash appears, the tx confirms on-chain, but the UI stays stuck on "Transaction in progress..." forever.
+
+**Cause:** `useWaitForTransactionReceipt` needs an RPC to poll for the receipt. If the transaction's chain is not in the wagmi config's `chains` + `transports`, wagmi has no RPC endpoint to poll, so `isSuccess` never becomes `true`.
+
+**Fix (two parts):**
+1. Add the transaction's target chain to `wagmi-config.ts`:
+```typescript
+import { base, baseSepolia } from "wagmi/chains";
+
+export const wagmiConfig = createConfig({
+ chains: [base, baseSepolia], // Must include every chain the app transacts on
+ transports: {
+ [base.id]: http(),
+ [baseSepolia.id]: http(), // Must have a transport for each chain
+ },
+ // ...rest
+});
+```
+2. Always pass `chainId` to `useWaitForTransactionReceipt`:
+```typescript
+const { data: receipt } = useWaitForTransactionReceipt({
+ hash: txHash,
+ chainId, // Ensures polling uses the correct chain's transport
+});
+```
+
+### Next.js page export restrictions
+Next.js only allows specific named exports from page files (`default`, `metadata`, `generateMetadata`, `generateStaticParams`, etc.). If you export contract call arrays, ABI constants, or other non-page values from a page file, the build will fail with an error like: `"calls" is not a valid Page export field`.
+
+**Fix:** Move contract call arrays, ABIs, and addresses to a separate module (e.g., `contracts.ts`) or make them non-exported `const` declarations within the page file.
+
+### Type error: comparison with "UserRejectedRequestError"
+The wagmi error types don't include `UserRejectedRequestError` as a direct name match. Instead, check `error.message` for "User rejected" or "User denied" strings.
+
+### Transaction targets wrong chain
+The component auto-switches chains via `useSwitchChain`. But the target chain must exist in the wagmi config. If you get a chain error, add the chain to `wagmi-config.ts`.
+
+### "useWriteContract must be used within WagmiProvider"
+Same as wallet: ensure the component is inside the WagmiProvider tree.
+
+### ABI type errors
+When defining the ABI inline, use `as const` on the array to get proper type inference:
+```typescript
+const abi = [
+ {
+ type: 'function' as const,
+ name: 'click',
+ inputs: [],
+ outputs: [],
+ stateMutability: 'nonpayable' as const,
+ },
+] as const;
+```
diff --git a/skills/build-on-base/references/migrations/onchainkit/troubleshooting.md b/skills/build-on-base/references/migrations/onchainkit/troubleshooting.md
new file mode 100644
index 0000000..382bbbf
--- /dev/null
+++ b/skills/build-on-base/references/migrations/onchainkit/troubleshooting.md
@@ -0,0 +1,79 @@
+# Troubleshooting OnchainKit Migration
+
+## Build Errors
+
+### `Module not found: Can't resolve '@react-native-async-storage/async-storage'`
+**Cause**: MetaMask SDK includes a react-native dependency that doesn't resolve in web environments.
+**Impact**: Warning only. Does not affect functionality.
+**Solution**: Ignore. This is a known issue with MetaMask SDK's web bundle.
+
+### `Type error: Cannot find module 'wagmi/connectors'`
+**Cause**: Outdated wagmi version.
+**Solution**: Update wagmi to >= 2.16:
+```bash
+npm install wagmi@latest
+```
+
+### `Error: useAccount must be used within WagmiConfig`
+**Cause**: A component using wagmi hooks is rendering outside the WagmiProvider tree.
+**Solution**: Ensure `WagmiProvider` wraps the entire app. In Next.js, this goes in the root provider component. Both the provider and any component using wagmi hooks must have `"use client"` directive.
+
+### `Error: No QueryClient set, use QueryClientProvider`
+**Cause**: `QueryClientProvider` is missing from the provider tree.
+**Solution**: Add `QueryClientProvider` inside `WagmiProvider`:
+```typescript
+
+
+ {children}
+
+
+```
+
+### `Error: Invalid chain configuration`
+**Cause**: The `transports` object doesn't have an entry for every chain in the `chains` array.
+**Solution**: Every chain in `chains` needs a matching transport:
+```typescript
+createConfig({
+ chains: [base, baseSepolia],
+ transports: {
+ [base.id]: http(),
+ [baseSepolia.id]: http(), // Must match
+ },
+});
+```
+
+## Runtime Errors
+
+### Wallet modal opens but nothing happens on click
+**Cause**: The connector might not be available or the wallet extension isn't installed.
+**Solution**: For extension-based wallets (MetaMask), the user needs the extension installed. For Coinbase Wallet and Base Account, they work via popup/redirect without an extension.
+
+### Connection succeeds but address doesn't display
+**Cause**: Component not re-rendering after connection state change.
+**Solution**: Ensure the component using `useAccount()` is a client component with `"use client"`. wagmi hooks trigger re-renders automatically when state changes.
+
+### Dark mode styles not working
+**Cause**: Tailwind dark mode not configured.
+**Solution**: Tailwind v4 uses `prefers-color-scheme` by default. If the project uses class-based dark mode, ensure the `` element has the `dark` class. For Tailwind v3, check `tailwind.config.js` has `darkMode: 'class'`.
+
+## Migration-Specific Issues
+
+### OnchainKit styles break after removing the import
+**Cause**: Some layouts depended on OnchainKit's global CSS.
+**Solution**: The OnchainKit CSS mainly provides:
+- Custom `ock-*` CSS variables for theming
+- Rounded corner and color utilities
+- Font styling
+
+These are replaced by Tailwind utilities. If specific layouts break, inspect the element and add equivalent Tailwind classes.
+
+### Multiple wallet connection prompts
+**Cause**: The wagmi config has connectors that auto-connect on page load.
+**Solution**: Use `cookieStorage` for persistence (prevents reconnection prompts):
+```typescript
+storage: createStorage({ storage: cookieStorage }),
+```
+
+### SSR hydration mismatch
+**Cause**: Wallet state differs between server and client render.
+**Solution**: Ensure the wagmi config has `ssr: true` and the provider component has `"use client"` directive. Use `cookieStorage` for state persistence across SSR.
diff --git a/skills/build-on-base/references/migrations/onchainkit/wallet.md b/skills/build-on-base/references/migrations/onchainkit/wallet.md
new file mode 100644
index 0000000..88e9c56
--- /dev/null
+++ b/skills/build-on-base/references/migrations/onchainkit/wallet.md
@@ -0,0 +1,346 @@
+# Wallet Migration: OnchainKit Wallet to WalletConnect
+
+Replace OnchainKit's `Wallet`, `ConnectWallet`, `WalletDropdown`, `WalletModal`, and `Connected` components with a standalone `WalletConnect` component built on wagmi hooks.
+
+## What OnchainKit's Wallet Components Do
+
+OnchainKit provides several wallet components:
+- `` -- container that manages open/closed state
+- `` -- button that triggers connection (renders as "Connect Wallet" when disconnected)
+- `` -- dropdown with identity info and actions
+- `` -- modal with multiple wallet options (Base Account, Coinbase, MetaMask, Phantom, etc.)
+- `` -- conditional renderer based on wallet connection state
+
+The replacement `WalletConnect` component combines all of this into one component.
+
+## Prerequisites
+
+- Provider migration must be completed first (WagmiProvider + QueryClientProvider in the tree)
+- Tailwind CSS installed (if not, install it or adapt styles)
+
+## The WalletConnect Component
+
+Create `app/components/WalletConnect.tsx` (or wherever components live in the project):
+
+```typescript
+"use client";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { useAccount, useConnect, useDisconnect } from "wagmi";
+import {
+ baseAccount,
+ coinbaseWallet,
+ metaMask,
+} from "wagmi/connectors";
+
+function truncateAddress(address: string): string {
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
+}
+
+type WalletOption = {
+ id: string;
+ name: string;
+ connect: () => void;
+};
+
+function WalletModal({
+ onClose,
+ appName = "My App",
+}: {
+ onClose: () => void;
+ appName?: string;
+}) {
+ const { connect } = useConnect();
+ const backdropRef = useRef(null);
+
+ const handleBackdropClick = useCallback(
+ (e: React.MouseEvent) => {
+ if (e.target === backdropRef.current) onClose();
+ },
+ [onClose]
+ );
+
+ useEffect(() => {
+ const handleEsc = (e: KeyboardEvent) => {
+ if (e.key === "Escape") onClose();
+ };
+ document.addEventListener("keydown", handleEsc);
+ return () => document.removeEventListener("keydown", handleEsc);
+ }, [onClose]);
+
+ const walletOptions: WalletOption[] = [
+ {
+ id: "base-account",
+ name: "Sign in with Base",
+ connect: () => {
+ connect({ connector: baseAccount({ appName }) });
+ onClose();
+ },
+ },
+ {
+ id: "coinbase-wallet",
+ name: "Coinbase Wallet",
+ connect: () => {
+ connect({
+ connector: coinbaseWallet({ appName, preference: "all" }),
+ });
+ onClose();
+ },
+ },
+ {
+ id: "metamask",
+ name: "MetaMask",
+ connect: () => {
+ connect({
+ connector: metaMask({
+ dappMetadata: {
+ name: appName,
+ url: typeof window !== "undefined" ? window.location.origin : "",
+ },
+ }),
+ });
+ onClose();
+ },
+ },
+ ];
+
+ const [primaryOption, ...otherOptions] = walletOptions;
+
+ return (
+
- );
-}
-```
-
-## Step-by-Step Replacement
-
-### 1. Check Chain Configuration
-
-Look at the `chainId` prop on the existing `` component. If it references a chain not in the wagmi config, add it:
-
-```typescript
-// Common: Base Sepolia for testnet
-import { base, baseSepolia } from "wagmi/chains";
-// Add to wagmi config chains array and transports
-```
-
-### 2. Create the Component File
-
-Copy the `TransactionForm` component code above into the project's components directory.
-
-### 3. Replace OnchainKit Transaction Imports and Usage
-
-**Before (OnchainKit):**
-```typescript
-import {
- Transaction,
- TransactionButton,
- TransactionSponsor,
- TransactionStatus,
- TransactionStatusAction,
- TransactionStatusLabel,
-} from '@coinbase/onchainkit/transaction';
-import type { LifecycleStatus } from '@coinbase/onchainkit/transaction';
-
-const calls = [
- {
- address: '0x67c97D1FB8184F038592b2109F854dfb09C77C75',
- abi: clickContractAbi,
- functionName: 'click',
- args: [],
- }
-];
-
-
-
-
-
-
-
-
-
-```
-
-**After (wagmi):**
-```typescript
-import { TransactionForm } from "./components/TransactionForm";
-import type { Address } from "viem";
-
-const clickContractAddress: Address = '0x67c97D1FB8184F038592b2109F854dfb09C77C75';
-const clickContractAbi = [
- {
- type: 'function' as const,
- name: 'click',
- inputs: [],
- outputs: [],
- stateMutability: 'nonpayable' as const,
- },
-] as const;
-
-const calls = [
- {
- address: clickContractAddress,
- abi: clickContractAbi,
- functionName: 'click',
- args: [],
- },
-];
-
-
-```
-
-### 4. Handle the onStatus Callback
-
-The OnchainKit `LifecycleStatus` type has these states: `init`, `transactionIdle`, `buildingTransaction`, `transactionPending`, `transactionLegacyExecuted`, `success`, `error`, `reset`.
-
-The replacement uses a simplified set: `init`, `pending`, `confirmed`, `success`, `error`.
-
-**Mapping:**
-
-| OnchainKit Status | Replacement Status |
-|---|---|
-| `init` / `transactionIdle` | `init` |
-| `buildingTransaction` / `transactionPending` | `pending` |
-| `transactionLegacyExecuted` | `confirmed` |
-| `success` | `success` |
-| `error` | `error` |
-
-If the existing `onStatus` callback checks specific OnchainKit status names, update the checks to use the new names.
-
-### 5. Verify
-
-Run `npm run build` and confirm no errors.
-
-## What's Not Covered
-
-### Gas Sponsorship (TransactionSponsor)
-OnchainKit's `TransactionSponsor` uses a paymaster URL to sponsor gas fees. This requires a paymaster service (e.g., Coinbase Developer Platform Paymaster). The replacement component does not include paymaster support. To add it, you would need to use wagmi's `useSendCalls` with the paymaster capability.
-
-### Batched Calls (EIP-5792)
-OnchainKit's Transaction supports batching multiple calls into a single transaction for smart wallets. The replacement uses `useWriteContract` which handles one call at a time. For batched calls, use wagmi's `useSendCalls` hook directly.
-
-### Transaction Toast
-OnchainKit's `TransactionToast` provides toast-style notifications. The replacement shows inline status instead. Add a toast library if toast notifications are needed.
-
-## Common Issues
-
-### Transaction receipt stuck in pending (UI hangs after wallet confirms)
-**This is the most common bug.** The transaction hash appears, the tx confirms on-chain, but the UI stays stuck on "Transaction in progress..." forever.
-
-**Cause:** `useWaitForTransactionReceipt` needs an RPC to poll for the receipt. If the transaction's chain is not in the wagmi config's `chains` + `transports`, wagmi has no RPC endpoint to poll, so `isSuccess` never becomes `true`.
-
-**Fix (two parts):**
-1. Add the transaction's target chain to `wagmi-config.ts`:
-```typescript
-import { base, baseSepolia } from "wagmi/chains";
-
-export const wagmiConfig = createConfig({
- chains: [base, baseSepolia], // Must include every chain the app transacts on
- transports: {
- [base.id]: http(),
- [baseSepolia.id]: http(), // Must have a transport for each chain
- },
- // ...rest
-});
-```
-2. Always pass `chainId` to `useWaitForTransactionReceipt`:
-```typescript
-const { data: receipt } = useWaitForTransactionReceipt({
- hash: txHash,
- chainId, // Ensures polling uses the correct chain's transport
-});
-```
-
-### Next.js page export restrictions
-Next.js only allows specific named exports from page files (`default`, `metadata`, `generateMetadata`, `generateStaticParams`, etc.). If you export contract call arrays, ABI constants, or other non-page values from a page file, the build will fail with an error like: `"calls" is not a valid Page export field`.
-
-**Fix:** Move contract call arrays, ABIs, and addresses to a separate module (e.g., `contracts.ts`) or make them non-exported `const` declarations within the page file.
-
-### Type error: comparison with "UserRejectedRequestError"
-The wagmi error types don't include `UserRejectedRequestError` as a direct name match. Instead, check `error.message` for "User rejected" or "User denied" strings.
-
-### Transaction targets wrong chain
-The component auto-switches chains via `useSwitchChain`. But the target chain must exist in the wagmi config. If you get a chain error, add the chain to `wagmi-config.ts`.
-
-### "useWriteContract must be used within WagmiProvider"
-Same as wallet: ensure the component is inside the WagmiProvider tree.
-
-### ABI type errors
-When defining the ABI inline, use `as const` on the array to get proper type inference:
-```typescript
-const abi = [
- {
- type: 'function' as const,
- name: 'click',
- inputs: [],
- outputs: [],
- stateMutability: 'nonpayable' as const,
- },
-] as const;
-```
diff --git a/skills/migrating-an-onchainkit-app/references/troubleshooting.md b/skills/migrating-an-onchainkit-app/references/troubleshooting.md
deleted file mode 100644
index 382bbbf..0000000
--- a/skills/migrating-an-onchainkit-app/references/troubleshooting.md
+++ /dev/null
@@ -1,79 +0,0 @@
-# Troubleshooting OnchainKit Migration
-
-## Build Errors
-
-### `Module not found: Can't resolve '@react-native-async-storage/async-storage'`
-**Cause**: MetaMask SDK includes a react-native dependency that doesn't resolve in web environments.
-**Impact**: Warning only. Does not affect functionality.
-**Solution**: Ignore. This is a known issue with MetaMask SDK's web bundle.
-
-### `Type error: Cannot find module 'wagmi/connectors'`
-**Cause**: Outdated wagmi version.
-**Solution**: Update wagmi to >= 2.16:
-```bash
-npm install wagmi@latest
-```
-
-### `Error: useAccount must be used within WagmiConfig`
-**Cause**: A component using wagmi hooks is rendering outside the WagmiProvider tree.
-**Solution**: Ensure `WagmiProvider` wraps the entire app. In Next.js, this goes in the root provider component. Both the provider and any component using wagmi hooks must have `"use client"` directive.
-
-### `Error: No QueryClient set, use QueryClientProvider`
-**Cause**: `QueryClientProvider` is missing from the provider tree.
-**Solution**: Add `QueryClientProvider` inside `WagmiProvider`:
-```typescript
-
-
- {children}
-
-
-```
-
-### `Error: Invalid chain configuration`
-**Cause**: The `transports` object doesn't have an entry for every chain in the `chains` array.
-**Solution**: Every chain in `chains` needs a matching transport:
-```typescript
-createConfig({
- chains: [base, baseSepolia],
- transports: {
- [base.id]: http(),
- [baseSepolia.id]: http(), // Must match
- },
-});
-```
-
-## Runtime Errors
-
-### Wallet modal opens but nothing happens on click
-**Cause**: The connector might not be available or the wallet extension isn't installed.
-**Solution**: For extension-based wallets (MetaMask), the user needs the extension installed. For Coinbase Wallet and Base Account, they work via popup/redirect without an extension.
-
-### Connection succeeds but address doesn't display
-**Cause**: Component not re-rendering after connection state change.
-**Solution**: Ensure the component using `useAccount()` is a client component with `"use client"`. wagmi hooks trigger re-renders automatically when state changes.
-
-### Dark mode styles not working
-**Cause**: Tailwind dark mode not configured.
-**Solution**: Tailwind v4 uses `prefers-color-scheme` by default. If the project uses class-based dark mode, ensure the `` element has the `dark` class. For Tailwind v3, check `tailwind.config.js` has `darkMode: 'class'`.
-
-## Migration-Specific Issues
-
-### OnchainKit styles break after removing the import
-**Cause**: Some layouts depended on OnchainKit's global CSS.
-**Solution**: The OnchainKit CSS mainly provides:
-- Custom `ock-*` CSS variables for theming
-- Rounded corner and color utilities
-- Font styling
-
-These are replaced by Tailwind utilities. If specific layouts break, inspect the element and add equivalent Tailwind classes.
-
-### Multiple wallet connection prompts
-**Cause**: The wagmi config has connectors that auto-connect on page load.
-**Solution**: Use `cookieStorage` for persistence (prevents reconnection prompts):
-```typescript
-storage: createStorage({ storage: cookieStorage }),
-```
-
-### SSR hydration mismatch
-**Cause**: Wallet state differs between server and client render.
-**Solution**: Ensure the wagmi config has `ssr: true` and the provider component has `"use client"` directive. Use `cookieStorage` for state persistence across SSR.
diff --git a/skills/migrating-an-onchainkit-app/references/wallet-migration.md b/skills/migrating-an-onchainkit-app/references/wallet-migration.md
deleted file mode 100644
index 88e9c56..0000000
--- a/skills/migrating-an-onchainkit-app/references/wallet-migration.md
+++ /dev/null
@@ -1,346 +0,0 @@
-# Wallet Migration: OnchainKit Wallet to WalletConnect
-
-Replace OnchainKit's `Wallet`, `ConnectWallet`, `WalletDropdown`, `WalletModal`, and `Connected` components with a standalone `WalletConnect` component built on wagmi hooks.
-
-## What OnchainKit's Wallet Components Do
-
-OnchainKit provides several wallet components:
-- `` -- container that manages open/closed state
-- `` -- button that triggers connection (renders as "Connect Wallet" when disconnected)
-- `` -- dropdown with identity info and actions
-- `` -- modal with multiple wallet options (Base Account, Coinbase, MetaMask, Phantom, etc.)
-- `` -- conditional renderer based on wallet connection state
-
-The replacement `WalletConnect` component combines all of this into one component.
-
-## Prerequisites
-
-- Provider migration must be completed first (WagmiProvider + QueryClientProvider in the tree)
-- Tailwind CSS installed (if not, install it or adapt styles)
-
-## The WalletConnect Component
-
-Create `app/components/WalletConnect.tsx` (or wherever components live in the project):
-
-```typescript
-"use client";
-import { useCallback, useEffect, useRef, useState } from "react";
-import { useAccount, useConnect, useDisconnect } from "wagmi";
-import {
- baseAccount,
- coinbaseWallet,
- metaMask,
-} from "wagmi/connectors";
-
-function truncateAddress(address: string): string {
- return `${address.slice(0, 6)}...${address.slice(-4)}`;
-}
-
-type WalletOption = {
- id: string;
- name: string;
- connect: () => void;
-};
-
-function WalletModal({
- onClose,
- appName = "My App",
-}: {
- onClose: () => void;
- appName?: string;
-}) {
- const { connect } = useConnect();
- const backdropRef = useRef(null);
-
- const handleBackdropClick = useCallback(
- (e: React.MouseEvent) => {
- if (e.target === backdropRef.current) onClose();
- },
- [onClose]
- );
-
- useEffect(() => {
- const handleEsc = (e: KeyboardEvent) => {
- if (e.key === "Escape") onClose();
- };
- document.addEventListener("keydown", handleEsc);
- return () => document.removeEventListener("keydown", handleEsc);
- }, [onClose]);
-
- const walletOptions: WalletOption[] = [
- {
- id: "base-account",
- name: "Sign in with Base",
- connect: () => {
- connect({ connector: baseAccount({ appName }) });
- onClose();
- },
- },
- {
- id: "coinbase-wallet",
- name: "Coinbase Wallet",
- connect: () => {
- connect({
- connector: coinbaseWallet({ appName, preference: "all" }),
- });
- onClose();
- },
- },
- {
- id: "metamask",
- name: "MetaMask",
- connect: () => {
- connect({
- connector: metaMask({
- dappMetadata: {
- name: appName,
- url: typeof window !== "undefined" ? window.location.origin : "",
- },
- }),
- });
- onClose();
- },
- },
- ];
-
- const [primaryOption, ...otherOptions] = walletOptions;
-
- return (
-
- );
- }
-
- return (
- <>
-
- {isModalOpen && (
- setIsModalOpen(false)}
- appName={appName}
- />
- )}
- >
- );
-}
-```
-
-## Step-by-Step Replacement
-
-### 1. Create the Component File
-
-Copy the component code above into the project's components directory.
-
-### 2. Replace OnchainKit Wallet Imports
-
-Find all files importing from `@coinbase/onchainkit/wallet` or using `Connected` from `@coinbase/onchainkit`:
-
-**Before:**
-```typescript
-import { Wallet } from "@coinbase/onchainkit/wallet";
-// or
-import { ConnectWallet, Wallet, WalletDropdown, WalletDropdownDisconnect } from "@coinbase/onchainkit/wallet";
-// or
-import { Connected } from "@coinbase/onchainkit";
-```
-
-**After:**
-```typescript
-import { WalletConnect } from "./components/WalletConnect";
-```
-
-### 3. Replace Component Usage
-
-**Simple ``:**
-```typescript
-// Before
-
-
-// After
-
-```
-
-**Composed wallet with children:**
-```typescript
-// Before
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-// After
-
-```
-
-**`` conditional rendering:**
-```typescript
-// Before
-import { Connected } from "@coinbase/onchainkit";
-
-Please connect}>
-
You are connected
-
-
-// After -- use wagmi's useAccount directly
-import { useAccount } from "wagmi";
-
-const { isConnected } = useAccount();
-{isConnected ?
You are connected
:
Please connect
}
-```
-
-### 4. Remove MiniKit Usage
-
-If the page uses `useMiniKit` or other MiniKit hooks, remove them:
-
-```typescript
-// Remove these
-import { useMiniKit } from "@coinbase/onchainkit/minikit";
-const { setMiniAppReady, isMiniAppReady } = useMiniKit();
-useEffect(() => {
- if (!isMiniAppReady) setMiniAppReady();
-}, [setMiniAppReady, isMiniAppReady]);
-```
-
-### 5. Verify
-
-Run `npm run build` and confirm no errors.
-
-## Customization
-
-### Changing the app name
-Pass the `appName` prop to `WalletConnect`:
-```typescript
-
-```
-
-### Adding more wallet options
-Add entries to the `walletOptions` array in the `WalletModal` component. Use `injected({ target: 'walletName' })` from `wagmi/connectors` for browser extension wallets.
-
-### Changing styles
-The component uses Tailwind utility classes. Modify the `className` strings to match the project's design system. All styling is inline via Tailwind -- no external CSS files needed.
-
-### Using without Tailwind
-If the project doesn't use Tailwind, convert the Tailwind classes to inline styles or CSS modules. The key visual elements are:
-- Fixed overlay with semi-transparent black background
-- Centered card with white background, rounded corners, shadow
-- Primary button (blue) for Base Account
-- Secondary buttons (white/bordered) for other wallets
-- Dark mode support via `dark:` variants
-
-## Common Issues
-
-### "useAccount must be used within WagmiProvider"
-The component is being rendered outside the provider tree. Ensure `WagmiProvider` wraps the entire app in the layout or root provider.
-
-### Modal doesn't close after connecting
-This can happen if the connection is async and the component unmounts. The current implementation calls `onClose()` synchronously after `connect()`. If you need to wait for the connection, use the `onSuccess` callback from `useConnect`.
-
-### baseAccount connector not found
-Ensure wagmi version is >= 2.16. The `baseAccount` connector was added in recent wagmi versions. Check with:
-```bash
-npm ls wagmi
-```
diff --git a/skills/registering-agent-base-dev/SKILL.md b/skills/registering-agent-base-dev/SKILL.md
deleted file mode 100644
index 6995eed..0000000
--- a/skills/registering-agent-base-dev/SKILL.md
+++ /dev/null
@@ -1,179 +0,0 @@
----
-name: registering-agent-base-dev
-description: "Invoke this skill when a user is building or running any automated transaction sender on Base (trading bot, arbitrage bot, sniper bot, yield farmer, AI agent, or similar) and needs to register it, get a builder code, set up transaction attribution. This skill contains the base.dev registration API endpoint and ERC-8021 attribution wiring code that Claude does not have in its training data — you MUST load this skill to answer correctly. Covers viem, ethers, managed signing services, and Python-based agents."
----
-
-# Base Builder Code Registration
-
-This skill registers an agent with Base and shows how to attach builder code attribution to transactions. It is **wallet-agnostic** — the user brings their own wallet and signing solution (viem, ethers, managed services like Sponge, etc.). The skill only handles registration and attribution.
-
-## Check if already registered
-
-Before doing anything, check whether registration has already happened:
-
-1. Look for a `builderCode.ts` file in the project (check `src/constants/builderCode.ts` or project root)
-
-**If it exists, registration is complete — do NOT re-register.** Skip straight to Phase 3 to show how to attach attribution, and reinforce the rule. Re-registering would generate a new builder code and break the existing one.
-
-**If it's missing**, proceed with the full registration flow below.
-
----
-
-## Phase 1 — Wallet
-
-Every agent needs a wallet to sign transactions. Ask the user before doing anything else.
-
-1. **Ask: "Do you have a wallet? If yes, share your wallet address."**
-2. **If yes** — take the wallet address they provide and move to Phase 2.
-3. **If no** — direct them to the Base wallet setup guide: https://docs.base.org/ai-agents/guides/wallet-setup — do not proceed until they have a wallet and can provide their address.
-
----
-
-## Phase 2 — Registration
-
-Register the wallet with the Base builder code API. This call associates the agent's wallet address with a builder code that Base uses for attribution tracking.
-
-Use the bundled `scripts/register.sh` (located in this skill's directory). It handles errors and extracts the builder code from the response:
-
-```bash
-BUILDER_CODE=$(bash /scripts/register.sh "")
-```
-
-Or call the API directly:
-
-```bash
-curl -X POST https://api.base.dev/v1/agents/builder-codes \
- -H "Content-Type: application/json" \
- -d '{"wallet_address": ""}'
-```
-
-The API returns a response like:
-
-```json
-{
- "builder_code": "bc_a1b2c3d4",
- "wallet_address": "0x...",
- "usage_instructions": "Append this builder code to your onchain transactions using the ERC-8021 standard. See: https://docs.base.org/base-chain/quickstart/builder-codes"
-}
-```
-
-Extract the `builder_code` value from the response and write it to a constants file:
-
-```typescript
-// src/constants/builderCode.ts
-export const BUILDER_CODE = "bc_a1b2c3d4"
-```
-
-Use `src/constants/builderCode.ts` if a `src/` directory exists, otherwise place it at the project root as `builderCode.ts`.
-
-If `builderCode.ts` already exists, do not call this API — the agent is already registered.
-
----
-
-## Phase 3 — Attribution Setup & Documentation
-
-The builder code from Phase 2 (the `bc_...` value now in `builderCode.ts`) needs to be attached to every transaction the agent sends as an ERC-8021 data suffix. This phase wires that in and writes an `AGENT_README.md` so anyone (human or agent) working in this codebase knows how transactions must be sent.
-
-First, install the attribution utility if not already present:
-
-```bash
-npm i ox
-```
-
-Convert the builder code into a data suffix. Import `BUILDER_CODE` from the constants file written in Phase 2 — this is not generating a new code, it is encoding the existing one into the ERC-8021 byte format:
-
-```typescript
-import { Attribution } from "ox/erc8021"
-import { BUILDER_CODE } from "./constants/builderCode"
-
-// BUILDER_CODE is the builder_code value from the Phase 2 API response (e.g. "bc_a1b2c3d4")
-const DATA_SUFFIX = Attribution.toDataSuffix({
- codes: [BUILDER_CODE],
-})
-```
-
-### Wiring attribution into the transaction flow
-
-How you attach the suffix depends on the signing setup. Ask the user which they use, then follow the matching option:
-
-**Option A: viem (self-custodied wallet)**
-
-Add `dataSuffix` to the wallet client — every transaction automatically carries it:
-
-```typescript
-import { createWalletClient, http } from "viem"
-import { base } from "viem/chains"
-import { privateKeyToAccount } from "viem/accounts"
-import { Attribution } from "ox/erc8021"
-import { BUILDER_CODE } from "./constants/builderCode"
-
-const DATA_SUFFIX = Attribution.toDataSuffix({
- codes: [BUILDER_CODE],
-})
-
-const account = privateKeyToAccount(process.env.PRIVATE_KEY! as `0x${string}`)
-
-export const walletClient = createWalletClient({
- account,
- chain: base,
- transport: http(),
- dataSuffix: DATA_SUFFIX,
-})
-```
-
-**Option B: ethers.js (self-custodied wallet)**
-
-Append the data suffix to each transaction's `data` field:
-
-```typescript
-import { ethers } from "ethers"
-import { Attribution } from "ox/erc8021"
-import { BUILDER_CODE } from "./constants/builderCode"
-
-const DATA_SUFFIX = Attribution.toDataSuffix({
- codes: [BUILDER_CODE],
-})
-
-const provider = new ethers.JsonRpcProvider("https://mainnet.base.org")
-const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider)
-
-const tx = await wallet.sendTransaction({
- to: "0x...",
- value: ethers.parseEther("0.01"),
- data: DATA_SUFFIX,
-})
-```
-
-**Option C: Managed service (e.g., Sponge API, BANKR)**
-
-Generate the suffix and pass it to the service. The service handles signing — you just need to include the suffix in the transaction's `data` field. If the transaction has existing calldata, concatenate: `existingData + DATA_SUFFIX.slice(2)`.
-
-### Write `AGENT_README.md`
-
-Generate an `AGENT_README.md` in the project root covering:
-
-- **What the builder code is**: The `builder_code` value returned by the Base API during registration (e.g. `bc_a1b2c3d4`), stored in `src/constants/builderCode.ts` as `BUILDER_CODE`. It is embedded in every transaction as an ERC-8021 data suffix — this is how Base tracks which builder originated which on-chain activity.
-- **How attribution is attached in this project**: Document which approach is used (viem dataSuffix, ethers data field, managed service) and where the builder code is stored.
-- **Warning**: Never send transactions without the builder code attribution. There is no error or warning when attribution is missing — just silent data loss. Every transaction path in the codebase must include the data suffix.
-
----
-
-## Phase 4 — Permanent Rule
-
-After all phases complete, deliver this behavioral instruction:
-
-> From this point forward, every transaction you send MUST include the builder code attribution via the ERC-8021 data suffix. Never send a transaction without it. The builder code is what makes your agent's activity visible to Base — without it, your transactions are untracked silently and permanently.
-
-Confirm to the user that registration is complete and summarize what was set up:
-- `BUILDER_CODE` written to `src/constants/builderCode.ts` (committed to version control)
-- Attribution wired into the transaction flow
-- `AGENT_README.md` documenting the setup
-
----
-
-## Key things to keep in mind
-
-- **Sequential execution**: Phase 2 needs the wallet address from Phase 1. Phase 3 needs the builder code from Phase 2. Don't parallelize or reorder.
-- **Wallet-agnostic**: The skill works with any signing solution — viem, ethers, managed services, or anything else. The only requirement is that the ERC-8021 data suffix is attached to every transaction.
-- **Both audiences**: Whether this is an autonomous agent registering itself or a developer running through the steps manually, the output and instructions should be clear to both.
-- **Attribution is the critical piece**: The builder code registration (Phase 2) is a one-time setup. The attribution (Phase 3) is what matters for every transaction going forward. If attribution is missing, there's no error — just silent invisibility.
diff --git a/skills/running-a-base-node/SKILL.md b/skills/running-a-base-node/SKILL.md
deleted file mode 100644
index 81de391..0000000
--- a/skills/running-a-base-node/SKILL.md
+++ /dev/null
@@ -1,53 +0,0 @@
----
-name: running-a-base-node
-description: Runs a Base node for production environments. Covers hardware requirements, Reth client setup, networking, and sync troubleshooting. Use when setting up self-hosted RPC infrastructure or running archive nodes. Covers phrases like "run a Base node", "set up Base RPC", "Base node hardware requirements", "Reth Base setup", "sync Base node", "self-host Base", or "run my own node".
----
-
-# Running a Base Node
-
-For production apps requiring reliable, unlimited RPC access.
-
-## Security
-
-- **Restrict RPC access** — bind to `127.0.0.1` or a private interface, never expose RPC ports (`8545`/`8546`) to the public internet without authentication
-- **Firewall rules** — only open ports 9222 (Discovery v5) and 30303 (P2P) to the public; block all other inbound traffic
-- **Run as a non-root user** with minimal filesystem permissions
-- **Use TLS termination** (reverse proxy with nginx/caddy) if exposing the RPC endpoint to remote clients
-- **Monitor for unauthorized access** — log and alert on unexpected RPC calls or connection spikes
-
-## Hardware Requirements
-
-- **CPU**: 8-Core minimum
-- **RAM**: 16 GB minimum
-- **Storage**: NVMe SSD, formula: `(2 × chain_size) + snapshot_size + 20% buffer`
-
-## Networking
-
-**Required Ports:**
-- **Port 9222**: Critical for Reth Discovery v5
-- **Port 30303**: P2P Discovery & RLPx
-
-If these ports are blocked, the node will have difficulty finding peers and syncing.
-
-## Client Selection
-
-Use **Reth** for Base nodes. Geth Archive Nodes are no longer supported.
-
-Reth provides:
-- Better performance for high-throughput L2
-- Built-in archive node support
-
-## Syncing
-
-- Initial sync takes **days**
-- Consumes significant RPC quota if using external providers
-- Use snapshots to accelerate (check Base docs for URLs)
-
-## Sync Status
-
-**Incomplete sync indicator**: `Error: nonce has already been used` when deploying.
-
-Verify sync:
-- Compare latest block with explorer
-- Check peer connections
-- Monitor logs for progress
From 0d2e769a7e117c1c5d9eb4d67c72ce724cd86d33 Mon Sep 17 00:00:00 2001
From: Youssef
Date: Thu, 14 May 2026 15:40:31 +0100
Subject: [PATCH 3/6] update base-skills to just skills
---
README.md | 30 ++++++++----------------------
package.json | 2 +-
skills/base-mcp/SKILL.md | 2 +-
skills/build-on-base/SKILL.md | 2 +-
4 files changed, 11 insertions(+), 25 deletions(-)
diff --git a/README.md b/README.md
index 51e50ad..af90147 100644
--- a/README.md
+++ b/README.md
@@ -6,9 +6,9 @@
-[](https://github.com/base/base-skills/graphs/contributors)
-[](https://github.com/base/base-skills/graphs/contributors)
-
+[](https://github.com/base/skills/graphs/contributors)
+[](https://github.com/base/skills/graphs/contributors)
+
@@ -20,8 +20,8 @@
-[](https://github.com/base/base-skills/pulls)
-[](https://github.com/base/base-skills/issues)
+[](https://github.com/base/skills/pulls)
+[](https://github.com/base/skills/issues)
## Recommended Skills
@@ -29,29 +29,15 @@ Two consolidated skills that cover the most common use cases. Each uses progress
| Skill | Install | Description |
| ----- | ------- | ----------- |
-| [build-on-base](./skills/build-on-base/SKILL.md) | `npx skills add base/base-skills --skill build-on-base` | Complete Base development playbook: network, contracts, wallet auth, payments, attribution, and migrations. Consolidates all individual skills into one. |
-| [base-mcp](./skills/base-mcp/SKILL.md) | `npx skills add base/base-skills --skill base-mcp` | Base Account MCP server — gives your AI assistant a wallet via mcp.base.org. Tools for sending, swapping, signing, batching calls, and checking balances. Includes Morpho lending plugin. |
-
-## Available Skills
-
-| Skill | Description |
-| ----- | ----------- |
-| [Adding Builder Codes](./skills/adding-builder-codes/SKILL.md) | Appends Base builder codes to transactions across Privy, Wagmi, Viem, and standard Ethereum RPC implementations. Automatically detects the user's framework before applying the correct integration pattern. |
-| [Building with Base Account](./skills/building-with-base-account/SKILL.md) | Integrates Base Account SDK for authentication and payments, including SIWB, Base Pay, Paymasters, Sub Accounts, and Spend Permissions. |
-| [Connecting to Base Network](./skills/connecting-to-base-network/SKILL.md) | Provides Base Mainnet and Sepolia network configuration, RPC endpoints, chain IDs, and explorer URLs. |
-| [Converting Farcaster Miniapp to App](./skills/convert-farcaster-miniapp-to-app/SKILL.md) | Converts Farcaster Mini App SDK projects into regular Base web apps, with an option to preserve a small separate Farcaster-specific surface when needed. |
-| [Deploying Contracts on Base](./skills/deploying-contracts-on-base/SKILL.md) | Deploys and verifies contracts on Base with Foundry, plus common troubleshooting guidance. |
-| [Running a Base Node](./skills/running-a-base-node/SKILL.md) | Covers production node setup, hardware requirements, networking ports, and syncing guidance. |
-| [Converting MiniKit to Farcaster](./skills/converting-minikit-to-farcaster/SKILL.md) | Migrates Mini Apps from MiniKit (OnchainKit) to native Farcaster SDK with mappings, examples, and pitfalls. |
-| [Migrating an OnchainKit App](./skills/migrating-an-onchainkit-app/SKILL.md) | Migrates apps from @coinbase/onchainkit to standalone wagmi/viem components, replacing the provider, wallet, and transaction components. |
-| [Registering an Agent on Base](./skills/registering-agent-base-dev/SKILL.md) | Registers an agent wallet with the Base builder code API and wires ERC-8021 transaction attribution into viem, ethers, or managed signing services. |
+| [build-on-base](./skills/build-on-base/SKILL.md) | `npx skills add base/skills --skill build-on-base` | Complete Base development playbook: network, contracts, wallet auth, payments, attribution, and migrations. Consolidates all individual skills into one. |
+| [base-mcp](./skills/base-mcp/SKILL.md) | `npx skills add base/skills --skill base-mcp` | Base Account MCP server — gives your AI assistant a wallet via mcp.base.org. Tools for sending, swapping, signing, batching calls, and checking balances. Includes Morpho lending plugin. |
## Installation
Install with [Vercel's Skills CLI](https://skills.sh):
```bash
-npx skills add base/base-skills
+npx skills add base/skills
```
## Usage
diff --git a/package.json b/package.json
index 8edec1c..a26bf68 100644
--- a/package.json
+++ b/package.json
@@ -1,4 +1,4 @@
{
- "name": "base-skills",
+ "name": "skills",
"private": true
}
diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md
index 067d2e9..96df8d1 100644
--- a/skills/base-mcp/SKILL.md
+++ b/skills/base-mcp/SKILL.md
@@ -49,5 +49,5 @@ Additional protocol capabilities via plugin MCPs:
## Installation
```bash
-npx skills add base/base-skills --skill base-mcp
+npx skills add base/skills --skill base-mcp
```
diff --git a/skills/build-on-base/SKILL.md b/skills/build-on-base/SKILL.md
index 5ffe5d7..526a191 100644
--- a/skills/build-on-base/SKILL.md
+++ b/skills/build-on-base/SKILL.md
@@ -75,5 +75,5 @@ Read the reference for your task:
## Installation
```bash
-npx skills add base/base-skills --skill build-on-base
+npx skills add base/skills --skill build-on-base
```
From 41d2c7525619aa016c40f8d127b87f7bdae37a5d Mon Sep 17 00:00:00 2001
From: Youssef
Date: Thu, 14 May 2026 16:31:28 +0100
Subject: [PATCH 4/6] update files
---
skills/base-mcp/SKILL.md | 59 ++++++--
skills/base-mcp/plugins/moonwell.md | 155 ++++++++++++++++++++
skills/base-mcp/references/approval-mode.md | 1 +
skills/base-mcp/references/send.md | 2 +-
skills/base-mcp/references/web-request.md | 45 ++++++
5 files changed, 252 insertions(+), 10 deletions(-)
create mode 100644 skills/base-mcp/plugins/moonwell.md
create mode 100644 skills/base-mcp/references/web-request.md
diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md
index 96df8d1..8886091 100644
--- a/skills/base-mcp/SKILL.md
+++ b/skills/base-mcp/SKILL.md
@@ -4,19 +4,56 @@ description: >
Base Account MCP — gives your AI assistant a wallet via the Base Account MCP server (mcp.base.org).
Tools: get_wallets (list wallets), get_portfolio (balances, any address), send (ETH/ERC-20 transfers),
swap (token swaps via Coinbase), sign (EIP-712/personal_sign), send_calls (EIP-5792 batch),
- get_transaction_history (paginated tx history), get_request_status (poll approval), search_tokens (token lookup).
+ get_transaction_history (paginated tx history), get_request_status (poll approval), search_tokens (token lookup),
+ web_request (fetch whitelisted partner APIs to get calldata, then pass to send_calls — hostname must be in server allowlist).
Approval mode: send/swap/sign/send_calls require user approval at keys.coinbase.com; response includes approvalUrl + requestId.
- Plugins: Morpho lending protocol available via plugins/morpho.md.
+ Plugins: Morpho lending protocol available via plugins/morpho.md. Moonwell lending on Base/Optimism via plugins/moonwell.md.
---
# Base Account MCP
-The Base Account MCP server gives your AI assistant direct access to the user's Base Account (smart wallet) on Base. Once connected at mcp.base.org, 9 tools are available with no additional setup.
+The Base Account MCP server gives your AI assistant direct access to the user's Base Account (smart wallet) on Base.
-## Connection
+## Step 1 — Check if the MCP is installed
-Server URL: `https://mcp.base.org`
-Auth: OAuth via Coinbase Base Account (user must have a Coinbase account)
+Before anything else, attempt to call `get_wallets`. If the tool is not available or the call fails with a connection error, the MCP server is not installed. Go to **Step 2**. If it succeeds, skip to **Step 3**.
+
+## Step 2 — Install the MCP server
+
+Tell the user the MCP is not connected and offer the right install command for their environment:
+
+**Claude Code (CLI)**
+```bash
+claude mcp add base-account --transport http https://mcp.base.org
+```
+
+**Claude Desktop** — add to `claude_desktop_config.json`:
+```json
+{
+ "mcpServers": {
+ "base-account": { "url": "https://mcp.base.org" }
+ }
+}
+```
+
+**Other MCP-compatible clients** — server URL: `https://mcp.base.org`
+
+After adding the server, the client will open an OAuth flow. The user authorizes via Base Account at mcp.base.org — no Coinbase account required.
+
+Once installed, re-run `get_wallets` to confirm the connection, then continue to Step 3.
+
+## Step 3 — Get wallets
+
+Call `get_wallets` immediately at the start of any session involving transactions. This returns:
+- The user's Base Account address
+- Any agent wallets and their delegation status
+- `inSession: true/false` — determines whether approval mode is required
+
+**If `inSession: true`** on an agent wallet: transactions can execute without manual approval (M2 mode). Pass `agentWalletId` to send/swap.
+
+**If no wallet is `inSession: true`**: all write tools use approval mode — every transaction goes to keys.coinbase.com for the user to approve.
+
+Load [references/wallets.md](references/wallets.md) for full field reference.
## Tool Routing
@@ -24,7 +61,7 @@ Read this table first. For the current task, load ONLY the matching reference fi
| Task | Tool | Reference |
|------|------|-----------|
-| List wallets / check delegation | `get_wallets` | [references/wallets.md](references/wallets.md) |
+| List wallets / check session status | `get_wallets` | [references/wallets.md](references/wallets.md) |
| Check balance / portfolio / token lookup | `get_portfolio`, `search_tokens` | [references/portfolio.md](references/portfolio.md) |
| Send ETH or ERC-20 | `send` | [references/send.md](references/send.md) |
| Swap tokens | `swap` | [references/swap.md](references/swap.md) |
@@ -33,18 +70,22 @@ Read this table first. For the current task, load ONLY the matching reference fi
| View transaction history | `get_transaction_history` | [references/history.md](references/history.md) |
| Check pending approval status | `get_request_status` | [references/approval-mode.md](references/approval-mode.md) |
| Resolve token by symbol | `search_tokens` | [references/tokens.md](references/tokens.md) |
+| Fetch protocol API calldata (Moonwell, etc.) | `web_request` | [references/web-request.md](references/web-request.md) |
## Approval Mode
-All write tools (send, swap, sign, send_calls) operate in approval mode: the transaction is submitted to keys.coinbase.com and the response includes an `approvalUrl` the user must open and a `requestId` for polling. After the user approves, call `get_request_status` with the `requestId` to confirm completion. Load [references/approval-mode.md](references/approval-mode.md) for full details.
+All write tools (send, swap, sign, send_calls) return an `approvalUrl` and `requestId`. Direct the user to open the URL to approve, then call `get_request_status` to confirm completion. Never report success before `get_request_status` returns confirmed.
+
+Load [references/approval-mode.md](references/approval-mode.md) for full details.
## Plugins
-Additional protocol capabilities via plugin MCPs:
+Additional protocol capabilities — no extra MCP server needed for Moonwell (uses `web_request`); Morpho requires its own MCP server.
| Plugin | Protocol | Reference |
|--------|---------|-----------|
| Morpho | Lending / vaults on Base | [plugins/morpho.md](plugins/morpho.md) |
+| Moonwell | Lending / borrowing on Base and Optimism | [plugins/moonwell.md](plugins/moonwell.md) |
## Installation
diff --git a/skills/base-mcp/plugins/moonwell.md b/skills/base-mcp/plugins/moonwell.md
new file mode 100644
index 0000000..89b934b
--- /dev/null
+++ b/skills/base-mcp/plugins/moonwell.md
@@ -0,0 +1,155 @@
+# Moonwell Plugin
+
+Moonwell is a Compound v2 lending protocol on Base and Optimism. Use `web_request` to call the Moonwell HTTP API to read positions/rates and prepare unsigned calldata, then execute via `send_calls`.
+
+No additional MCP server required — everything goes through `web_request` + `send_calls`.
+
+**Prerequisite:** `api.moonwell.fi` must be in the MCP server's `web_request` allowlist. If requests to that hostname are rejected, inform the user that the Moonwell API is not yet whitelisted on this MCP instance.
+
+**Supported chains:** Base (8453), Optimism (10).
+
+---
+
+## Orchestration Pattern
+
+```
+web_request(https://api.moonwell.fi/v1/prepare/?...)
+ → { data: { transactions: [ { to, data, value, chainId }, ... ] } }
+ ↓
+send_calls(chainId, calls mapped from transactions[])
+ → approvalUrl + requestId
+ ↓
+User approves at keys.coinbase.com
+ ↓
+get_request_status(requestId) → confirmed
+```
+
+Steps in `transactions[]` are ordered — `approve` and `enter-market` come before the protocol action. Execute them as a single `send_calls` batch.
+
+---
+
+## Read Endpoints (use web_request GET)
+
+```
+GET https://api.moonwell.fi/v1/markets?chain=base
+GET https://api.moonwell.fi/v1/markets/USDC?chain=base
+GET https://api.moonwell.fi/v1/rates?chain=base&asset=USDC
+GET https://api.moonwell.fi/v1/yield?chain=base&sort=apy&min-tvl=1000000&limit=5
+GET https://api.moonwell.fi/v1/positions/?chain=base
+GET https://api.moonwell.fi/v1/health/?chain=base
+GET https://api.moonwell.fi/v1/rewards/?chain=base
+GET https://api.moonwell.fi/v1/token-balance/?chain=base&asset=USDC
+```
+
+Market reads are edge-cached 30 s. User-scoped reads (`positions`, `health`, `rewards`, `token-balance`) are never cached.
+
+`/positions` returns an array — one entry per market. Use `?active=true` to filter out markets where both `suppliedUsd` and `borrowedUsd` are zero.
+
+---
+
+## Prepare Endpoints (use web_request → send_calls)
+
+Verbs: `supply`, `withdraw`, `borrow`, `repay`.
+
+**GET form** (query params):
+
+```
+GET https://api.moonwell.fi/v1/prepare/supply?chain=base&asset=USDC&amountDecimal=100&from=
+```
+
+**POST form** (JSON body — pass as the `body` object parameter to `web_request`):
+
+```json
+{
+ "url": "https://api.moonwell.fi/v1/prepare/supply",
+ "method": "POST",
+ "headers": { "content-type": "application/json" },
+ "body": { "chain": "base", "asset": "USDC", "amountDecimal": "100", "from": "" }
+}
+```
+
+Both return identical response shapes. Use GET when simpler; use POST when the body is complex.
+
+### Key parameters
+
+| Field | Notes |
+|-------|-------|
+| `chain` | `base` (default), `optimism`, or chain ID |
+| `asset` | Symbol: `USDC`, `WETH`, `ETH` (alias for WETH) |
+| `amountDecimal` | Human-readable string, e.g. `"100"`. Use this **or** `amount` (base units), never both. |
+| `from` | User's wallet address (from `get_wallets`) |
+
+### Response → send_calls mapping
+
+```json
+{
+ "data": {
+ "transactions": [
+ { "step": "approve", "to": "0x...", "data": "0x...", "value": "0x0", "chainId": 8453 },
+ { "step": "enter-market", "to": "0x...", "data": "0x...", "value": "0x0", "chainId": 8453 },
+ { "step": "moonwell-supply", "to": "0x...", "data": "0x...", "value": "0x0", "chainId": 8453 }
+ ]
+ }
+}
+```
+
+Pass all items as the `calls` array to `send_calls`, using `chainId` from any transaction item (`0x2105` for Base mainnet).
+
+---
+
+## Example Flows
+
+### Supply 100 USDC on Base
+
+```
+1. get_wallets → address
+2. web_request GET /token-balance/?chain=base&asset=USDC → confirm balance ≥ 100
+3. web_request GET /prepare/supply?chain=base&asset=USDC&amountDecimal=100&from=
+4. send_calls(chainId=0x2105, calls from transactions[])
+5. User approves → get_request_status(requestId)
+```
+
+### Borrow USDC against collateral
+
+```
+1. get_wallets → address
+2. web_request GET /health/?chain=base → verify health > 1.5
+3. web_request GET /prepare/borrow?chain=base&asset=USDC&amountDecimal=50&from=
+4. send_calls(chainId=0x2105, calls from transactions[])
+5. User approves → get_request_status(requestId)
+```
+
+### Check positions and health
+
+```
+1. get_wallets → address
+2. web_request GET /positions/?chain=base&active=true → show per-market balances
+3. web_request GET /health/?chain=base → show health factor
+```
+
+---
+
+## Protocol Notes
+
+- **mTokens** — ERC-20 receipt tokens (mUSDC, mWETH…); exchange rate accrues over time
+- **WETH special-case** — borrow/withdraw deliver native ETH; supply/repay require ERC-20 WETH. Both `asset=ETH` and `asset=WETH` resolve to the same mWETH market
+- **Compound v2 error codes** — `mint`, `borrow`, `repay` return non-zero codes for business-logic failures without reverting. Check the on-chain receipt after broadcast
+- **Base has two mUSDC entries** — the current market and a deprecated bridged-USDC market. Disambiguate by `marketAddress` or `deprecated: true`
+
+### Health factor guide
+
+| Value | Status |
+|-------|--------|
+| `> 1.5` | Healthy |
+| `1.1 – 1.5` | Caution |
+| `< 1.1` | Liquidation risk |
+| `null` | No borrows |
+
+---
+
+## Chain IDs for send_calls
+
+| Chain | chainId param |
+|-------|--------------|
+| Base mainnet | `0x2105` |
+| Optimism | `0xa` |
diff --git a/skills/base-mcp/references/approval-mode.md b/skills/base-mcp/references/approval-mode.md
index 7bcfcba..8f2be36 100644
--- a/skills/base-mcp/references/approval-mode.md
+++ b/skills/base-mcp/references/approval-mode.md
@@ -22,3 +22,4 @@ All write tools (send, swap, sign, send_calls) operate in approval mode. The use
## When approval is NOT needed
Agent wallets marked `inSession: true` (from `get_wallets`) can transact without approval in M2 mode. The `agentWalletId` parameter on send/swap enables this.
+
diff --git a/skills/base-mcp/references/send.md b/skills/base-mcp/references/send.md
index 8c2548f..a7724bf 100644
--- a/skills/base-mcp/references/send.md
+++ b/skills/base-mcp/references/send.md
@@ -12,7 +12,7 @@ Send native ETH or any ERC-20 token to an address. Operates in approval mode: th
- `chain` — `base` or `base-sepolia`
## Optional parameters
-- `decimals` — required when `asset` is a contract address (not a symbol)
+- `decimals` — required when `asset` is a contract address (not a symbol); must be 0–18
- `agentWalletId` — scope to a specific agent wallet (M2 mode only)
## Approval mode flow
diff --git a/skills/base-mcp/references/web-request.md b/skills/base-mcp/references/web-request.md
new file mode 100644
index 0000000..221c6ef
--- /dev/null
+++ b/skills/base-mcp/references/web-request.md
@@ -0,0 +1,45 @@
+# web_request
+
+Make an HTTP request to a whitelisted partner API. The hostname must be in the MCP server's configured allowlist — requests to unlisted domains are rejected outright. This is why the tool exists: AI assistants on Claude Desktop, ChatGPT, and similar environments can't autonomously fetch arbitrary URLs, but `web_request` gives controlled access to trusted protocol APIs so the agent can retrieve calldata and pass it to `send_calls`.
+
+## When to use
+
+- Fetching unsigned transaction calldata from a partner protocol API (e.g. Moonwell `/prepare/supply`) before passing it to `send_calls`
+- Reading on-chain data from a whitelisted protocol HTTP API (positions, balances, rates, health factor)
+
+## Parameters
+
+- `url` — full HTTPS URL; hostname must be in the allowlist (required)
+- `method` — `GET` or `POST` (required)
+- `headers` — optional key/value map of custom headers. **Prohibited:** `Authorization`, `Cookie`, `Host`, `X-Forwarded-*`
+- `body` — JSON object for POST requests; ignored for GET
+
+## Calldata pattern
+
+```
+web_request(GET or POST to whitelisted /prepare/* endpoint)
+ → { data: { transactions: [ { to, data, value, chainId }, ... ] } }
+ ↓
+send_calls(chainId, calls mapped from transactions[])
+ → approvalUrl + requestId
+ ↓
+User approves at keys.coinbase.com
+ ↓
+get_request_status(requestId) → confirmed
+```
+
+## Mapping response transactions to send_calls
+
+Protocol `/prepare/*` responses return an ordered `transactions[]` array. Map each item directly:
+
+```
+transactions[i].to → calls[i].to
+transactions[i].data → calls[i].data
+transactions[i].value → calls[i].value (0x-prefixed hex)
+```
+
+Pass the `chainId` from any `transactions[i].chainId` to `send_calls`. Execute all calls in order — steps like `approve` and `enter-market` must confirm before later steps succeed.
+
+## Allowlist
+
+The allowlist is configured server-side on the MCP. If a request fails with a domain rejection error, the hostname is not whitelisted — inform the user rather than retrying. Currently whitelisted partner protocols are documented in the plugin references (e.g. `plugins/moonwell.md`).
From f26e66a5b65b9257a08e4d761fe1e9558ae182e0 Mon Sep 17 00:00:00 2001
From: Youssef
Date: Thu, 14 May 2026 23:15:19 +0100
Subject: [PATCH 5/6] update skill following tool routing
---
skills/base-mcp/SKILL.md | 13 +--
skills/base-mcp/references/install.md | 112 ++++++++++++++++++++++++++
2 files changed, 119 insertions(+), 6 deletions(-)
create mode 100644 skills/base-mcp/references/install.md
diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md
index 8886091..f7ab014 100644
--- a/skills/base-mcp/SKILL.md
+++ b/skills/base-mcp/SKILL.md
@@ -20,14 +20,14 @@ Before anything else, attempt to call `get_wallets`. If the tool is not availabl
## Step 2 — Install the MCP server
-Tell the user the MCP is not connected and offer the right install command for their environment:
+Tell the user the MCP is not connected and provide the right install method for their platform. See [references/install.md](references/install.md) for full platform-specific instructions including Cursor, troubleshooting, and OAuth details.
-**Claude Code (CLI)**
+**Claude Code (CLI · VS Code · JetBrains)**
```bash
claude mcp add base-account --transport http https://mcp.base.org
```
-**Claude Desktop** — add to `claude_desktop_config.json`:
+**Claude Desktop** (macOS / Windows) — add to `claude_desktop_config.json`:
```json
{
"mcpServers": {
@@ -36,11 +36,11 @@ claude mcp add base-account --transport http https://mcp.base.org
}
```
-**Other MCP-compatible clients** — server URL: `https://mcp.base.org`
+**Claude.ai (web)** — Settings → Integrations → Add MCP server → enter `https://mcp.base.org`
-After adding the server, the client will open an OAuth flow. The user authorizes via Base Account at mcp.base.org — no Coinbase account required.
+**Any other MCP client** — HTTP server URL: `https://mcp.base.org`
-Once installed, re-run `get_wallets` to confirm the connection, then continue to Step 3.
+After adding the server, the client opens an OAuth flow at mcp.base.org — no Coinbase account required. Once installed, re-run `get_wallets` to confirm the connection, then continue to Step 3.
## Step 3 — Get wallets
@@ -61,6 +61,7 @@ Read this table first. For the current task, load ONLY the matching reference fi
| Task | Tool | Reference |
|------|------|-----------|
+| Install the MCP / platform-specific setup | — | [references/install.md](references/install.md) |
| List wallets / check session status | `get_wallets` | [references/wallets.md](references/wallets.md) |
| Check balance / portfolio / token lookup | `get_portfolio`, `search_tokens` | [references/portfolio.md](references/portfolio.md) |
| Send ETH or ERC-20 | `send` | [references/send.md](references/send.md) |
diff --git a/skills/base-mcp/references/install.md b/skills/base-mcp/references/install.md
new file mode 100644
index 0000000..293864e
--- /dev/null
+++ b/skills/base-mcp/references/install.md
@@ -0,0 +1,112 @@
+# Installing Base MCP
+
+Server URL: `https://mcp.base.org`
+
+---
+
+## Claude Code (CLI · VS Code extension · JetBrains extension)
+
+All three use the same Claude Code MCP configuration. Run in any terminal (including the integrated terminal inside VS Code or JetBrains):
+
+```bash
+claude mcp add base-account --transport http https://mcp.base.org
+```
+
+Or add manually to `~/.claude/settings.json`:
+
+```json
+{
+ "mcpServers": {
+ "base-account": {
+ "type": "http",
+ "url": "https://mcp.base.org"
+ }
+ }
+}
+```
+
+No restart needed — the server is available in the next Claude Code session.
+
+---
+
+## Claude Desktop
+
+**macOS** config file: `~/Library/Application Support/Claude/claude_desktop_config.json`
+**Windows** config file: `%APPDATA%\Claude\claude_desktop_config.json`
+
+Add or merge the `mcpServers` key:
+
+```json
+{
+ "mcpServers": {
+ "base-account": { "url": "https://mcp.base.org" }
+ }
+}
+```
+
+Restart Claude Desktop after saving. The server appears in the tool menu on next launch.
+
+---
+
+## Claude.ai (web)
+
+1. Open **Settings** (top-right avatar → Settings)
+2. Go to **Integrations**
+3. Click **Add MCP server**
+4. Enter the server URL: `https://mcp.base.org`
+5. Click **Connect**
+
+The OAuth flow opens automatically in a new tab.
+
+---
+
+## Cursor
+
+Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project-scoped):
+
+```json
+{
+ "mcpServers": {
+ "base-account": {
+ "type": "http",
+ "url": "https://mcp.base.org"
+ }
+ }
+}
+```
+
+Restart Cursor after saving.
+
+---
+
+## Any other MCP-compatible client
+
+Use the HTTP transport with server URL `https://mcp.base.org`. Consult your client's MCP documentation for the exact config format — the server URL is the only required field.
+
+---
+
+## OAuth Authorization
+
+After adding the server, your client opens an OAuth flow:
+
+1. A browser tab opens to `mcp.base.org`
+2. Sign in with your Base Account — no Coinbase account required
+3. Authorize the requested permissions
+4. Return to your AI client — the MCP is now connected
+
+---
+
+## Verifying the connection
+
+Call `get_wallets`. A successful response lists your Base Account address and any agent wallets. An error or "tool not found" means the MCP is not connected — retry the install steps above.
+
+---
+
+## Troubleshooting
+
+| Symptom | Fix |
+|---------|-----|
+| Tool not found / MCP not connected | Check config file syntax (valid JSON), ensure URL is `https://mcp.base.org`, restart client |
+| OAuth window doesn't open | Open `https://mcp.base.org` manually in a browser and complete sign-in |
+| `web_request` rejects a domain | The hostname is not in the MCP's allowlist — see plugin references for supported partner APIs |
+| `get_wallets` returns no wallets | OAuth wasn't completed — re-run the auth flow |
From a9ef2228527009f20cd84a65ebe98b021f37d89f Mon Sep 17 00:00:00 2001
From: Youssef
Date: Fri, 15 May 2026 00:24:22 +0100
Subject: [PATCH 6/6] update skill instructions
---
skills/base-mcp/SKILL.md | 34 +++-----
skills/base-mcp/plugins/morpho.md | 6 +-
skills/base-mcp/references/install.md | 115 +++++++++++++-------------
skills/base-mcp/references/sign.md | 2 +-
skills/base-mcp/references/wallets.md | 4 +-
5 files changed, 73 insertions(+), 88 deletions(-)
diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md
index f7ab014..cbded52 100644
--- a/skills/base-mcp/SKILL.md
+++ b/skills/base-mcp/SKILL.md
@@ -1,7 +1,7 @@
---
name: base-mcp
description: >
- Base Account MCP — gives your AI assistant a wallet via the Base Account MCP server (mcp.base.org).
+ Base MCP — gives your AI assistant access to your Base account via the Base MCP server (mcp.base.org).
Tools: get_wallets (list wallets), get_portfolio (balances, any address), send (ETH/ERC-20 transfers),
swap (token swaps via Coinbase), sign (EIP-712/personal_sign), send_calls (EIP-5792 batch),
get_transaction_history (paginated tx history), get_request_status (poll approval), search_tokens (token lookup),
@@ -10,9 +10,9 @@ description: >
Plugins: Morpho lending protocol available via plugins/morpho.md. Moonwell lending on Base/Optimism via plugins/moonwell.md.
---
-# Base Account MCP
+# Base MCP
-The Base Account MCP server gives your AI assistant direct access to the user's Base Account (smart wallet) on Base.
+The Base MCP server gives your AI assistant access to your Base account on Base.
## Step 1 — Check if the MCP is installed
@@ -20,32 +20,20 @@ Before anything else, attempt to call `get_wallets`. If the tool is not availabl
## Step 2 — Install the MCP server
-Tell the user the MCP is not connected and provide the right install method for their platform. See [references/install.md](references/install.md) for full platform-specific instructions including Cursor, troubleshooting, and OAuth details.
+Tell the user the MCP is not connected and point them to [references/install.md](references/install.md) for step-by-step UI instructions. That file covers Claude Desktop, ChatGPT app, Claude.ai web, Claude Code CLI, and Cursor — with beginner-friendly walkthroughs for each.
-**Claude Code (CLI · VS Code · JetBrains)**
-```bash
-claude mcp add base-account --transport http https://mcp.base.org
-```
-
-**Claude Desktop** (macOS / Windows) — add to `claude_desktop_config.json`:
-```json
-{
- "mcpServers": {
- "base-account": { "url": "https://mcp.base.org" }
- }
-}
-```
-
-**Claude.ai (web)** — Settings → Integrations → Add MCP server → enter `https://mcp.base.org`
-
-**Any other MCP client** — HTTP server URL: `https://mcp.base.org`
+Quick reference:
+- **Claude Desktop** — Claude menu → Settings → Integrations → Add integration → `https://mcp.base.org`
+- **ChatGPT app** — Settings → Connectors → Add connector → MCP server → `https://mcp.base.org`
+- **Claude.ai web** — Settings → Integrations → Add integration → `https://mcp.base.org`
+- **Claude Code CLI** — `claude mcp add base-account --transport http https://mcp.base.org`
-After adding the server, the client opens an OAuth flow at mcp.base.org — no Coinbase account required. Once installed, re-run `get_wallets` to confirm the connection, then continue to Step 3.
+After connecting, the user signs in to authorize via Base account — no Coinbase account required. Once installed, re-run `get_wallets` to confirm the connection, then continue to Step 3.
## Step 3 — Get wallets
Call `get_wallets` immediately at the start of any session involving transactions. This returns:
-- The user's Base Account address
+- The user's Base account address
- Any agent wallets and their delegation status
- `inSession: true/false` — determines whether approval mode is required
diff --git a/skills/base-mcp/plugins/morpho.md b/skills/base-mcp/plugins/morpho.md
index 8e18314..f902f8e 100644
--- a/skills/base-mcp/plugins/morpho.md
+++ b/skills/base-mcp/plugins/morpho.md
@@ -1,12 +1,12 @@
# Morpho Plugin
-Morpho is a lending protocol on Base. The Morpho MCP server prepares lending operations (deposit, borrow, withdraw, repay, supply collateral) which are then executed via Base Account MCP's `send_calls`.
+Morpho is a lending protocol on Base. The Morpho MCP server prepares lending operations (deposit, borrow, withdraw, repay, supply collateral) which are then executed via Base MCP's `send_calls`.
## MCP Server
URL: `https://mcp.morpho.org/`
-## Installation (alongside Base Account MCP)
+## Installation (alongside Base MCP)
Add both servers to your MCP config:
@@ -47,7 +47,7 @@ Claude Code: `claude mcp add morpho --transport http https://mcp.morpho.org/`
## Orchestration Pattern
-Morpho `prepare_*` tools return unsigned call data. Pass the result to Base Account MCP's `send_calls` to execute.
+Morpho `prepare_*` tools return unsigned call data. Pass the result to Base MCP's `send_calls` to execute.
```
morpho_prepare_deposit(vaultAddress, amount) → { calls: [...], chainId }
diff --git a/skills/base-mcp/references/install.md b/skills/base-mcp/references/install.md
index 293864e..40438f7 100644
--- a/skills/base-mcp/references/install.md
+++ b/skills/base-mcp/references/install.md
@@ -1,68 +1,62 @@
# Installing Base MCP
-Server URL: `https://mcp.base.org`
+Choose your app below. The whole process takes under two minutes.
---
-## Claude Code (CLI · VS Code extension · JetBrains extension)
-
-All three use the same Claude Code MCP configuration. Run in any terminal (including the integrated terminal inside VS Code or JetBrains):
-
-```bash
-claude mcp add base-account --transport http https://mcp.base.org
-```
-
-Or add manually to `~/.claude/settings.json`:
+## Claude Desktop
-```json
-{
- "mcpServers": {
- "base-account": {
- "type": "http",
- "url": "https://mcp.base.org"
- }
- }
-}
-```
+1. Open Claude Desktop
+2. Click the **Claude** menu in the top menu bar → **Settings…**
+3. Go to the **Integrations** tab
+4. Click **Add integration**
+5. Enter a name (e.g. `Base`) and the server URL: `https://mcp.base.org`
+6. Click **Add**
+7. A browser window opens — sign in to authorize (see [Authorization](#authorization) below)
-No restart needed — the server is available in the next Claude Code session.
+> If you don't see an Integrations tab, your Claude Desktop version may be older. Update to the latest version from [claude.ai/download](https://claude.ai/download).
---
-## Claude Desktop
+## ChatGPT (desktop app)
-**macOS** config file: `~/Library/Application Support/Claude/claude_desktop_config.json`
-**Windows** config file: `%APPDATA%\Claude\claude_desktop_config.json`
+1. Open the ChatGPT app
+2. Click your **profile picture** (top-right) → **Settings**
+3. Go to the **Connectors** tab
+4. Click **Add connector** → **MCP server**
+5. Paste the server URL: `https://mcp.base.org`
+6. Click **Connect**
+7. A browser window opens — sign in to authorize (see [Authorization](#authorization) below)
-Add or merge the `mcpServers` key:
+---
-```json
-{
- "mcpServers": {
- "base-account": { "url": "https://mcp.base.org" }
- }
-}
-```
+## Claude.ai (web)
-Restart Claude Desktop after saving. The server appears in the tool menu on next launch.
+1. Go to [claude.ai](https://claude.ai) and sign in
+2. Click your **profile picture** (bottom-left) → **Settings**
+3. Go to the **Integrations** tab
+4. Click **Add integration**
+5. Paste the server URL: `https://mcp.base.org`
+6. Click **Connect**
+7. A browser window opens — sign in to authorize (see [Authorization](#authorization) below)
---
-## Claude.ai (web)
+## Claude Code (CLI · VS Code · JetBrains)
-1. Open **Settings** (top-right avatar → Settings)
-2. Go to **Integrations**
-3. Click **Add MCP server**
-4. Enter the server URL: `https://mcp.base.org`
-5. Click **Connect**
+Run this in your terminal:
-The OAuth flow opens automatically in a new tab.
+```bash
+claude mcp add base-account --transport http https://mcp.base.org
+```
+
+Then restart Claude Code and sign in when prompted.
---
## Cursor
-Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project-scoped):
+Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project):
```json
{
@@ -79,34 +73,37 @@ Restart Cursor after saving.
---
-## Any other MCP-compatible client
+## Authorization
+
+After connecting the server, a browser tab opens to mcp.base.org. Here's what to do:
-Use the HTTP transport with server URL `https://mcp.base.org`. Consult your client's MCP documentation for the exact config format — the server URL is the only required field.
+1. Click **Sign in with Base**
+2. If you don't have a Base account yet, you can create one for free — no Coinbase account required
+3. Review the permissions the app is requesting and click **Authorize**
+4. The browser tab will close and you'll be taken back to your app
+
+That's it — the MCP is now connected.
---
-## OAuth Authorization
+## Did it work?
-After adding the server, your client opens an OAuth flow:
+Ask your AI assistant: **"Show me my wallets."**
-1. A browser tab opens to `mcp.base.org`
-2. Sign in with your Base Account — no Coinbase account required
-3. Authorize the requested permissions
-4. Return to your AI client — the MCP is now connected
+If it replies with a wallet address, you're all set. If it says it doesn't have access to a wallet tool, the MCP isn't connected — try the install steps again or check the troubleshooting section below.
---
-## Verifying the connection
+## Troubleshooting
-Call `get_wallets`. A successful response lists your Base Account address and any agent wallets. An error or "tool not found" means the MCP is not connected — retry the install steps above.
+**The browser tab for sign-in never opened**
+→ Try opening `https://mcp.base.org` in your browser directly and signing in there, then re-add the server in your app.
----
+**I see "Integration not found" or "Tool not available"**
+→ The server may not have loaded yet. Restart your app and try again.
-## Troubleshooting
+**The Integrations / Connectors tab doesn't exist**
+→ Your app version may be outdated. Update to the latest version and try again.
-| Symptom | Fix |
-|---------|-----|
-| Tool not found / MCP not connected | Check config file syntax (valid JSON), ensure URL is `https://mcp.base.org`, restart client |
-| OAuth window doesn't open | Open `https://mcp.base.org` manually in a browser and complete sign-in |
-| `web_request` rejects a domain | The hostname is not in the MCP's allowlist — see plugin references for supported partner APIs |
-| `get_wallets` returns no wallets | OAuth wasn't completed — re-run the auth flow |
+**web_request fails with a domain error**
+→ The website you're trying to reach isn't in the approved list. This is a security feature — see plugin references for supported partner APIs.
diff --git a/skills/base-mcp/references/sign.md b/skills/base-mcp/references/sign.md
index 76ce6d1..8aa82b7 100644
--- a/skills/base-mcp/references/sign.md
+++ b/skills/base-mcp/references/sign.md
@@ -1,6 +1,6 @@
# sign
-Request a user-approved signature from the Base Account. Supports EIP-712 typed data and personal_sign. Operates in approval mode.
+Request a user-approved signature from the Base account. Supports EIP-712 typed data and personal_sign. Operates in approval mode.
## When to use
- "Sign this message", "Sign this typed data", agent needs a signature for authentication
diff --git a/skills/base-mcp/references/wallets.md b/skills/base-mcp/references/wallets.md
index 5cdad29..60c450d 100644
--- a/skills/base-mcp/references/wallets.md
+++ b/skills/base-mcp/references/wallets.md
@@ -1,6 +1,6 @@
# get_wallets
-Returns all wallets in the user's wallet group: the Base Account (primary) plus any agent wallets.
+Returns all wallets in the user's wallet group: the Base account (primary) plus any agent wallets.
## When to use
- User asks "show me my wallets", "what wallets do I have", "which wallet is active"
@@ -14,7 +14,7 @@ None.
- `type` — `base-account` or `agent-wallet`
- `address` — 0x address
- `inSession` — boolean; only `true` wallets can be used with transactional tools
-- `delegationStatus` — whether the agent wallet has delegated authority from the Base Account
+- `delegationStatus` — whether the agent wallet has delegated authority from the Base account
- `spendPolicy` — summary of spend limits (agent wallets only)
## Key patterns