From f99730d0bc182f9708176b13187af138706e30d1 Mon Sep 17 00:00:00 2001 From: Youssef Date: Tue, 12 May 2026 23:24:49 +0100 Subject: [PATCH 01/32] 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 @@ [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr-raw/base/base-skills)](https://github.com/base/base-skills/pulls) [![GitHub Issues](https://img.shields.io/github/issues-raw/base/base-skills.svg)](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
Hello {username}
; +} +``` + +### After (Farcaster SDK) +```typescript +import { sdk } from '@farcaster/miniapp-sdk'; + +function App() { + const [isReady, setIsReady] = useState(false); + const [context, setContext] = useState(null); + + useEffect(() => { + const init = async () => { + // Get context first (must await - it's a Promise) + const context = await sdk.context; + setContext(context); + + // Signal ready to hide splash screen + await sdk.actions.ready(); + setIsReady(true); + }; + init(); + }, []); + + // Access user info + const userFid = context?.user?.fid; + const username = context?.user?.username; + + return
Hello {username}
; +} +``` + +### Context Structure (Same for Both) +```typescript +type MiniAppContext = { + user: { + fid: number; + username?: string; + displayName?: string; + pfpUrl?: string; + }; + client: { + clientFid: number; + added: boolean; + notificationDetails?: { + url: string; + token: string; + }; + safeAreaInsets?: { + top: number; + bottom: number; + left: number; + right: number; + }; + }; + location?: LocationContext; +}; +``` + +--- + +## useClose + +Closes the mini app. + +### Before (MiniKit) +```typescript +import { useClose } from '@coinbase/onchainkit/minikit'; + +function CloseButton() { + const close = useClose(); + + return ; +} +``` + +### After (Farcaster SDK) +```typescript +import { sdk } from '@farcaster/miniapp-sdk'; + +function CloseButton() { + const handleClose = async () => { + await sdk.actions.close(); + }; + + return ; +} +``` + +--- + +## useOpenUrl + +Opens an external URL in the browser. + +### Before (MiniKit) +```typescript +import { useOpenUrl } from '@coinbase/onchainkit/minikit'; + +function LinkButton() { + const openUrl = useOpenUrl(); + + const handleClick = () => { + openUrl('https://example.com'); + }; + + return ; +} +``` + +### After (Farcaster SDK) +```typescript +import { sdk } from '@farcaster/miniapp-sdk'; + +function LinkButton() { + const handleClick = async () => { + await sdk.actions.openUrl('https://example.com'); + }; + + return ; +} +``` + +--- + +## useViewProfile + +Opens a Farcaster user's profile. + +### Before (MiniKit) +```typescript +import { useViewProfile } from '@coinbase/onchainkit/minikit'; + +function ProfileLink({ fid }) { + const viewProfile = useViewProfile(); + + const handleClick = () => { + viewProfile(fid); + }; + + return ; +} +``` + +### After (Farcaster SDK) +```typescript +import { sdk } from '@farcaster/miniapp-sdk'; + +function ProfileLink({ fid }) { + const handleClick = async () => { + await sdk.actions.viewProfile({ fid }); + }; + + return ; +} +``` + +**Note**: The SDK requires an object with `fid` property, not just the fid directly. + +--- + +## useViewCast + +Opens a specific cast. + +### Before (MiniKit) +```typescript +import { useViewCast } from '@coinbase/onchainkit/minikit'; + +function CastLink({ hash }) { + const viewCast = useViewCast(); + + const handleClick = () => { + viewCast(hash); + }; + + return ; +} +``` + +### After (Farcaster SDK) +```typescript +import { sdk } from '@farcaster/miniapp-sdk'; + +function CastLink({ hash }) { + const handleClick = async () => { + await sdk.actions.viewCast({ hash }); + }; + + return ; +} +``` + +--- + +## useComposeCast + +Opens the cast composer with prefilled content. + +### Before (MiniKit) +```typescript +import { useComposeCast } from '@coinbase/onchainkit/minikit'; + +function ShareButton() { + const { composeCast } = useComposeCast(); + + const handleShare = () => { + composeCast({ + text: 'Check out this app!', + embeds: ['https://myapp.com'] + }); + }; + + return ; +} +``` + +### After (Farcaster SDK) +```typescript +import { sdk } from '@farcaster/miniapp-sdk'; + +function ShareButton() { + const handleShare = async () => { + const result = await sdk.actions.composeCast({ + text: 'Check out this app!', + embeds: ['https://myapp.com'] + }); + + // result.cast contains the posted cast if successful + if (result?.cast) { + console.log('Cast posted:', result.cast.hash); + } + }; + + return ; +} +``` + +### Full Options +```typescript +await sdk.actions.composeCast({ + text: string; // Suggested text (user can modify) + embeds?: string[]; // URLs to embed (max 2) + parent?: { // Reply to a cast + hash: string; + }; + channelKey?: string; // Post to a channel + close?: boolean; // Close app after posting +}); +``` + +--- + +## useAddFrame + +Prompts user to add/save the mini app. + +### Before (MiniKit) +```typescript +import { useAddFrame } from '@coinbase/onchainkit/minikit'; + +function SaveButton() { + const addFrame = useAddFrame(); + + const handleAdd = async () => { + const result = await addFrame(); + if (result) { + console.log('Added! Token:', result.token); + // Save result.url and result.token for notifications + } + }; + + return ; +} +``` + +### After (Farcaster SDK) +```typescript +import { sdk } from '@farcaster/miniapp-sdk'; + +function SaveButton() { + const handleAdd = async () => { + const result = await sdk.actions.addMiniApp(); + if (result) { + console.log('Added! Token:', result.token); + // Save result.url and result.token for notifications + } + }; + + return ; +} +``` + +--- + +## useAuthenticate + +Authenticates the user with Sign In with Farcaster. + +### Before (MiniKit) +```typescript +import { useAuthenticate } from '@coinbase/onchainkit/minikit'; + +function AuthButton() { + const authenticate = useAuthenticate(); + + const handleAuth = async () => { + const result = await authenticate(); + if (result) { + // Send result to your backend for verification + await verifyOnBackend(result); + } + }; + + return ; +} +``` + +### After (Farcaster SDK) +```typescript +import { sdk } from '@farcaster/miniapp-sdk'; + +function AuthButton() { + const handleAuth = async () => { + const result = await sdk.actions.signIn({ + // Optional: specify nonce for verification + nonce: 'your-random-nonce' + }); + + if (result) { + // result contains signature for verification + await verifyOnBackend(result); + } + }; + + return ; +} +``` + +**Important**: Always verify the signature on your backend for security-critical operations. + +--- + +## useNotification + +Notifications require server-side implementation. See [NOTIFICATIONS.md](NOTIFICATIONS.md) for details. + +### Before (MiniKit) +```typescript +import { useNotification } from '@coinbase/onchainkit/minikit'; + +function NotifyButton() { + const sendNotification = useNotification(); + + const handleNotify = async () => { + await sendNotification({ + title: 'Hello!', + body: 'You have a new message' + }); + }; + + return ; +} +``` + +### After (Farcaster SDK) +Notifications are sent via webhook from your server, not from the client. + +```typescript +// Client: Just trigger your backend +function NotifyButton() { + const handleNotify = async () => { + await fetch('/api/send-notification', { + method: 'POST', + body: JSON.stringify({ + title: 'Hello!', + body: 'You have a new message' + }) + }); + }; + + return ; +} +``` + +See [NOTIFICATIONS.md](NOTIFICATIONS.md) for server-side implementation. diff --git a/skills/build-on-base/references/migrations/minikit-to-farcaster/overview.md b/skills/build-on-base/references/migrations/minikit-to-farcaster/overview.md new file mode 100644 index 0000000..b75c138 --- /dev/null +++ b/skills/build-on-base/references/migrations/minikit-to-farcaster/overview.md @@ -0,0 +1,82 @@ +# MiniKit to Farcaster SDK + +Converts Mini Apps from MiniKit (OnchainKit) to native Farcaster SDK. + +## Breaking Changes (SDK v0.2.0+) + +1. `sdk.context` is a **Promise** — must await +2. `sdk.isInMiniApp()` accepts **no parameters** +3. `sdk.actions.setPrimaryButton()` has no onClick callback + +Check version: `npm list @farcaster/miniapp-sdk` + +## Quick Reference + +| MiniKit | Farcaster SDK | Notes | +|---------|---------------|-------| +| `useMiniKit().setFrameReady()` | `await sdk.actions.ready()` | | +| `useMiniKit().context` | `await sdk.context` | **Async** | +| `useMiniKit().isSDKLoaded` | `await sdk.isInMiniApp()` | No params | +| `useClose()` | `await sdk.actions.close()` | | +| `useOpenUrl(url)` | `await sdk.actions.openUrl(url)` | | +| `useViewProfile(fid)` | `await sdk.actions.viewProfile({ fid })` | | +| `useViewCast(hash)` | `await sdk.actions.viewCast({ hash })` | | +| `useComposeCast()` | `await sdk.actions.composeCast({ text, embeds })` | | +| `useAddFrame()` | `await sdk.actions.addMiniApp()` | | +| `usePrimaryButton(opts, cb)` | `await sdk.actions.setPrimaryButton(opts)` | No callback | +| `useAuthenticate()` | `sdk.quickAuth.getToken()` | See [auth.md](auth.md) | + +## Context Access Pattern + +```typescript +// WRONG +const fid = sdk.context?.user?.fid; + +// CORRECT +const context = await sdk.context; +const fid = context?.user?.fid; +``` + +In React components, use state: + +```typescript +const [context, setContext] = useState(null); + +useEffect(() => { + const load = async () => { + const ctx = await sdk.context; + setContext(ctx); + }; + load(); +}, []); +``` + +## Conversion Workflow + +1. Verify Node.js >= 22.11.0 +2. Update dependencies — see [dependencies.md](dependencies.md) +3. Replace imports: `@coinbase/onchainkit/minikit` → `@farcaster/miniapp-sdk` +4. Convert hooks using reference above +5. Add FrameProvider — see [provider.md](provider.md) +6. Update manifest: `frame` → `miniapp` — see [manifest.md](manifest.md) + +## Common Errors + +**"Property 'user' does not exist on type 'Promise'"** +→ Await `sdk.context` before accessing properties + +**"Expected 0 arguments, but got 1"** +→ Remove parameters from `sdk.isInMiniApp()` + +**Context is null in components** +→ Ensure FrameProvider is in your provider chain + +## References + +- [mapping.md](mapping.md) — Complete hook-by-hook conversion reference +- [examples.md](examples.md) — Before/after code examples +- [provider.md](provider.md) — Provider setup with FrameProvider +- [pitfalls.md](pitfalls.md) — Common errors and solutions +- [dependencies.md](dependencies.md) — Package updates +- [auth.md](auth.md) — Quick Auth migration +- [manifest.md](manifest.md) — farcaster.json changes diff --git a/skills/build-on-base/references/migrations/minikit-to-farcaster/pitfalls.md b/skills/build-on-base/references/migrations/minikit-to-farcaster/pitfalls.md new file mode 100644 index 0000000..eacd8d6 --- /dev/null +++ b/skills/build-on-base/references/migrations/minikit-to-farcaster/pitfalls.md @@ -0,0 +1,225 @@ +# Common Pitfalls & Errors + +## Contents +- Type errors (sdk.context, isInMiniApp, setPrimaryButton) +- Runtime issues (context null, detection fails) +- React patterns (useEffect with async) +- Sign-in migration + +--- + +## Type Errors + +### "Property 'user' does not exist on type 'Promise'" + +Accessing `sdk.context` without awaiting. + +```typescript +// WRONG +const fid = sdk.context?.user?.fid; + +// CORRECT +const context = await sdk.context; +const fid = context?.user?.fid; +``` + +### "Expected 0 arguments, but got 1" + +Passing parameters to `sdk.isInMiniApp()`. + +```typescript +// WRONG +await sdk.isInMiniApp({ timeoutMs: 500 }); + +// CORRECT +await sdk.isInMiniApp(); +``` + +Custom timeout workaround: +```typescript +const checkWithTimeout = async (ms = 5000) => { + try { + return await Promise.race([ + sdk.isInMiniApp(), + new Promise((_, r) => setTimeout(() => r(new Error('Timeout')), ms)) + ]); + } catch { + return false; + } +}; +``` + +### "Type 'Promise' is not assignable..." + +Assigning `sdk.context` to state without awaiting. + +```typescript +// WRONG +const context = sdk.context; +setFrameContext({ context, isInMiniApp: true }); + +// CORRECT +const context = await sdk.context; +setFrameContext({ context, isInMiniApp: true }); +``` + +### "'onClick' does not exist in type 'SetPrimaryButtonOptions'" + +`setPrimaryButton` no longer supports callbacks. + +```typescript +// WRONG (MiniKit pattern) +usePrimaryButton( + { text: "Click" }, + () => handleClick() +); + +// CORRECT - state only, no callback +await sdk.actions.setPrimaryButton({ + text: "Click", + disabled: false, + hidden: false, + loading: false +}); +``` + +For click handling, use regular React buttons. + +--- + +## Runtime Issues + +### isInMiniApp returns false unexpectedly + +Possible causes: +- Not running in iframe or React Native WebView +- Server-side rendering (detection is client-side only) +- Missing `'use client'` directive + +### Context is null in components + +FrameProvider not in provider chain. + +```typescript +// WRONG +export function Providers({ children }) { + return {children}; +} + +// CORRECT +export function Providers({ children }) { + return ( + + {children} + + ); +} +``` + +### Context is null even when isInMiniApp is true + +Not awaiting `sdk.context`: + +```typescript +// WRONG +const context = sdk.context; // Promise, not data + +// CORRECT +const context = await sdk.context; +``` + +--- + +## React Patterns + +### Async useEffect + +```typescript +// WRONG - returns Promise +useEffect(async () => { + await sdk.actions.ready(); +}, []); + +// CORRECT - wrap in function +useEffect(() => { + const init = async () => { + await sdk.actions.ready(); + }; + init(); +}, []); +``` + +### Loading context in components + +```typescript +function MyComponent() { + const [context, setContext] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const load = async () => { + try { + const isInMiniApp = await sdk.isInMiniApp(); + if (isInMiniApp) { + const ctx = await sdk.context; + setContext(ctx); + } + } finally { + setLoading(false); + } + }; + load(); + }, []); + + if (loading) return null; + return
{context?.user?.fid}
; +} +``` + +--- + +## Sign-In Migration + +### "This comparison appears to be unintentional..." + +`signIn` returns `SignInResult`, not boolean. + +```typescript +// WRONG (MiniKit pattern) +const result = await signIn({ nonce }); +if (result === false) { ... } + +// CORRECT +const result = await sdk.actions.signIn({ nonce }); +if (!result) { + // Sign-in cancelled or failed +} +``` + +For SDK v0.2.0+, prefer Quick Auth: + +```typescript +const { token } = await sdk.quickAuth.getToken(); +// Or use authenticated fetch +const res = await sdk.quickAuth.fetch('/api/auth'); +``` + +--- + +## Validation Commands + +After conversion, verify: + +```bash +# No MiniKit imports remaining +grep -r "@coinbase/onchainkit/minikit" src/ + +# Check sdk.context usage (should be awaited) +grep -r "sdk\.context" src/ + +# Check isInMiniApp calls (no parameters) +grep -r "isInMiniApp(" src/ + +# Build and type check +npm run build && npx tsc --noEmit +``` diff --git a/skills/build-on-base/references/migrations/minikit-to-farcaster/provider.md b/skills/build-on-base/references/migrations/minikit-to-farcaster/provider.md new file mode 100644 index 0000000..df9b597 --- /dev/null +++ b/skills/build-on-base/references/migrations/minikit-to-farcaster/provider.md @@ -0,0 +1,170 @@ +# Provider Migration + +Remove `MiniKitProvider`, add FrameProvider and Wagmi setup. + +## Contents +- FrameProvider setup +- Wagmi provider setup +- Combined providers +- Usage in components + +--- + +## Step 1: Create FrameProvider + +`src/components/providers/FrameProvider.tsx`: + +```typescript +'use client' + +import { sdk } from '@farcaster/miniapp-sdk'; +import { createContext, useContext, useEffect, useState, ReactNode } from "react"; + +type FrameContextType = { + context: any; + isInMiniApp: boolean; +} | null; + +const FrameContext = createContext(null); + +export const useFrameContext = () => useContext(FrameContext); + +export default function FrameProvider({ children }: { children: ReactNode }) { + const [frameContext, setFrameContext] = useState(null); + + useEffect(() => { + const init = async () => { + try { + // No parameters in v0.2.0+ + const isInMiniApp = await sdk.isInMiniApp(); + + if (isInMiniApp) { + // Must await - context is a Promise + const context = await sdk.context; + setFrameContext({ context, isInMiniApp: true }); + } else { + setFrameContext({ context: null, isInMiniApp: false }); + } + } catch (error) { + console.error('FrameProvider init error:', error); + setFrameContext({ context: null, isInMiniApp: false }); + } + }; + init(); + }, []); + + return ( + + {children} + + ); +} +``` + +--- + +## Step 2: Create Wagmi Provider + +`src/components/providers/WagmiProvider.tsx`: + +```typescript +'use client' + +import { createConfig, http, WagmiProvider as WagmiBase } from 'wagmi'; +import { base } from 'wagmi/chains'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { farcasterMiniApp } from '@farcaster/miniapp-wagmi-connector'; +import { ReactNode, useState } from 'react'; + +const config = createConfig({ + chains: [base], + transports: { [base.id]: http() }, + connectors: [farcasterMiniApp()], +}); + +export default function WagmiProvider({ children }: { children: ReactNode }) { + const [queryClient] = useState(() => new QueryClient()); + + return ( + + + {children} + + + ); +} +``` + +--- + +## Step 3: Combine Providers + +`src/app/providers.tsx`: + +```typescript +'use client' + +import { ReactNode } from 'react'; +import FrameProvider from '@/components/providers/FrameProvider'; +import WagmiProvider from '@/components/providers/WagmiProvider'; + +export function Providers({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} +``` + +--- + +## Step 4: Use in Layout + +`src/app/layout.tsx`: + +```typescript +import { Providers } from './providers'; + +export default function RootLayout({ children }) { + return ( + + + {children} + + + ); +} +``` + +--- + +## Using the Context + +```typescript +import { useFrameContext } from '@/components/providers/FrameProvider'; + +function MyComponent() { + const frameContext = useFrameContext(); + + if (!frameContext) return
Loading...
; + if (!frameContext.isInMiniApp) return
Open in Farcaster
; + + return
Welcome {frameContext.context?.user?.displayName}
; +} +``` + +--- + +## 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 ( +
+ {status.statusName === "success" ? ( +
+ + +
+ ) : ( + + )} + + +
+ ); +} + +function TransactionStatusDisplay({ + status, + chainId, +}: { + status: LifecycleStatus; + chainId?: number; +}) { + if (status.statusName === "init") return null; + + const explorerBase = + chainId === 84532 + ? "https://sepolia.basescan.org" + : "https://basescan.org"; + + return ( +
+ {status.statusName === "pending" && ( +

+ Confirm in wallet. +

+ )} + {status.statusName === "confirmed" && ( +

+ Transaction in progress... +

+ )} + {status.statusName === "success" && ( +
+

Successful!

+ + View on explorer + +
+ )} + {status.statusName === "error" && ( +

+ {status.statusData.message} +

+ )} +
+ ); +} +``` + +## 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 ( +
+
+ + +

+ Connect Wallet +

+ +
+ + +
+
+
+
+
+ + or use another wallet + +
+
+ + {otherOptions.map((wallet) => ( + + ))} +
+
+
+ ); +} + +export function WalletConnect({ appName = "My App" }: { appName?: string }) { + const { address, isConnected } = useAccount(); + const { disconnect } = useDisconnect(); + const [isModalOpen, setIsModalOpen] = useState(false); + + if (isConnected && address) { + return ( +
+ + {truncateAddress(address)} + + +
+ ); + } + + 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/build-on-base/references/network.md b/skills/build-on-base/references/network.md new file mode 100644 index 0000000..e358cca --- /dev/null +++ b/skills/build-on-base/references/network.md @@ -0,0 +1,40 @@ +# Connecting to Base Network + +## Mainnet + +| Property | Value | +|----------|-------| +| Network Name | Base | +| Chain ID | 8453 | +| RPC Endpoint | `https://mainnet.base.org` | +| Currency | ETH | +| Explorer | https://basescan.org | + +## Testnet (Sepolia) + +| Property | Value | +|----------|-------| +| Network Name | Base Sepolia | +| Chain ID | 84532 | +| RPC Endpoint | `https://sepolia.base.org` | +| Currency | ETH | +| Explorer | https://sepolia.basescan.org | + +## Security + +- **Never use public RPC endpoints in production** — they are rate-limited and offer no privacy guarantees; use a dedicated node provider or self-hosted node +- **Never embed RPC API keys in client-side code** — proxy requests through a backend to protect provider credentials +- **Validate chain IDs** before signing transactions to prevent cross-chain replay attacks +- **Use HTTPS RPC endpoints only** — reject any `http://` endpoints to prevent credential interception + +## Critical Notes + +- Public RPC endpoints are **rate-limited** - not for production +- For production: use node providers or run your own node (see [run-node.md](run-node.md)) +- Testnet ETH available from faucets in Base documentation + +## Wallet Setup + +1. Add network with chain ID and RPC from tables above +2. For testnet, use Sepolia configuration +3. Bridge ETH from Ethereum or use faucets diff --git a/skills/build-on-base/references/run-node.md b/skills/build-on-base/references/run-node.md new file mode 100644 index 0000000..45bca4a --- /dev/null +++ b/skills/build-on-base/references/run-node.md @@ -0,0 +1,48 @@ +# 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 da4b3835abf04af88597321f01d21ec10eecad6f Mon Sep 17 00:00:00 2001 From: Youssef Date: Thu, 14 May 2026 15:34:15 +0100 Subject: [PATCH 02/32] remove old skills --- skills/adding-builder-codes/SKILL.md | 164 ---- .../adding-builder-codes/references/privy.md | 60 -- skills/adding-builder-codes/references/rpc.md | 117 --- .../references/smart-wallets.md | 65 -- .../adding-builder-codes/references/viem.md | 75 -- .../adding-builder-codes/references/wagmi.md | 96 --- .../references/agents/register.md | 4 +- .../minikit-to-farcaster/overview.md | 7 + .../scripts}/analyze_project.py | 0 .../scripts/register.sh | 0 .../scripts}/validate_conversion.py | 0 skills/building-with-base-account/SKILL.md | 78 -- .../references/authentication.md | 234 ------ .../references/capabilities.md | 263 ------ .../references/payments.md | 225 ----- .../references/prolinks.md | 192 ----- .../references/sub-accounts.md | 250 ------ .../references/subscriptions.md | 238 ------ .../references/troubleshooting.md | 146 ---- skills/connecting-to-base-network/SKILL.md | 45 - .../convert-farcaster-miniapp-to-app/SKILL.md | 795 ------------------ .../converting-minikit-to-farcaster/AUTH.md | 48 -- .../DEPENDENCIES.md | 54 -- .../EXAMPLES.md | 202 ----- .../converting-minikit-to-farcaster/LICENSE | 21 - .../MANIFEST.md | 50 -- .../MAPPING.md | 452 ---------- .../PITFALLS.md | 225 ----- .../PROVIDER.md | 170 ---- .../converting-minikit-to-farcaster/README.md | 24 - .../converting-minikit-to-farcaster/SKILL.md | 85 -- skills/deploying-contracts-on-base/SKILL.md | 149 ---- skills/migrating-an-onchainkit-app/SKILL.md | 141 ---- .../references/provider-migration.md | 193 ----- .../references/transaction-migration.md | 528 ------------ .../references/troubleshooting.md | 79 -- .../references/wallet-migration.md | 346 -------- skills/registering-agent-base-dev/SKILL.md | 179 ---- skills/running-a-base-node/SKILL.md | 53 -- 39 files changed, 9 insertions(+), 6044 deletions(-) delete mode 100644 skills/adding-builder-codes/SKILL.md delete mode 100644 skills/adding-builder-codes/references/privy.md delete mode 100644 skills/adding-builder-codes/references/rpc.md delete mode 100644 skills/adding-builder-codes/references/smart-wallets.md delete mode 100644 skills/adding-builder-codes/references/viem.md delete mode 100644 skills/adding-builder-codes/references/wagmi.md rename skills/{converting-minikit-to-farcaster => build-on-base/scripts}/analyze_project.py (100%) rename skills/{registering-agent-base-dev => build-on-base}/scripts/register.sh (100%) rename skills/{converting-minikit-to-farcaster => build-on-base/scripts}/validate_conversion.py (100%) delete mode 100644 skills/building-with-base-account/SKILL.md delete mode 100644 skills/building-with-base-account/references/authentication.md delete mode 100644 skills/building-with-base-account/references/capabilities.md delete mode 100644 skills/building-with-base-account/references/payments.md delete mode 100644 skills/building-with-base-account/references/prolinks.md delete mode 100644 skills/building-with-base-account/references/sub-accounts.md delete mode 100644 skills/building-with-base-account/references/subscriptions.md delete mode 100644 skills/building-with-base-account/references/troubleshooting.md delete mode 100644 skills/connecting-to-base-network/SKILL.md delete mode 100644 skills/convert-farcaster-miniapp-to-app/SKILL.md delete mode 100644 skills/converting-minikit-to-farcaster/AUTH.md delete mode 100644 skills/converting-minikit-to-farcaster/DEPENDENCIES.md delete mode 100644 skills/converting-minikit-to-farcaster/EXAMPLES.md delete mode 100644 skills/converting-minikit-to-farcaster/LICENSE delete mode 100644 skills/converting-minikit-to-farcaster/MANIFEST.md delete mode 100644 skills/converting-minikit-to-farcaster/MAPPING.md delete mode 100644 skills/converting-minikit-to-farcaster/PITFALLS.md delete mode 100644 skills/converting-minikit-to-farcaster/PROVIDER.md delete mode 100644 skills/converting-minikit-to-farcaster/README.md delete mode 100644 skills/converting-minikit-to-farcaster/SKILL.md delete mode 100644 skills/deploying-contracts-on-base/SKILL.md delete mode 100644 skills/migrating-an-onchainkit-app/SKILL.md delete mode 100644 skills/migrating-an-onchainkit-app/references/provider-migration.md delete mode 100644 skills/migrating-an-onchainkit-app/references/transaction-migration.md delete mode 100644 skills/migrating-an-onchainkit-app/references/troubleshooting.md delete mode 100644 skills/migrating-an-onchainkit-app/references/wallet-migration.md delete mode 100644 skills/registering-agent-base-dev/SKILL.md delete mode 100644 skills/running-a-base-node/SKILL.md diff --git a/skills/adding-builder-codes/SKILL.md b/skills/adding-builder-codes/SKILL.md deleted file mode 100644 index fb7be0e..0000000 --- a/skills/adding-builder-codes/SKILL.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -name: adding-builder-codes -description: Integrate Base Builder Codes (ERC-8021) into web3 applications for onchain transaction attribution and referral fee earning. Use when a project needs to append a builder code or dataSuffix to transactions on Base L2, whether using Wagmi, Viem, Privy, ethers.js, or raw window.ethereum. Covers phrases like "add builder codes", "integrate builder codes", "earn referral fees on Base transactions", "append a builder code to my transactions", "transaction attribution", "Builder Code integration", or "attribute transactions to my app". Handles project analysis to detect frameworks, locating transaction call sites, and replacing them with attributed versions. ---- - -# 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 This Skill - -Use this skill 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 [references/privy.md](references/privy.md) -- **Wagmi** (without Privy) → See [references/wagmi.md](references/wagmi.md) -- **Viem only** (no React framework) → See [references/viem.md](references/viem.md) -- **Standard RPC** (ethers.js or raw `window.ethereum`) → See [references/rpc.md](references/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 [references/privy.md](references/privy.md) — plugin-based, one config change required. - -#### Wagmi Implementation - -See [references/wagmi.md](references/wagmi.md) — add `dataSuffix` to Wagmi client config. - -#### Viem Implementation - -See [references/viem.md](references/viem.md) — add `dataSuffix` to wallet client. - -#### Standard RPC Implementation - -See [references/rpc.md](references/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 [references/smart-wallets.md](references/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/adding-builder-codes/references/privy.md b/skills/adding-builder-codes/references/privy.md deleted file mode 100644 index 75a40c6..0000000 --- a/skills/adding-builder-codes/references/privy.md +++ /dev/null @@ -1,60 +0,0 @@ -# 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/adding-builder-codes/references/rpc.md b/skills/adding-builder-codes/references/rpc.md deleted file mode 100644 index dd5a9d2..0000000 --- a/skills/adding-builder-codes/references/rpc.md +++ /dev/null @@ -1,117 +0,0 @@ -# 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/adding-builder-codes/references/smart-wallets.md b/skills/adding-builder-codes/references/smart-wallets.md deleted file mode 100644 index 8b0df56..0000000 --- a/skills/adding-builder-codes/references/smart-wallets.md +++ /dev/null @@ -1,65 +0,0 @@ -# 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/adding-builder-codes/references/viem.md b/skills/adding-builder-codes/references/viem.md deleted file mode 100644 index ea30e58..0000000 --- a/skills/adding-builder-codes/references/viem.md +++ /dev/null @@ -1,75 +0,0 @@ -# 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/adding-builder-codes/references/wagmi.md b/skills/adding-builder-codes/references/wagmi.md deleted file mode 100644 index 897db2b..0000000 --- a/skills/adding-builder-codes/references/wagmi.md +++ /dev/null @@ -1,96 +0,0 @@ -# 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/agents/register.md b/skills/build-on-base/references/agents/register.md index 2bf11d1..c3f9fa3 100644 --- a/skills/build-on-base/references/agents/register.md +++ b/skills/build-on-base/references/agents/register.md @@ -28,10 +28,10 @@ Every agent needs a wallet to sign transactions. Ask the user before doing anyth 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: +Use the bundled `scripts/register.sh` (located at the skill root). It handles errors and extracts the builder code from the response: ```bash -BUILDER_CODE=$(bash skill/scripts/register.sh "") +BUILDER_CODE=$(bash scripts/register.sh "") ``` Or call the API directly: diff --git a/skills/build-on-base/references/migrations/minikit-to-farcaster/overview.md b/skills/build-on-base/references/migrations/minikit-to-farcaster/overview.md index b75c138..8acb2b2 100644 --- a/skills/build-on-base/references/migrations/minikit-to-farcaster/overview.md +++ b/skills/build-on-base/references/migrations/minikit-to-farcaster/overview.md @@ -80,3 +80,10 @@ useEffect(() => { - [dependencies.md](dependencies.md) — Package updates - [auth.md](auth.md) — Quick Auth migration - [manifest.md](manifest.md) — farcaster.json changes + +## Helper Scripts + +Two Python scripts are bundled at `scripts/` (skill root) to automate analysis and validation: + +- **`scripts/analyze_project.py `** — Scans all source files and reports every MiniKit import, hook usage, and provider location. Run before starting conversion to understand blast radius. +- **`scripts/validate_conversion.py `** — Validates the converted project: no remaining MiniKit imports/hooks/providers, Farcaster SDK wired correctly, `package.json` updated, manifest uses `miniapp` key. Exit code 0 = pass, 1 = errors found. diff --git a/skills/converting-minikit-to-farcaster/analyze_project.py b/skills/build-on-base/scripts/analyze_project.py similarity index 100% rename from skills/converting-minikit-to-farcaster/analyze_project.py rename to skills/build-on-base/scripts/analyze_project.py diff --git a/skills/registering-agent-base-dev/scripts/register.sh b/skills/build-on-base/scripts/register.sh similarity index 100% rename from skills/registering-agent-base-dev/scripts/register.sh rename to skills/build-on-base/scripts/register.sh diff --git a/skills/converting-minikit-to-farcaster/validate_conversion.py b/skills/build-on-base/scripts/validate_conversion.py similarity index 100% rename from skills/converting-minikit-to-farcaster/validate_conversion.py rename to skills/build-on-base/scripts/validate_conversion.py diff --git a/skills/building-with-base-account/SKILL.md b/skills/building-with-base-account/SKILL.md deleted file mode 100644 index faf5e8f..0000000 --- a/skills/building-with-base-account/SKILL.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -name: building-with-base-account -description: Integrates Base Account SDK for authentication and payments. Covers Sign in with Base (SIWB), Base Pay, Paymasters, Sub Accounts, Spend Permissions, Prolinks, and batch transactions. Use when building apps with wallet authentication, USDC payments, sponsored transactions, smart wallet features, recurring subscriptions, shareable payment links, or any onchain interaction on Base. Covers phrases like "add sign in with Base", "SIWB button", "accept USDC payments", "Base Pay", "paymaster setup", "gas sponsorship", "smart wallet", "sub account", "spend permissions", or "payment link". ---- - -# 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 | [references/authentication.md](references/authentication.md) | Wallet auth, SIWE, backend verification, SignInWithBaseButton, Wagmi/Privy setup | -| Base Pay | [references/payments.md](references/payments.md) | One-tap USDC payments, payerInfo, server-side verification, BasePayButton | -| Subscriptions | [references/subscriptions.md](references/subscriptions.md) | Recurring charges, spend permissions, CDP wallet setup, charge/revoke lifecycle | -| Sub Accounts | [references/sub-accounts.md](references/sub-accounts.md) | App-specific embedded wallets, key generation, funding | -| Capabilities | [references/capabilities.md](references/capabilities.md) | Batch transactions, gas sponsorship (paymasters), atomic execution, auxiliaryFunds, attribution | -| Prolinks | [references/prolinks.md](references/prolinks.md) | Shareable payment links, QR codes, encoded transaction URLs | -| Troubleshooting | [references/troubleshooting.md](references/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/building-with-base-account/references/authentication.md b/skills/building-with-base-account/references/authentication.md deleted file mode 100644 index b111d3a..0000000 --- a/skills/building-with-base-account/references/authentication.md +++ /dev/null @@ -1,234 +0,0 @@ -# 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/building-with-base-account/references/capabilities.md b/skills/building-with-base-account/references/capabilities.md deleted file mode 100644 index 0b26b21..0000000 --- a/skills/building-with-base-account/references/capabilities.md +++ /dev/null @@ -1,263 +0,0 @@ -# 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/building-with-base-account/references/payments.md b/skills/building-with-base-account/references/payments.md deleted file mode 100644 index afd9287..0000000 --- a/skills/building-with-base-account/references/payments.md +++ /dev/null @@ -1,225 +0,0 @@ -# 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/building-with-base-account/references/prolinks.md b/skills/building-with-base-account/references/prolinks.md deleted file mode 100644 index 0b74411..0000000 --- a/skills/building-with-base-account/references/prolinks.md +++ /dev/null @@ -1,192 +0,0 @@ -# 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/building-with-base-account/references/sub-accounts.md b/skills/building-with-base-account/references/sub-accounts.md deleted file mode 100644 index 9e52765..0000000 --- a/skills/building-with-base-account/references/sub-accounts.md +++ /dev/null @@ -1,250 +0,0 @@ -# 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/building-with-base-account/references/subscriptions.md b/skills/building-with-base-account/references/subscriptions.md deleted file mode 100644 index 4d56c9f..0000000 --- a/skills/building-with-base-account/references/subscriptions.md +++ /dev/null @@ -1,238 +0,0 @@ -# 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/building-with-base-account/references/troubleshooting.md b/skills/building-with-base-account/references/troubleshooting.md deleted file mode 100644 index db090e8..0000000 --- a/skills/building-with-base-account/references/troubleshooting.md +++ /dev/null @@ -1,146 +0,0 @@ -# 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/connecting-to-base-network/SKILL.md b/skills/connecting-to-base-network/SKILL.md deleted file mode 100644 index 9af7267..0000000 --- a/skills/connecting-to-base-network/SKILL.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -name: connecting-to-base-network -description: Provides Base network configuration including RPC endpoints, chain IDs, and explorer URLs. Use when connecting wallets, configuring development environments, or setting up Base Mainnet or Sepolia testnet. Covers phrases like "Base RPC URL", "Base chain ID", "connect to Base", "add Base to wallet", "Base Sepolia config", "Base explorer URL", "what network is Base", or "Base testnet setup". ---- - -# Connecting to Base Network - -## Mainnet - -| Property | Value | -|----------|-------| -| Network Name | Base | -| Chain ID | 8453 | -| RPC Endpoint | `https://mainnet.base.org` | -| Currency | ETH | -| Explorer | https://basescan.org | - -## Testnet (Sepolia) - -| Property | Value | -|----------|-------| -| Network Name | Base Sepolia | -| Chain ID | 84532 | -| RPC Endpoint | `https://sepolia.base.org` | -| Currency | ETH | -| Explorer | https://sepolia.basescan.org | - -## Security - -- **Never use public RPC endpoints in production** — they are rate-limited and offer no privacy guarantees; use a dedicated node provider or self-hosted node -- **Never embed RPC API keys in client-side code** — proxy requests through a backend to protect provider credentials -- **Validate chain IDs** before signing transactions to prevent cross-chain replay attacks -- **Use HTTPS RPC endpoints only** — reject any `http://` endpoints to prevent credential interception - -## Critical Notes - -- Public RPC endpoints are **rate-limited** - not for production -- For production: use node providers or run your own node -- Testnet ETH available from faucets in Base documentation - -## Wallet Setup - -1. Add network with chain ID and RPC from tables above -2. For testnet, use Sepolia configuration -3. Bridge ETH from Ethereum or use faucets diff --git a/skills/convert-farcaster-miniapp-to-app/SKILL.md b/skills/convert-farcaster-miniapp-to-app/SKILL.md deleted file mode 100644 index 402e1f2..0000000 --- a/skills/convert-farcaster-miniapp-to-app/SKILL.md +++ /dev/null @@ -1,795 +0,0 @@ ---- -name: convert-farcaster-miniapp-to-app -description: Converts Farcaster miniapp SDK projects into regular Base/web apps. Starts with an interactive quiz to choose between the default regular-app conversion and a narrowly isolated Farcaster surface when something truly needs to remain separate. Handles wagmi connectors, providers, auth, SDK actions, manifest routes, meta tags, dependencies, and read-only preservation. ---- - -# 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/converting-minikit-to-farcaster/AUTH.md b/skills/converting-minikit-to-farcaster/AUTH.md deleted file mode 100644 index 8bb9bc3..0000000 --- a/skills/converting-minikit-to-farcaster/AUTH.md +++ /dev/null @@ -1,48 +0,0 @@ -# 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/converting-minikit-to-farcaster/DEPENDENCIES.md b/skills/converting-minikit-to-farcaster/DEPENDENCIES.md deleted file mode 100644 index e897688..0000000 --- a/skills/converting-minikit-to-farcaster/DEPENDENCIES.md +++ /dev/null @@ -1,54 +0,0 @@ -# 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/converting-minikit-to-farcaster/EXAMPLES.md b/skills/converting-minikit-to-farcaster/EXAMPLES.md deleted file mode 100644 index 0cb6ffe..0000000 --- a/skills/converting-minikit-to-farcaster/EXAMPLES.md +++ /dev/null @@ -1,202 +0,0 @@ -# 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/converting-minikit-to-farcaster/LICENSE b/skills/converting-minikit-to-farcaster/LICENSE deleted file mode 100644 index b77bf2a..0000000 --- a/skills/converting-minikit-to-farcaster/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/skills/converting-minikit-to-farcaster/MANIFEST.md b/skills/converting-minikit-to-farcaster/MANIFEST.md deleted file mode 100644 index f2c5f17..0000000 --- a/skills/converting-minikit-to-farcaster/MANIFEST.md +++ /dev/null @@ -1,50 +0,0 @@ -# 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/converting-minikit-to-farcaster/MAPPING.md b/skills/converting-minikit-to-farcaster/MAPPING.md deleted file mode 100644 index a1e24a3..0000000 --- a/skills/converting-minikit-to-farcaster/MAPPING.md +++ /dev/null @@ -1,452 +0,0 @@ -# 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
Hello {username}
; -} -``` - -### After (Farcaster SDK) -```typescript -import { sdk } from '@farcaster/miniapp-sdk'; - -function App() { - const [isReady, setIsReady] = useState(false); - const [context, setContext] = useState(null); - - useEffect(() => { - const init = async () => { - // Get context first (must await - it's a Promise) - const context = await sdk.context; - setContext(context); - - // Signal ready to hide splash screen - await sdk.actions.ready(); - setIsReady(true); - }; - init(); - }, []); - - // Access user info - const userFid = context?.user?.fid; - const username = context?.user?.username; - - return
Hello {username}
; -} -``` - -### Context Structure (Same for Both) -```typescript -type MiniAppContext = { - user: { - fid: number; - username?: string; - displayName?: string; - pfpUrl?: string; - }; - client: { - clientFid: number; - added: boolean; - notificationDetails?: { - url: string; - token: string; - }; - safeAreaInsets?: { - top: number; - bottom: number; - left: number; - right: number; - }; - }; - location?: LocationContext; -}; -``` - ---- - -## useClose - -Closes the mini app. - -### Before (MiniKit) -```typescript -import { useClose } from '@coinbase/onchainkit/minikit'; - -function CloseButton() { - const close = useClose(); - - return ; -} -``` - -### After (Farcaster SDK) -```typescript -import { sdk } from '@farcaster/miniapp-sdk'; - -function CloseButton() { - const handleClose = async () => { - await sdk.actions.close(); - }; - - return ; -} -``` - ---- - -## useOpenUrl - -Opens an external URL in the browser. - -### Before (MiniKit) -```typescript -import { useOpenUrl } from '@coinbase/onchainkit/minikit'; - -function LinkButton() { - const openUrl = useOpenUrl(); - - const handleClick = () => { - openUrl('https://example.com'); - }; - - return ; -} -``` - -### After (Farcaster SDK) -```typescript -import { sdk } from '@farcaster/miniapp-sdk'; - -function LinkButton() { - const handleClick = async () => { - await sdk.actions.openUrl('https://example.com'); - }; - - return ; -} -``` - ---- - -## useViewProfile - -Opens a Farcaster user's profile. - -### Before (MiniKit) -```typescript -import { useViewProfile } from '@coinbase/onchainkit/minikit'; - -function ProfileLink({ fid }) { - const viewProfile = useViewProfile(); - - const handleClick = () => { - viewProfile(fid); - }; - - return ; -} -``` - -### After (Farcaster SDK) -```typescript -import { sdk } from '@farcaster/miniapp-sdk'; - -function ProfileLink({ fid }) { - const handleClick = async () => { - await sdk.actions.viewProfile({ fid }); - }; - - return ; -} -``` - -**Note**: The SDK requires an object with `fid` property, not just the fid directly. - ---- - -## useViewCast - -Opens a specific cast. - -### Before (MiniKit) -```typescript -import { useViewCast } from '@coinbase/onchainkit/minikit'; - -function CastLink({ hash }) { - const viewCast = useViewCast(); - - const handleClick = () => { - viewCast(hash); - }; - - return ; -} -``` - -### After (Farcaster SDK) -```typescript -import { sdk } from '@farcaster/miniapp-sdk'; - -function CastLink({ hash }) { - const handleClick = async () => { - await sdk.actions.viewCast({ hash }); - }; - - return ; -} -``` - ---- - -## useComposeCast - -Opens the cast composer with prefilled content. - -### Before (MiniKit) -```typescript -import { useComposeCast } from '@coinbase/onchainkit/minikit'; - -function ShareButton() { - const { composeCast } = useComposeCast(); - - const handleShare = () => { - composeCast({ - text: 'Check out this app!', - embeds: ['https://myapp.com'] - }); - }; - - return ; -} -``` - -### After (Farcaster SDK) -```typescript -import { sdk } from '@farcaster/miniapp-sdk'; - -function ShareButton() { - const handleShare = async () => { - const result = await sdk.actions.composeCast({ - text: 'Check out this app!', - embeds: ['https://myapp.com'] - }); - - // result.cast contains the posted cast if successful - if (result?.cast) { - console.log('Cast posted:', result.cast.hash); - } - }; - - return ; -} -``` - -### Full Options -```typescript -await sdk.actions.composeCast({ - text: string; // Suggested text (user can modify) - embeds?: string[]; // URLs to embed (max 2) - parent?: { // Reply to a cast - hash: string; - }; - channelKey?: string; // Post to a channel - close?: boolean; // Close app after posting -}); -``` - ---- - -## useAddFrame - -Prompts user to add/save the mini app. - -### Before (MiniKit) -```typescript -import { useAddFrame } from '@coinbase/onchainkit/minikit'; - -function SaveButton() { - const addFrame = useAddFrame(); - - const handleAdd = async () => { - const result = await addFrame(); - if (result) { - console.log('Added! Token:', result.token); - // Save result.url and result.token for notifications - } - }; - - return ; -} -``` - -### After (Farcaster SDK) -```typescript -import { sdk } from '@farcaster/miniapp-sdk'; - -function SaveButton() { - const handleAdd = async () => { - const result = await sdk.actions.addMiniApp(); - if (result) { - console.log('Added! Token:', result.token); - // Save result.url and result.token for notifications - } - }; - - return ; -} -``` - ---- - -## useAuthenticate - -Authenticates the user with Sign In with Farcaster. - -### Before (MiniKit) -```typescript -import { useAuthenticate } from '@coinbase/onchainkit/minikit'; - -function AuthButton() { - const authenticate = useAuthenticate(); - - const handleAuth = async () => { - const result = await authenticate(); - if (result) { - // Send result to your backend for verification - await verifyOnBackend(result); - } - }; - - return ; -} -``` - -### After (Farcaster SDK) -```typescript -import { sdk } from '@farcaster/miniapp-sdk'; - -function AuthButton() { - const handleAuth = async () => { - const result = await sdk.actions.signIn({ - // Optional: specify nonce for verification - nonce: 'your-random-nonce' - }); - - if (result) { - // result contains signature for verification - await verifyOnBackend(result); - } - }; - - return ; -} -``` - -**Important**: Always verify the signature on your backend for security-critical operations. - ---- - -## useNotification - -Notifications require server-side implementation. See [NOTIFICATIONS.md](NOTIFICATIONS.md) for details. - -### Before (MiniKit) -```typescript -import { useNotification } from '@coinbase/onchainkit/minikit'; - -function NotifyButton() { - const sendNotification = useNotification(); - - const handleNotify = async () => { - await sendNotification({ - title: 'Hello!', - body: 'You have a new message' - }); - }; - - return ; -} -``` - -### After (Farcaster SDK) -Notifications are sent via webhook from your server, not from the client. - -```typescript -// Client: Just trigger your backend -function NotifyButton() { - const handleNotify = async () => { - await fetch('/api/send-notification', { - method: 'POST', - body: JSON.stringify({ - title: 'Hello!', - body: 'You have a new message' - }) - }); - }; - - return ; -} -``` - -See [NOTIFICATIONS.md](NOTIFICATIONS.md) for server-side implementation. diff --git a/skills/converting-minikit-to-farcaster/PITFALLS.md b/skills/converting-minikit-to-farcaster/PITFALLS.md deleted file mode 100644 index eacd8d6..0000000 --- a/skills/converting-minikit-to-farcaster/PITFALLS.md +++ /dev/null @@ -1,225 +0,0 @@ -# Common Pitfalls & Errors - -## Contents -- Type errors (sdk.context, isInMiniApp, setPrimaryButton) -- Runtime issues (context null, detection fails) -- React patterns (useEffect with async) -- Sign-in migration - ---- - -## Type Errors - -### "Property 'user' does not exist on type 'Promise'" - -Accessing `sdk.context` without awaiting. - -```typescript -// WRONG -const fid = sdk.context?.user?.fid; - -// CORRECT -const context = await sdk.context; -const fid = context?.user?.fid; -``` - -### "Expected 0 arguments, but got 1" - -Passing parameters to `sdk.isInMiniApp()`. - -```typescript -// WRONG -await sdk.isInMiniApp({ timeoutMs: 500 }); - -// CORRECT -await sdk.isInMiniApp(); -``` - -Custom timeout workaround: -```typescript -const checkWithTimeout = async (ms = 5000) => { - try { - return await Promise.race([ - sdk.isInMiniApp(), - new Promise((_, r) => setTimeout(() => r(new Error('Timeout')), ms)) - ]); - } catch { - return false; - } -}; -``` - -### "Type 'Promise' is not assignable..." - -Assigning `sdk.context` to state without awaiting. - -```typescript -// WRONG -const context = sdk.context; -setFrameContext({ context, isInMiniApp: true }); - -// CORRECT -const context = await sdk.context; -setFrameContext({ context, isInMiniApp: true }); -``` - -### "'onClick' does not exist in type 'SetPrimaryButtonOptions'" - -`setPrimaryButton` no longer supports callbacks. - -```typescript -// WRONG (MiniKit pattern) -usePrimaryButton( - { text: "Click" }, - () => handleClick() -); - -// CORRECT - state only, no callback -await sdk.actions.setPrimaryButton({ - text: "Click", - disabled: false, - hidden: false, - loading: false -}); -``` - -For click handling, use regular React buttons. - ---- - -## Runtime Issues - -### isInMiniApp returns false unexpectedly - -Possible causes: -- Not running in iframe or React Native WebView -- Server-side rendering (detection is client-side only) -- Missing `'use client'` directive - -### Context is null in components - -FrameProvider not in provider chain. - -```typescript -// WRONG -export function Providers({ children }) { - return {children}; -} - -// CORRECT -export function Providers({ children }) { - return ( - - {children} - - ); -} -``` - -### Context is null even when isInMiniApp is true - -Not awaiting `sdk.context`: - -```typescript -// WRONG -const context = sdk.context; // Promise, not data - -// CORRECT -const context = await sdk.context; -``` - ---- - -## React Patterns - -### Async useEffect - -```typescript -// WRONG - returns Promise -useEffect(async () => { - await sdk.actions.ready(); -}, []); - -// CORRECT - wrap in function -useEffect(() => { - const init = async () => { - await sdk.actions.ready(); - }; - init(); -}, []); -``` - -### Loading context in components - -```typescript -function MyComponent() { - const [context, setContext] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - const load = async () => { - try { - const isInMiniApp = await sdk.isInMiniApp(); - if (isInMiniApp) { - const ctx = await sdk.context; - setContext(ctx); - } - } finally { - setLoading(false); - } - }; - load(); - }, []); - - if (loading) return null; - return
{context?.user?.fid}
; -} -``` - ---- - -## Sign-In Migration - -### "This comparison appears to be unintentional..." - -`signIn` returns `SignInResult`, not boolean. - -```typescript -// WRONG (MiniKit pattern) -const result = await signIn({ nonce }); -if (result === false) { ... } - -// CORRECT -const result = await sdk.actions.signIn({ nonce }); -if (!result) { - // Sign-in cancelled or failed -} -``` - -For SDK v0.2.0+, prefer Quick Auth: - -```typescript -const { token } = await sdk.quickAuth.getToken(); -// Or use authenticated fetch -const res = await sdk.quickAuth.fetch('/api/auth'); -``` - ---- - -## Validation Commands - -After conversion, verify: - -```bash -# No MiniKit imports remaining -grep -r "@coinbase/onchainkit/minikit" src/ - -# Check sdk.context usage (should be awaited) -grep -r "sdk\.context" src/ - -# Check isInMiniApp calls (no parameters) -grep -r "isInMiniApp(" src/ - -# Build and type check -npm run build && npx tsc --noEmit -``` diff --git a/skills/converting-minikit-to-farcaster/PROVIDER.md b/skills/converting-minikit-to-farcaster/PROVIDER.md deleted file mode 100644 index df9b597..0000000 --- a/skills/converting-minikit-to-farcaster/PROVIDER.md +++ /dev/null @@ -1,170 +0,0 @@ -# Provider Migration - -Remove `MiniKitProvider`, add FrameProvider and Wagmi setup. - -## Contents -- FrameProvider setup -- Wagmi provider setup -- Combined providers -- Usage in components - ---- - -## Step 1: Create FrameProvider - -`src/components/providers/FrameProvider.tsx`: - -```typescript -'use client' - -import { sdk } from '@farcaster/miniapp-sdk'; -import { createContext, useContext, useEffect, useState, ReactNode } from "react"; - -type FrameContextType = { - context: any; - isInMiniApp: boolean; -} | null; - -const FrameContext = createContext(null); - -export const useFrameContext = () => useContext(FrameContext); - -export default function FrameProvider({ children }: { children: ReactNode }) { - const [frameContext, setFrameContext] = useState(null); - - useEffect(() => { - const init = async () => { - try { - // No parameters in v0.2.0+ - const isInMiniApp = await sdk.isInMiniApp(); - - if (isInMiniApp) { - // Must await - context is a Promise - const context = await sdk.context; - setFrameContext({ context, isInMiniApp: true }); - } else { - setFrameContext({ context: null, isInMiniApp: false }); - } - } catch (error) { - console.error('FrameProvider init error:', error); - setFrameContext({ context: null, isInMiniApp: false }); - } - }; - init(); - }, []); - - return ( - - {children} - - ); -} -``` - ---- - -## Step 2: Create Wagmi Provider - -`src/components/providers/WagmiProvider.tsx`: - -```typescript -'use client' - -import { createConfig, http, WagmiProvider as WagmiBase } from 'wagmi'; -import { base } from 'wagmi/chains'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { farcasterMiniApp } from '@farcaster/miniapp-wagmi-connector'; -import { ReactNode, useState } from 'react'; - -const config = createConfig({ - chains: [base], - transports: { [base.id]: http() }, - connectors: [farcasterMiniApp()], -}); - -export default function WagmiProvider({ children }: { children: ReactNode }) { - const [queryClient] = useState(() => new QueryClient()); - - return ( - - - {children} - - - ); -} -``` - ---- - -## Step 3: Combine Providers - -`src/app/providers.tsx`: - -```typescript -'use client' - -import { ReactNode } from 'react'; -import FrameProvider from '@/components/providers/FrameProvider'; -import WagmiProvider from '@/components/providers/WagmiProvider'; - -export function Providers({ children }: { children: ReactNode }) { - return ( - - - {children} - - - ); -} -``` - ---- - -## Step 4: Use in Layout - -`src/app/layout.tsx`: - -```typescript -import { Providers } from './providers'; - -export default function RootLayout({ children }) { - return ( - - - {children} - - - ); -} -``` - ---- - -## Using the Context - -```typescript -import { useFrameContext } from '@/components/providers/FrameProvider'; - -function MyComponent() { - const frameContext = useFrameContext(); - - if (!frameContext) return
Loading...
; - if (!frameContext.isInMiniApp) return
Open in Farcaster
; - - return
Welcome {frameContext.context?.user?.displayName}
; -} -``` - ---- - -## 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/converting-minikit-to-farcaster/README.md b/skills/converting-minikit-to-farcaster/README.md deleted file mode 100644 index b19f286..0000000 --- a/skills/converting-minikit-to-farcaster/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# MiniKit to Farcaster SDK - -Skill for converting Mini Apps from MiniKit (OnchainKit) to native Farcaster SDK. - -## Requirements - -- Node.js 22.11.0+ - -## Files - -| File | Purpose | -|------|---------| -| [SKILL.md](SKILL.md) | Main conversion reference | -| [EXAMPLES.md](EXAMPLES.md) | Before/after code examples | -| [PROVIDER.md](PROVIDER.md) | Provider setup | -| [DEPENDENCIES.md](DEPENDENCIES.md) | Package updates | -| [AUTH.md](AUTH.md) | Quick Auth migration | -| [MANIFEST.md](MANIFEST.md) | farcaster.json changes | -| [PITFALLS.md](PITFALLS.md) | Common errors and solutions | - -## Links - -- [Farcaster SDK docs](https://miniapps.farcaster.xyz/docs/getting-started) -- [MiniKit docs](https://docs.base.org/onchainkit/latest/components/minikit/overview) diff --git a/skills/converting-minikit-to-farcaster/SKILL.md b/skills/converting-minikit-to-farcaster/SKILL.md deleted file mode 100644 index 698ba15..0000000 --- a/skills/converting-minikit-to-farcaster/SKILL.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -name: converting-minikit-to-farcaster -description: Converts Mini Apps from MiniKit (OnchainKit) to native Farcaster SDK. Use when migrating from @coinbase/onchainkit/minikit, converting MiniKit hooks, removing MiniKitProvider, or when user mentions MiniKit, OnchainKit, or Farcaster SDK migration. ---- - -# MiniKit to Farcaster SDK - -## Breaking Changes (SDK v0.2.0+) - -1. `sdk.context` is a **Promise** — must await -2. `sdk.isInMiniApp()` accepts **no parameters** -3. `sdk.actions.setPrimaryButton()` has no onClick callback - -Check version: `npm list @farcaster/miniapp-sdk` - -## Quick Reference - -| MiniKit | Farcaster SDK | Notes | -|---------|---------------|-------| -| `useMiniKit().setFrameReady()` | `await sdk.actions.ready()` | | -| `useMiniKit().context` | `await sdk.context` | **Async** | -| `useMiniKit().isSDKLoaded` | `await sdk.isInMiniApp()` | No params | -| `useClose()` | `await sdk.actions.close()` | | -| `useOpenUrl(url)` | `await sdk.actions.openUrl(url)` | | -| `useViewProfile(fid)` | `await sdk.actions.viewProfile({ fid })` | | -| `useViewCast(hash)` | `await sdk.actions.viewCast({ hash })` | | -| `useComposeCast()` | `await sdk.actions.composeCast({ text, embeds })` | | -| `useAddFrame()` | `await sdk.actions.addMiniApp()` | | -| `usePrimaryButton(opts, cb)` | `await sdk.actions.setPrimaryButton(opts)` | No callback | -| `useAuthenticate()` | `sdk.quickAuth.getToken()` | See [AUTH.md](AUTH.md) | - -## Context Access Pattern - -```typescript -// WRONG -const fid = sdk.context?.user?.fid; - -// CORRECT -const context = await sdk.context; -const fid = context?.user?.fid; -``` - -In React components, use state: - -```typescript -const [context, setContext] = useState(null); - -useEffect(() => { - const load = async () => { - const ctx = await sdk.context; - setContext(ctx); - }; - load(); -}, []); -``` - -## Conversion Workflow - -1. Verify Node.js >= 22.11.0 -2. Update dependencies — see [DEPENDENCIES.md](DEPENDENCIES.md) -3. Replace imports: `@coinbase/onchainkit/minikit` → `@farcaster/miniapp-sdk` -4. Convert hooks using reference above -5. Add FrameProvider — see [PROVIDER.md](PROVIDER.md) -6. Update manifest: `frame` → `miniapp` — see [MANIFEST.md](MANIFEST.md) - -## Common Errors - -**"Property 'user' does not exist on type 'Promise'"** -→ Await `sdk.context` before accessing properties - -**"Expected 0 arguments, but got 1"** -→ Remove parameters from `sdk.isInMiniApp()` - -**Context is null in components** -→ Ensure FrameProvider is in your provider chain - -## References - -- [MAPPING.md](MAPPING.md) — Complete hook-by-hook conversion reference -- [EXAMPLES.md](EXAMPLES.md) — Before/after code examples -- [PROVIDER.md](PROVIDER.md) — Provider setup with FrameProvider -- [PITFALLS.md](PITFALLS.md) — Common errors and solutions -- [DEPENDENCIES.md](DEPENDENCIES.md) — Package updates -- [AUTH.md](AUTH.md) — Quick Auth migration -- [MANIFEST.md](MANIFEST.md) — farcaster.json changes diff --git a/skills/deploying-contracts-on-base/SKILL.md b/skills/deploying-contracts-on-base/SKILL.md deleted file mode 100644 index 567516b..0000000 --- a/skills/deploying-contracts-on-base/SKILL.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -name: deploying-contracts-on-base -description: Deploys smart contracts to Base using Foundry. Covers forge create commands, contract verification, testnet faucet setup via CDP, and BaseScan API key configuration. Use when deploying Solidity contracts to Base Mainnet or Sepolia testnet. Covers phrases like "deploy contract to Base", "forge create on Base", "verify contract on BaseScan", "get testnet ETH", "Base Sepolia faucet", "how do I deploy to Base", or "publish my contract". ---- - -# 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/migrating-an-onchainkit-app/SKILL.md b/skills/migrating-an-onchainkit-app/SKILL.md deleted file mode 100644 index 9c1f709..0000000 --- a/skills/migrating-an-onchainkit-app/SKILL.md +++ /dev/null @@ -1,141 +0,0 @@ ---- -name: migrating-an-onchainkit-app -description: > - Migrates apps from @coinbase/onchainkit to standalone wagmi/viem components. - Handles provider replacement (OnchainKitProvider to WagmiProvider), - wallet component replacement (Wallet/ConnectWallet to custom WalletConnect), - and transaction component replacement. Use when the user says "migrate my - onchainkit", "replace onchainkit provider", "migrate my wallet component", - "replace my onchainkit wallet", "migrate my transaction component", - "remove onchainkit dependency", or "move off onchainkit". ---- - -# OnchainKit Migration Skill - -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 [references/provider-migration.md](references/provider-migration.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 [references/wallet-migration.md](references/wallet-migration.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 [references/transaction-migration.md](references/transaction-migration.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 - -### 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/migrating-an-onchainkit-app/references/provider-migration.md b/skills/migrating-an-onchainkit-app/references/provider-migration.md deleted file mode 100644 index 0fc7adf..0000000 --- a/skills/migrating-an-onchainkit-app/references/provider-migration.md +++ /dev/null @@ -1,193 +0,0 @@ -# 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/migrating-an-onchainkit-app/references/transaction-migration.md b/skills/migrating-an-onchainkit-app/references/transaction-migration.md deleted file mode 100644 index 86c2089..0000000 --- a/skills/migrating-an-onchainkit-app/references/transaction-migration.md +++ /dev/null @@ -1,528 +0,0 @@ -# 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 ( -
- {status.statusName === "success" ? ( -
- - -
- ) : ( - - )} - - -
- ); -} - -function TransactionStatusDisplay({ - status, - chainId, -}: { - status: LifecycleStatus; - chainId?: number; -}) { - if (status.statusName === "init") return null; - - const explorerBase = - chainId === 84532 - ? "https://sepolia.basescan.org" - : "https://basescan.org"; - - return ( -
- {status.statusName === "pending" && ( -

- Confirm in wallet. -

- )} - {status.statusName === "confirmed" && ( -

- Transaction in progress... -

- )} - {status.statusName === "success" && ( -
-

Successful!

- - View on explorer - -
- )} - {status.statusName === "error" && ( -

- {status.statusData.message} -

- )} -
- ); -} -``` - -## 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 ( -
-
- - -

- Connect Wallet -

- -
- - -
-
-
-
-
- - or use another wallet - -
-
- - {otherOptions.map((wallet) => ( - - ))} -
-
-
- ); -} - -export function WalletConnect({ appName = "My App" }: { appName?: string }) { - const { address, isConnected } = useAccount(); - const { disconnect } = useDisconnect(); - const [isModalOpen, setIsModalOpen] = useState(false); - - if (isConnected && address) { - return ( -
- - {truncateAddress(address)} - - -
- ); - } - - 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 03/32] 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 @@ -[![GitHub contributors](https://img.shields.io/github/contributors/base/base-skills)](https://github.com/base/base-skills/graphs/contributors) -[![GitHub commit activity](https://img.shields.io/github/commit-activity/w/base/base-skills)](https://github.com/base/base-skills/graphs/contributors) -![GitHub repo size](https://img.shields.io/github/repo-size/base/base-skills) +[![GitHub contributors](https://img.shields.io/github/contributors/base/skills)](https://github.com/base/skills/graphs/contributors) +[![GitHub commit activity](https://img.shields.io/github/commit-activity/w/base/skills)](https://github.com/base/skills/graphs/contributors) +![GitHub repo size](https://img.shields.io/github/repo-size/base/skills) @@ -20,8 +20,8 @@ -[![GitHub pull requests by-label](https://img.shields.io/github/issues-pr-raw/base/base-skills)](https://github.com/base/base-skills/pulls) -[![GitHub Issues](https://img.shields.io/github/issues-raw/base/base-skills.svg)](https://github.com/base/base-skills/issues) +[![GitHub pull requests by-label](https://img.shields.io/github/issues-pr-raw/base/skills)](https://github.com/base/skills/pulls) +[![GitHub Issues](https://img.shields.io/github/issues-raw/base/skills.svg)](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 04/32] 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 05/32] 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 06/32] 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 From 53807634bc0214512c887287dedbb9e7dc4e663e Mon Sep 17 00:00:00 2001 From: Youssef Date: Mon, 18 May 2026 21:56:25 +0100 Subject: [PATCH 07/32] update plugins --- skills/base-mcp/SKILL.md | 4 +- skills/base-mcp/plugins/avantis.md | 510 +++++++++++++++++++++++++++++ skills/base-mcp/plugins/uniswap.md | 447 +++++++++++++++++++++++++ 3 files changed, 960 insertions(+), 1 deletion(-) create mode 100644 skills/base-mcp/plugins/avantis.md create mode 100644 skills/base-mcp/plugins/uniswap.md diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md index cbded52..1e2531c 100644 --- a/skills/base-mcp/SKILL.md +++ b/skills/base-mcp/SKILL.md @@ -7,7 +7,7 @@ description: > 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. Moonwell lending on Base/Optimism via plugins/moonwell.md. + Plugins: Morpho lending protocol available via plugins/morpho.md. Moonwell lending on Base/Optimism via plugins/moonwell.md. Uniswap swaps and LP on Base via plugins/uniswap.md. Avantis perpetual futures on Base via plugins/avantis.md. --- # Base MCP @@ -75,6 +75,8 @@ Additional protocol capabilities — no extra MCP server needed for Moonwell (us |--------|---------|-----------| | Morpho | Lending / vaults on Base | [plugins/morpho.md](plugins/morpho.md) | | Moonwell | Lending / borrowing on Base and Optimism | [plugins/moonwell.md](plugins/moonwell.md) | +| Uniswap | Token swaps and LP positions on Base | [plugins/uniswap.md](plugins/uniswap.md) | +| Avantis | Perpetual futures trading on Base | [plugins/avantis.md](plugins/avantis.md) | ## Installation diff --git a/skills/base-mcp/plugins/avantis.md b/skills/base-mcp/plugins/avantis.md new file mode 100644 index 0000000..7386888 --- /dev/null +++ b/skills/base-mcp/plugins/avantis.md @@ -0,0 +1,510 @@ +# Avantis Plugin + +Avantis is a perpetual futures DEX on Base mainnet (`chainId` 8453). Use `web_request` to fetch unsigned calldata from the Avantis tx-builder, then preview or execute it with account MCP `send_calls`. + +Do not sign, approve, or submit transactions unless the user explicitly asks. Generating calldata and `send_calls` approval links is safe, but the user must approve any real transaction. + +Prerequisite: `tx-builder.avantisfi.com`, `data.avantisfi.com`, `core.avantisfi.com`, and `api.avantisfi.com` must be in the account MCP `web_request` allowlist. + +No API key or Authorization header is required for the documented public endpoints. + +--- + +## API Services + +| Service | Base URL | Purpose | +| --- | --- | --- | +| tx-builder | `https://tx-builder.avantisfi.com` | GET-only unsigned calldata builder for Avantis Trading and USDC calls | +| data | `https://data.avantisfi.com/v2/trading` | Pair configs, leverage rules, fees, open interest, market status | +| core | `https://core.avantisfi.com` | Current open positions, limit orders, and open interest | +| history | `https://api.avantisfi.com` | Closed/all trade history, PnL, referral stats, market-order settlement status | + +Source of truth for tx-builder shape: + +``` +GET https://tx-builder.avantisfi.com/openapi.json +GET https://tx-builder.avantisfi.com/docs +``` + +--- + +## Base-Only Rules + +- All tx-builder calldata is for Base mainnet only. +- All tx-builder responses return `chainId: 8453`. +- There is no supported chain selector query parameter. +- Collateral is USDC only. ETH is used only for gas and Avantis execution fee `value`. +- Canonical Base USDC: `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. +- Default USDC spender is Avantis `TradingStorage`: `0x8a311D7048c35985aa31C131B9A13e03a5f7422d`. + +Fetch current contract addresses when needed: + +``` +GET https://tx-builder.avantisfi.com/addresses +``` + +--- + +## tx-builder Response Shape + +All calldata-producing tx-builder endpoints return an envelope: + +```json +{ + "ok": true, + "data": { + "to": "0x44914408af82bC9983bbb330e3578E1105e11d4e", + "from": "0x1111111111111111111111111111111111111111", + "data": "0x19cde9a1...", + "value": "0x13e52b9abe000", + "chainId": 8453, + "description": "Open long BTC/USD 10x with 100 USDC (market)", + "meta": {} + } +} +``` + +Only `response.data.to`, `response.data.value`, and `response.data.data` are passed to account MCP `send_calls`. + +```json +{ + "chain": "base", + "calls": [ + { + "to": "", + "value": "", + "data": "" + } + ] +} +``` + +`from` is informational and identifies who must sign. If `delegate=` is used, `from` becomes the delegate address. `nonce` and gas fields are intentionally omitted; the wallet manages them. + +--- + +## Units And Scaling + +tx-builder request inputs use human decimals: + +| Surface | Unit behavior | +| --- | --- | +| tx-builder `collateralUsdc`, `amountUsdc`, `openPrice`, `takeProfit`, `stopLoss`, `leverage`, `slippagePercent` | Human decimals, not raw scaled integers | +| data API `/v2/trading` | Human decimals | +| core `/user-data` positions and limit orders | Raw strings: USDC fields divide by `1e6`, price and leverage fields divide by `1e10` | +| history API | Mixed, mostly human decimals; check each endpoint shape | +| tx-builder response `value` | Hex wei string | + +Do not pass `1e6` USDC base units or `1e10` price units to tx-builder query parameters. + +--- + +## Orchestration Pattern + +For an open trade: + +``` +get_wallets -> trader address +web_request GET /v2/trading -> validate pair, market status, leverage, and min position +web_request GET /user-data?trader=... -> inspect existing positions/orders +web_request GET /token/approve if allowance may be missing -> send_calls preview +web_request GET /trade/open -> send_calls preview +poll /v2/market-order-initiated/status/ only after a real tx is submitted +web_request GET /user-data?trader=... -> confirm final state only after execution +``` + +For management actions (close, cancel, margin, TP/SL), always read `core /user-data` first and use a real `positions[].index` or `limitOrders[].index`. The tx-builder can encode calldata for a requested index even if that position/order does not exist, so preflight is required to avoid likely reverts. + +--- + +## Step 1 - Validate Pair, Leverage, Liquidity + +``` +GET https://data.avantisfi.com/v2/trading +``` + +Top-level shape: + +```json +{ + "dataVersion": 1.5, + "pairInfos": { "1": {} }, + "groupInfo": { "0": {} }, + "pairCount": 102 +} +``` + +Use `pairInfos[""]` to inspect a pair. Important fields: + +| Field | Meaning | +| --- | --- | +| `index` | Pair index used by tx-builder and on-chain calls | +| `from`, `to` | Symbol components, for example `BTC` and `USD` | +| `isPairListed` | Must be true to open new trades | +| `leverages.minLeverage`, `leverages.maxLeverage` | Fixed-fee leverage envelope for `market`, `limit`, `stop_limit` | +| `leverages.pnlMinLeverage`, `leverages.pnlMaxLeverage` | ZFP leverage envelope for `market_zero_fee` | +| `pairMinLevPosUSDC` | Minimum notional: `collateralUsdc * leverage` | +| `pairOI`, `pairMaxOI` | Pair open interest and cap | +| `groupIndex` | Lookup key into `groupInfo` | +| `feed.attributes.is_open` or `feed.attributes.isOpen` | Market-open flag | +| `lazerFeed.state` | `stable` generally maps to `priceSourcing=1` for Lazer endpoints | + +Liquidity check: + +``` +pairAvailable = pairMaxOI - pairOI +groupAvailable = groupInfo[pair.groupIndex].groupMaxOI - groupInfo[pair.groupIndex].groupOI +available = min(pairAvailable, groupAvailable) +positionSize = collateralUsdc * leverage +``` + +For BTC/USD around a small test amount, `collateralUsdc=1` and `leverage=100` can satisfy the 100 USDC minimum notional for `market_zero_fee` when 100x is inside the ZFP leverage range. + +--- + +## Step 2 - Check User Positions And Limit Orders + +``` +GET https://core.avantisfi.com/user-data?trader=
+``` + +Response: + +```json +{ + "positions": [], + "limitOrders": [] +} +``` + +Key fields: + +| Field | Scaling | Use | +| --- | --- | --- | +| `pairIndex` | none | tx-builder `pairIndex` | +| `index` | none | tx-builder `tradeIndex` | +| `buy` | none | `true` is long, `false` is short | +| `collateral` | divide by `1e6` | Use as `collateralUsdc` when closing full size | +| `leverage` | divide by `1e10` | Display and validation | +| `openPrice`, `tp`, `sl`, `liquidationPrice` | divide by `1e10` | Display and TP/SL decisions | +| `isPnl` | none | true means ZFP trade | + +Unknown or malformed traders can return empty arrays rather than an error. + +--- + +## Step 3 - Approve USDC + +Exact approval: + +``` +GET https://tx-builder.avantisfi.com/token/approve + ?trader=
+ &amountUsdc=1 +``` + +Unlimited approval: + +``` +GET https://tx-builder.avantisfi.com/token/approve?trader=
+``` + +Optional custom spender: + +``` +GET https://tx-builder.avantisfi.com/token/approve + ?trader=
+ &amountUsdc=100 + &spender=
+``` + +`spender` defaults to `TradingStorage`. Pass the returned `response.data` call to `send_calls`. Approval must be confirmed on-chain before trade calls that require allowance can succeed, unless approval and action are submitted as a valid batch and the wallet/account contract supports the batch. + +--- + +## Step 4 - Open A Trade + +``` +GET https://tx-builder.avantisfi.com/trade/open + ?trader=
+ &pair=BTC/USD + &side=long + &orderType=market + &collateralUsdc=100 + &leverage=10 + &slippagePercent=1 +``` + +Parameters: + +| Parameter | Required | Notes | +| --- | --- | --- | +| `trader` | yes | EVM address that owns the position | +| `pair` or `pairIndex` | yes | Pair symbols accept `/`, `-`, or `_`, for example `BTC/USD`, `btc-usd`, `BTC_USD` | +| `side` | yes | `long` or `short` | +| `orderType` | no | `market`, `limit`, `stop_limit`, or `market_zero_fee`; default is `market` | +| `collateralUsdc` | yes | Human-decimal USDC, must be greater than zero | +| `leverage` | yes | Human multiplier; pair envelope is enforced | +| `slippagePercent` | no | Human percent, default `1`; must be greater than 0 and <= 100 | +| `openPrice` | required for limit/stop_limit | Human-decimal price; optional market override | +| `takeProfit` | no | Human-decimal TP price | +| `stopLoss` | no | Human-decimal SL price | +| `executionFeeEth` | no | Default about `0.00035` ETH; max 1 ETH | +| `delegate` | no | Wraps call in `Trading.delegatedAction(trader, calldata)` | +| `skipValidation` | no | `true` bypasses pre-trade checks; avoid unless explicitly requested | + +Order types: + +| `orderType` | Meaning | +| --- | --- | +| `market` | Fixed-fee market open; price is auto-resolved if `openPrice` omitted | +| `limit` | Limit order; `openPrice` required | +| `stop_limit` | Stop-limit order; `openPrice` required | +| `market_zero_fee` | Zero-Fee Protocol / ZFP market open; uses `pnlMinLeverage` and `pnlMaxLeverage` | + +Example ZFP around a small notional: + +``` +GET https://tx-builder.avantisfi.com/trade/open + ?trader=
+ &pair=BTC/USD + &side=long + &orderType=market_zero_fee + &collateralUsdc=1 + &leverage=100 + &slippagePercent=1 +``` + +Example limit: + +``` +GET https://tx-builder.avantisfi.com/trade/open + ?trader=
+ &pair=BTC/USD + &side=long + &orderType=limit + &openPrice=90000 + &collateralUsdc=2 + &leverage=50 + &takeProfit=100000 + &stopLoss=80000 +``` + +--- + +## Step 5 - Close, Cancel, Margin, TP/SL + +Read `core /user-data` first. Use real indices from the returned arrays. + +Close full or partial collateral: + +``` +GET https://tx-builder.avantisfi.com/trade/close + ?trader=
+ &pairIndex= + &tradeIndex= + &collateralUsdc= +``` + +Cancel a resting limit or stop-limit order: + +``` +GET https://tx-builder.avantisfi.com/trade/cancel + ?trader=
+ &pairIndex= + &tradeIndex= +``` + +Deposit or withdraw margin: + +``` +GET https://tx-builder.avantisfi.com/margin/update + ?trader=
+ &pairIndex= + &tradeIndex= + &action=deposit + &collateralUsdc=1 +``` + +`action` is `deposit` or `withdraw`. If `priceUpdateData` and `priceSourcing` are omitted, tx-builder fetches required Pyth bytes server-side. + +Set TP and SL together: + +``` +GET https://tx-builder.avantisfi.com/tpsl/update + ?trader=
+ &pairIndex= + &tradeIndex= + &takeProfit=100000 + &stopLoss=80000 +``` + +`takeProfit` is required and must be greater than zero. `stopLoss` is required; pass `stopLoss=0` to clear SL. + +Open limit order modification is not exposed as a current tx-builder endpoint. To replace a resting limit order, cancel the existing order with `/trade/cancel`, then create a new `/trade/open` limit or stop-limit order. + +--- + +## Delegated Trading + +Set a delegate: + +``` +GET https://tx-builder.avantisfi.com/delegate/set + ?trader=
+ &delegate= +``` + +Remove a delegate: + +``` +GET https://tx-builder.avantisfi.com/delegate/remove?trader=
+``` + +After a delegate is set, trade-side tx-builder endpoints accept `delegate=`. The response `from` becomes the delegate, and the delegate signs/broadcasts. The position still belongs to `trader`. + +--- + +## Batching With send_calls + +`send_calls` accepts multiple Base calls: + +```json +{ + "chain": "base", + "calls": [ + { + "to": "", + "value": "", + "data": "" + }, + { + "to": "", + "value": "", + "data": "" + } + ] +} +``` + +Useful preview batches: + +- Approval plus open trade. +- Approval plus margin deposit. +- Cancel resting order plus create replacement limit order. +- Multiple independent generated calls, if all are Base calls and logically safe to preview together. + +Keep approval before the action that needs allowance. Do not combine calls from different chains. + +--- + +## Settlement Polling + +Market opens and closes settle after the submitted transaction emits an initiated event. Only poll when you have a real tx hash from a submitted transaction. + +``` +GET https://api.avantisfi.com/v2/market-order-initiated/status/ +``` + +Expected logical statuses: + +- `executed` +- `canceled` +- `pending` + +Unknown hashes can return HTTP 200 with: + +```json +{ + "success": false, + "errorMessage": "Market order not found for the given transaction hash" +} +``` + +Use exponential backoff and stop after a reasonable timeout. Do not claim a position opened or closed until on-chain state or the settlement API confirms it. + +--- + +## Query Trade History And PnL + +History endpoints use a legacy envelope: + +```json +{ "success": true } +{ "success": false, "errorMessage": "..." } +``` + +Always check `success` before reading data. + +| Endpoint | Purpose | +| --- | --- | +| `GET https://api.avantisfi.com/v2/history/portfolio/history/
/0/20` | Closed trades, paginated; limit max 20 | +| `GET https://api.avantisfi.com/v2/history/portfolio/all/
/0/20` | All trades, open and closed | +| `GET https://api.avantisfi.com/v2/history/portfolio/top/
` | Top 3 by net PnL | +| `GET https://api.avantisfi.com/v2/history/portfolio/top/
/5` | Top N by net PnL | +| `GET https://api.avantisfi.com/v2/history/portfolio/profit-loss/
` | Aggregate PnL | +| `GET https://api.avantisfi.com/v2/history/portfolio/profit-loss/
/grouped` | Aggregate PnL by pair | +| `GET https://api.avantisfi.com/v2/history/referral/stats/
` | Referral stats | + +Observed edge case: for a wallet with no visible portfolio, some history endpoints may return `success:false` with `Unable to get the portfolio.` while others return `success:true` with empty data. Treat this as an empty/unknown portfolio unless the user expected existing history. + +PnL convention: + +- For ZFP trades, prefer `_mapped_netPnl` where present. +- For fixed-fee trades, prefer `_mapped_grossPnl` where present. + +--- + +## Error Handling + +tx-builder errors: + +```json +{ + "ok": false, + "error": { + "code": "BAD_REQUEST", + "message": "Position size 0.01 USDC is below the minimum of 100 USDC for BTC/USD (collateral 0.01 x leverage 1)" + } +} +``` + +Common tx-builder error codes: + +| Code | Meaning | +| --- | --- | +| `VALIDATION_ERROR` | Query shape problem: bad address, missing required field, numeric range error | +| `BAD_REQUEST` | Domain validation failed: min position, leverage envelope, liquidity, invalid TP/SL | +| `UPSTREAM_ERROR` | Data or price feed dependency failed | +| `NOT_FOUND` | Unknown route or pair index | +| `INTERNAL_ERROR` | Unexpected service error | + +Recommended handling: + +- Surface validation messages directly. +- For `/trade/open`, inspect `meta.validation` on success and show the user the position size, min position, leverage envelope, and available liquidity when useful. +- For history endpoints, check `success`; if false, show `errorMessage`. +- For management actions, do not rely on tx-builder to prove the position/order exists. Verify with `core /user-data`. + +--- + +## Current tx-builder Endpoint Inventory + +| Endpoint | Calldata? | Purpose | +| --- | --- | --- | +| `GET /` | No | Service index | +| `GET /health` | No | Health and chainId | +| `GET /addresses` | No | Contract addresses | +| `GET /pairs` | No | Pair summaries | +| `GET /pairs/` | No | Single pair details | +| `GET /trade/open` | Yes | Open market, ZFP, limit, or stop-limit trade | +| `GET /trade/close` | Yes | Close a trade | +| `GET /trade/cancel` | Yes | Cancel a resting limit or stop-limit order | +| `GET /margin/update` | Yes | Deposit or withdraw collateral | +| `GET /tpsl/update` | Yes | Update TP and SL | +| `GET /delegate/set` | Yes | Set delegate | +| `GET /delegate/remove` | Yes | Remove delegate | +| `GET /token/approve` | Yes | Approve USDC | +| `GET /docs` | No | Swagger UI | +| `GET /openapi.json` | No | OpenAPI spec | + diff --git a/skills/base-mcp/plugins/uniswap.md b/skills/base-mcp/plugins/uniswap.md new file mode 100644 index 0000000..16a473b --- /dev/null +++ b/skills/base-mcp/plugins/uniswap.md @@ -0,0 +1,447 @@ +# Uniswap Plugin + +Uniswap on Base: token swaps using the proxy-approval flow (no Permit2 signing) and LP position management for V2, V3, and V4. Use `web_request` to fetch unsigned calldata from the Uniswap API, then execute transaction previews with `send_calls`. + +No additional MCP server is required. + +**Prerequisite:** `trade-api.gateway.uniswap.org` and `liquidity.api.uniswap.org` must be in the MCP server's `web_request` allowlist. If requests are rejected by the allowlist, inform the user. + +**Chain:** Base mainnet (chainId `8453` / `0x2105`) + +--- + +## Auth Headers + +Use these headers for all requests: + +```json +{ + "Content-Type": "application/json", + "x-api-key": "NeoYO3V50_koJAipDEalYWbMO1XMaFPAQmpOm6_Npo0" +} +``` + +For the swap proxy-approval flow, also include this header on **all** swap endpoints: `/check_approval`, `/quote`, and `/swap`. + +```json +{ + "x-permit2-disabled": "true" +} +``` + +Without `x-permit2-disabled`, Uniswap can return Permit2 or Universal Router behavior instead of the proxy-approval flow described here. + +--- + +## Swap Flow: Proxy Approval, No Permit2 + +Base URL: `https://trade-api.gateway.uniswap.org/v1` + +```text +POST /check_approval -> if approval non-null, include approval calldata in send_calls +POST /quote -> get best route, read-only +POST /swap -> get unsigned swap tx, include swap calldata in send_calls +``` + +### 1. `POST /check_approval` + +Headers: auth headers plus `"x-permit2-disabled": "true"`. + +```json +{ + "walletAddress": "
", + "token": "", + "amount": "", + "chainId": 8453, + "includeGasInfo": true +} +``` + +Response: + +```json +{ + "approval": { + "to": "", + "data": "", + "value": "0x00", + "chainId": 8453 + } +} +``` + +`approval` can be `null`, especially for native ETH. If it is non-null, pass it to `send_calls`. You can batch the approval and swap together after `/swap` returns. + +### 2. `POST /quote` + +Headers: auth headers plus `"x-permit2-disabled": "true"`. + +```json +{ + "type": "EXACT_INPUT", + "amount": "", + "tokenIn": "
", + "tokenOut": "
", + "tokenInChainId": 8453, + "tokenOutChainId": 8453, + "swapper": "
", + "autoSlippage": "DEFAULT", + "protocols": ["V4", "V3", "V2"], + "routingPreference": "BEST_PRICE" +} +``` + +Use `"slippageTolerance": <0-20>` instead of `autoSlippage` if the user specifies slippage. + +Response includes a top-level `quote` object plus metadata. Keep the response as a flat object for `/swap`; do not nest it under a `quote` key. + +### 3. `POST /swap` + +Headers: auth headers plus `"x-permit2-disabled": "true"`. + +Use the `/quote` response as the body, but remove any null or absent permit fields before sending: + +```js +const swapBody = { ...quoteResponse }; +if (swapBody.permitData == null) delete swapBody.permitData; +if (swapBody.permitTransaction == null) delete swapBody.permitTransaction; +delete swapBody.signature; +``` + +Do not send `signature`, and do not send `permitData` or `permitTransaction` when they are `null`. + +Response: + +```json +{ + "swap": { + "to": "", + "data": "", + "value": "0x00", + "chainId": 8453 + }, + "gasFee": "..." +} +``` + +### Swap `send_calls` + +Approval and swap can be sent separately or batched in one `send_calls` preview: + +```json +{ + "chain": "base", + "calls": [ + { "to": "", "value": "", "data": "" }, + { "to": "", "value": "", "data": "" } + ] +} +``` + +### Swap Orchestration + +```text +1. get_wallets -> address; convert tokenIn amount to base units +2. web_request POST /check_approval with x-permit2-disabled +3. web_request POST /quote with x-permit2-disabled +4. Build swapBody from quoteResponse and remove null permit fields +5. web_request POST /swap with x-permit2-disabled +6. send_calls("base", approval + swap calls if approval exists, otherwise swap only) +7. Open the approvalUrl if requested; do not approve unless the user explicitly asks +8. get_request_status only after the user acts +``` + +--- + +## LP Flow + +Base URL: `https://liquidity.api.uniswap.org/lp` + +Use this host for LP endpoints in this plugin environment. Do not switch to `api.uniswap.org` or `trade-api.gateway.uniswap.org/v1/lp/...` unless that host is explicitly available to the API key and MCP allowlist. + +### Pool Discovery: `POST /lp/pool_info` + +Use pool discovery before creating V3/V4 LP positions. `poolReference` must be a valid pool address for V3 or pool ID for V4. + +```json +{ + "protocol": "V4", + "chainId": 8453, + "poolParameters": { + "tokenAddressA": "0x0000000000000000000000000000000000000000", + "tokenAddressB": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "fee": 3000, + "tickSpacing": 60 + } +} +``` + +Known Base V4 ETH/USDC `fee=3000`, `tickSpacing=60` pool reference observed in testing: + +```text +0xe070797535b13431808f8fc81fdbe7b41362960ed0b55bc2b6117c49c51b7eb9 +``` + +Pool references can change by pair, fee, tick spacing, and protocol. Prefer querying `/lp/pool_info` instead of hard-coding a pool reference unless the user explicitly selected a pool. + +### Approval Step: `POST /lp/check_approval` + +Use this before LP create/increase/decrease operations. The body uses `lpTokens` as an array of token/amount objects. + +```json +{ + "protocol": "V4", + "walletAddress": "
", + "chainId": 8453, + "lpTokens": [ + { + "tokenAddress": "0x0000000000000000000000000000000000000000", + "amount": "" + }, + { + "tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "" + } + ], + "action": "CREATE", + "generatePermitAsTransaction": true, + "simulateTransaction": true +} +``` + +`action`: `"CREATE"` | `"INCREASE"` | `"DECREASE"` | `"MIGRATE"` + +Response: + +```json +{ + "requestId": "...", + "transactions": [ + { + "transaction": { + "to": "...", + "from": "...", + "data": "...", + "value": "0x00", + "chainId": 8453 + }, + "cancelApproval": false, + "action": "CREATE" + } + ] +} +``` + +If `transactions` is empty, no approval transaction is needed. If it has entries, map every `transactions[*].transaction` to `send_calls`. + +```js +const approvalCalls = approvalResponse.transactions.map((entry) => ({ + to: entry.transaction.to, + value: entry.transaction.value ?? "0x0", + data: entry.transaction.data +})); +``` + +### Create V3/V4 Position: `POST /lp/create` + +```json +{ + "walletAddress": "
", + "chainId": 8453, + "protocol": "V4", + "existingPool": { + "token0Address": "0x0000000000000000000000000000000000000000", + "token1Address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "poolReference": "0xe070797535b13431808f8fc81fdbe7b41362960ed0b55bc2b6117c49c51b7eb9" + }, + "independentToken": { + "tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "1000000" + }, + "tickBounds": { + "tickLower": -200400, + "tickUpper": -199200 + }, + "simulateTransaction": false, + "nativeTokenBalance": "1000000000000000" +} +``` + +Use `newPool` instead of `existingPool` when creating a pool: + +```json +{ + "token0Address": "
", + "token1Address": "
", + "fee": 3000, + "tickSpacing": 60, + "initialPrice": "" +} +``` + +Response: + +```json +{ + "create": { "to": "...", "data": "...", "value": "...", "chainId": 8453 }, + "token0": { "tokenAddress": "...", "amount": "..." }, + "token1": { "tokenAddress": "...", "amount": "..." }, + "adjustedMinPrice": "...", + "adjustedMaxPrice": "...", + "tickLower": -200400, + "tickUpper": -199200 +} +``` + +Prefer `tickBounds` when possible. `priceBounds` can be accepted by the API, but its price units are easy to misread; validate carefully before using it. + +### Manage Existing Positions + +| Action | Endpoint | Key params | +| --- | --- | --- | +| Add liquidity | `POST /lp/increase` | `walletAddress`, `chainId`, `protocol`, `token0Address`, `token1Address`, `nftTokenId`, `independentToken { tokenAddress, amount }` | +| Remove liquidity | `POST /lp/decrease` | `walletAddress`, `chainId`, `protocol`, `token0Address`, `token1Address`, `nftTokenId`, `liquidityPercentageToDecrease` (0-100) | +| Collect fees | `POST /lp/claim_fees` | `walletAddress`, `chainId`, `protocol`, `tokenId`; optional `simulateTransaction` | +| Create V2 position | `POST /lp/create_classic` | `walletAddress`, `poolParameters { token0Address, token1Address, chainId }`, `independentToken { tokenAddress, amount }` | + +Optional on LP operation endpoints: `"slippageTolerance": ` where `0.5` means 0.5%. + +Important: LP APIs can return calldata for syntactically valid `nftTokenId` values even if the connected wallet may not own the position. Treat generated calldata as a transaction preview input, not proof of ownership or guaranteed execution. + +### Claim Fees: `POST /lp/claim_fees` + +```json +{ + "protocol": "V4", + "walletAddress": "
", + "chainId": 8453, + "tokenId": "", + "simulateTransaction": false +} +``` + +Response: + +```json +{ + "claim": { "to": "...", "data": "...", "value": "0x00", "chainId": 8453 }, + "token0": { "tokenAddress": "...", "amount": "..." }, + "token1": { "tokenAddress": "...", "amount": "..." } +} +``` + +No approval step is needed for fee collection. + +### Create V2 Position: `POST /lp/create_classic` + +```json +{ + "walletAddress": "
", + "poolParameters": { + "token0Address": "0x4200000000000000000000000000000000000006", + "token1Address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "chainId": 8453 + }, + "independentToken": { + "tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "1000000" + }, + "simulateTransaction": false +} +``` + +For V2 create, `/lp/check_approval` may return multiple approval transactions. Batch all approvals plus the `create` transaction if you want one `send_calls` approval link. + +### LP `send_calls` + +For any LP operation response field such as `create`, `increase`, `decrease`, or `claim`, map to: + +```json +{ + "chain": "base", + "calls": [ + { "to": "", "value": "", "data": "" } + ] +} +``` + +For approval + action batching: + +```js +const operationTx = + operationResponse.create ?? + operationResponse.increase ?? + operationResponse.decrease ?? + operationResponse.claim; + +const calls = [ + ...approvalResponse.transactions.map((entry) => ({ + to: entry.transaction.to, + value: entry.transaction.value ?? "0x0", + data: entry.transaction.data + })), + { + to: operationTx.to, + value: operationTx.value ?? "0x0", + data: operationTx.data + } +]; +``` + +### LP Orchestration + +```text +1. get_wallets -> address +2. For V3/V4 create, call /lp/pool_info to discover poolReference +3. Build LP token amount list in base units +4. web_request POST /lp/check_approval +5. web_request POST /lp/create, /lp/increase, /lp/decrease, /lp/claim_fees, or /lp/create_classic +6. send_calls("base", approval calls + operation call) +7. Open the approvalUrl if requested; do not approve unless the user explicitly asks +8. get_request_status only after the user acts +``` + +--- + +## Example Prompts + +**Swap 1 USDC to WETH on Base** +1. `get_wallets` -> address; use USDC address `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`, amount `1000000`. +2. `web_request POST /check_approval` with `x-permit2-disabled: true`. +3. `web_request POST /quote` with `x-permit2-disabled: true`. +4. Remove null permit fields from quote response. +5. `web_request POST /swap` with `x-permit2-disabled: true`. +6. `send_calls` approval + swap, or swap only if no approval was returned. + +**Create a V4 ETH/USDC LP position on Base** +1. `get_wallets` -> address. +2. `web_request POST /lp/pool_info` to find the pool reference. +3. `web_request POST /lp/check_approval` using `lpTokens` array. +4. `web_request POST /lp/create` using `existingPool.poolReference` and `tickBounds`. +5. `send_calls` approval transactions plus create transaction. + +**Add liquidity to an existing V4 position** +1. `get_wallets` -> address. +2. Confirm the wallet owns the `nftTokenId` or warn that generated calldata may still fail. +3. `web_request POST /lp/check_approval` with action `"INCREASE"`. +4. `web_request POST /lp/increase`. +5. `send_calls` approval transactions plus increase transaction. + +**Collect LP fees** +1. `get_wallets` -> address. +2. `web_request POST /lp/claim_fees` with `tokenId`. +3. `send_calls` claim transaction. + +--- + +## Notes + +- Native ETH address: `0x0000000000000000000000000000000000000000` +- USDC on Base: `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` +- WETH on Base: `0x4200000000000000000000000000000000000006` +- Token amounts are base units: USDC = 1e6 per token, ETH/WETH = 1e18 per token. +- Use `chain: "base"` with `send_calls`, not numeric chain id. +- For swap proxy approval, include `x-permit2-disabled: true` on `/check_approval`, `/quote`, and `/swap`. +- For swap proxy approval, remove null `permitData` and `permitTransaction` fields before calling `/swap`. +- `/quote` response fields must be spread directly into `/swap` body, not nested under a `quote` key. +- Do not send `signature` in the proxy swap flow. From 882b64ac898282e6cc429035c20141f3d653c9db Mon Sep 17 00:00:00 2001 From: Youssef Date: Tue, 19 May 2026 17:18:14 +0100 Subject: [PATCH 08/32] update references --- skills/base-mcp/SKILL.md | 98 ++++++------ skills/base-mcp/plugins/avantis.md | 35 ++++- skills/base-mcp/plugins/moonwell.md | 2 +- skills/base-mcp/references/batch-calls.md | 4 +- 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/tone.md | 44 ++++++ skills/base-mcp/references/tools.md | 178 ++++++++++++++++++++++ skills/base-mcp/references/wallets.md | 23 --- skills/base-mcp/references/web-request.md | 45 ------ 14 files changed, 306 insertions(+), 258 deletions(-) delete mode 100644 skills/base-mcp/references/history.md delete mode 100644 skills/base-mcp/references/portfolio.md delete mode 100644 skills/base-mcp/references/send.md delete mode 100644 skills/base-mcp/references/sign.md delete mode 100644 skills/base-mcp/references/swap.md delete mode 100644 skills/base-mcp/references/tokens.md create mode 100644 skills/base-mcp/references/tone.md create mode 100644 skills/base-mcp/references/tools.md delete mode 100644 skills/base-mcp/references/wallets.md delete mode 100644 skills/base-mcp/references/web-request.md diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md index 1e2531c..c19bd15 100644 --- a/skills/base-mcp/SKILL.md +++ b/skills/base-mcp/SKILL.md @@ -2,81 +2,79 @@ name: base-mcp description: > 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), - web_request (fetch whitelisted partner APIs to get calldata, then pass to send_calls — hostname must be in server allowlist). + Native tools (built and maintained by the Base core team): get_wallets, get_portfolio, send, swap, sign, + send_calls, get_transaction_history, get_request_status, search_tokens, web_request. 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. Moonwell lending on Base/Optimism via plugins/moonwell.md. Uniswap swaps and LP on Base via plugins/uniswap.md. Avantis perpetual futures on Base via plugins/avantis.md. + Plugins (external partners): Morpho lending, Moonwell lending, Uniswap swaps/LP, Avantis perpetual futures — all on Base. --- # Base MCP -The Base MCP server gives your AI assistant access to your Base account on Base. +## Detection -## Step 1 — Check if the MCP is installed +Call `get_wallets` immediately on session start. -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**. +- **Tool unavailable or connection error** → the MCP server is not installed. Tell the user clearly, direct them to install it at https://docs.base.org/ai-agents/quickstart, and stop. Do not attempt to offer any functionality without the MCP. +- **Call succeeds** → MCP is connected. Load `references/tone.md`, then continue to Onboarding. -## Step 2 — Install the MCP server +## Tone -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. +Load [references/tone.md](references/tone.md) at session start. Its rules apply for the entire conversation. -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` +## Onboarding -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. +Runs once per session after MCP is confirmed connected. -## Step 3 — Get wallets +1. **Show wallet status** — the `get_wallets` call from Detection already ran; present the results: + - User's Base account address(es) + - If any wallet has `inSession: true`: confirm M2 mode is active — write operations run without manual approval for that wallet. + - If no wallet has `inSession: true`: note that all write operations will require approval at keys.coinbase.com. -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 +2. **Show capability summary** — present what is available: -**If `inSession: true`** on an agent wallet: transactions can execute without manual approval (M2 mode). Pass `agentWalletId` to send/swap. + **Native tools** (built and maintained by the Base core team): + - Send ETH or any token + - Swap tokens via Coinbase + - Sign messages and typed data + - Batch contract calls + - View portfolio and balances + - View transaction history + - Search tokens by symbol or name -**If no wallet is `inSession: true`**: all write tools use approval mode — every transaction goes to keys.coinbase.com for the user to approve. + **Plugins** (provided by external partners, subject to their own terms): + - Morpho — lending and vaults on Base + - Moonwell — lending and borrowing on Base and Optimism + - Uniswap — token swaps and LP positions on Base + - Avantis — perpetual futures trading on Base -Load [references/wallets.md](references/wallets.md) for full field reference. + > The core wallet tools are built and maintained by the Base core team. The plugins are provided by external partners and governed by their own terms. -## Tool Routing +## Tools -Read this table first. For the current task, load ONLY the matching reference file — do not preload all references. +Load [references/tools.md](references/tools.md) for the full tool catalogue. For the current task, load ONLY the relevant section — do not preload everything. | 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) | -| Swap tokens | `swap` | [references/swap.md](references/swap.md) | -| Sign a message or typed data | `sign` | [references/sign.md](references/sign.md) | +| List wallets / check session status | `get_wallets` | [references/tools.md](references/tools.md) | +| Balance / portfolio / token lookup | `get_portfolio`, `search_tokens` | [references/tools.md](references/tools.md) | +| Send ETH or ERC-20 | `send` | [references/tools.md](references/tools.md) | +| Swap tokens | `swap` | [references/tools.md](references/tools.md) | +| Sign message or typed data | `sign` | [references/tools.md](references/tools.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) | -| 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) 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. +| Transaction history | `get_transaction_history` | [references/tools.md](references/tools.md) | +| Poll approval status | `get_request_status` | [references/approval-mode.md](references/approval-mode.md) | +| Fetch protocol calldata | `web_request` | [references/tools.md](references/tools.md) | +| Platform install | — | [references/install.md](references/install.md) | +| Tone and language rules | — | [references/tone.md](references/tone.md) | ## Plugins -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) | -| Uniswap | Token swaps and LP positions on Base | [plugins/uniswap.md](plugins/uniswap.md) | -| Avantis | Perpetual futures trading on Base | [plugins/avantis.md](plugins/avantis.md) | +| Plugin | Protocol | Operated by | Reference | +|--------|----------|-------------|-----------| +| Morpho | Lending / vaults on Base | Morpho Labs | [plugins/morpho.md](plugins/morpho.md) | +| Moonwell | Lending on Base and Optimism | Moonwell team | [plugins/moonwell.md](plugins/moonwell.md) | +| Uniswap | Swaps and LP on Base | Uniswap Labs | [plugins/uniswap.md](plugins/uniswap.md) | +| Avantis | Perpetual futures on Base | Avantis team | [plugins/avantis.md](plugins/avantis.md) | ## Installation diff --git a/skills/base-mcp/plugins/avantis.md b/skills/base-mcp/plugins/avantis.md index 7386888..9f2f6a3 100644 --- a/skills/base-mcp/plugins/avantis.md +++ b/skills/base-mcp/plugins/avantis.md @@ -138,7 +138,7 @@ Use `pairInfos[""]` to inspect a pair. Important fields: | Field | Meaning | | --- | --- | -| `index` | Pair index used by tx-builder and on-chain calls | +| `index` | Pair index used by tx-builder and onchain calls | | `from`, `to` | Symbol components, for example `BTC` and `USD` | | `isPairListed` | Must be true to open new trades | | `leverages.minLeverage`, `leverages.maxLeverage` | Fixed-fee leverage envelope for `market`, `limit`, `stop_limit` | @@ -149,6 +149,23 @@ Use `pairInfos[""]` to inspect a pair. Important fields: | `feed.attributes.is_open` or `feed.attributes.isOpen` | Market-open flag | | `lazerFeed.state` | `stable` generally maps to `priceSourcing=1` for Lazer endpoints | +Minimum position check (BELOW_MIN_POS): + +The tx-builder rejects with `400 BAD_REQUEST` when `collateralUsdc * leverage < pairMinLevPosUSDC`. This is the `BELOW_MIN_POS` condition. Always validate before calling `/trade/open`: + +``` +positionSize = collateralUsdc * leverage +if positionSize < pair.pairMinLevPosUSDC -> BELOW_MIN_POS error, increase collateral or leverage +``` + +To compute the minimum collateral required for a given leverage: + +``` +minCollateral = ceil(pair.pairMinLevPosUSDC / leverage) +``` + +Example: `pairMinLevPosUSDC=100`, `leverage=10` → minimum `collateralUsdc` is `10`. With `leverage=1` → minimum is `100`. + Liquidity check: ``` @@ -218,7 +235,7 @@ GET https://tx-builder.avantisfi.com/token/approve &spender=
``` -`spender` defaults to `TradingStorage`. Pass the returned `response.data` call to `send_calls`. Approval must be confirmed on-chain before trade calls that require allowance can succeed, unless approval and action are submitted as a valid batch and the wallet/account contract supports the batch. +`spender` defaults to `TradingStorage`. Pass the returned `response.data` call to `send_calls`. Approval must be confirmed onchain before trade calls that require allowance can succeed, unless approval and action are submitted as a valid batch and the wallet/account contract supports the batch. --- @@ -421,7 +438,7 @@ Unknown hashes can return HTTP 200 with: } ``` -Use exponential backoff and stop after a reasonable timeout. Do not claim a position opened or closed until on-chain state or the settlement API confirms it. +Use exponential backoff and stop after a reasonable timeout. Do not claim a position opened or closed until onchain state or the settlement API confirms it. --- @@ -486,6 +503,18 @@ Recommended handling: - For history endpoints, check `success`; if false, show `errorMessage`. - For management actions, do not rely on tx-builder to prove the position/order exists. Verify with `core /user-data`. +BELOW_MIN_POS recovery: + +When the error message indicates a minimum position violation (`collateral * leverage < pairMinLevPosUSDC`), do not retry blindly. Compute what is needed and suggest corrections: + +``` +minPositionUsdc = pair.pairMinLevPosUSDC // from data API or meta.validation.minPositionUsdc +minCollateral = ceil(minPositionUsdc / requestedLeverage) +minLeverage = ceil(minPositionUsdc / requestedCollateral) +``` + +Present the user with concrete options: increase collateral to `minCollateral`, increase leverage to `minLeverage` (within the pair envelope), or both. Do not silently adjust parameters without user confirmation. + --- ## Current tx-builder Endpoint Inventory diff --git a/skills/base-mcp/plugins/moonwell.md b/skills/base-mcp/plugins/moonwell.md index 89b934b..7eb525a 100644 --- a/skills/base-mcp/plugins/moonwell.md +++ b/skills/base-mcp/plugins/moonwell.md @@ -133,7 +133,7 @@ Pass all items as the `calls` array to `send_calls`, using `chainId` from any tr - **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 +- **Compound v2 error codes** — `mint`, `borrow`, `repay` return non-zero codes for business-logic failures without reverting. Check the onchain 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 diff --git a/skills/base-mcp/references/batch-calls.md b/skills/base-mcp/references/batch-calls.md index 0f52cfb..56871c4 100644 --- a/skills/base-mcp/references/batch-calls.md +++ b/skills/base-mcp/references/batch-calls.md @@ -1,4 +1,6 @@ -# send_calls +# send_calls (Batch Calls) + +> **Batching is always preferred** for plugin interactions that require a token approval followed by a protocol action (e.g. approve + supply, approve + deposit). Also use batching any time a plugin's `/prepare/*` endpoint returns multiple transactions in its `transactions[]` array. Never split these into sequential single `send` calls when `send_calls` can execute them atomically in a single user approval. Submit a batch of EIP-5792 wallet_sendCalls for user approval. Use for arbitrary contract interactions, multi-step transactions, or batched operations. diff --git a/skills/base-mcp/references/history.md b/skills/base-mcp/references/history.md deleted file mode 100644 index bbe351d..0000000 --- a/skills/base-mcp/references/history.md +++ /dev/null @@ -1,22 +0,0 @@ -# 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 deleted file mode 100644 index 20899b0..0000000 --- a/skills/base-mcp/references/portfolio.md +++ /dev/null @@ -1,21 +0,0 @@ -# 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 deleted file mode 100644 index a7724bf..0000000 --- a/skills/base-mcp/references/send.md +++ /dev/null @@ -1,27 +0,0 @@ -# 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); must be 0–18 -- `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 deleted file mode 100644 index 8aa82b7..0000000 --- a/skills/base-mcp/references/sign.md +++ /dev/null @@ -1,21 +0,0 @@ -# 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 deleted file mode 100644 index f79201d..0000000 --- a/skills/base-mcp/references/swap.md +++ /dev/null @@ -1,20 +0,0 @@ -# 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 deleted file mode 100644 index 3ec0397..0000000 --- a/skills/base-mcp/references/tokens.md +++ /dev/null @@ -1,24 +0,0 @@ -# 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/tone.md b/skills/base-mcp/references/tone.md new file mode 100644 index 0000000..2b2b953 --- /dev/null +++ b/skills/base-mcp/references/tone.md @@ -0,0 +1,44 @@ +# Tone + +These rules apply for the entire conversation. Load this file at session start. + +## Language rules (always enforced) + +- Write **onchain** — never "on-chain" or "on chain" +- Never use the word **web3** +- Never say "on-chain" in any form + +## Detecting user sophistication + +Infer from available signals — do not ask the user directly. + +**Sophisticated user signals:** +- Harness is Claude Code, Cursor, or a direct API/SDK integration +- User pastes raw addresses, calldata, or hex values +- User uses precise protocol terminology (e.g. "health factor", "calldata", "EIP-712", "send_calls") + +**Beginner user signals:** +- Harness is Claude.ai app or ChatGPT desktop/web +- User asks "how do I", "what is", "can you help me" +- No address or technical data pasted; plain conversational language + +## Stating the assumed level + +At the start of the first substantive response, briefly state the assumed level so the user can correct it if wrong: + +- Beginner assumed: *"I'll keep things straightforward — let me know if you want more technical detail."* +- Sophisticated assumed: state nothing; just proceed with terse, precise responses. + +## Beginner mode + +- Use plain terms: "your wallet address", "approve the transaction in your browser", "this may take a few seconds to confirm" +- Avoid raw hex, ABI references, and protocol jargon without a plain-English explanation alongside +- Explain approval steps in order: "First open this link, then come back and let me know when you've approved it" +- Use friendly formatting: short paragraphs, bullet points for steps + +## Sophisticated mode + +- Be terse and precise +- Skip hand-holding and step-by-step preamble +- Use parameter names and return field names directly (e.g. "`inSession: true`", "`approvalUrl`") +- Omit explanations the user clearly already knows diff --git a/skills/base-mcp/references/tools.md b/skills/base-mcp/references/tools.md new file mode 100644 index 0000000..23c56d3 --- /dev/null +++ b/skills/base-mcp/references/tools.md @@ -0,0 +1,178 @@ +# Native Tools + +All tools below are built and maintained by the Base core team via the Base MCP server. + +--- + +## get_wallets + +Returns all wallets in the user's wallet group. + +**Parameters:** none + +**Return fields (per wallet):** `id`, `type` (`base-account` or `agent-wallet`), `address`, `inSession`, `delegationStatus`, `spendPolicy` + +**Key patterns:** +- Call at session start — `inSession` determines whether approval mode applies +- Agent wallets with `inSession: true` can transact without manual approval (M2 mode) +- If no wallet is `inSession: true`, all write tools use approval mode + +--- + +## get_portfolio + +Returns portfolio value and per-asset breakdown for any wallet address. Onchain data is public — any address can be queried. + +**Parameters:** +- `address` — optional; defaults to session wallet +- `chain` — optional: `base` or `ethereum` +- `query` — optional token filter (e.g. `USDC`) +- `limit` / `offset` — pagination +- `includePnl` — include unrealized/realized P&L (default false) + +**Key patterns:** +- Omit `address` to query the session wallet; pass it to query any other address +- Use `query` to filter to a specific token before displaying +- If a token isn't found, call `search_tokens` first to resolve its contract address + +--- + +## search_tokens + +Search for token metadata by symbol or name. + +**Parameters:** +- `query` — required; symbol or name (e.g. `USDC`, `WETH`) +- `chain` — optional: `base` or `base-sepolia` + +**Return fields (per result):** `name`, `symbol`, `address` (contract), `decimals`, `chain` + +**Key patterns:** +- Always use the returned `address` AND `decimals` together when passing a contract address to `send` +- For ETH and USDC, pass the symbol directly to `send`/`swap` — no lookup needed +- If multiple results, prefer the `base` mainnet result unless the user specified otherwise + +--- + +## send + +Send native ETH or any ERC-20 token. Operates in approval mode. + +**Required parameters:** +- `recipient` — 0x address, ENS name, basename (e.g. `vitalik.eth`), cb.id, or 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 (0–18) +- `agentWalletId` — scope to a specific agent wallet (M2 mode) + +**Key patterns:** +- For non-standard tokens, call `search_tokens` first to get `address` + `decimals` +- Basenames and ENS names resolve automatically — no need to look them up first +- Never report success before `get_request_status` confirms completion + +--- + +## swap + +Swap between two tokens via the Coinbase swap service. Mainnet only — testnets not supported. Operates in approval mode. + +**Required parameters:** +- `fromAsset` — symbol or contract address +- `toAsset` — symbol or contract address +- `amount` — human-readable decimal amount of `fromAsset` +- `chain` — target chain (e.g. `base`); testnets are not supported + +**Key patterns:** +- For unknown tokens, call `search_tokens` first to resolve the contract address +- If user requests a testnet swap, explain it is not supported +- Never report success before `get_request_status` confirms completion + +--- + +## sign + +Request a user-approved signature. Supports EIP-712 typed data and personal_sign. Operates in approval mode. + +**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 + +**Key patterns:** +- Use `0x45` for simple text messages (SIWE, auth challenges) +- Use `0x01` for structured typed data (permit signatures, EIP-712 auth) +- Poll `get_request_status` after approval to retrieve the signature value + +--- + +## send_calls + +Submit a batch of EIP-5792 calls. See [references/batch-calls.md](batch-calls.md) for full details and the batching-preferred rule. + +**Required parameters:** +- `chainId` — hex chain ID with 0x prefix (`0x2105` for Base mainnet, `0x14a34` for Base Sepolia) +- `calls` — array of `{ to, value, data }` objects + +--- + +## get_transaction_history + +Returns paginated transaction history for any wallet address in reverse chronological order. + +**Parameters:** +- `address` — optional; defaults to session wallet +- `chain` — optional: `base` or `ethereum` +- `asset` — optional symbol filter (e.g. `USDC`) +- `limit` — 1–200, defaults to 50 +- `cursor` — pagination cursor from previous response's `nextCursor` + +**Key patterns:** +- Date range filtering is not supported — paginate to find transactions in a specific period +- Use `asset` to narrow results to a specific token +- Continue paginating while `hasMore` is true + +--- + +## get_request_status + +Poll the status of a pending approval request. + +**Parameters:** +- `requestId` — required; from the write tool response + +**Key patterns:** +- Call once after the user confirms they approved — do not poll in a tight loop +- Never report a write operation as successful before this returns a confirmed status +- See [references/approval-mode.md](approval-mode.md) for the full approval flow + +--- + +## web_request + +Make an HTTP request to a whitelisted partner API. The hostname must be in the MCP server's allowlist — requests to unlisted domains are rejected. This tool exists because AI assistants in most harnesses cannot fetch arbitrary URLs; `web_request` provides controlled access to trusted protocol APIs so the agent can retrieve calldata and pass it to `send_calls`. + +**Parameters:** +- `url` — full HTTPS URL; hostname must be allowlisted (required) +- `method` — `GET` or `POST` (required) +- `headers` — optional key/value map. Prohibited: `Authorization`, `Cookie`, `Host`, `X-Forwarded-*` +- `body` — JSON object for POST requests; ignored for GET + +**Calldata pattern:** +``` +web_request → { data: { transactions: [ { to, data, value, chainId } ] } } + ↓ map transactions[] to send_calls calls[] +send_calls → approvalUrl + requestId + ↓ user approves +get_request_status → confirmed +``` + +Map each `transactions[i]` directly: `to → calls[i].to`, `data → calls[i].data`, `value → calls[i].value`. Pass `chainId` from any `transactions[i].chainId`. + +**Key patterns:** +- If a request fails with a domain rejection, the hostname is not allowlisted — inform the user, do not retry +- Currently allowlisted partner protocols are documented in the plugin files under `plugins/` +- Always prefer `send_calls` batching when the response contains multiple transactions — see [references/batch-calls.md](batch-calls.md) diff --git a/skills/base-mcp/references/wallets.md b/skills/base-mcp/references/wallets.md deleted file mode 100644 index 60c450d..0000000 --- a/skills/base-mcp/references/wallets.md +++ /dev/null @@ -1,23 +0,0 @@ -# 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/base-mcp/references/web-request.md b/skills/base-mcp/references/web-request.md deleted file mode 100644 index 221c6ef..0000000 --- a/skills/base-mcp/references/web-request.md +++ /dev/null @@ -1,45 +0,0 @@ -# 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 fe21427b69ca36cebf741a7e20d31d314ad9da4b Mon Sep 17 00:00:00 2001 From: Youssef Date: Tue, 19 May 2026 21:59:33 +0100 Subject: [PATCH 09/32] release base-mcp v0.1.1 Co-Authored-By: Claude Sonnet 4.6 --- skills/base-mcp/base-mcp-v0.1.1.zip | Bin 0 -> 23427 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 skills/base-mcp/base-mcp-v0.1.1.zip diff --git a/skills/base-mcp/base-mcp-v0.1.1.zip b/skills/base-mcp/base-mcp-v0.1.1.zip new file mode 100644 index 0000000000000000000000000000000000000000..6a1e255b6d7906c81a2a60379fb601de587efa0a GIT binary patch literal 23427 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

JDpyI8rS_66HJyK>)5K)U|}{k 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 Date: Tue, 19 May 2026 22:01:23 +0100 Subject: [PATCH 10/32] remove zip artifact from repo Co-Authored-By: Claude Sonnet 4.6 --- skills/base-mcp/base-mcp-v0.1.1.zip | Bin 23427 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 skills/base-mcp/base-mcp-v0.1.1.zip diff --git a/skills/base-mcp/base-mcp-v0.1.1.zip b/skills/base-mcp/base-mcp-v0.1.1.zip deleted file mode 100644 index 6a1e255b6d7906c81a2a60379fb601de587efa0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23427 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

JDpyI8rS_66HJyK>)5K)U|}{k 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 Date: Tue, 19 May 2026 22:06:49 +0100 Subject: [PATCH 11/32] add base-mcp v0.1.1 release zip Co-Authored-By: Claude Sonnet 4.6 --- skills/base-mcp/base-mcp-v0.1.1.zip | Bin 0 -> 22985 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 skills/base-mcp/base-mcp-v0.1.1.zip diff --git a/skills/base-mcp/base-mcp-v0.1.1.zip b/skills/base-mcp/base-mcp-v0.1.1.zip new file mode 100644 index 0000000000000000000000000000000000000000..8dd02a1a9ffbb38a3b2bb55f56df380cb8d63084 GIT binary patch literal 22985 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

-H1^jLiR;%K z%L4u30oTt4dfR0e<0s_Sp^Dh!W+oKC%z2xvQqv4sPo#AH?QNCkpp-K7Gp=5 z&egmT9jH4>3bN}@A$lQY;=6QvAXw}T0XuN}fJTWCC{D@?SvXs8+rxg2ed~9JG|F;Q zgb$s-Ftz_JkS2$Cr8Ec`)E3Nu>{5+h->+ExygzTpDe_J&T2(1{jS$>bl*~uH|NMIO zzJI|)Q)CvrImu*r+$R0=X_`@iza(Bs9>1BK3omBK_WPr{pZR8p7$q=cTcA$H)E0gOQ`h2Cals7c@Z}ci`L7&9&ynLZPzua z8W%?Np>VieL1>$)s3V&zADj3zt6PgN zY3q}}<4^aO*CQR7k&f59A^Fi&jOeSxM?-=XwWB}(JP28{P z80+Qd-W(=(+vVS!NpCf$K4_+vrW4PKpJU(tQO2v`WJ6ih9RJ8aIBQhG(M{Hi@EcjP z)kDqxM(2f0J}L;4v(UL@s8Z&rZje!2_B+!Ih%1-sIK|y~@(O=3OVfGTRoY-9o;8)w z-00Bj3&mS6!&%;J{$(TOA>FfGENk3#-l-WwsCdu7VKuC({WI41{c>bqFFydyes`kH zbWT7H;ks-iobcdUZ)IdNYhh07bEnsA8?g_c=tg3N(lkd!q@4G{u=J{$H8|8L=cX$! zRE~rSE7Kn>d5kLCWv^%kt7Jahi{5_P-dkAM%ZZ0e5%&I}ble>sCJ-r?mY`tH0?A&| zjUpmVQRS#Z_t8(p9^>KKUpohi)sz@STp&QEYfwtY0?RdYDqAbsZ!f~9Ef#Y1b=60G zUi&(^MoO_lT>y&p3B4Jk}ZLEpdfMUuONx(!#QAJ=wa&L3+Cs zcuztkMBXdvP$pA^%4|53D4R>8?Iyd0-~(IwChcK~^_)5NUf?>sL??j9)&e;d@a#*@AnioV)8$?(xLckJ8eoRXKz5GbgA zWG4-yP@0>!HMY(|hZ$)~wQv~xG~Ey!dT@S+bIsBqIS(Q+X?2=5&2L)eBk8?gXYWLu zngt;ZxfWOl%TFBQ;?`mYIYQ|w63^nmL&L%R2Q)0js*Mlf@K=UkpwWS)**V+^we01p;$9z- z{AldcDsp>65)Ry{$8Terw5HS(tmp$w=X*VwS#ifdS_NVbR1t(L;?-mjWbH4Ck zBCX)oKgRCoV&m35M3!ZmIr&Ll(3a=)x-vH6Sy9qYjUin_V*o@A7vc4Y#8+M9`?@(r z;SG*(tI+M_QyB(7haqvw`!>UM&6fOO2apwUUOE~>c0cErBz6;DA625@wD!A11*isR z^_v1Bt>PwmA|HVtEw>(xy=B{PIGsb}WS-;bsN-HFbpEW-`nO!`g#hb+#_tqc|9AfH zHCum9!~M@(p^Nu_=l>rC*ZE2bvRx|UT#(Rg>>QkE%I{X!e!v)0^wgkFyC*0zbO~~ zMf`o%@Fx)m=jVEa%KU@)k4woPm*jF5@h9l-C29Es>W?|aUsnFT+Lv=bztA@Fzqj%a znV`$y%K`6SV2tAL!M_QAUxr?e@ce=bsQw;$Io@-bc=@vUizu%CC*q~6qmDY^g=660 QkY7Ah7kOT^{?D)f0hb%7 literal 0 HcmV?d00001 From 86dc963f766a7dd47e7c05003254575fe95d9acd Mon Sep 17 00:00:00 2001 From: Youssef Date: Tue, 19 May 2026 23:37:58 +0100 Subject: [PATCH 12/32] update skill and readme --- skills/base-mcp/README.md | 21 +++++++++++++++++++++ skills/base-mcp/SKILL.md | 6 ++++-- 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 skills/base-mcp/README.md diff --git a/skills/base-mcp/README.md b/skills/base-mcp/README.md new file mode 100644 index 0000000..8b82aaa --- /dev/null +++ b/skills/base-mcp/README.md @@ -0,0 +1,21 @@ +# Base Account MCP — Disclaimer + +> ⚠️ **Important: Read Before Use** + +**What this is.** The Base Account MCP is a hosted Model Context Protocol server operated by Base that lets AI agents interact with a user's Base Account in a user-authorized way. After authenticating and connecting to the MCP server, the MCP server can read account state and construct transactions for the user to approve and sign. The MCP server itself does not sign or broadcast transactions. By using this MCP server, you agree to the Base Account and Base App Terms of Service. + +**Third-party AI hosts, agents, and protocols.** The Base Account MCP is designed to be used with third-party AI hosts and may, depending on the skills loaded into the AI host, prepare transactions that interact with third-party onchain protocols. Those AI hosts and third-party protocols are not operated by Base and are governed by their own terms of service, privacy policies, and (where applicable) jurisdictional eligibility requirements. You are solely responsible for reviewing and complying with each third party's terms and confirming you are eligible to use them. + +**Not official third-party software.** Skills or plugins in the base-skills repo that reference third-party protocols (e.g., Uniswap, Morpho, Moonwell, Avantis) are authored by Base for use with the Base Account MCP. They are not official software of, endorsed by, or operated by those third parties. Inclusion of a skill in this repository does not constitute an endorsement, audit, or guarantee of the underlying protocol. Each third-party protocol is governed by its own terms of service and privacy policy, which the user is solely responsible for reviewing and complying with. + +**AI outputs may be inaccurate.** AI agents can misinterpret instructions, hallucinate parameters (including amounts, recipients, and contract addresses), or be influenced by adversarial inputs encountered in API responses, web content, or other sources. The Base Account MCP relies on the AI agent to interpret your intent correctly. Base does not validate or guarantee AI agent outputs. Review every action proposed by an AI agent before approving it. + +**Not professional advice.** Nothing produced by or in connection with the Base Account MCP — including any output of an AI agent using the service — constitutes investment, financial, legal, tax, or other professional advice. + +**Your responsibility for compliance.** You are solely responsible for ensuring that your use of the Base Account MCP, any connected AI host or agent, your wallet, and any third-party protocol complies with all laws and regulations applicable to you, including sanctions, securities, derivatives, tax, and consumer protection laws. + +**Smart contract and onchain risk.** Interacting with onchain protocols may result in partial or total loss of funds due to smart contract vulnerabilities, oracle failures, liquidations, slippage, MEV, governance actions, network congestion, protocol shutdowns, or other risks. Base does not control any third-party onchain protocol that you choose to interact with through the Base Account MCP. + +**Service availability and changes.** The Base Account MCP is provided "AS IS" and "AS AVAILABLE." Base may modify, suspend, or discontinue the service or any feature of it at any time and with or without notice. Base does not warrant that the service will be error-free, uninterrupted, secure, or compatible with any particular AI host, wallet, or third-party protocol. + +**Limitation of liability.** To the maximum extent permitted by applicable law, Base and its affiliates and their respective officers, directors, employees, and agents will not be liable for any indirect, incidental, special, consequential, exemplary, or punitive damages, or for any loss of profits, revenues, data, or digital assets, arising out of or related to your use of (or inability to use) the Base Account MCP or any third-party AI host, agent, or protocol used in connection with it, whether based on warranty, contract, tort (including negligence), statute, or any other legal theory. diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md index c19bd15..51d7671 100644 --- a/skills/base-mcp/SKILL.md +++ b/skills/base-mcp/SKILL.md @@ -41,13 +41,15 @@ Runs once per session after MCP is confirmed connected. - View transaction history - Search tokens by symbol or name - **Plugins** (provided by external partners, subject to their own terms): + **Plugins** (APIs provided by external partners, subject to their own terms): - Morpho — lending and vaults on Base - Moonwell — lending and borrowing on Base and Optimism - Uniswap — token swaps and LP positions on Base - Avantis — perpetual futures trading on Base - > The core wallet tools are built and maintained by the Base core team. The plugins are provided by external partners and governed by their own terms. +3. **Show this disclaimer** — display it verbatim before proceeding: + + > By using the Base Account MCP, you agree to the Base Account and Base App Terms of Service. Plugins available in the Base repo are authored by Base, not by the third-party protocols they reference. ## Tools From fa1802ac6956e56bb152f22bf34054ab2e9ac714 Mon Sep 17 00:00:00 2001 From: Youssef Date: Tue, 19 May 2026 23:38:21 +0100 Subject: [PATCH 13/32] update language --- skills/base-mcp/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md index 51d7671..81b553a 100644 --- a/skills/base-mcp/SKILL.md +++ b/skills/base-mcp/SKILL.md @@ -41,7 +41,7 @@ Runs once per session after MCP is confirmed connected. - View transaction history - Search tokens by symbol or name - **Plugins** (APIs provided by external partners, subject to their own terms): + **Plugins** (APIs provided by external partners, but plugins authored by Base core team): - Morpho — lending and vaults on Base - Moonwell — lending and borrowing on Base and Optimism - Uniswap — token swaps and LP positions on Base From c77eb511c84d18ca5d33c1f6bb3f7066cad2cc36 Mon Sep 17 00:00:00 2001 From: Youssef Date: Tue, 19 May 2026 23:42:07 +0100 Subject: [PATCH 14/32] update zip --- skills/base-mcp/base-mcp-v0.1.1.zip | Bin 22985 -> 47997 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/skills/base-mcp/base-mcp-v0.1.1.zip b/skills/base-mcp/base-mcp-v0.1.1.zip index 8dd02a1a9ffbb38a3b2bb55f56df380cb8d63084..690b5d00deaa3bbb36fe3aaa130b0ee3df6a1a65 100644 GIT binary patch delta 5389 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 delta 35 mcmezSjp^iO#tlnCH-Fe9$RfhZ#sCDoKq$z Date: Tue, 19 May 2026 23:53:16 +0100 Subject: [PATCH 15/32] update zip file --- skills/base-mcp-v0.1.1.zip | Bin 0 -> 29912 bytes skills/base-mcp/base-mcp-v0.1.1.zip | Bin 47997 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 skills/base-mcp-v0.1.1.zip delete mode 100644 skills/base-mcp/base-mcp-v0.1.1.zip diff --git a/skills/base-mcp-v0.1.1.zip b/skills/base-mcp-v0.1.1.zip new file mode 100644 index 0000000000000000000000000000000000000000..b65d4808a9a1516d65d7c101636bbda0d45d0fc2 GIT binary patch literal 29912 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

ec(ye%7u1%d zKRIKO@!?y#Zn5bRl22iqzJd{Vyx`rw*hS&z)X@Vx&u%S@cY7OHKFDK|ZtFx02?rqW z2sgR;iqp&L5#+Wfqm(^sT8zqcG)Pykuk-;GhG(5rVbg2XZDPbJ1aKO!FO1U=x87ss zYBouTICBUN@i)_H0WzumC0acCboF^81=IPnX? zE58V@uZ|!$yqs=lH@l~IVnavQGBb`_hou&=OBtCcRbGtr^YP58I&KQqJCJ7|bA?+K zhjbe9*iw4}6B`e}rJiO+$;OPki`@80-WIbtL!d4?PIO&O!t3GIh})l*~20xa9=JKs{%()W7>yRIm4@M>z&rQd-+)BdoUT!?0fUdgRC;D{40F{n<{#Wp*(a$8`&? zgon2VYa-jIO7c4b>|RlC$3A+h9y!dJt~|{xXnznkOe&{b4@Zz`--`ZR_Jp&fCgbs% z>$Hqj-Y4ZC*_=lQ(Yw#O21`l?=@H;@!m1w$#obe*5XNLs;o!($#W+a1U5cX2BNKJ( zJpCTcbtYW(yl3pKyc8L?gC>CDmH{4lvFT=Z8g)mu&q0J&XRIdftJ)zgX6r_=7Chb( zbyP|RD$Gr)M1o27*TVu22=jxp5vUdoX!s-^?=ATlrHAEOx>5J>32W@*AUqA`=dPAi z!%M~q5!trKkTDj@wwmkX1dV;s((ejOY@*L^@=$0*NVG$B{j!RUANst5lg3;1Zd1Jr z!_mAofnO0OLK44)>_OdOi9}cu(87)MtwA8Bg0^NG!I$mE;?qSwBJX@kacHA`+K6zH@A)JNu4e)fwa4 z{ZstXnF1ppI$PA#dj_EslR>OUM3iskS{{KwFLm||qW#PB@);8vXe;ITJcdG9vxlG6 zVq5EA(813?c^%WLGOw6mM(W>T2!3Fs+Oj?pm&WHdn$FT1l#C<3UxLI%|J;=Vvx-sc zboj7fIBv_8dtIcBo)yy}+w_K1UBrSfH%jQ4o@Va^7}ceo0hur&@ntXep?ZF4c(X0Y zEMzzNn+TbY%>*Cb1B=PVHdEHHBh62I%p^oQv_AGPFsx?JAJ=4ph^_ZgiZh$-74NWf zckt;Kat{H9Ousw^zotHH{^4;NT8PU{}{au1SA>h7D$M3SC|Q zetG);b9oJqK#2IOrcQUD-H!hE?~5e#_D6Q<|8%ZFhJ*i4Yp4I1`)_k6*#1ek;9$)8 z%R$WjomNjj7>0WB@lV&GmSJ7L3v1cm5Mud{NBsu` zyV0K@e+L8F(g?PJ0|5+1u0H_s!}AZ*zwXokc`DGxMzFp^?ZWy_@ts{I$dLP)@Bat_ zs$vbRZw>pw$gG}Kt-%!89?pBGNAj;elhXS82lILucTss0tJ;;ffZeW9bLKp&XCaX z$2<5h!e6MXKmed3uCQF70I*!>zvD8)_}5(i85T5Dz#eM;{>^7G{e;Y4onD5p`CTw# z_>PS<_g}O5hhWfhE7(G;@81nBL?Y+^yqmwMxPq{O9%6=lH&B3IzZ;_mf6eBfVL{70 zU<*K~zk6~_iZEpU41m9@zk+ar)?LAJg5tn(V*3uKTIFY)U;uwxT=El0XqFK+{|FoA zbNz!=e@^W$5{(cHXxRrWEhq*oEvD~i;c5Mr)<1)T=4W6tH?-fuEkfY_`1D{N-fvSh z5Mt1bBP=l}5-c%}?~sageoO5C1r)mb3cD}+{mAmt{W-l~ZNNfUK{Gb6te^m}tiF$W z#gBi>>K|Z17rU^_;P0az$naNWezD$#uz_yF!m@!Pz_R&1>Y-Wx4K}|6Lx%LYfP*84 P{O>`Q7LyQ@aB%+vjLlo# literal 0 HcmV?d00001 diff --git a/skills/base-mcp/base-mcp-v0.1.1.zip b/skills/base-mcp/base-mcp-v0.1.1.zip deleted file mode 100644 index 690b5d00deaa3bbb36fe3aaa130b0ee3df6a1a65..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47997 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 HKmYn)<=G3t From d754e1923bf0c5f5f75b7b35bcec6fcdf04f1a20 Mon Sep 17 00:00:00 2001 From: Youssef Date: Tue, 19 May 2026 23:54:09 +0100 Subject: [PATCH 16/32] update path --- skills/{ => base-mcp}/base-mcp-v0.1.1.zip | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename skills/{ => base-mcp}/base-mcp-v0.1.1.zip (100%) diff --git a/skills/base-mcp-v0.1.1.zip b/skills/base-mcp/base-mcp-v0.1.1.zip similarity index 100% rename from skills/base-mcp-v0.1.1.zip rename to skills/base-mcp/base-mcp-v0.1.1.zip From d6bfa901714b886e3582db0508f49303cd1128c4 Mon Sep 17 00:00:00 2001 From: Youssef Date: Wed, 20 May 2026 00:00:31 +0100 Subject: [PATCH 17/32] update to force onboarding --- skills/base-mcp/SKILL.md | 17 +++++++++++++++++ skills/base-mcp/plugins/avantis.md | 10 ++++++++++ skills/base-mcp/plugins/moonwell.md | 10 ++++++++++ skills/base-mcp/plugins/morpho.md | 10 ++++++++++ skills/base-mcp/plugins/uniswap.md | 10 ++++++++++ 5 files changed, 57 insertions(+) diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md index 81b553a..5f83917 100644 --- a/skills/base-mcp/SKILL.md +++ b/skills/base-mcp/SKILL.md @@ -10,6 +10,20 @@ description: > # Base MCP +> [!IMPORTANT] +> ## ⚠️ MANDATORY: RUN THE FULL ONBOARDING FLOW AT THE START OF EVERY CONVERSATION ⚠️ +> +> **Every new conversation that touches Base MCP — including plugin-only conversations (Morpho, Moonwell, Uniswap, Avantis) — MUST begin with the Detection → Onboarding sequence below.** +> +> Do NOT skip Detection. Do NOT skip Onboarding. Do NOT load a plugin before completing both. +> +> Order is strictly: +> 1. **Detection** — call `get_wallets`, confirm MCP is connected +> 2. **Onboarding** — show wallet status, capability summary, and disclaimer +> 3. **Then and only then** — proceed with the user's request or load a plugin +> +> This is required even if the user jumps straight to a plugin topic. The onboarding establishes wallet context that every subsequent tool call depends on. + ## Detection Call `get_wallets` immediately on session start. @@ -23,6 +37,9 @@ Load [references/tone.md](references/tone.md) at session start. Its rules apply ## Onboarding +> [!WARNING] +> **This section is NOT optional.** Onboarding runs at the start of EVERY conversation — including conversations that jump straight to a plugin topic. Never proceed to plugin or tool use without completing all three steps below. + Runs once per session after MCP is confirmed connected. 1. **Show wallet status** — the `get_wallets` call from Detection already ran; present the results: diff --git a/skills/base-mcp/plugins/avantis.md b/skills/base-mcp/plugins/avantis.md index 9f2f6a3..bb4113a 100644 --- a/skills/base-mcp/plugins/avantis.md +++ b/skills/base-mcp/plugins/avantis.md @@ -1,5 +1,15 @@ # Avantis Plugin +> [!IMPORTANT] +> ## ⚠️ STOP — COMPLETE ONBOARDING BEFORE USING THIS PLUGIN ⚠️ +> +> **Before executing any Avantis trade or query, you MUST complete the full Base MCP onboarding flow defined in `SKILL.md`:** +> +> 1. Call `get_wallets` (Detection) +> 2. Present wallet status, capability summary, and disclaimer (Onboarding) +> +> Do NOT call any Avantis endpoint until onboarding is complete. The user's wallet address — used as `trader` in every tx-builder call — is only confirmed during Detection. + Avantis is a perpetual futures DEX on Base mainnet (`chainId` 8453). Use `web_request` to fetch unsigned calldata from the Avantis tx-builder, then preview or execute it with account MCP `send_calls`. Do not sign, approve, or submit transactions unless the user explicitly asks. Generating calldata and `send_calls` approval links is safe, but the user must approve any real transaction. diff --git a/skills/base-mcp/plugins/moonwell.md b/skills/base-mcp/plugins/moonwell.md index 7eb525a..7a6676a 100644 --- a/skills/base-mcp/plugins/moonwell.md +++ b/skills/base-mcp/plugins/moonwell.md @@ -1,5 +1,15 @@ # Moonwell Plugin +> [!IMPORTANT] +> ## ⚠️ STOP — COMPLETE ONBOARDING BEFORE USING THIS PLUGIN ⚠️ +> +> **Before executing any Moonwell request, you MUST complete the full Base MCP onboarding flow defined in `SKILL.md`:** +> +> 1. Call `get_wallets` (Detection) +> 2. Present wallet status, capability summary, and disclaimer (Onboarding) +> +> Do NOT call any Moonwell endpoint until onboarding is complete. The user's wallet address — required by every Moonwell `prepare` and position query — is only confirmed during Detection. + 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`. diff --git a/skills/base-mcp/plugins/morpho.md b/skills/base-mcp/plugins/morpho.md index f902f8e..d260da2 100644 --- a/skills/base-mcp/plugins/morpho.md +++ b/skills/base-mcp/plugins/morpho.md @@ -1,5 +1,15 @@ # Morpho Plugin +> [!IMPORTANT] +> ## ⚠️ STOP — COMPLETE ONBOARDING BEFORE USING THIS PLUGIN ⚠️ +> +> **Before executing any Morpho tool or responding to a Morpho-related request, you MUST complete the full Base MCP onboarding flow defined in `SKILL.md`:** +> +> 1. Call `get_wallets` (Detection) +> 2. Present wallet status, capability summary, and disclaimer (Onboarding) +> +> Do NOT call any Morpho tool until onboarding is complete. The user's wallet address — required by every Morpho operation — is only confirmed during Detection. + 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 diff --git a/skills/base-mcp/plugins/uniswap.md b/skills/base-mcp/plugins/uniswap.md index 16a473b..a1f2734 100644 --- a/skills/base-mcp/plugins/uniswap.md +++ b/skills/base-mcp/plugins/uniswap.md @@ -1,5 +1,15 @@ # Uniswap Plugin +> [!IMPORTANT] +> ## ⚠️ STOP — COMPLETE ONBOARDING BEFORE USING THIS PLUGIN ⚠️ +> +> **Before executing any Uniswap swap or LP operation, you MUST complete the full Base MCP onboarding flow defined in `SKILL.md`:** +> +> 1. Call `get_wallets` (Detection) +> 2. Present wallet status, capability summary, and disclaimer (Onboarding) +> +> Do NOT call any Uniswap endpoint until onboarding is complete. The user's wallet address — passed as `walletAddress` in every swap and LP call — is only confirmed during Detection. + Uniswap on Base: token swaps using the proxy-approval flow (no Permit2 signing) and LP position management for V2, V3, and V4. Use `web_request` to fetch unsigned calldata from the Uniswap API, then execute transaction previews with `send_calls`. No additional MCP server is required. From 4fc229481dd4452b5c9f0b42d1d74b5b964fb79e Mon Sep 17 00:00:00 2001 From: Youssef Date: Wed, 20 May 2026 00:02:11 +0100 Subject: [PATCH 18/32] update zip --- skills/base-mcp/base-mcp-v0.1.1.zip | Bin 29912 -> 31517 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/skills/base-mcp/base-mcp-v0.1.1.zip b/skills/base-mcp/base-mcp-v0.1.1.zip index b65d4808a9a1516d65d7c101636bbda0d45d0fc2..993642491e11bfe9f24f7cf289e1e1091dbe7a58 100644 GIT binary patch delta 17912 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 delta 16324 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 Date: Wed, 20 May 2026 00:37:46 +0100 Subject: [PATCH 19/32] update tools --- skills/base-mcp/base-mcp-v0.1.1.zip | Bin 31517 -> 32095 bytes skills/base-mcp/references/tools.md | 14 ++++++++++++++ 2 files changed, 14 insertions(+) diff --git a/skills/base-mcp/base-mcp-v0.1.1.zip b/skills/base-mcp/base-mcp-v0.1.1.zip index 993642491e11bfe9f24f7cf289e1e1091dbe7a58..0ab583fe29f2aada06fe2580e14ebafb1b472fbb 100644 GIT binary patch delta 3729 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 P3+{iP_g}&9e^C7o3mU8b diff --git a/skills/base-mcp/references/tools.md b/skills/base-mcp/references/tools.md index 23c56d3..6647030 100644 --- a/skills/base-mcp/references/tools.md +++ b/skills/base-mcp/references/tools.md @@ -176,3 +176,17 @@ Map each `transactions[i]` directly: `to → calls[i].to`, `data → calls[i].da - If a request fails with a domain rejection, the hostname is not allowlisted — inform the user, do not retry - Currently allowlisted partner protocols are documented in the plugin files under `plugins/` - Always prefer `send_calls` batching when the response contains multiple transactions — see [references/batch-calls.md](batch-calls.md) + +**Custom (non-native) plugins — domain not allowlisted:** + +When a user asks you to interact with a protocol that is not one of the four native plugins (Morpho, Moonwell, Uniswap, Avantis) and `web_request` rejects the domain: + +1. **Construct the calldata URL as a GET endpoint** — most protocol APIs expose a GET endpoint that encodes all parameters in the query string (address, amount, slippage, etc.). Build the full URL with all required parameters. +2. **Try to fetch it directly** — attempt to retrieve the URL using whatever HTTP fetch capability is available to you in the current harness. +3. **If you cannot fetch it** (e.g., the harness restricts fetching to URLs explicitly sent by the user in the chat): do NOT silently fail or retry with `web_request`. Instead, tell the user: + - What the constructed URL is (show the full URL with parameters) + - That you need them to paste it into the chat so you can read the response + - Example message: _"I can't fetch external URLs directly in this environment. Please open this URL in your browser or paste it into the chat: ``"_ +4. Once the user sends the response (JSON or raw text), parse it and continue the normal `send_calls` flow. + +This fallback applies **only to custom protocols**. Native plugins (Morpho, Moonwell, Uniswap, Avantis) are always allowlisted — use `web_request` for them without this fallback. From bfed4a4258040b5a4fb5bf78963eace516d2dbfd Mon Sep 17 00:00:00 2001 From: Youssef Date: Thu, 21 May 2026 00:01:54 +0100 Subject: [PATCH 20/32] update skill approval mode --- skills/base-mcp/SKILL.md | 5 +---- skills/base-mcp/references/approval-mode.md | 3 --- skills/base-mcp/references/tone.md | 2 +- skills/base-mcp/references/tools.md | 8 +++----- 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md index 5f83917..9bb8a60 100644 --- a/skills/base-mcp/SKILL.md +++ b/skills/base-mcp/SKILL.md @@ -42,10 +42,7 @@ Load [references/tone.md](references/tone.md) at session start. Its rules apply Runs once per session after MCP is confirmed connected. -1. **Show wallet status** — the `get_wallets` call from Detection already ran; present the results: - - User's Base account address(es) - - If any wallet has `inSession: true`: confirm M2 mode is active — write operations run without manual approval for that wallet. - - If no wallet has `inSession: true`: note that all write operations will require approval at keys.coinbase.com. +1. **Show wallet status** — the `get_wallets` call from Detection already ran; present the user's Base account address(es). All write operations require approval at keys.coinbase.com. 2. **Show capability summary** — present what is available: diff --git a/skills/base-mcp/references/approval-mode.md b/skills/base-mcp/references/approval-mode.md index 8f2be36..9b13a24 100644 --- a/skills/base-mcp/references/approval-mode.md +++ b/skills/base-mcp/references/approval-mode.md @@ -20,6 +20,3 @@ All write tools (send, swap, sign, send_calls) operate in approval mode. The use - 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/tone.md b/skills/base-mcp/references/tone.md index 2b2b953..a91c100 100644 --- a/skills/base-mcp/references/tone.md +++ b/skills/base-mcp/references/tone.md @@ -40,5 +40,5 @@ At the start of the first substantive response, briefly state the assumed level - Be terse and precise - Skip hand-holding and step-by-step preamble -- Use parameter names and return field names directly (e.g. "`inSession: true`", "`approvalUrl`") +- Use parameter names and return field names directly (e.g. "`approvalUrl`", "`requestId`") - Omit explanations the user clearly already knows diff --git a/skills/base-mcp/references/tools.md b/skills/base-mcp/references/tools.md index 6647030..c9ac515 100644 --- a/skills/base-mcp/references/tools.md +++ b/skills/base-mcp/references/tools.md @@ -10,12 +10,11 @@ Returns all wallets in the user's wallet group. **Parameters:** none -**Return fields (per wallet):** `id`, `type` (`base-account` or `agent-wallet`), `address`, `inSession`, `delegationStatus`, `spendPolicy` +**Return fields (per wallet):** `id`, `type` (`base-account` or `agent-wallet`), `address` **Key patterns:** -- Call at session start — `inSession` determines whether approval mode applies -- Agent wallets with `inSession: true` can transact without manual approval (M2 mode) -- If no wallet is `inSession: true`, all write tools use approval mode +- Call at session start to confirm the MCP is connected and to retrieve the user's address(es) +- All write tools (send, swap, sign, send_calls) operate in approval mode — see [approval-mode.md](approval-mode.md) --- @@ -66,7 +65,6 @@ Send native ETH or any ERC-20 token. Operates in approval mode. **Optional parameters:** - `decimals` — required when `asset` is a contract address (0–18) -- `agentWalletId` — scope to a specific agent wallet (M2 mode) **Key patterns:** - For non-standard tokens, call `search_tokens` first to get `address` + `decimals` From 387c5b0a788ae93abfa2a2a1de5082e0cf13f4f8 Mon Sep 17 00:00:00 2001 From: Youssef Date: Thu, 21 May 2026 00:07:24 +0100 Subject: [PATCH 21/32] update zip --- skills/base-mcp/base-mcp-v0.1.1.zip | Bin 32095 -> 31884 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/skills/base-mcp/base-mcp-v0.1.1.zip b/skills/base-mcp/base-mcp-v0.1.1.zip index 0ab583fe29f2aada06fe2580e14ebafb1b472fbb..30c40400f22baa3926b0482a346603eac9c93dd4 100644 GIT binary patch delta 7854 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
JOj-}>IS<6~UxuZdA z0biQ`i9-Jv;eV?=B!004rW9>1^u0qi?6`Tzg` delta 8159 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 p4gZ)9^FM}` Date: Thu, 21 May 2026 13:50:56 +0100 Subject: [PATCH 22/32] update onboarding and add custom plugins --- README.md | 2 +- skills/base-mcp/SKILL.md | 104 ++++------ skills/base-mcp/base-mcp-v0.1.1.zip | Bin 31884 -> 25265 bytes skills/base-mcp/plugins/avantis.md | 9 +- skills/base-mcp/plugins/moonwell.md | 11 +- skills/base-mcp/plugins/morpho.md | 80 +++----- skills/base-mcp/plugins/uniswap.md | 9 +- skills/base-mcp/references/approval-mode.md | 30 +-- skills/base-mcp/references/batch-calls.md | 46 +++-- skills/base-mcp/references/custom-plugins.md | 31 +++ skills/base-mcp/references/tools.md | 190 ------------------- 11 files changed, 143 insertions(+), 369 deletions(-) create mode 100644 skills/base-mcp/references/custom-plugins.md delete mode 100644 skills/base-mcp/references/tools.md diff --git a/README.md b/README.md index af90147..cc709d5 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ 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/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. | +| [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. Sending, swapping, signing, batched calls, balances, and partner plugins for lending, swaps, and more. | ## Installation diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md index 9bb8a60..67995df 100644 --- a/skills/base-mcp/SKILL.md +++ b/skills/base-mcp/SKILL.md @@ -1,96 +1,70 @@ --- name: base-mcp +version: 0.2.0 description: > - Base MCP — gives your AI assistant access to your Base account via the Base MCP server (mcp.base.org). - Native tools (built and maintained by the Base core team): get_wallets, get_portfolio, send, swap, sign, - send_calls, get_transaction_history, get_request_status, search_tokens, web_request. - Approval mode: send/swap/sign/send_calls require user approval at keys.coinbase.com; response includes approvalUrl + requestId. - Plugins (external partners): Morpho lending, Moonwell lending, Uniswap swaps/LP, Avantis perpetual futures — all on Base. + Base MCP — gives your AI assistant access to a Base account via the Base MCP server (mcp.base.org). + Wallet, portfolio, sending, swapping, signing, batched contract calls, and transaction history on Base. + Extensible via partner plugins (e.g. lending, swaps, perps). The MCP itself is the source of truth for the + current tool list — tool names, parameters, and the set of available plugins may change over time. --- # Base MCP > [!IMPORTANT] -> ## ⚠️ MANDATORY: RUN THE FULL ONBOARDING FLOW AT THE START OF EVERY CONVERSATION ⚠️ +> ## Run onboarding at the start of every conversation that touches Base MCP > -> **Every new conversation that touches Base MCP — including plugin-only conversations (Morpho, Moonwell, Uniswap, Avantis) — MUST begin with the Detection → Onboarding sequence below.** -> -> Do NOT skip Detection. Do NOT skip Onboarding. Do NOT load a plugin before completing both. -> -> Order is strictly: -> 1. **Detection** — call `get_wallets`, confirm MCP is connected -> 2. **Onboarding** — show wallet status, capability summary, and disclaimer -> 3. **Then and only then** — proceed with the user's request or load a plugin -> -> This is required even if the user jumps straight to a plugin topic. The onboarding establishes wallet context that every subsequent tool call depends on. +> Including conversations that jump straight to a plugin topic. Onboarding is short — see below. ## Detection -Call `get_wallets` immediately on session start. +The Base MCP exposes its tools to the harness when connected. If no Base MCP tool is callable, the MCP server is not installed: direct the user to https://docs.base.org/ai-agents/quickstart (or load [references/install.md](references/install.md) for app-specific steps) and stop. -- **Tool unavailable or connection error** → the MCP server is not installed. Tell the user clearly, direct them to install it at https://docs.base.org/ai-agents/quickstart, and stop. Do not attempt to offer any functionality without the MCP. -- **Call succeeds** → MCP is connected. Load `references/tone.md`, then continue to Onboarding. +If Base MCP tools are available, load [references/tone.md](references/tone.md) — its rules apply for the entire conversation — then continue to Onboarding. -## Tone +## Onboarding -Load [references/tone.md](references/tone.md) at session start. Its rules apply for the entire conversation. +Keep it short. Do this once per session, before doing real work: -## Onboarding +1. **Briefly mention what's available** — one or two sentences. The user has a Base account wallet and can do things like check balances, send and swap tokens, sign messages, batch contract calls, and (if installed) use partner plugins for DeFi, swaps, and other onchain actions. Do not enumerate every tool — the agent discovers tools and plugins directly from the MCP. -> [!WARNING] -> **This section is NOT optional.** Onboarding runs at the start of EVERY conversation — including conversations that jump straight to a plugin topic. Never proceed to plugin or tool use without completing all three steps below. +2. **Show this disclaimer verbatim** before proceeding: -Runs once per session after MCP is confirmed connected. + > By using the Base Account MCP, you agree to the Base Account and Base App Terms of Service. Plugins available in the Base repo are authored by Base, not by the third-party protocols they reference. -1. **Show wallet status** — the `get_wallets` call from Detection already ran; present the user's Base account address(es). All write operations require approval at keys.coinbase.com. +3. **Wallet address and balance are optional** — only fetch and display them when the user asks, or when a pending operation actually needs the address (e.g., a write call, a position lookup). Don't volunteer a wallet dump up front. -2. **Show capability summary** — present what is available: +## Tools - **Native tools** (built and maintained by the Base core team): - - Send ETH or any token - - Swap tokens via Coinbase - - Sign messages and typed data - - Batch contract calls - - View portfolio and balances - - View transaction history - - Search tokens by symbol or name +The Base MCP advertises its own tool catalog to the harness. Read the tool descriptions exposed by the MCP — they are the source of truth and may change over time. Do not assume a fixed list; do not preload a tool catalog from this skill. - **Plugins** (APIs provided by external partners, but plugins authored by Base core team): - - Morpho — lending and vaults on Base - - Moonwell — lending and borrowing on Base and Optimism - - Uniswap — token swaps and LP positions on Base - - Avantis — perpetual futures trading on Base +Two patterns deserve their own references because they span multiple tools: -3. **Show this disclaimer** — display it verbatim before proceeding: +| Topic | Reference | +|-------|-----------| +| Approval flow (for any write tool that returns an approval URL) | [references/approval-mode.md](references/approval-mode.md) | +| Batched contract calls (EIP-5792) | [references/batch-calls.md](references/batch-calls.md) | +| Custom / non-native plugins and the `web_request` allowlist | [references/custom-plugins.md](references/custom-plugins.md) | +| Platform install steps | [references/install.md](references/install.md) | +| Tone and language rules | [references/tone.md](references/tone.md) | - > By using the Base Account MCP, you agree to the Base Account and Base App Terms of Service. Plugins available in the Base repo are authored by Base, not by the third-party protocols they reference. +## Plugins -## Tools +Plugins extend Base MCP with partner-specific functionality (lending, swaps, perps, etc.). The available set may change and users might drop additional instructions in the chat or custom plugins that would allow you to use other protocols with the MCP. -Load [references/tools.md](references/tools.md) for the full tool catalogue. For the current task, load ONLY the relevant section — do not preload everything. - -| Task | Tool | Reference | -|------|------|-----------| -| List wallets / check session status | `get_wallets` | [references/tools.md](references/tools.md) | -| Balance / portfolio / token lookup | `get_portfolio`, `search_tokens` | [references/tools.md](references/tools.md) | -| Send ETH or ERC-20 | `send` | [references/tools.md](references/tools.md) | -| Swap tokens | `swap` | [references/tools.md](references/tools.md) | -| Sign message or typed data | `sign` | [references/tools.md](references/tools.md) | -| Batch contract calls | `send_calls` | [references/batch-calls.md](references/batch-calls.md) | -| Transaction history | `get_transaction_history` | [references/tools.md](references/tools.md) | -| Poll approval status | `get_request_status` | [references/approval-mode.md](references/approval-mode.md) | -| Fetch protocol calldata | `web_request` | [references/tools.md](references/tools.md) | -| Platform install | — | [references/install.md](references/install.md) | -| Tone and language rules | — | [references/tone.md](references/tone.md) | +Plugins currently maintained alongside this skill (the **native plugins**): -## Plugins +| Plugin | Reference | +|--------|-----------| +| Morpho | [plugins/morpho.md](plugins/morpho.md) | +| Moonwell | [plugins/moonwell.md](plugins/moonwell.md) | +| Uniswap | [plugins/uniswap.md](plugins/uniswap.md) | +| Avantis | [plugins/avantis.md](plugins/avantis.md) | + +Load a plugin reference only when the user's request matches it. For a plugin's own tools, defer to the descriptions the plugin's MCP exposes — this skill does not duplicate them. + +### Native plugins vs. custom / user-supplied plugins -| Plugin | Protocol | Operated by | Reference | -|--------|----------|-------------|-----------| -| Morpho | Lending / vaults on Base | Morpho Labs | [plugins/morpho.md](plugins/morpho.md) | -| Moonwell | Lending on Base and Optimism | Moonwell team | [plugins/moonwell.md](plugins/moonwell.md) | -| Uniswap | Swaps and LP on Base | Uniswap Labs | [plugins/uniswap.md](plugins/uniswap.md) | -| Avantis | Perpetual futures on Base | Avantis team | [plugins/avantis.md](plugins/avantis.md) | +Native plugins are allowlisted in the Base MCP `web_request` tool and work everywhere. Custom or user-supplied plugins usually aren't allowlisted — load [references/custom-plugins.md](references/custom-plugins.md) for the decision tree on which HTTP path to use (harness HTTP tool vs. user-paste fallback, and the GET-only constraint on Claude/ChatGPT consumer surfaces). ## Installation diff --git a/skills/base-mcp/base-mcp-v0.1.1.zip b/skills/base-mcp/base-mcp-v0.1.1.zip index 30c40400f22baa3926b0482a346603eac9c93dd4..b35c23ba1b14bad856c8d8cd7ec92b16278ced41 100644 GIT binary patch delta 20916 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 literal 31884 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

M^f(U&zlmp_ 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 diff --git a/skills/base-mcp/plugins/avantis.md b/skills/base-mcp/plugins/avantis.md index bb4113a..e7b0a27 100644 --- a/skills/base-mcp/plugins/avantis.md +++ b/skills/base-mcp/plugins/avantis.md @@ -1,14 +1,7 @@ # Avantis Plugin > [!IMPORTANT] -> ## ⚠️ STOP — COMPLETE ONBOARDING BEFORE USING THIS PLUGIN ⚠️ -> -> **Before executing any Avantis trade or query, you MUST complete the full Base MCP onboarding flow defined in `SKILL.md`:** -> -> 1. Call `get_wallets` (Detection) -> 2. Present wallet status, capability summary, and disclaimer (Onboarding) -> -> Do NOT call any Avantis endpoint until onboarding is complete. The user's wallet address — used as `trader` in every tx-builder call — is only confirmed during Detection. +> Complete the short Base MCP onboarding flow defined in `SKILL.md` before calling any Avantis endpoint. The user's wallet address — used as `trader` in every tx-builder call — is fetched lazily when needed. Avantis is a perpetual futures DEX on Base mainnet (`chainId` 8453). Use `web_request` to fetch unsigned calldata from the Avantis tx-builder, then preview or execute it with account MCP `send_calls`. diff --git a/skills/base-mcp/plugins/moonwell.md b/skills/base-mcp/plugins/moonwell.md index 7a6676a..b7f6f16 100644 --- a/skills/base-mcp/plugins/moonwell.md +++ b/skills/base-mcp/plugins/moonwell.md @@ -1,14 +1,7 @@ # Moonwell Plugin > [!IMPORTANT] -> ## ⚠️ STOP — COMPLETE ONBOARDING BEFORE USING THIS PLUGIN ⚠️ -> -> **Before executing any Moonwell request, you MUST complete the full Base MCP onboarding flow defined in `SKILL.md`:** -> -> 1. Call `get_wallets` (Detection) -> 2. Present wallet status, capability summary, and disclaimer (Onboarding) -> -> Do NOT call any Moonwell endpoint until onboarding is complete. The user's wallet address — required by every Moonwell `prepare` and position query — is only confirmed during Detection. +> Complete the short Base MCP onboarding flow defined in `SKILL.md` before calling any Moonwell endpoint. The user's wallet address — required for `prepare` and position queries — is fetched lazily when needed. 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`. @@ -29,7 +22,7 @@ web_request(https://api.moonwell.fi/v1/prepare/?...) send_calls(chainId, calls mapped from transactions[]) → approvalUrl + requestId ↓ -User approves at keys.coinbase.com +User approves at the returned approval URL (present as "Approve Transaction" — see ../references/approval-mode.md) ↓ get_request_status(requestId) → confirmed ``` diff --git a/skills/base-mcp/plugins/morpho.md b/skills/base-mcp/plugins/morpho.md index d260da2..6e215a6 100644 --- a/skills/base-mcp/plugins/morpho.md +++ b/skills/base-mcp/plugins/morpho.md @@ -1,16 +1,11 @@ # Morpho Plugin > [!IMPORTANT] -> ## ⚠️ STOP — COMPLETE ONBOARDING BEFORE USING THIS PLUGIN ⚠️ -> -> **Before executing any Morpho tool or responding to a Morpho-related request, you MUST complete the full Base MCP onboarding flow defined in `SKILL.md`:** -> -> 1. Call `get_wallets` (Detection) -> 2. Present wallet status, capability summary, and disclaimer (Onboarding) -> -> Do NOT call any Morpho tool until onboarding is complete. The user's wallet address — required by every Morpho operation — is only confirmed during Detection. +> Complete the short Base MCP onboarding flow defined in `SKILL.md` before calling any Morpho tool — the user's wallet address (required by Morpho write/position calls) is fetched lazily, and the disclaimer must be shown once per session. -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`. +Morpho is a lending protocol on Base. The Morpho MCP server prepares lending operations (deposit, borrow, withdraw, repay, supply collateral) and returns unsigned calldata that is then executed via Base MCP's batched-calls tool. + +The exact list of Morpho tools, their parameters, and supported chains are exposed by the Morpho MCP itself — read its tool descriptions rather than relying on a fixed catalog in this file. Tools may be added, renamed, or removed over time. ## MCP Server @@ -18,82 +13,55 @@ URL: `https://mcp.morpho.org/` ## Installation (alongside Base MCP) -Add both servers to your MCP config: - ```json { "mcpServers": { "base-account": { "url": "https://mcp.base.org" }, - "morpho": { "url": "https://mcp.morpho.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 MCP's `send_calls` to execute. +Morpho's prepare-style tools (deposit, withdraw, supply, borrow, repay, supply/withdraw collateral) return an unsigned `calls` array plus a `chainId`. Pass them straight to Base MCP's batched-calls tool. ``` -morpho_prepare_deposit(vaultAddress, amount) → { calls: [...], chainId } -↓ -send_calls(chainId, calls) → approvalUrl + requestId -↓ -User approves at keys.coinbase.com -↓ -get_request_status(requestId) → confirmed +morpho prepare tool → { calls: [...], chainId } + ↓ +batched-calls tool (chainId, calls) → approval URL + request ID + ↓ user approves +status-poll tool (request ID) → confirmed ``` +See [../references/batch-calls.md](../references/batch-calls.md) and [../references/approval-mode.md](../references/approval-mode.md). + ## 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 +1. Query Morpho vaults filtered by USDC, sorted by APY. +2. Call the Morpho prepare-deposit tool for the chosen vault and amount. +3. Pass the returned `calls` + `chainId` to Base MCP's batched-calls tool. +4. Hand the user the approval link; once they confirm, poll the request status. ``` Show all my Morpho positions on Base ``` -1. `get_wallets` (get user's address) -2. `morpho_get_positions` (user's address) +1. Fetch the user's address (if not already known). +2. Call the Morpho positions tool with that 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 +1. Fetch the user's address. +2. Call the Morpho positions tool and report the health factor from the response. ## 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 +- Morpho's prepare tools typically simulate before returning — review simulation output before submitting the batch. +- Use the Morpho simulation tool for novel or large operations. +- Always check the supported-chains tool for the current list rather than assuming a fixed set. diff --git a/skills/base-mcp/plugins/uniswap.md b/skills/base-mcp/plugins/uniswap.md index a1f2734..ecc410d 100644 --- a/skills/base-mcp/plugins/uniswap.md +++ b/skills/base-mcp/plugins/uniswap.md @@ -1,14 +1,7 @@ # Uniswap Plugin > [!IMPORTANT] -> ## ⚠️ STOP — COMPLETE ONBOARDING BEFORE USING THIS PLUGIN ⚠️ -> -> **Before executing any Uniswap swap or LP operation, you MUST complete the full Base MCP onboarding flow defined in `SKILL.md`:** -> -> 1. Call `get_wallets` (Detection) -> 2. Present wallet status, capability summary, and disclaimer (Onboarding) -> -> Do NOT call any Uniswap endpoint until onboarding is complete. The user's wallet address — passed as `walletAddress` in every swap and LP call — is only confirmed during Detection. +> Complete the short Base MCP onboarding flow defined in `SKILL.md` before calling any Uniswap endpoint. The user's wallet address — passed as `walletAddress` in every swap and LP call — is fetched lazily when needed. Uniswap on Base: token swaps using the proxy-approval flow (no Permit2 signing) and LP position management for V2, V3, and V4. Use `web_request` to fetch unsigned calldata from the Uniswap API, then execute transaction previews with `send_calls`. diff --git a/skills/base-mcp/references/approval-mode.md b/skills/base-mcp/references/approval-mode.md index 9b13a24..923e5a6 100644 --- a/skills/base-mcp/references/approval-mode.md +++ b/skills/base-mcp/references/approval-mode.md @@ -1,22 +1,24 @@ +> Today the Base MCP exposes a single execution mode for write tools: **approval mode** (the user manually approves each transaction via a returned URL). Other modes may be added later. Treat the tool descriptions exposed by the MCP as the source of truth — if a future write tool returns a different shape or skips the approval step, follow what the MCP describes, not this file. + # 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. +In approval mode, every write call (send, swap, sign, batched calls, and any plugin-prepared transaction routed through Base MCP) returns an **approval URL** plus a **request ID**. The user opens the URL, approves the transaction in their wallet UI, and then the agent polls the request ID for completion. ## 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) +1. **Call the write tool.** The response includes: + - an approval URL (the field name is on the MCP response — typically `approvalUrl`) + - a request ID (typically `requestId`) +2. **Show the user the link.** Present it as **"Approve Transaction"** (or similar neutral language). Do not name or describe the wallet provider behind the link, even when the URL hostname suggests one — the underlying wallet UI is an implementation detail and may change. Just give the user the link to click. + - Beginner-friendly phrasing: _"Open this to approve the transaction: [Approve Transaction]()"_ + - Terse phrasing: _"[Approve Transaction]()"_ +3. **Wait for the user to confirm they approved.** Don't poll in a tight loop while they're still acting. +4. **Call the status-poll tool** (typically `get_request_status`) with the request ID once. +5. **Only report success** when the status tool confirms completion. ## 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 +- Reporting success before the status tool confirms it — the user may not have approved yet. +- Skipping the approval link — the transaction cannot complete without user action. +- Naming the wallet/approval provider, or surfacing the raw hostname as the link text — say "Approve Transaction". +- Polling the status tool in a tight loop instead of once after the user confirms. diff --git a/skills/base-mcp/references/batch-calls.md b/skills/base-mcp/references/batch-calls.md index 56871c4..259e676 100644 --- a/skills/base-mcp/references/batch-calls.md +++ b/skills/base-mcp/references/batch-calls.md @@ -1,25 +1,35 @@ -# send_calls (Batch Calls) +# Batched Contract Calls (EIP-5792) -> **Batching is always preferred** for plugin interactions that require a token approval followed by a protocol action (e.g. approve + supply, approve + deposit). Also use batching any time a plugin's `/prepare/*` endpoint returns multiple transactions in its `transactions[]` array. Never split these into sequential single `send` calls when `send_calls` can execute them atomically in a single user approval. +Base MCP exposes a batched-calls tool (typically `send_calls`) that submits multiple EIP-5792 `wallet_sendCalls` for a single user approval. Use it for arbitrary contract interactions, multi-step transactions, or any flow that combines an approval with a follow-up action. -Submit a batch of EIP-5792 wallet_sendCalls for user approval. Use for arbitrary contract interactions, multi-step transactions, or batched operations. +> **Batching is preferred whenever a flow involves a token approval followed by a protocol action** (approve + deposit, approve + supply, approve + swap, etc.). Also batch whenever a plugin or protocol endpoint returns multiple transactions in a single response. Don't split these into sequential single-`send` calls when one batched approval can execute them atomically. ## 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) + +- Protocol interactions not covered by `send` or `swap` (DeFi, NFT mints, approvals, governance actions). +- Combining multiple operations into one user approval. +- Executing a transaction array returned by a plugin's prepare-style endpoint — pass the array straight through. + +## Shape + +The MCP advertises the exact parameter names and types — defer to its tool description. The general shape is: + +- A `chainId` (hex with `0x` prefix — `0x2105` for Base mainnet, `0x14a34` for Base Sepolia). +- A `calls` array of `{ to, value, data }` objects: + - `to` — target address, `0x`-prefixed (required) - `value` — hex ETH in wei (e.g. `0x0`), optional - - `data` — calldata hex (e.g. `0xa9059cbb...`), optional + - `data` — calldata hex, optional + +## Approval flow + +Same as any write tool: the response returns an approval URL and request ID. See [approval-mode.md](approval-mode.md). -## Approval mode flow -Same as send: get `approvalUrl` + `requestId`, direct user to URL, poll `get_request_status`. +## Generic orchestration -## 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 +``` +plugin or protocol API → { transactions: [...] } (or equivalent) + ↓ map each transaction to a { to, value, data } call +batched-calls tool (chainId, calls) → approval URL + request ID + ↓ user approves +status-poll tool (request ID) → confirmed +``` diff --git a/skills/base-mcp/references/custom-plugins.md b/skills/base-mcp/references/custom-plugins.md new file mode 100644 index 0000000..6ce85b6 --- /dev/null +++ b/skills/base-mcp/references/custom-plugins.md @@ -0,0 +1,31 @@ +# Custom / Non-Native Plugins + +The native plugins shipped with this skill (Morpho, Moonwell, Uniswap, Avantis) have their HTTP APIs **allowlisted in the Base MCP `web_request` tool**. This matters because Claude and ChatGPT restrict which APIs an agent can call directly from their surface — `web_request` is what makes those calls possible. For native plugins, `web_request` works out of the box. + +If the user introduces a **custom plugin** or pastes instructions to interact with a protocol that isn't one of the native plugins, its API host is almost certainly **not** in the `web_request` allowlist and the call will be rejected. Do not silently retry against an allowlist-rejected host — pick one of the paths below based on your environment. + +## Path 1 — Harness has a direct HTTP tool + +If the current environment lets you call HTTP APIs directly (e.g. Claude Code, Codex, Cursor, or any harness where you have a fetch / curl / shell tool), **use that tool to make the call yourself**. Do not route it through `web_request`. + +Any HTTP method works here (GET, POST, etc.), assuming the harness's HTTP tool supports it. + +## Path 2 — Harness has no HTTP tool (typical on Claude / ChatGPT consumer surfaces) + +When the only way to fetch an external response is to ask the user to do it, you are effectively limited to **GET endpoints**. The user can paste a URL into a browser and copy back the JSON, but they cannot reasonably send a POST/PUT/DELETE with custom headers and a body. + +So for non-native plugins on Claude or ChatGPT consumer surfaces: + +- **Only GET-style APIs are viable.** If the protocol's API requires POST or other write methods to retrieve calldata, surface this limitation to the user — explain that their environment can't fetch the response and that they would need a harness with HTTP tools (e.g. Claude Code) to proceed. +- For GET endpoints: + 1. Construct the full URL with every query parameter encoded inline (address, amount, slippage, chain, etc.). + 2. Show the URL to the user and ask them to open it in a browser (or run it with `curl`) and paste the response back into the chat. + 3. Parse what they paste and continue the flow (e.g. map returned calldata into the batched-calls tool, then walk through the approval flow — see [approval-mode.md](approval-mode.md) and [batch-calls.md](batch-calls.md)). + +## Decision summary + +| Situation | What to do | +|-----------|------------| +| Native plugin, any harness | Use `web_request` directly. | +| Non-native plugin, harness has an HTTP tool (Claude Code, Codex, Cursor, …) | Call the API with the harness's HTTP tool. Any method is fine. | +| Non-native plugin, no HTTP tool (Claude / ChatGPT consumer apps) | GET only. Construct the URL, hand it to the user, parse the response they paste back. If the API needs POST, tell the user this surface can't support it. | diff --git a/skills/base-mcp/references/tools.md b/skills/base-mcp/references/tools.md deleted file mode 100644 index c9ac515..0000000 --- a/skills/base-mcp/references/tools.md +++ /dev/null @@ -1,190 +0,0 @@ -# Native Tools - -All tools below are built and maintained by the Base core team via the Base MCP server. - ---- - -## get_wallets - -Returns all wallets in the user's wallet group. - -**Parameters:** none - -**Return fields (per wallet):** `id`, `type` (`base-account` or `agent-wallet`), `address` - -**Key patterns:** -- Call at session start to confirm the MCP is connected and to retrieve the user's address(es) -- All write tools (send, swap, sign, send_calls) operate in approval mode — see [approval-mode.md](approval-mode.md) - ---- - -## get_portfolio - -Returns portfolio value and per-asset breakdown for any wallet address. Onchain data is public — any address can be queried. - -**Parameters:** -- `address` — optional; defaults to session wallet -- `chain` — optional: `base` or `ethereum` -- `query` — optional token filter (e.g. `USDC`) -- `limit` / `offset` — pagination -- `includePnl` — include unrealized/realized P&L (default false) - -**Key patterns:** -- Omit `address` to query the session wallet; pass it to query any other address -- Use `query` to filter to a specific token before displaying -- If a token isn't found, call `search_tokens` first to resolve its contract address - ---- - -## search_tokens - -Search for token metadata by symbol or name. - -**Parameters:** -- `query` — required; symbol or name (e.g. `USDC`, `WETH`) -- `chain` — optional: `base` or `base-sepolia` - -**Return fields (per result):** `name`, `symbol`, `address` (contract), `decimals`, `chain` - -**Key patterns:** -- Always use the returned `address` AND `decimals` together when passing a contract address to `send` -- For ETH and USDC, pass the symbol directly to `send`/`swap` — no lookup needed -- If multiple results, prefer the `base` mainnet result unless the user specified otherwise - ---- - -## send - -Send native ETH or any ERC-20 token. Operates in approval mode. - -**Required parameters:** -- `recipient` — 0x address, ENS name, basename (e.g. `vitalik.eth`), cb.id, or 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 (0–18) - -**Key patterns:** -- For non-standard tokens, call `search_tokens` first to get `address` + `decimals` -- Basenames and ENS names resolve automatically — no need to look them up first -- Never report success before `get_request_status` confirms completion - ---- - -## swap - -Swap between two tokens via the Coinbase swap service. Mainnet only — testnets not supported. Operates in approval mode. - -**Required parameters:** -- `fromAsset` — symbol or contract address -- `toAsset` — symbol or contract address -- `amount` — human-readable decimal amount of `fromAsset` -- `chain` — target chain (e.g. `base`); testnets are not supported - -**Key patterns:** -- For unknown tokens, call `search_tokens` first to resolve the contract address -- If user requests a testnet swap, explain it is not supported -- Never report success before `get_request_status` confirms completion - ---- - -## sign - -Request a user-approved signature. Supports EIP-712 typed data and personal_sign. Operates in approval mode. - -**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 - -**Key patterns:** -- Use `0x45` for simple text messages (SIWE, auth challenges) -- Use `0x01` for structured typed data (permit signatures, EIP-712 auth) -- Poll `get_request_status` after approval to retrieve the signature value - ---- - -## send_calls - -Submit a batch of EIP-5792 calls. See [references/batch-calls.md](batch-calls.md) for full details and the batching-preferred rule. - -**Required parameters:** -- `chainId` — hex chain ID with 0x prefix (`0x2105` for Base mainnet, `0x14a34` for Base Sepolia) -- `calls` — array of `{ to, value, data }` objects - ---- - -## get_transaction_history - -Returns paginated transaction history for any wallet address in reverse chronological order. - -**Parameters:** -- `address` — optional; defaults to session wallet -- `chain` — optional: `base` or `ethereum` -- `asset` — optional symbol filter (e.g. `USDC`) -- `limit` — 1–200, defaults to 50 -- `cursor` — pagination cursor from previous response's `nextCursor` - -**Key patterns:** -- Date range filtering is not supported — paginate to find transactions in a specific period -- Use `asset` to narrow results to a specific token -- Continue paginating while `hasMore` is true - ---- - -## get_request_status - -Poll the status of a pending approval request. - -**Parameters:** -- `requestId` — required; from the write tool response - -**Key patterns:** -- Call once after the user confirms they approved — do not poll in a tight loop -- Never report a write operation as successful before this returns a confirmed status -- See [references/approval-mode.md](approval-mode.md) for the full approval flow - ---- - -## web_request - -Make an HTTP request to a whitelisted partner API. The hostname must be in the MCP server's allowlist — requests to unlisted domains are rejected. This tool exists because AI assistants in most harnesses cannot fetch arbitrary URLs; `web_request` provides controlled access to trusted protocol APIs so the agent can retrieve calldata and pass it to `send_calls`. - -**Parameters:** -- `url` — full HTTPS URL; hostname must be allowlisted (required) -- `method` — `GET` or `POST` (required) -- `headers` — optional key/value map. Prohibited: `Authorization`, `Cookie`, `Host`, `X-Forwarded-*` -- `body` — JSON object for POST requests; ignored for GET - -**Calldata pattern:** -``` -web_request → { data: { transactions: [ { to, data, value, chainId } ] } } - ↓ map transactions[] to send_calls calls[] -send_calls → approvalUrl + requestId - ↓ user approves -get_request_status → confirmed -``` - -Map each `transactions[i]` directly: `to → calls[i].to`, `data → calls[i].data`, `value → calls[i].value`. Pass `chainId` from any `transactions[i].chainId`. - -**Key patterns:** -- If a request fails with a domain rejection, the hostname is not allowlisted — inform the user, do not retry -- Currently allowlisted partner protocols are documented in the plugin files under `plugins/` -- Always prefer `send_calls` batching when the response contains multiple transactions — see [references/batch-calls.md](batch-calls.md) - -**Custom (non-native) plugins — domain not allowlisted:** - -When a user asks you to interact with a protocol that is not one of the four native plugins (Morpho, Moonwell, Uniswap, Avantis) and `web_request` rejects the domain: - -1. **Construct the calldata URL as a GET endpoint** — most protocol APIs expose a GET endpoint that encodes all parameters in the query string (address, amount, slippage, etc.). Build the full URL with all required parameters. -2. **Try to fetch it directly** — attempt to retrieve the URL using whatever HTTP fetch capability is available to you in the current harness. -3. **If you cannot fetch it** (e.g., the harness restricts fetching to URLs explicitly sent by the user in the chat): do NOT silently fail or retry with `web_request`. Instead, tell the user: - - What the constructed URL is (show the full URL with parameters) - - That you need them to paste it into the chat so you can read the response - - Example message: _"I can't fetch external URLs directly in this environment. Please open this URL in your browser or paste it into the chat: ``"_ -4. Once the user sends the response (JSON or raw text), parse it and continue the normal `send_calls` flow. - -This fallback applies **only to custom protocols**. Native plugins (Morpho, Moonwell, Uniswap, Avantis) are always allowlisted — use `web_request` for them without this fallback. From d67dae19ee38cd4c526ad2b803180605f0d69eb1 Mon Sep 17 00:00:00 2001 From: Youssef Date: Thu, 21 May 2026 13:54:52 +0100 Subject: [PATCH 23/32] update custom plugins --- skills/base-mcp/base-mcp-v0.1.1.zip | Bin 25265 -> 25327 bytes skills/base-mcp/references/custom-plugins.md | 16 +++++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/skills/base-mcp/base-mcp-v0.1.1.zip b/skills/base-mcp/base-mcp-v0.1.1.zip index b35c23ba1b14bad856c8d8cd7ec92b16278ced41..5012f79a330852a892b764ba76d209050c84afe8 100644 GIT binary patch delta 1650 zcmV-&295c##R2cd0Siz|0|W{H00000!jTIh2WtQiYHI)wlW{i`u}Ahc0lkwxH!KHc z01s+r01vZqHx&dCy>7K!x5lIBd<6gi-3tH!C6i7!7n80#7Jp^{4{B!s4{CLI3jhHG z^#K3?1QY-O0EJe|ZW}ig-RmnZkcBN_G)B^0cB$hg0rC)P8C?WLL(N=ySkI7Ck|RZc z7x{>OLq9BE(sM~_^hnZrAxk6jKF&SoTs@)d!3W#X6}`7+{$2&WVOq4qy*7R_SvE{2 zJ?qmm@{R6$u77E(Lqnm_c=DjzmM-5}*EhDHch;IMw{1apM*FSm3wp6pCTKsUMj=vY zSUY;XTrTLv;*F=-Otr1uw%P~8)kY4`&&o5syI#sAdllQK13H!8eZEXm=P(^a5tqpK$6Q-6lkJ)1xk-c`s=wRT*Ec1LRm zC{lfXaBEdDef{$HSq;d%MR=rtV2{%lsU?i3-g>WBEtm9*b!Wg8XHmA+J$SN1AiIto zuk7b?GI^6epdAM&g0uBdA&UY^Rhm?aH=7Ys^a_}H97LBx6|}{Gf|ZNxlyogaQq(&e zY-L-7Mt|Mfn+s$$yw7}Is19C`B6KFFNwuB4tJnpl4Pcthj18zD&+4?XahP!s@_od3 zDExnuRHF{2QFNu8;vLvqSP!#I|9_F=Uj9qBRF__Da&X_~#p_ zDi$^^FalwGoTogIsS6(ucuVh|+qI}ABkjO8#&mHAZ||Ga!2z5Xs3vyOF=$<9j>fVs zUw_>!3tD`*#Vg7xQG)V*=(M?yIdc;i{s12&)c4i_CVhsyf^cW8CP;O+{7S_4?mKurf>mGV&*4et^Otx!0H$kzo#^bj$1 zkql8-Vn)CEIMN9n*l1A}qT#r5NjxfiSIw zw!B`0IZ$F$7`mHFrL);{IpP@58cP_K-l#UR(O0S$p{w9se=2t<0>=1%Ys`Dck!ppHC)pwEsbdBpCRxL+oVS;NeC~t`g>xwYm4Ui{x3tW+>1{FhpTiz@sg= zz?s(3BE@VpZ<1JQ6;v@geq3s;?sQP%sFYid2nluhbKk0HeTg^v#A7muJv+f8O*hdY zx?#EkCfcE`$uP?}MhB3Ajr#H5T7Sq?ib2N}?v=nv){Mx2P2F1z2j~$!#g&ncG5-M8 zgM%qPng%7IdA#<@DS*P#L529j_*TPJFI8OwMF>iDb}#|xwAlNgej&5hCc8k1e9ua{ zZNOhiC0UN~igfc2QKGSS(Un-xO2NNR=v2}NQ*l;ak*(Ea@Ni>qXmJJtrzH3=$f}-d<|Lpwk!YN4>Sl(UG@ET11VM#xB zBFsSA6kUk{pQ{NJFZP^HBtPc$sY7}zw7r0D#osbu5x(6sI+LaUh8~mU-rJ;Mu?U|R zZ{UnXN*jNCXVXfqlQ*s`T=e}fvl&5I909_Ub6qb3YXA?E5n&dyyItV{0lkx*UpfP3 z01uN9VH}h8UuX@zZna#u#-r$b1polulWSl<17-jZlM!JYlgVHn16M%+lTbGmlmB2$ w1I|VOlTbGmlVf301Mx}#lh8^Ylh$EC0tio&7-CieAXbxbVmk(;T>t<80Lk|T#{d8T delta 1595 zcmV-B2E_UA#R0L!0Siz|0|W{H00000OOXp92af*^YL5R7lW{i`u}Ahc0YQ^KH!KHv z{|;(-{|>WpHx&dCL2k8NXO`&|J_P^(D+>SsC6i7!7n80#7JqsF4r+S;4r+CH3jhHG z^#K3?1QY-O0DV@=a@#f(-Rmpvj2DqK6g^3I*`E8VtSQh%>QMWNDIdC+xD(<|%R%4T$Bt?9Y0GrBd}_o~h4&7(3w`x#XVkwV4V z(fieENpF@PJk95-u5Djy9}rg?*+4%l&vbRMq`RKie>ncDV;}A)SXGP|UCE?ONf(t0@0KfaIOTK^sIPQUrF_bex_@UAsK8T!+*E4EMX0y5aeyM# z=R3Di1=F{$f1lKV%ss**{R4aKwn!~uJhj$)y{?Tz=@>H-3N*lm5pBo!c!7!`i#>QdBLCE`vaW5oU zGbZvUu;wzSx0Vowysj|}M0Fh87Nb$pPMp#%(qeZgl_Vo+b@6c6L#sk1Ouz|RgF7W& z($;p4*gR@yO@k!B_w0<8*h(*Bl=sRR_8zoOUjGFv21Mf4Rrjv=E=6m2+wv*(h&#y?*{Rk5&H zh7kzk<2+?WrY?Nk<1M{=t~a8V0ci)eF{X=6c)YKU2M2K8pqki8!=QCJa5R=ReRsLa zXn*W`<{X-M;rH-CLTzguVA6-kzxob&V-F5Z!xpnuwajk-?|hVV z0fKi8kLvWZ$>ejziH26F*{iK!k)Q*I_z6-l2nQ!K2RUl-o8!g6V_=qKjA#}w~>2N!~r=s&OSMk8W8VoSC?d%1-ns&g6>F1|GkW7QVm)za4rW>V)aL?cs(I{i6eG! zO*F+RG6)j@aoY?V_$g@2`m0`cVvtTmi%s>%|T!;n>DI|Fw58h0=5u8fKb z?GH-Kk?&PbHx;Z;Qc0FWz#=yi-5YsfTL=a&0Q~m!oT3ZY#r0U>otVzuEFMm*J_0h5 zI5IpM4j`2(e##+nxK6qsBgTUwBKh$fz;r3lMtnL!+o)Ek&;kB(q{2S-T7O}RTrScL zD>KPt-9YK}!{}tPQ?;1NBcvXK3r##Tec3H98i1QOCxJWrk%`@h5I1zyqhp*FLltKgY#{pr2ByI69H zhfl?wjfdl9W)3a&|1P0#Ut|B7A>W1E8G@oHc=&DipRS&h+!DhD0hMk*)gP90?Cd|b zb4Z=1d{<_7_)OhJsbf`ld)Yd0*?Qae_$80`CZ`w0YQ`7UpfML|C4`U z9g{R*XbnMbwOnVG=@mW&001kKsbD_>dH<7tU>%bOVIBh+K>(9bHx-jTVN3&%MgWsg tHx-gc5|guGKm)T%0FzKR6_X2MNdn4FlU8C@0^(JZu3|d|W?cXP007#o@}~d* diff --git a/skills/base-mcp/references/custom-plugins.md b/skills/base-mcp/references/custom-plugins.md index 6ce85b6..48594d6 100644 --- a/skills/base-mcp/references/custom-plugins.md +++ b/skills/base-mcp/references/custom-plugins.md @@ -10,17 +10,19 @@ If the current environment lets you call HTTP APIs directly (e.g. Claude Code, C Any HTTP method works here (GET, POST, etc.), assuming the harness's HTTP tool supports it. -## Path 2 — Harness has no HTTP tool (typical on Claude / ChatGPT consumer surfaces) +## Path 2 — Claude / ChatGPT consumer surfaces -When the only way to fetch an external response is to ask the user to do it, you are effectively limited to **GET endpoints**. The user can paste a URL into a browser and copy back the JSON, but they cannot reasonably send a POST/PUT/DELETE with custom headers and a body. +Claude and ChatGPT *can* fetch GET URLs themselves, but for security reasons they will only fetch URLs that the **user has pasted into the chat**. The agent cannot freely construct and fetch arbitrary URLs on its own. -So for non-native plugins on Claude or ChatGPT consumer surfaces: +That makes the flow effectively **GET-only**: there's no equivalent escape hatch for POST/PUT/DELETE with custom headers and a body, because the user can't realistically inject those into the chat in a fetchable form. -- **Only GET-style APIs are viable.** If the protocol's API requires POST or other write methods to retrieve calldata, surface this limitation to the user — explain that their environment can't fetch the response and that they would need a harness with HTTP tools (e.g. Claude Code) to proceed. +So for non-native plugins on Claude / ChatGPT consumer surfaces: + +- **Only GET-style APIs are viable.** If the protocol's API requires POST or other write methods to retrieve calldata, surface this limitation to the user — explain that their environment can't perform the fetch and that they would need a harness with HTTP tools (e.g. Claude Code) to proceed. - For GET endpoints: 1. Construct the full URL with every query parameter encoded inline (address, amount, slippage, chain, etc.). - 2. Show the URL to the user and ask them to open it in a browser (or run it with `curl`) and paste the response back into the chat. - 3. Parse what they paste and continue the flow (e.g. map returned calldata into the batched-calls tool, then walk through the approval flow — see [approval-mode.md](approval-mode.md) and [batch-calls.md](batch-calls.md)). + 2. Show the URL to the user and ask them to paste it back into the chat. Once pasted, you can fetch it yourself — that's the security model these surfaces enforce. + 3. Parse the response and continue the flow (e.g. map returned calldata into the batched-calls tool, then walk through the approval flow — see [approval-mode.md](approval-mode.md) and [batch-calls.md](batch-calls.md)). ## Decision summary @@ -28,4 +30,4 @@ So for non-native plugins on Claude or ChatGPT consumer surfaces: |-----------|------------| | Native plugin, any harness | Use `web_request` directly. | | Non-native plugin, harness has an HTTP tool (Claude Code, Codex, Cursor, …) | Call the API with the harness's HTTP tool. Any method is fine. | -| Non-native plugin, no HTTP tool (Claude / ChatGPT consumer apps) | GET only. Construct the URL, hand it to the user, parse the response they paste back. If the API needs POST, tell the user this surface can't support it. | +| Non-native plugin, Claude / ChatGPT consumer apps | GET only. Construct the URL, ask the user to paste it into the chat so you're allowed to fetch it, then parse the response. If the API needs POST, tell the user this surface can't support it. | From 97b4bd5033247f9ee63f270b88f653188f38cb25 Mon Sep 17 00:00:00 2001 From: Youssef Date: Thu, 21 May 2026 14:09:26 +0100 Subject: [PATCH 24/32] update installation and custom plugins --- skills/base-mcp/base-mcp-v0.1.1.zip | Bin 25327 -> 25978 bytes skills/base-mcp/references/custom-plugins.md | 33 ++-- skills/base-mcp/references/install.md | 155 ++++++++++++------- 3 files changed, 122 insertions(+), 66 deletions(-) diff --git a/skills/base-mcp/base-mcp-v0.1.1.zip b/skills/base-mcp/base-mcp-v0.1.1.zip index 5012f79a330852a892b764ba76d209050c84afe8..a8904faf5f2a582648e0fd351c8634459631e012 100644 GIT binary patch delta 3723 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 aiR}{tq(0Kv)|jBu6j@^{4;F?0E&Cr}zzsA2 delta 3080 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 C;=bwt diff --git a/skills/base-mcp/references/custom-plugins.md b/skills/base-mcp/references/custom-plugins.md index 48594d6..5e12157 100644 --- a/skills/base-mcp/references/custom-plugins.md +++ b/skills/base-mcp/references/custom-plugins.md @@ -1,20 +1,31 @@ -# Custom / Non-Native Plugins +# Custom / Non-Native Plugins and the `web_request` Allowlist -The native plugins shipped with this skill (Morpho, Moonwell, Uniswap, Avantis) have their HTTP APIs **allowlisted in the Base MCP `web_request` tool**. This matters because Claude and ChatGPT restrict which APIs an agent can call directly from their surface — `web_request` is what makes those calls possible. For native plugins, `web_request` works out of the box. +The native plugins shipped with this skill (Morpho, Moonwell, Uniswap, Avantis) have their HTTP APIs **allowlisted in the Base MCP `web_request` tool**. This matters because Claude and ChatGPT restrict which APIs an agent can call directly from their surface — `web_request` is what makes those calls possible. -If the user introduces a **custom plugin** or pastes instructions to interact with a protocol that isn't one of the native plugins, its API host is almost certainly **not** in the `web_request` allowlist and the call will be rejected. Do not silently retry against an allowlist-rejected host — pick one of the paths below based on your environment. +Custom or user-supplied plugins (anything outside the four native ones) are almost certainly **not** in the allowlist and will be rejected by `web_request`. -## Path 1 — Harness has a direct HTTP tool +## Priority order for HTTP calls -If the current environment lets you call HTTP APIs directly (e.g. Claude Code, Codex, Cursor, or any harness where you have a fetch / curl / shell tool), **use that tool to make the call yourself**. Do not route it through `web_request`. +Use this order **for every plugin call — native or not**: -Any HTTP method works here (GET, POST, etc.), assuming the harness's HTTP tool supports it. +### 1. Harness HTTP tool (preferred whenever available) -## Path 2 — Claude / ChatGPT consumer surfaces +If the current environment lets you call HTTP APIs directly — e.g. Claude Code, Codex, Cursor, or any harness where you have a fetch / curl / shell tool — **use that tool first**, even for native plugins. It supports any HTTP method (GET, POST, etc.), avoids the allowlist entirely, and gives you the full response without round-tripping through the MCP. + +Only fall back to `web_request` if you don't have a usable HTTP tool in the current harness. + +### 2. `web_request` (when no harness HTTP tool exists) + +If the harness has no direct HTTP capability, route the call through Base MCP's `web_request`: + +- **Native plugin host** — works out of the box (the host is allowlisted). +- **Non-native plugin host** — will be rejected. Do not silently retry. Move to path 3. + +### 3. User-paste fallback (Claude / ChatGPT consumer surfaces, non-native hosts) Claude and ChatGPT *can* fetch GET URLs themselves, but for security reasons they will only fetch URLs that the **user has pasted into the chat**. The agent cannot freely construct and fetch arbitrary URLs on its own. -That makes the flow effectively **GET-only**: there's no equivalent escape hatch for POST/PUT/DELETE with custom headers and a body, because the user can't realistically inject those into the chat in a fetchable form. +That makes the fallback effectively **GET-only**: there's no equivalent escape hatch for POST/PUT/DELETE with custom headers and a body, because the user can't realistically inject those into the chat in a fetchable form. So for non-native plugins on Claude / ChatGPT consumer surfaces: @@ -28,6 +39,6 @@ So for non-native plugins on Claude / ChatGPT consumer surfaces: | Situation | What to do | |-----------|------------| -| Native plugin, any harness | Use `web_request` directly. | -| Non-native plugin, harness has an HTTP tool (Claude Code, Codex, Cursor, …) | Call the API with the harness's HTTP tool. Any method is fine. | -| Non-native plugin, Claude / ChatGPT consumer apps | GET only. Construct the URL, ask the user to paste it into the chat so you're allowed to fetch it, then parse the response. If the API needs POST, tell the user this surface can't support it. | +| Any plugin, harness has an HTTP tool (Claude Code, Codex, Cursor, …) | **Use the harness's HTTP tool first.** Any method is fine. Don't route through `web_request` when a direct call is available. | +| Native plugin, no harness HTTP tool | Use `web_request` — the host is allowlisted. | +| Non-native plugin, no harness HTTP tool (Claude / ChatGPT consumer apps) | GET only. Construct the URL, ask the user to paste it into the chat so you're allowed to fetch it, then parse the response. If the API needs POST, tell the user this surface can't support it. | diff --git a/skills/base-mcp/references/install.md b/skills/base-mcp/references/install.md index 40438f7..d943f21 100644 --- a/skills/base-mcp/references/install.md +++ b/skills/base-mcp/references/install.md @@ -1,109 +1,154 @@ # Installing Base MCP -Choose your app below. The whole process takes under two minutes. +> Canonical source: ****. That page is kept up to date with the latest one-click install links, deep-links, and connector flows for each surface. Send the user there first; the instructions below are a backup so the agent can still walk a user through install without leaving the chat. + +The MCP server URL is the same everywhere: **`https://mcp.base.org`** --- -## Claude Desktop +## Claude (Claude.ai web, Claude Desktop, iOS, Android) -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) +One-click add: -> 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). +``` +https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=Base&connectorUrl=https%3A%2F%2Fmcp.base.org +``` ---- +Or manually: -## ChatGPT (desktop app) +1. Open **Customize → Connectors → Add custom connector** +2. Fill in: + - **Name**: `Base` + - **Remote MCP server URL**: `https://mcp.base.org` +3. Click **Add** -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) +A browser tab opens to authorize on first use — sign in with Base. --- -## Claude.ai (web) +## ChatGPT + +Open (or **Settings → Connectors**), then: -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) +1. Enable **Developer Mode** if prompted (under Advanced) +2. Click **Create** → **New App** modal opens +3. Fill in: + - **Name**: `Base` + - **Description** (optional): `Wallet and onchain tools for Base` + - **MCP Server URL**: `https://mcp.base.org` + - **Authentication**: `OAuth` +4. Check **I understand and want to continue** on the risk warning +5. Click **Create** + +ChatGPT prompts for authorization the first time a wallet tool is called. --- -## Claude Code (CLI · VS Code · JetBrains) +## Claude Code + +Add to the current project: + +```bash +claude mcp add --transport http base https://mcp.base.org +``` + +Install globally across all projects: + +```bash +claude mcp add --transport http --scope user base https://mcp.base.org +``` + +Verify: + +```bash +claude mcp list +``` + +The `base` server shows with a tool count once active. Inside a session, `/mcp` also shows server status. + +--- -Run this in your terminal: +## Codex ```bash -claude mcp add base-account --transport http https://mcp.base.org +codex mcp add base --url https://mcp.base.org/ ``` -Then restart Claude Code and sign in when prompted. +Or in `codex.toml`: + +```toml +[mcp_servers.base] +url = "https://mcp.base.org/" +``` --- ## Cursor -Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project): +Deep link install: + +``` +cursor://anysphere.cursor-deeplink/mcp/install?name=base&config=eyJ1cmwiOiJodHRwczovL21jcC5iYXNlLm9yZyJ9 +``` + +Or manually in `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project): ```json { "mcpServers": { - "base-account": { - "type": "http", + "base": { "url": "https://mcp.base.org" } } } ``` -Restart Cursor after saving. +Restart Cursor, then **Settings → MCP** to confirm `base` is active. --- -## Authorization +## Hermes -After connecting the server, a browser tab opens to mcp.base.org. Here's what to do: +Hand the agent the quickstart and let it install itself: -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 +``` +Install the Base MCP server from https://docs.base.org/ai-agents/quickstart +``` -That's it — the MCP is now connected. +Manual install — edit `~/.hermes/config.yaml`: ---- +```yaml +mcp_servers: + base: + url: "https://mcp.base.org" +``` -## Did it work? +Then start a new chat (or `/reload-mcp` in an existing one). -Ask your AI assistant: **"Show me my wallets."** +--- -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. +## Authorization (first use) + +The first time a wallet tool is called, an auth modal opens for the user to authorize the Base Account. Click **Allow** once. (See the live demo on the quickstart page.) After that, write operations still require per-transaction approval — see [approval-mode.md](approval-mode.md). --- -## Troubleshooting +## Did it work? + +Ask the assistant: -**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. +> Show me my wallets -**I see "Integration not found" or "Tool not available"** -→ The server may not have loaded yet. Restart your app and try again. +If it replies with a wallet address, the MCP is connected. If it says it doesn't have wallet tools, the MCP isn't loaded — retry the install steps or fall back to the quickstart link. -**The Integrations / Connectors tab doesn't exist** -→ Your app version may be outdated. Update to the latest version and try again. +--- + +## Troubleshooting -**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. +| Symptom | Try | +|---------|-----| +| No browser tab for sign-in | Open `https://mcp.base.org` directly, sign in, then re-add the server. | +| "Integration not found" / "Tool not available" | Restart the app — the server may not have finished loading. | +| Integrations / Connectors tab missing | App version is too old — update to the latest. | +| `web_request` rejects a hostname | The hostname isn't in the allowlist. For native plugins, this shouldn't happen; for custom plugins see [custom-plugins.md](custom-plugins.md). | +| Anything else | Send the user to . | From d1f30b8dbfaf75c65455b4f2d18a46ca39cd6c5d Mon Sep 17 00:00:00 2001 From: Youssef Date: Thu, 21 May 2026 14:17:42 +0100 Subject: [PATCH 25/32] update version --- skills/base-mcp/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md index 67995df..5e34cb8 100644 --- a/skills/base-mcp/SKILL.md +++ b/skills/base-mcp/SKILL.md @@ -1,6 +1,6 @@ --- name: base-mcp -version: 0.2.0 +version: 0.1.0 description: > Base MCP — gives your AI assistant access to a Base account via the Base MCP server (mcp.base.org). Wallet, portfolio, sending, swapping, signing, batched contract calls, and transaction history on Base. From fc221cbdd725920173ce55c67b223bfce4ebb30b Mon Sep 17 00:00:00 2001 From: Montana Wong Date: Thu, 21 May 2026 10:31:11 -0400 Subject: [PATCH 26/32] add uniswap slippage warnings and rename Base Account MCP to Base MCP Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/base-mcp/README.md | 20 ++++++++++---------- skills/base-mcp/plugins/uniswap.md | 19 +++++++++++++++++-- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/skills/base-mcp/README.md b/skills/base-mcp/README.md index 8b82aaa..cca0cc2 100644 --- a/skills/base-mcp/README.md +++ b/skills/base-mcp/README.md @@ -1,21 +1,21 @@ -# Base Account MCP — Disclaimer +# Base MCP — Disclaimer > ⚠️ **Important: Read Before Use** -**What this is.** The Base Account MCP is a hosted Model Context Protocol server operated by Base that lets AI agents interact with a user's Base Account in a user-authorized way. After authenticating and connecting to the MCP server, the MCP server can read account state and construct transactions for the user to approve and sign. The MCP server itself does not sign or broadcast transactions. By using this MCP server, you agree to the Base Account and Base App Terms of Service. +**What this is.** The Base MCP is a hosted Model Context Protocol server operated by Base that lets AI agents interact with a user's Base Account in a user-authorized way. After authenticating and connecting to the MCP server, the MCP server can read account state and construct transactions for the user to approve and sign. The MCP server itself does not sign or broadcast transactions. By using this MCP server, you agree to the Base Account and Base App Terms of Service. -**Third-party AI hosts, agents, and protocols.** The Base Account MCP is designed to be used with third-party AI hosts and may, depending on the skills loaded into the AI host, prepare transactions that interact with third-party onchain protocols. Those AI hosts and third-party protocols are not operated by Base and are governed by their own terms of service, privacy policies, and (where applicable) jurisdictional eligibility requirements. You are solely responsible for reviewing and complying with each third party's terms and confirming you are eligible to use them. +**Third-party AI hosts, agents, and protocols.** The Base MCP is designed to be used with third-party AI hosts and may, depending on the skills loaded into the AI host, prepare transactions that interact with third-party onchain protocols. Those AI hosts and third-party protocols are not operated by Base and are governed by their own terms of service, privacy policies, and (where applicable) jurisdictional eligibility requirements. You are solely responsible for reviewing and complying with each third party's terms and confirming you are eligible to use them. -**Not official third-party software.** Skills or plugins in the base-skills repo that reference third-party protocols (e.g., Uniswap, Morpho, Moonwell, Avantis) are authored by Base for use with the Base Account MCP. They are not official software of, endorsed by, or operated by those third parties. Inclusion of a skill in this repository does not constitute an endorsement, audit, or guarantee of the underlying protocol. Each third-party protocol is governed by its own terms of service and privacy policy, which the user is solely responsible for reviewing and complying with. +**Not official third-party software.** Skills or plugins in the base-skills repo that reference third-party protocols (e.g., Uniswap, Morpho, Moonwell, Avantis) are authored by Base for use with the Base MCP. They are not official software of, endorsed by, or operated by those third parties. Inclusion of a skill in this repository does not constitute an endorsement, audit, or guarantee of the underlying protocol. Each third-party protocol is governed by its own terms of service and privacy policy, which the user is solely responsible for reviewing and complying with. -**AI outputs may be inaccurate.** AI agents can misinterpret instructions, hallucinate parameters (including amounts, recipients, and contract addresses), or be influenced by adversarial inputs encountered in API responses, web content, or other sources. The Base Account MCP relies on the AI agent to interpret your intent correctly. Base does not validate or guarantee AI agent outputs. Review every action proposed by an AI agent before approving it. +**AI outputs may be inaccurate.** AI agents can misinterpret instructions, hallucinate parameters (including amounts, recipients, and contract addresses), or be influenced by adversarial inputs encountered in API responses, web content, or other sources. The Base MCP relies on the AI agent to interpret your intent correctly. Base does not validate or guarantee AI agent outputs. Review every action proposed by an AI agent before approving it. -**Not professional advice.** Nothing produced by or in connection with the Base Account MCP — including any output of an AI agent using the service — constitutes investment, financial, legal, tax, or other professional advice. +**Not professional advice.** Nothing produced by or in connection with the Base MCP — including any output of an AI agent using the service — constitutes investment, financial, legal, tax, or other professional advice. -**Your responsibility for compliance.** You are solely responsible for ensuring that your use of the Base Account MCP, any connected AI host or agent, your wallet, and any third-party protocol complies with all laws and regulations applicable to you, including sanctions, securities, derivatives, tax, and consumer protection laws. +**Your responsibility for compliance.** You are solely responsible for ensuring that your use of the Base MCP, any connected AI host or agent, your wallet, and any third-party protocol complies with all laws and regulations applicable to you, including sanctions, securities, derivatives, tax, and consumer protection laws. -**Smart contract and onchain risk.** Interacting with onchain protocols may result in partial or total loss of funds due to smart contract vulnerabilities, oracle failures, liquidations, slippage, MEV, governance actions, network congestion, protocol shutdowns, or other risks. Base does not control any third-party onchain protocol that you choose to interact with through the Base Account MCP. +**Smart contract and onchain risk.** Interacting with onchain protocols may result in partial or total loss of funds due to smart contract vulnerabilities, oracle failures, liquidations, slippage, MEV, governance actions, network congestion, protocol shutdowns, or other risks. Base does not control any third-party onchain protocol that you choose to interact with through the Base MCP. -**Service availability and changes.** The Base Account MCP is provided "AS IS" and "AS AVAILABLE." Base may modify, suspend, or discontinue the service or any feature of it at any time and with or without notice. Base does not warrant that the service will be error-free, uninterrupted, secure, or compatible with any particular AI host, wallet, or third-party protocol. +**Service availability and changes.** The Base MCP is provided "AS IS" and "AS AVAILABLE." Base may modify, suspend, or discontinue the service or any feature of it at any time and with or without notice. Base does not warrant that the service will be error-free, uninterrupted, secure, or compatible with any particular AI host, wallet, or third-party protocol. -**Limitation of liability.** To the maximum extent permitted by applicable law, Base and its affiliates and their respective officers, directors, employees, and agents will not be liable for any indirect, incidental, special, consequential, exemplary, or punitive damages, or for any loss of profits, revenues, data, or digital assets, arising out of or related to your use of (or inability to use) the Base Account MCP or any third-party AI host, agent, or protocol used in connection with it, whether based on warranty, contract, tort (including negligence), statute, or any other legal theory. +**Limitation of liability.** To the maximum extent permitted by applicable law, Base and its affiliates and their respective officers, directors, employees, and agents will not be liable for any indirect, incidental, special, consequential, exemplary, or punitive damages, or for any loss of profits, revenues, data, or digital assets, arising out of or related to your use of (or inability to use) the Base MCP or any third-party AI host, agent, or protocol used in connection with it, whether based on warranty, contract, tort (including negligence), statute, or any other legal theory. diff --git a/skills/base-mcp/plugins/uniswap.md b/skills/base-mcp/plugins/uniswap.md index ecc410d..9b986bf 100644 --- a/skills/base-mcp/plugins/uniswap.md +++ b/skills/base-mcp/plugins/uniswap.md @@ -94,7 +94,7 @@ Headers: auth headers plus `"x-permit2-disabled": "true"`. } ``` -Use `"slippageTolerance": <0-20>` instead of `autoSlippage` if the user specifies slippage. +Use `"slippageTolerance": <0-20>` instead of `autoSlippage` if the user specifies slippage. See [Slippage Warnings](#slippage-warnings) before submitting elevated values. Response includes a top-level `quote` object plus metadata. Keep the response as a flat object for `/swap`; do not nest it under a `quote` key. @@ -307,7 +307,7 @@ Prefer `tickBounds` when possible. `priceBounds` can be accepted by the API, but | Collect fees | `POST /lp/claim_fees` | `walletAddress`, `chainId`, `protocol`, `tokenId`; optional `simulateTransaction` | | Create V2 position | `POST /lp/create_classic` | `walletAddress`, `poolParameters { token0Address, token1Address, chainId }`, `independentToken { tokenAddress, amount }` | -Optional on LP operation endpoints: `"slippageTolerance": ` where `0.5` means 0.5%. +Optional on LP operation endpoints: `"slippageTolerance": ` where `0.5` means 0.5%. See [Slippage Warnings](#slippage-warnings) before submitting elevated values. Important: LP APIs can return calldata for syntactically valid `nftTokenId` values even if the connected wallet may not own the position. Treat generated calldata as a transaction preview input, not proof of ownership or guaranteed execution. @@ -437,6 +437,21 @@ const calls = [ --- +## Slippage Warnings + +High slippage tolerance exposes the user to worse fills and sandwich/MEV attacks. Before calling `/swap` or any LP endpoint with an elevated value, warn the user and get explicit confirmation: + +| Tolerance | Level | Action | +| --- | --- | --- | +| ≤ 1% | Normal | Proceed. | +| > 1% and ≤ 5% | Elevated | Mention the value and ask the user to confirm. | +| > 5% and ≤ 20% | High | Warn that the trade can fill significantly below quote and is a likely sandwich target. Require explicit confirmation. | +| > 20% | Very high | Strongly warn; do not submit without the user re-confirming the exact number. | + +Apply the same thresholds to swap and LP slippage. If the user did not specify a value, prefer `autoSlippage: "DEFAULT"` on swaps rather than picking a high number. + +--- + ## Notes - Native ETH address: `0x0000000000000000000000000000000000000000` From 15d84f45986167d085d82b9ca0f7d0fd18214179 Mon Sep 17 00:00:00 2001 From: Montana Wong Date: Thu, 21 May 2026 10:48:32 -0400 Subject: [PATCH 27/32] rename remaining Base Account MCP references to Base MCP Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 2 +- skills/base-mcp/SKILL.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cc709d5..3d9cfba 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ 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/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. Sending, swapping, signing, batched calls, balances, and partner plugins for lending, swaps, and more. | +| [base-mcp](./skills/base-mcp/SKILL.md) | `npx skills add base/skills --skill base-mcp` | Base MCP server — gives your AI assistant a wallet via mcp.base.org. Sending, swapping, signing, batched calls, balances, and partner plugins for lending, swaps, and more. | ## Installation diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md index 5e34cb8..f915afe 100644 --- a/skills/base-mcp/SKILL.md +++ b/skills/base-mcp/SKILL.md @@ -29,7 +29,7 @@ Keep it short. Do this once per session, before doing real work: 2. **Show this disclaimer verbatim** before proceeding: - > By using the Base Account MCP, you agree to the Base Account and Base App Terms of Service. Plugins available in the Base repo are authored by Base, not by the third-party protocols they reference. + > By using the Base MCP, you agree to the Base Account and Base App Terms of Service. Plugins available in the Base repo are authored by Base, not by the third-party protocols they reference. 3. **Wallet address and balance are optional** — only fetch and display them when the user asks, or when a pending operation actually needs the address (e.g., a write call, a position lookup). Don't volunteer a wallet dump up front. From 02c5c32b0c0ed01e9bb1639e07fb6522fe13eb54 Mon Sep 17 00:00:00 2001 From: Youssef Date: Thu, 21 May 2026 18:21:27 +0100 Subject: [PATCH 28/32] update zip file --- skills/base-mcp/base-mcp-v0.1.1.zip | Bin 25978 -> 26358 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/skills/base-mcp/base-mcp-v0.1.1.zip b/skills/base-mcp/base-mcp-v0.1.1.zip index a8904faf5f2a582648e0fd351c8634459631e012..a0c139a7f0ab01803c88cb4109db7c9c371aa2b9 100644 GIT binary patch delta 8728 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 RB_UufYYO-Z^MCI5e*hBKaKHcn delta 8310 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ⅇ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 From 21f84df3ee7a97a1176d89abbdb291c5a0ce534c Mon Sep 17 00:00:00 2001 From: Youssef Date: Fri, 22 May 2026 13:30:42 +0100 Subject: [PATCH 29/32] update skill --- skills/base-mcp/SKILL.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md index f915afe..ab9dfa1 100644 --- a/skills/base-mcp/SKILL.md +++ b/skills/base-mcp/SKILL.md @@ -19,7 +19,7 @@ description: > The Base MCP exposes its tools to the harness when connected. If no Base MCP tool is callable, the MCP server is not installed: direct the user to https://docs.base.org/ai-agents/quickstart (or load [references/install.md](references/install.md) for app-specific steps) and stop. -If Base MCP tools are available, load [references/tone.md](references/tone.md) — its rules apply for the entire conversation — then continue to Onboarding. +If Base MCP tools are available, load [references/tone.md](references/tone.md) — its rules apply for the entire conversation — then continue to Onboarding. If sibling files aren't readable (e.g. you were handed just this `SKILL.md` body), fetch the same relative path from `https://base-a060aa97-youssef-update-agents.mintlify.app/ai-agents/skills/references/tone.md` using the harness's HTTP/fetch tool, or the `web_request` tool from Base MCP if no native web tool is available. The same fallback applies to every other reference and plugin link in this file (see "Loading referenced files" below). ## Onboarding @@ -47,6 +47,12 @@ Two patterns deserve their own references because they span multiple tools: | Platform install steps | [references/install.md](references/install.md) | | Tone and language rules | [references/tone.md](references/tone.md) | +### Loading referenced files + +- **Default — local.** Read each `references/…` or `plugins/…` link from the same directory as this `SKILL.md`. +- **Fallback — web.** If the sibling file isn't readable, fetch the same relative path from `https://base-a060aa97-youssef-update-agents.mintlify.app/ai-agents/skills/` using the harness's HTTP tool. If no harness HTTP tool exists, use the `web_request` tool from Base MCP. +- **Lazy.** Only load a reference or plugin when the conversation actually needs it. Don't preload the catalog. + ## Plugins Plugins extend Base MCP with partner-specific functionality (lending, swaps, perps, etc.). The available set may change and users might drop additional instructions in the chat or custom plugins that would allow you to use other protocols with the MCP. @@ -60,7 +66,7 @@ Plugins currently maintained alongside this skill (the **native plugins**): | Uniswap | [plugins/uniswap.md](plugins/uniswap.md) | | Avantis | [plugins/avantis.md](plugins/avantis.md) | -Load a plugin reference only when the user's request matches it. For a plugin's own tools, defer to the descriptions the plugin's MCP exposes — this skill does not duplicate them. +Load a plugin reference only when the user's request matches it, following the same local-first, web-fallback rule as references (see [Loading referenced files](#loading-referenced-files) above). For a plugin's own tools, defer to the descriptions the plugin's MCP exposes — this skill does not duplicate them. ### Native plugins vs. custom / user-supplied plugins From 991bf3d424107c9e4b7d94ae8b6b4a4e069b29b6 Mon Sep 17 00:00:00 2001 From: Youssef Date: Fri, 22 May 2026 13:36:22 +0100 Subject: [PATCH 30/32] update skill and add virtuals --- skills/base-mcp/SKILL.md | 1 + skills/base-mcp/plugins/virtuals.md | 136 ++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 skills/base-mcp/plugins/virtuals.md diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md index ab9dfa1..60a9dfc 100644 --- a/skills/base-mcp/SKILL.md +++ b/skills/base-mcp/SKILL.md @@ -65,6 +65,7 @@ Plugins currently maintained alongside this skill (the **native plugins**): | Moonwell | [plugins/moonwell.md](plugins/moonwell.md) | | Uniswap | [plugins/uniswap.md](plugins/uniswap.md) | | Avantis | [plugins/avantis.md](plugins/avantis.md) | +| Virtuals | [plugins/virtuals.md](plugins/virtuals.md) | Load a plugin reference only when the user's request matches it, following the same local-first, web-fallback rule as references (see [Loading referenced files](#loading-referenced-files) above). For a plugin's own tools, defer to the descriptions the plugin's MCP exposes — this skill does not duplicate them. diff --git a/skills/base-mcp/plugins/virtuals.md b/skills/base-mcp/plugins/virtuals.md new file mode 100644 index 0000000..d2da872 --- /dev/null +++ b/skills/base-mcp/plugins/virtuals.md @@ -0,0 +1,136 @@ +# Virtuals Plugin + +> [!IMPORTANT] +> Complete the short Base MCP onboarding flow defined in `SKILL.md` before calling any Virtuals tool. Virtuals is **session-authenticated**: every tool requires a JWT `token` parameter obtained via a SIWE login that the user must approve through Base MCP. Run the [Auth flow](#auth-flow) once per session and reuse the token for subsequent calls. + +Virtuals (ACP — Agent Commerce Protocol) is a platform for creating and operating autonomous AI agents that can transact onchain, hold payment cards, and own email identities. The Virtuals MCP server prepares and executes agent management, agent card, and agent email operations. The Base MCP wallet is used only to sign the SIWE login challenge — Virtuals does not route onchain transactions through Base MCP after auth. + +The exact list of Virtuals tools, their parameters, and the capabilities they expose are advertised by the Virtuals MCP itself — read the tool descriptions rather than relying on a fixed catalog in this file. Tools may be added, renamed, or removed. + +## MCP Server + +URL: `https://mcp.acp.virtuals.io/` + +## Installation (alongside Base MCP) + +```json +{ + "mcpServers": { + "base-account": { "url": "https://mcp.base.org" }, + "virtuals": { "url": "https://mcp.acp.virtuals.io/" } + } +} +``` + +Claude Code: `claude mcp add virtuals --transport http https://mcp.acp.virtuals.io/` + +## Capabilities Overview + +Three main capability groups (consult the MCP tool catalog for the current exact tool names): + +- **Agent management** — create agents, list your agents, prepare and poll the launch flow. +- **Agent cards** — sign agents up for payment cards, issue cards, set spend limits, manage 3DS codes, update card profile, set up payment methods. +- **Agent email** — give agents an email identity, read/search the inbox, fetch threads/attachments, compose and reply to emails, extract OTPs or links from messages. + +## Auth Flow + +Virtuals authentication is stateless from the MCP's perspective — no session is stored server-side. Every authenticated tool requires the JWT `token` from `login_complete` as an explicit parameter. Run this flow **once at the start of the session** and reuse the token until it expires (~1 hour); use `login_refresh` with the refresh token thereafter. + +``` +get_wallets (Base MCP) → baseAccount.address + ↓ +login_start (Virtuals) → SIWE message (with nonce + 30-min expiry) + ↓ +sign type=personal_sign (Base MCP) → approvalUrl + requestId + ↓ user approves at the link +get_request_status (Base MCP) → { signature, status: "signed" } + ↓ +login_complete (Virtuals) message + signature → { token, refreshToken, walletAddress } + ↓ +Reuse `token` as the `token` parameter on every subsequent Virtuals tool call. +``` + +### Step-by-step + +1. **Fetch the wallet address.** Call Base MCP `get_wallets` and use `baseAccount.address`. +2. **Start login.** Call Virtuals `login_start` with that address. Returns the SIWE `message` to sign and a `nonce` valid for 30 minutes. +3. **Sign with Base MCP.** Call `sign` with `type: "personal_sign"` and `data: { message: }`. Returns an `approvalUrl` and `requestId`. +4. **Present the approval link.** Show the user the approval URL as **"Approve Sign-In"** (or similar neutral language — see [../references/approval-mode.md](../references/approval-mode.md)). Wait for them to confirm. +5. **Poll for the signature.** Call Base MCP `get_request_status` once after the user confirms; the result includes the `signature` value. +6. **Complete login.** Call Virtuals `login_complete` with the **exact** `message` from step 2 and the `signature` from step 5. Returns `{ token, refreshToken, walletAddress }`. +7. **Store and reuse the token.** Pass `token` as the `token` parameter on every subsequent Virtuals tool call. Refresh with `login_refresh` once the JWT expires. + +## Troubleshooting + +The Base MCP wallet is a Coinbase Smart Wallet (contract account, not a plain EOA). The SIWE signing path has a few sharp edges — these are the failure modes we've observed. + +### 1. `Invalid SIWE signature` (401) on `login_complete` + +The most common cause: the signature returned by Base MCP `sign` is an **ERC-6492 wrapped** signature (used for counterfactual or contract-deployment-attached signing), and the Virtuals verifier expects a plain **ERC-1271** signature. + +You can recognize ERC-6492 wrapped signatures by: +- Length: thousands of hex characters (the inner ERC-1271 signature is much shorter). +- They end with the magic suffix `6492649264926492649264926492649264926492649264926492649264926492`. + +**Recovery:** restart the auth flow from `login_start`. Subsequent approvals from the same wallet can produce a plain ERC-1271 signature that Virtuals accepts; the wrapping behavior isn't deterministic from one approval to the next. If it keeps returning ERC-6492 wrapped signatures, ask the user to confirm via the same approval URL again — repeated retries typically resolve to a plain ERC-1271 signature within a few attempts. + +Do **not** try to unwrap the ERC-6492 envelope manually and submit just the inner bytes — Virtuals rejects that too, because the inner Coinbase Smart Wallet signature format isn't a vanilla ERC-1271 `(r, s, v)` either. + +### 2. Address case mismatch + +Pass the wallet address to `login_start` exactly as returned by Base MCP `get_wallets`. The returned address is lowercase, which is fine — Virtuals normalises it. But when calling `login_complete`, **the `message` you pass must be the verbatim string returned by `login_start`** (which uses the EIP-55 checksummed address). Do not lowercase or otherwise reformat the message. A single character change in casing inside the message hashes to a different value than what was signed and the server will return `Invalid SIWE signature`. + +Verified working pattern: +- `login_start` input: lowercase address (e.g. `0xca8f1eb...`) → fine +- `login_complete` `message`: verbatim string from the `login_start` response (contains the checksummed `0xCa8F1eB...`) → required + +### 3. Nonce expired + +SIWE nonces expire 30 minutes after `login_start`. If the user took a long time to approve, or you waited and tried again later, `login_complete` will fail. Restart from `login_start` to get a fresh nonce — do not reuse an old one. + +### 4. Message whitespace / newlines + +The SIWE `message` field in `sign` (as `data.message`) and the `message` field in `login_complete` must be byte-identical to what `login_start` returned. JSON-escape `\n` correctly when embedding in the `sign` tool's `data` payload. When passing the message to `login_complete`, use real newlines (the same characters the JSON `\n` escapes decoded to). Any mismatch — extra trailing whitespace, CRLF vs LF, etc. — breaks the hash and yields `Invalid SIWE signature`. + +### 5. Wallet not deployed on Base + +ERC-1271 verification calls the wallet contract on Base. If the Coinbase Smart Wallet has never been activated on Base mainnet, the contract isn't deployed and verification will fail even with a structurally correct signature. Confirm deployment by checking `eth_getCode` for the address on Base mainnet — non-empty bytecode means it's deployed. Most Base MCP users will already have a deployed wallet; if not, ask the user to perform a no-op transaction first (any Base transaction will deploy the wallet). + +### 6. Token expired mid-session + +JWTs returned by `login_complete` expire after about an hour. When a Virtuals tool returns a 401, call `login_refresh` with the stored `refreshToken` to get a new access token; only re-run the full SIWE flow if the refresh token is also rejected. + +## Example Prompts + +``` +Log me into Virtuals +``` +1. `get_wallets` (Base MCP) → `baseAccount.address`. +2. `login_start` (Virtuals) with that address → SIWE message. +3. `sign` (Base MCP) with `type: "personal_sign"`, `data: { message }` → approval URL. +4. Show the user the approval link; wait for confirmation. +5. `get_request_status` (Base MCP) → signature. +6. `login_complete` (Virtuals) with the message + signature → token. +7. Confirm: *"You're signed in. Your Virtuals session is good for about an hour."* + +``` +List all my Virtuals agents +``` +1. Ensure session token is current (run [Auth flow](#auth-flow) if not). +2. Call the Virtuals agent-list tool with `token`. + +``` +Create a Virtuals agent with email and a payment card +``` +1. Ensure session token. +2. Call the Virtuals agent-create tool. +3. For email: call the email-identity creation tool with the new `agentId`. +4. For a card: call the card signup tool, poll until verified, then issue a card and set spend limits. + +## Important Notes + +- **Stateless auth.** The token must be passed to every Virtuals tool — the MCP server doesn't remember it between calls. +- **Session scope.** Only one wallet can be authenticated per token. To switch wallets, run the full SIWE flow again with the new address. +- **No onchain operations through Base MCP after auth.** The Base MCP wallet is only used for the SIWE signature. Virtuals' tools (card issuance, email, agent ops) operate against Virtuals' own backend. +- **Sensitive outputs.** Agent card details and email contents can include private information. Don't echo card numbers, 3DS codes, OTPs, or email bodies to chat unless the user has clearly asked for them. +- **Wallet vs base account.** Use `baseAccount.address` from `get_wallets` for SIWE — not the agent wallets (`agentWallets[]`), which are session-scoped delegations for transactional flows. From bad5c4c79c9f85fe06f65a448732625b20a42e77 Mon Sep 17 00:00:00 2001 From: Youssef Date: Fri, 22 May 2026 15:37:08 +0100 Subject: [PATCH 31/32] add bankr + aero --- skills/base-mcp/SKILL.md | 2 + skills/base-mcp/base-mcp-v0.1.1.zip | Bin 26358 -> 42463 bytes skills/base-mcp/plugins/aerodrome.md | 411 ++++++++++++++++++++ skills/base-mcp/plugins/bankr.md | 189 +++++++++ skills/base-mcp/references/approval-mode.md | 14 +- 5 files changed, 613 insertions(+), 3 deletions(-) create mode 100644 skills/base-mcp/plugins/aerodrome.md create mode 100644 skills/base-mcp/plugins/bankr.md diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md index 60a9dfc..e3efaae 100644 --- a/skills/base-mcp/SKILL.md +++ b/skills/base-mcp/SKILL.md @@ -66,6 +66,8 @@ Plugins currently maintained alongside this skill (the **native plugins**): | Uniswap | [plugins/uniswap.md](plugins/uniswap.md) | | Avantis | [plugins/avantis.md](plugins/avantis.md) | | Virtuals | [plugins/virtuals.md](plugins/virtuals.md) | +| Aerodrome (CLI-only) | [plugins/aerodrome.md](plugins/aerodrome.md) | +| Bankr | [plugins/bankr.md](plugins/bankr.md) | Load a plugin reference only when the user's request matches it, following the same local-first, web-fallback rule as references (see [Loading referenced files](#loading-referenced-files) above). For a plugin's own tools, defer to the descriptions the plugin's MCP exposes — this skill does not duplicate them. diff --git a/skills/base-mcp/base-mcp-v0.1.1.zip b/skills/base-mcp/base-mcp-v0.1.1.zip index a0c139a7f0ab01803c88cb4109db7c9c371aa2b9..480430568ce29ee8e4ea790e19cf31ea0abcc1b3 100644 GIT binary patch delta 20396 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 diff --git a/skills/base-mcp/plugins/aerodrome.md b/skills/base-mcp/plugins/aerodrome.md new file mode 100644 index 0000000..f0fca32 --- /dev/null +++ b/skills/base-mcp/plugins/aerodrome.md @@ -0,0 +1,411 @@ +# Aerodrome Plugin + +> [!IMPORTANT] +> Complete the short Base MCP onboarding flow defined in `SKILL.md` before calling any Aerodrome flow. + +> [!WARNING] +> ## CLI-only plugin +> +> This plugin uses **sugar-sdk** (a Python library) to build Aerodrome calldata locally, then submits it via Base MCP's `send_calls`. It only works in harnesses that have a Bash/CLI tool — **Claude Code, Codex, Cursor's terminal, etc.** It does **not** work on chat-only surfaces (ChatGPT, Claude.ai) because those have no shell to run Python in. If you detect a chat-only environment, tell the user this plugin requires CLI/terminal access and stop. + +Aerodrome is the leading DEX on Base (a Velodrome fork). This plugin uses the [velodrome-finance/sugar-sdk](https://github.com/velodrome-finance/sugar-sdk) Python library to discover pools, build swap routes, and prepare deposit/withdraw/stake/claim calldata. Calldata is then submitted to Base MCP's `send_calls` for user approval. + +No additional MCP server is required. + +**Prerequisite:** Python 3 (3.11+ recommended) available on the user's machine. The harness must be able to run `pip` and execute Python scripts via Bash. + +**Chain:** Base mainnet (chainId `8453` / `0x2105`) + +--- + +## Architecture + +``` +sugar-sdk (Python, queries + calldata builder) + ↓ + monkey-patch sign_and_send_tx to capture {to, data, value} instead of signing + ↓ + pass captured calls to Base MCP send_calls + ↓ + user approves → get_request_status confirms +``` + +Key contracts on Base (from sugar-sdk config): + +| Contract | Address | +|----------|---------| +| Sugar (read-only data) | `0x69dD9db6d8f8E7d83887A704f447b1a584b599A1` | +| Router (V2 LP ops) | `0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43` | +| Universal Router (swaps) | `0x01D40099fCD87C018969B0e8D4aB1633Fb34763C` | +| Slipstream (CL pools) | `0x0AD09A66af0154a84e86F761313d02d0abB6edd5` | +| NFPM (CL positions) | `0x827922686190790b37229fd06084350E74485b72` | +| AERO token | `0x940181a94A35A4569E4529A3CDfB74e38FD98631` | +| WETH | `0x4200000000000000000000000000000000000006` | +| USDC | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | + +--- + +## Setup + +Run these once per project. If a venv already exists at `~/aerodrome-mcp/venv`, skip to step 3. + +```bash +mkdir -p ~/aerodrome-mcp && cd ~/aerodrome-mcp +python3 -m venv venv +./venv/bin/pip install --quiet "setuptools<74" # pkg_resources for sugar-sdk's setup.py +./venv/bin/pip install --quiet --no-build-isolation \ + "git+https://github.com/velodrome-finance/sugar-sdk.git@main" +``` + +**Why these flags:** +- `setuptools<74` — sugar-sdk's `setup.py` imports `pkg_resources`, which was removed from setuptools 74+. +- `--no-build-isolation` — forces pip to use our patched setuptools during the build instead of the latest. + +--- + +## Public RPC limitations + +The public `https://mainnet.base.org` RPC enforces two limits that break sugar-sdk's default batching: + +1. **Max 10 calls per JSON-RPC batch** (returns `{"code": -32014, "message": "maximum 10 calls in 1 batch"}`) +2. **Rate-limits concurrent batch requests** (returns `{"code": -32016, "message": "over rate limit"}` when too many batches fire via `asyncio.gather`) + +Apply these patches at the start of every sugar-sdk script to make it work on the public RPC. For production usage, prefer a paid RPC (Alchemy, QuickNode) and skip the patches. + +```python +# patches.py +import asyncio +from sugar.chains import AsyncChain, CommonChain +from sugar.helpers import chunk +from sugar.price import Price + +async def _safe_apaginate(self, f): + """Sequential single-call paginator — rate-limit-safe for public RPCs.""" + all_results = [] + for offset in range(0, self.settings.pools_count_upper_bound, self.settings.pool_page_size): + try: + async with self.web3.batch_requests() as batcher: + batcher.add(f(self.settings.pool_page_size, offset)) + results = await batcher.async_execute() + for r in results: + if isinstance(r, list): + all_results.extend(r) + except Exception: + pass # skip rate-limited / failed pages + return all_results + +async def _safe_get_prices(self, tokens): + """Fetches native ETH + stable token in a guaranteed first batch, then the rest.""" + tokens = list(tokens) + connectors = self.settings.connector_tokens_addrs + rates = {} + ref_idx = [i for i, t in enumerate(tokens) + if t.symbol == self.settings.native_token_symbol + or t.token_address == self.settings.stable_token_addr] + if ref_idx: + try: + async with self.web3.batch_requests() as batcher: + batcher.add(self.prices.functions.getManyRatesToEthWithCustomConnectors( + [tokens[i].wrapped_token_address or tokens[i].token_address for i in ref_idx], + False, connectors, 10)) + results = await batcher.async_execute() + if results and isinstance(results[0], list): + for pos, i in enumerate(ref_idx): + rates[i] = results[0][pos] + except Exception: + pass + non_ref = [(i, t) for i, t in enumerate(tokens) if i not in rates] + for batch in chunk(non_ref, self.settings.price_batch_size): + idxs, tkns = zip(*batch) + try: + async with self.web3.batch_requests() as batcher: + batcher.add(self.prices.functions.getManyRatesToEthWithCustomConnectors( + [t.wrapped_token_address or t.token_address for t in tkns], + False, connectors, 10)) + results = await batcher.async_execute() + if results and isinstance(results[0], list): + for pos, i in enumerate(idxs): + rates[i] = results[0][pos] + except Exception: + for i in idxs: rates[i] = 0 + return [rates.get(i, 0) for i in range(len(tokens))] + +def _safe_prepare_prices(self, tokens, prices): + """Handles usd_rate=0 fallback gracefully.""" + eth_decimals = self.settings.native_token_decimals + rates_in_eth = {} + for cnt, rate in enumerate(prices): + t = tokens[cnt] + nr = rate if t.decimals == eth_decimals else ( + rate // (10 ** (eth_decimals - t.decimals)) if t.decimals < eth_decimals + else rate * (10 ** (t.decimals - eth_decimals))) + rates_in_eth[t.token_address] = nr + eth_rate = rates_in_eth.get(self.settings.native_token_symbol, 0) + usd_rate = rates_in_eth.get(self.settings.stable_token_addr, 0) + if usd_rate == 0 or eth_rate == 0: + return [Price(token=t, price=0.0) for t in tokens] + eth_usd_price = (eth_rate * 10 ** eth_decimals) // usd_rate + return [Price(token=t, + price=(rates_in_eth.get(t.token_address, 0) * eth_usd_price // 10 ** eth_decimals) / 10 ** eth_decimals) + for t in tokens] + +def _safe_quote_chunked(self): + """Chunk paths into batches of ≤10 to respect public RPC batch limits.""" + async def _impl(_self, from_token, to_token, amount_in, pools, paths): + all_quotes = [] + for path_batch in chunk(paths, 10): + pool_batch = _self.paths_to_pools(pools, path_batch) + try: + async with _self.web3.batch_requests() as batcher: + batcher, inputs = _self.prepare_quote_batch( + from_token, to_token, batcher, pool_batch, amount_in, path_batch) + results = await batcher.async_execute() + all_quotes.extend(_self.prepare_quotes(inputs, results)) + except Exception: + pass + return all_quotes + return _impl + +def apply_patches(): + AsyncChain.apaginate = _safe_apaginate + AsyncChain._get_prices = _safe_get_prices + AsyncChain._get_quotes_for_paths = _safe_quote_chunked() + CommonChain.prepare_prices = _safe_prepare_prices +``` + +--- + +## Calldata bridge: intercepting sign_and_send_tx + +sugar-sdk's write methods (`swap`, `deposit`, `withdraw`, `stake`, `claim_emissions`) all call `self.sign_and_send_tx(contract_fn, value=...)` internally, which signs with `SUGAR_PK` and broadcasts via `eth_sendRawTransaction`. We override it to capture the unsigned `{to, data, value}` instead, then hand the captured calls to `send_calls` for user approval. + +```python +# bridge.py +import os, asyncio +os.environ.setdefault("SUGAR_PK", "0x" + "aa" * 32) # dummy — we never actually sign + +from sugar.chains import AsyncChain + +BASE_WALLET = "" + +class _FakeAccount: + address = BASE_WALLET # sugar-sdk reads .address in several places + +_captured = [] + +async def _capture(self, tx, value: int = 0, wait: bool = True): + tx_dict = await tx.build_transaction({ + "from": BASE_WALLET, "value": value, "nonce": 0, "gas": 800_000, + }) + _captured.append({ + "to": tx_dict["to"], + "data": tx_dict.get("data", "0x"), + "value": hex(value), # use the value param — tx_dict["value"] may be 0 for payable fns + }) + return {"transactionHash": b"\x00" * 32, "status": 1} # fake receipt + +def install_bridge(base_wallet_addr): + global BASE_WALLET + BASE_WALLET = base_wallet_addr + _FakeAccount.address = base_wallet_addr + AsyncChain.sign_and_send_tx = _capture + AsyncChain.account = property(lambda self: _FakeAccount()) + _captured.clear() + +def captured_calls(): + return list(_captured) +``` + +--- + +## Tokens: native ETH vs ERC-20 WETH + +Sugar-SDK exposes both. They behave very differently for swaps: + +- **Native ETH token** (`symbol="ETH"`, `token_address="ETH"`, `wrapped_token_address=WETH`). Pass this when the user wants to spend native ETH (msg.value). `swap_from_quote` sets `value = quote.input.amount_in`. +- **WETH ERC-20** (`symbol="WETH"`, `token_address=WETH`, `wrapped_token_address=None`). Pass this only when the user already holds WETH. The router uses ERC-20 approval; msg.value is 0. + +Pick the right one based on what the user is actually holding. For "swap 0.001 ETH to USDC" → use the native ETH token. + +```python +eth_native = next(t for t in tokens if t.symbol == "ETH" and t.wrapped_token_address) +weth_erc20 = next(t for t in tokens if t.symbol == "WETH") # only if user holds WETH +``` + +--- + +## Orchestration patterns + +### Swap (ETH → USDC, native) + +```python +import asyncio +from sugar.chains import AsyncBaseChain +import patches, bridge + +patches.apply_patches() +bridge.install_bridge("") + +async def build(): + async with AsyncBaseChain(rpc_uri="https://mainnet.base.org") as chain: + tokens = await chain.get_all_tokens() + eth = next(t for t in tokens if t.symbol == "ETH" and t.wrapped_token_address) + usdc = next(t for t in tokens if t.token_address == "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913") + quote = await chain.get_quote(eth, usdc, amount=int(0.001 * 1e18)) + await chain.swap_from_quote(quote, slippage=0.02) + return bridge.captured_calls() + +calls = asyncio.run(build()) +# calls = [{to: WETH, data: approve(...), value: 0x0}, +# {to: UniversalRouter, data: execute(...), value: 0x38d7ea4c68000}] +``` + +The first captured call is a WETH approval to the Universal Router. For a **native ETH** swap, this approval is technically unnecessary (the router accepts msg.value directly). You can drop it to reduce gas — submit only the Universal Router execute call. For a **WETH ERC-20** swap, the approve is required. + +Submit via Base MCP: + +```json +{ "chain": "base", "calls": [] } +``` + +### Swap (USDC → AERO) + +```python +quote = await chain.get_quote(usdc, aero, amount=int(1 * 1e6)) +await chain.swap_from_quote(quote, slippage=0.02) +# captures: USDC approve(Universal Router, ...) + Universal Router execute (value=0x0) +``` + +Batch both calls in one `send_calls` so the approve and swap execute atomically. + +### Basic pool deposit (vAMM/sAMM) + +Sugar's `get_pools()` requires prices for all tokens, which is slow on the public RPC. For a known pool, scan `Sugar.all()` directly for the LP address: + +```python +from sugar.pool import LiquidityPool + +ETH_USDC_VAMM = "0xcDAC0d6c6C59727a65F871236188350531885C43" # known vAMM-WETH/USDC + +tokens = await chain.get_all_tokens() +token_map = {t.token_address: t for t in tokens} +ref_prices = await chain.get_prices([eth_native, usdc_token]) +price_map = {p.token.token_address: p for p in ref_prices} + +full_pool = None +for offset in range(0, 9000, chain.settings.pool_page_size): + batch = await chain.sugar.functions.all(chain.settings.pool_page_size, offset, 0).call() + if not batch: break + for raw in batch: + if raw[0].lower() == ETH_USDC_VAMM.lower(): + full_pool = LiquidityPool.from_tuple(raw, token_map, price_map, + chain_id=chain.chain_id, chain_name=chain.name) + break + if full_pool: break + +# Quote and build deposit calldata +amount_eth = int(0.001 * 1e18) +if full_pool.token0.token_address.lower() == "0x4200000000000000000000000000000000000006": + q = await chain.quote_basic_deposit(full_pool, amount_token0=amount_eth) +else: + q = await chain.quote_basic_deposit(full_pool, amount_token1=amount_eth) +await chain.deposit(q, slippage=0.02) +# captures: USDC approve(Router, ...) + Router.addLiquidityETH(...) with value=msg.value +``` + +The deposit calldata includes a **30-minute deadline** — if the user takes longer to approve, rebuild the calldata. + +### Withdraw / stake / claim + +```python +positions = await chain.get_positions(BASE_WALLET) + +# Withdraw 50% of a basic position +from sugar.withdraw import Withdrawal +w = Withdrawal.from_position(positions[0], fraction=0.5) +await chain.withdraw(w, slippage=0.02) + +# Stake an unstaked basic LP in its gauge +unstaked = [p for p in positions if p.gauge and not p.staked] +if unstaked: await chain.stake(unstaked[0]) + +# Claim emissions from a staked position +staked = [p for p in positions if p.staked] +if staked: await chain.claim_emissions(staked[0]) +``` + +Each captured set of calls maps directly to `send_calls`. + +--- + +## What works vs. what doesn't + +Tested behaviors on Base mainnet (public RPC, 2026-05): + +| Capability | Status | Notes | +|------------|--------|-------| +| `get_all_tokens` | ✅ Works | Returns ~4-7k tokens depending on RPC stability | +| `get_pools_for_swaps` | ✅ Works | Returns ~5k basic pools — no CL pools | +| `get_quote` (basic pools) | ✅ Works | Multi-hop routing | +| `get_prices` (small samples) | ✅ Works | Include native ETH + USDC in input | +| `get_prices` (all tokens) | ⚠️ Flaky | Public RPC rate limits cause USDC fallback to 0 → all-zero prices | +| `get_pools()` (full) | ⚠️ Slow / flaky | Needs full price set; better to scan `Sugar.all()` for specific LPs | +| Basic pool ops (swap/deposit/withdraw) | ✅ Works | Via Universal Router (swap) / Router (LP) | +| `get_positions` | ✅ Works | Returns 0 for fresh wallets | +| CL pool quotes / deposit | ❌ Not accessible | Sugar's `all()` does not enumerate Slipstream CL pools. Build CL calldata directly against NFPM contract instead. | +| Native ETH swap | ✅ Works | Use the native ETH token (`symbol="ETH"`, `wrapped_token_address=WETH`) — WETH approve is captured but unnecessary; drop it from the batch | + +For production usage: install a paid RPC (Alchemy, QuickNode), drop the rate-limit patches, and the full pool/price flows become reliable. + +--- + +## Example prompts + +**Swap 0.001 ETH for USDC** +1. `get_wallets` → address. +2. Run sugar-sdk swap script with the native ETH token; capture calldata. +3. `send_calls` with just the Universal Router execute call (drop the WETH approve for native swaps). +4. Open the approval URL; poll `get_request_status` after the user acts. + +**Swap 1 USDC for AERO** +1. `get_wallets` → address. +2. Run sugar-sdk swap script with USDC → AERO; capture both calls (approve + execute). +3. `send_calls` with both calls batched. +4. Open approval URL; poll. + +**Provide liquidity to vAMM-WETH/USDC with 0.001 ETH** +1. `get_wallets` → address. +2. Scan `Sugar.all()` for `0xcDAC0d6c6C59727a65F871236188350531885C43`. +3. Quote `0.001 ETH` deposit → returns required USDC amount. +4. Capture approve + `addLiquidityETH` calls. +5. `send_calls` immediately (deadline is 30 min from build time). +6. Open approval URL; poll. + +**Withdraw a position** +1. `get_wallets` → address. +2. Run sugar-sdk `get_positions` → pick a position. +3. Build `Withdrawal.from_position(pos, fraction=1.0)` and capture. +4. `send_calls` (approve LP token + `removeLiquidity`). +5. Open approval URL; poll. + +--- + +## Slippage warnings + +Same thresholds as other DEX plugins. Pass `slippage=0.01` (1%) by default to `swap_from_quote`, `deposit`, `withdraw`: + +| Tolerance | Level | Action | +| --- | --- | --- | +| ≤ 1% | Normal | Proceed. | +| > 1% and ≤ 5% | Elevated | Mention the value and ask the user to confirm. | +| > 5% and ≤ 20% | High | Warn that the trade can fill significantly below quote. Require explicit confirmation. | +| > 20% | Very high | Strongly warn; do not submit without the user re-confirming the exact number. | + +--- + +## Notes + +- Sugar-SDK versions: pin to a specific commit/tag for reproducibility. The `@main` branch tested against here has `get_positions`, `withdraw`, `stake`, `unstake`, `claim_emissions`, `claim_fees`. The `@v0.3.1` tag does **not** include these — install from `main` for full functionality. +- The captured `from` address must match the address that ultimately signs the `send_calls` request — fetch it via Base MCP's `get_wallets` and pass it to `install_bridge()`. +- For CL pool operations (Slipstream / V3-style with tick ranges), sugar-sdk's `quote_concentrated_deposit` + `deposit` work if you can construct a full `LiquidityPool` object for the CL pool. Since `Sugar.all()` doesn't enumerate them, you'd need to fetch CL pool state directly from the Slipstream factory or NFPM contract — out of scope for this plugin's current scope. +- Always use `chain: "base"` (string) with `send_calls`, not the numeric chainId. diff --git a/skills/base-mcp/plugins/bankr.md b/skills/base-mcp/plugins/bankr.md new file mode 100644 index 0000000..347a920 --- /dev/null +++ b/skills/base-mcp/plugins/bankr.md @@ -0,0 +1,189 @@ +# Bankr Plugin + +> [!IMPORTANT] +> Complete the short Base MCP onboarding flow defined in `SKILL.md` before calling any Bankr flow. This plugin reads from the Bankr public API and then routes the actual purchase through Base MCP's `swap` tool — there is no separate Bankr MCP server. + +[Bankr](https://bankr.bot) is a launch and discovery surface for tokens on Base. The public API exposes the latest deployed token launches (name, symbol, contract address, deployer, links). This plugin uses that feed to surface fresh launches to the user, then buys the selected token through Base MCP's `swap` tool — Bankr is only the discovery layer; the swap is a regular Base MCP `swap` call paying ETH (or USDC) for the target ERC-20. + +No additional MCP server is required. + +**Prerequisite:** `api.bankr.bot` must be on the Base MCP `web_request` allowlist. If requests are rejected, inform the user and fall back to the harness's HTTP/fetch tool if one is available. + +**Chain:** Base mainnet (chainId `8453` / `0x2105`) + +--- + +## API + +Base URL: `https://api.bankr.bot` + +### `GET /token-launches` + +Returns the most recent token launches on Base, newest first. No auth required, no query parameters. + +```json +{ + "launches": [ + { + "activityId": "6a1067ea1d736e44884096d5", + "status": "deployed", + "launchType": "doppler", + "tokenName": "Whop", + "tokenSymbol": "WHOP", + "chain": "base", + "tokenAddress": "0xe7d8e68525af7e10a16724bbd3001c0828828ba3", + "poolId": "0x2fee469c920ad9cd8d7fed1510c6034531e0f9fb7c94dbeea35623a358b7580f", + "txHash": "0xc989ca12...", + "deployer": { + "walletAddress": "0x67cb...", + "xUsername": "TheLordSherlock", + "xProfileImageUrl": "https://pbs.twimg.com/..." + }, + "feeRecipient": { "walletAddress": "0xccebfd...." }, + "tweetUrl": "https://x.com/i/status/...", + "websiteUrl": "https://whop.com", + "metadataUri": "ipfs://bafkrei...", + "timestamp": 1779460074566 + } + ] +} +``` + +Field notes: + +- `tokenAddress` — the ERC-20 contract on Base. Pass this verbatim to `swap` as `tokenTo`. +- `status` — always `"deployed"` in the current feed; treat anything else as a non-tradable preview and skip. +- `chain` — always `"base"` in the current feed; skip anything else. +- `launchType` — currently `"doppler"` (Doppler v3/v4 pools). Other values may appear later; the swap path is the same as long as the token has a tradeable pool. +- `timestamp` — milliseconds since epoch (note: more than 13 digits in the sample because the API uses a high-precision counter; treat as monotonically decreasing in array order). +- `deployer.xUsername`, `tweetUrl`, `websiteUrl` — optional context to surface to the user before they buy. + +The API returns roughly 50 launches per call. There is no pagination parameter; if you need older launches, you'll see them shift out as new ones land. + +--- + +## Orchestration + +```text +1. web_request GET https://api.bankr.bot/token-launches +2. Filter to status="deployed" and chain="base", take the first N (default 5–10) +3. Show the user a compact list (symbol — name, deployer @handle, age) +4. Wait for the user to pick one and confirm an amount +5. get_wallets → address (only if not already cached) +6. swap (Base MCP) with tokenFrom=ETH (or USDC), tokenTo=, amount= +7. Open the approvalUrl +8. get_request_status only after the user acts +``` + +Do not auto-buy. Always require an explicit "buy X amount of " confirmation from the user before calling `swap` — the launches feed contains low-liquidity and meme tokens, and the swap is irreversible. + +### Discovery call + +```text +web_request: + method: GET + url: https://api.bankr.bot/token-launches +``` + +Filter client-side: + +```js +const fresh = response.launches + .filter((l) => l.status === "deployed" && l.chain === "base") + .slice(0, 10); +``` + +### Presenting launches to the user + +Surface enough context that the user can judge whether to buy — at minimum: symbol, name, deployer handle (if any), website/tweet link, and how recent the launch is. Do **not** echo the full IPFS metadata or all 50 entries; that's noise. + +Example summary line per launch: + +```text +WHOP — Whop · by @TheLordSherlock · launched 2m ago · whop.com + 0xe7d8e68525af7e10a16724bbd3001c0828828ba3 +``` + +### Swap call + +The actual purchase is a regular Base MCP `swap` call. Read the `swap` tool's own parameter descriptions from the MCP — they are the source of truth. Typical shape: + +```json +{ + "chain": "base", + "tokenFrom": "0x0000000000000000000000000000000000000000", + "tokenTo": "", + "amount": "", + "slippage": 0.05 +} +``` + +- `tokenFrom`: native ETH = `0x0000000000000000000000000000000000000000`; USDC on Base = `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. +- `amount`: base units. For 0.001 ETH pass `"1000000000000000"`; for 1 USDC pass `"1000000"`. +- `slippage`: see [Slippage Warnings](#slippage-warnings). New launches frequently need elevated slippage — warn before submitting. + +The `swap` tool returns an `approvalUrl` and `requestId` like any other write call. Surface the URL to the user neutrally ("Approve Swap"), then poll `get_request_status` once they've acted. The full approval/polling pattern is in [`../references/approval-mode.md`](../references/approval-mode.md). + +--- + +## Example Prompts + +**Show me the latest token launches on Base** +1. `web_request` GET `https://api.bankr.bot/token-launches`. +2. Filter to `status="deployed"` and `chain="base"`; take the top 10. +3. Show symbol, name, deployer handle, website/tweet, and contract address. +4. Do **not** auto-buy. Ask the user which one (and how much) they want. + +**Buy 0.001 ETH worth of the newest token on Bankr** +1. `web_request` GET `https://api.bankr.bot/token-launches`. +2. Take `launches[0]` (or the first one matching `status="deployed"`). +3. Show: symbol, name, address, deployer. Ask the user to confirm — "Buy 0.001 ETH of `` (`

`)?". +4. On confirmation: `swap` with `tokenFrom=ETH`, `tokenTo=`, `amount="1000000000000000"`, `slippage=0.05`. +5. Open the approval URL; poll `get_request_status` once the user has approved. + +**Buy 5 USDC of $WHOP** +1. `web_request` GET `https://api.bankr.bot/token-launches`. +2. Find the entry with `tokenSymbol="WHOP"`; if multiple, prefer the most recent and confirm the contract address with the user. +3. `swap` with `tokenFrom=USDC`, `tokenTo=`, `amount="5000000"`, `slippage=0.05`. +4. Open the approval URL; poll. + +**Are there any launches from @0xtinylabs in the last hour?** +1. `web_request` GET `https://api.bankr.bot/token-launches`. +2. Filter by `deployer.xUsername === "0xtinylabs"` and `timestamp` within the last hour (use the array's relative ordering — the feed is newest first). +3. List matches with symbol, name, address, tweet/website. + +--- + +## Slippage Warnings + +New launches commonly have thin liquidity and volatile prices. Use the same thresholds as other DEX plugins, but be more vocal about elevated values for Bankr launches specifically: + +| Tolerance | Level | Action | +| --- | --- | --- | +| ≤ 1% | Normal | Often **not enough** for fresh launches — the swap may revert. | +| > 1% and ≤ 5% | Elevated | Reasonable default for newly launched tokens. Mention the value. | +| > 5% and ≤ 20% | High | Warn that the trade may fill significantly below quote and is a likely sandwich target. Require explicit confirmation. | +| > 20% | Very high | Strongly warn; do not submit without the user re-confirming the exact number. | + +If the user did not specify a slippage, default to `0.05` (5%) for Bankr launches and call that out before submitting. + +--- + +## Safety Notes + +- **Symbol collisions.** Multiple launches can share the same symbol (the sample feed contains three `simstudioai` launches with different symbols and addresses). Always disambiguate by `tokenAddress` and confirm with the user before swapping. +- **No endorsement.** The Bankr feed is unfiltered. The Base MCP and this plugin do not vet, endorse, or audit listed tokens — many are low-liquidity, short-lived, or meme tokens. Mention this once before the first buy of a session. +- **Adversarial metadata.** Token names, symbols, deployer handles, and website URLs are user-supplied and can be misleading or impersonate legitimate projects. Don't follow links from the feed; surface them to the user for context only. +- **Address case.** Pass `tokenAddress` to `swap` verbatim — lowercased addresses from the API work fine; do not re-checksum or modify them. +- **Buy size.** Do not propose a default buy amount. The user must specify the amount. + +--- + +## Notes + +- Native ETH address: `0x0000000000000000000000000000000000000000` +- USDC on Base: `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` +- WETH on Base: `0x4200000000000000000000000000000000000006` +- Token amounts are base units: USDC = 1e6 per token, ETH/WETH = 1e18 per token. Base units for arbitrary launched tokens depend on the token's `decimals()` — most Doppler launches use 18, but if a swap returns a quote that's wildly off, fetch the token's decimals from the Base MCP portfolio or via an `eth_call` before retrying. +- Always use `chain: "base"` (string) with `swap`, not the numeric chainId. +- The feed updates frequently (new launches every few minutes during peak hours). If the user asks "what's brand new", fetch again rather than reusing an earlier response. diff --git a/skills/base-mcp/references/approval-mode.md b/skills/base-mcp/references/approval-mode.md index 923e5a6..a7abe73 100644 --- a/skills/base-mcp/references/approval-mode.md +++ b/skills/base-mcp/references/approval-mode.md @@ -12,9 +12,15 @@ In approval mode, every write call (send, swap, sign, batched calls, and any plu 2. **Show the user the link.** Present it as **"Approve Transaction"** (or similar neutral language). Do not name or describe the wallet provider behind the link, even when the URL hostname suggests one — the underlying wallet UI is an implementation detail and may change. Just give the user the link to click. - Beginner-friendly phrasing: _"Open this to approve the transaction: [Approve Transaction]()"_ - Terse phrasing: _"[Approve Transaction]()"_ -3. **Wait for the user to confirm they approved.** Don't poll in a tight loop while they're still acting. -4. **Call the status-poll tool** (typically `get_request_status`) with the request ID once. -5. **Only report success** when the status tool confirms completion. +3. **In CLI harnesses, also open the link automatically.** When you're running in an environment with a Bash/shell tool (Claude Code, Codex, Cursor terminal, etc.), don't just print the URL — also open it in the user's default browser so they don't have to click. Always print the link too as a fallback, then run the platform-appropriate open command: + - macOS: `open ""` + - Linux: `xdg-open ""` (fall back to `wslview` under WSL) + - Windows: `start "" ""` (or via PowerShell: `Start-Process ""`) + + Skip this step on chat-only surfaces (ChatGPT, Claude.ai) — they don't have a shell, so just show the link. +4. **Wait for the user to confirm they approved.** Don't poll in a tight loop while they're still acting. +5. **Call the status-poll tool** (typically `get_request_status`) with the request ID once. +6. **Only report success** when the status tool confirms completion. ## Common mistakes @@ -22,3 +28,5 @@ In approval mode, every write call (send, swap, sign, batched calls, and any plu - Skipping the approval link — the transaction cannot complete without user action. - Naming the wallet/approval provider, or surfacing the raw hostname as the link text — say "Approve Transaction". - Polling the status tool in a tight loop instead of once after the user confirms. +- Forgetting to also auto-open the link in CLI harnesses where a shell is available — printing alone makes the user copy-paste unnecessarily. +- Trying to auto-open in chat-only harnesses where no shell exists — that just produces an error. From 980f98c283b93abf6ef3e17faa65f5bd04e928be Mon Sep 17 00:00:00 2001 From: Youssef Date: Fri, 22 May 2026 15:47:13 +0100 Subject: [PATCH 32/32] update bankr --- skills/base-mcp/base-mcp-v0.1.1.zip | Bin 42463 -> 43160 bytes skills/base-mcp/plugins/bankr.md | 51 ++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/skills/base-mcp/base-mcp-v0.1.1.zip b/skills/base-mcp/base-mcp-v0.1.1.zip index 480430568ce29ee8e4ea790e19cf31ea0abcc1b3..ed5dc358a56eb217804ce027e8202722b85eff96 100644 GIT binary patch delta 5681 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 (`
`)?". +3. On confirmation: `swap` with `tokenFrom=ETH`, `tokenTo=
`, `amount="1000000000000000"`, `slippage=0.05`. +4. Open the approval URL; poll. + --- ## Slippage Warnings