From f99730d0bc182f9708176b13187af138706e30d1 Mon Sep 17 00:00:00 2001 From: Youssef Date: Tue, 12 May 2026 23:24:49 +0100 Subject: [PATCH 1/6] add build-on-base and base-mcp skills Co-Authored-By: Claude Sonnet 4.6 (1M context) --- README.md | 9 + skills/base-mcp/SKILL.md | 53 ++ skills/base-mcp/plugins/morpho.md | 89 ++ skills/base-mcp/references/approval-mode.md | 24 + skills/base-mcp/references/batch-calls.md | 23 + skills/base-mcp/references/history.md | 22 + skills/base-mcp/references/portfolio.md | 21 + skills/base-mcp/references/send.md | 27 + skills/base-mcp/references/sign.md | 21 + skills/base-mcp/references/swap.md | 20 + skills/base-mcp/references/tokens.md | 24 + skills/base-mcp/references/wallets.md | 23 + skills/build-on-base/SKILL.md | 79 ++ .../references/agents/register.md | 174 ++++ .../references/base-account/authentication.md | 234 ++++++ .../references/base-account/capabilities.md | 263 ++++++ .../references/base-account/overview.md | 73 ++ .../references/base-account/payments.md | 225 +++++ .../references/base-account/prolinks.md | 192 +++++ .../references/base-account/sub-accounts.md | 250 ++++++ .../references/base-account/subscriptions.md | 238 ++++++ .../base-account/troubleshooting.md | 146 ++++ .../references/builder-codes/overview.md | 159 ++++ .../references/builder-codes/privy.md | 60 ++ .../references/builder-codes/rpc.md | 117 +++ .../references/builder-codes/smart-wallets.md | 65 ++ .../references/builder-codes/viem.md | 75 ++ .../references/builder-codes/wagmi.md | 96 +++ .../references/deploy-contracts.md | 144 ++++ .../migrations/farcaster-miniapp-to-app.md | 790 ++++++++++++++++++ .../migrations/minikit-to-farcaster/auth.md | 48 ++ .../minikit-to-farcaster/dependencies.md | 54 ++ .../minikit-to-farcaster/examples.md | 202 +++++ .../minikit-to-farcaster/manifest.md | 50 ++ .../minikit-to-farcaster/mapping.md | 452 ++++++++++ .../minikit-to-farcaster/overview.md | 82 ++ .../minikit-to-farcaster/pitfalls.md | 225 +++++ .../minikit-to-farcaster/provider.md | 170 ++++ .../migrations/onchainkit/overview.md | 131 +++ .../migrations/onchainkit/provider.md | 193 +++++ .../migrations/onchainkit/transaction.md | 528 ++++++++++++ .../migrations/onchainkit/troubleshooting.md | 79 ++ .../migrations/onchainkit/wallet.md | 346 ++++++++ skills/build-on-base/references/network.md | 40 + skills/build-on-base/references/run-node.md | 48 ++ 45 files changed, 6384 insertions(+) create mode 100644 skills/base-mcp/SKILL.md create mode 100644 skills/base-mcp/plugins/morpho.md create mode 100644 skills/base-mcp/references/approval-mode.md create mode 100644 skills/base-mcp/references/batch-calls.md create mode 100644 skills/base-mcp/references/history.md create mode 100644 skills/base-mcp/references/portfolio.md create mode 100644 skills/base-mcp/references/send.md create mode 100644 skills/base-mcp/references/sign.md create mode 100644 skills/base-mcp/references/swap.md create mode 100644 skills/base-mcp/references/tokens.md create mode 100644 skills/base-mcp/references/wallets.md create mode 100644 skills/build-on-base/SKILL.md create mode 100644 skills/build-on-base/references/agents/register.md create mode 100644 skills/build-on-base/references/base-account/authentication.md create mode 100644 skills/build-on-base/references/base-account/capabilities.md create mode 100644 skills/build-on-base/references/base-account/overview.md create mode 100644 skills/build-on-base/references/base-account/payments.md create mode 100644 skills/build-on-base/references/base-account/prolinks.md create mode 100644 skills/build-on-base/references/base-account/sub-accounts.md create mode 100644 skills/build-on-base/references/base-account/subscriptions.md create mode 100644 skills/build-on-base/references/base-account/troubleshooting.md create mode 100644 skills/build-on-base/references/builder-codes/overview.md create mode 100644 skills/build-on-base/references/builder-codes/privy.md create mode 100644 skills/build-on-base/references/builder-codes/rpc.md create mode 100644 skills/build-on-base/references/builder-codes/smart-wallets.md create mode 100644 skills/build-on-base/references/builder-codes/viem.md create mode 100644 skills/build-on-base/references/builder-codes/wagmi.md create mode 100644 skills/build-on-base/references/deploy-contracts.md create mode 100644 skills/build-on-base/references/migrations/farcaster-miniapp-to-app.md create mode 100644 skills/build-on-base/references/migrations/minikit-to-farcaster/auth.md create mode 100644 skills/build-on-base/references/migrations/minikit-to-farcaster/dependencies.md create mode 100644 skills/build-on-base/references/migrations/minikit-to-farcaster/examples.md create mode 100644 skills/build-on-base/references/migrations/minikit-to-farcaster/manifest.md create mode 100644 skills/build-on-base/references/migrations/minikit-to-farcaster/mapping.md create mode 100644 skills/build-on-base/references/migrations/minikit-to-farcaster/overview.md create mode 100644 skills/build-on-base/references/migrations/minikit-to-farcaster/pitfalls.md create mode 100644 skills/build-on-base/references/migrations/minikit-to-farcaster/provider.md create mode 100644 skills/build-on-base/references/migrations/onchainkit/overview.md create mode 100644 skills/build-on-base/references/migrations/onchainkit/provider.md create mode 100644 skills/build-on-base/references/migrations/onchainkit/transaction.md create mode 100644 skills/build-on-base/references/migrations/onchainkit/troubleshooting.md create mode 100644 skills/build-on-base/references/migrations/onchainkit/wallet.md create mode 100644 skills/build-on-base/references/network.md create mode 100644 skills/build-on-base/references/run-node.md diff --git a/README.md b/README.md index d9dcbde..51e50ad 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,15 @@ [![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 2/6] 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 3/6] update base-skills to just skills --- README.md | 30 ++++++++---------------------- package.json | 2 +- skills/base-mcp/SKILL.md | 2 +- skills/build-on-base/SKILL.md | 2 +- 4 files changed, 11 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 51e50ad..af90147 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ -[![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 4/6] update files --- skills/base-mcp/SKILL.md | 59 ++++++-- skills/base-mcp/plugins/moonwell.md | 155 ++++++++++++++++++++ skills/base-mcp/references/approval-mode.md | 1 + skills/base-mcp/references/send.md | 2 +- skills/base-mcp/references/web-request.md | 45 ++++++ 5 files changed, 252 insertions(+), 10 deletions(-) create mode 100644 skills/base-mcp/plugins/moonwell.md create mode 100644 skills/base-mcp/references/web-request.md diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md index 96df8d1..8886091 100644 --- a/skills/base-mcp/SKILL.md +++ b/skills/base-mcp/SKILL.md @@ -4,19 +4,56 @@ description: > Base Account MCP — gives your AI assistant a wallet via the Base Account MCP server (mcp.base.org). Tools: get_wallets (list wallets), get_portfolio (balances, any address), send (ETH/ERC-20 transfers), swap (token swaps via Coinbase), sign (EIP-712/personal_sign), send_calls (EIP-5792 batch), - get_transaction_history (paginated tx history), get_request_status (poll approval), search_tokens (token lookup). + get_transaction_history (paginated tx history), get_request_status (poll approval), search_tokens (token lookup), + web_request (fetch whitelisted partner APIs to get calldata, then pass to send_calls — hostname must be in server allowlist). Approval mode: send/swap/sign/send_calls require user approval at keys.coinbase.com; response includes approvalUrl + requestId. - Plugins: Morpho lending protocol available via plugins/morpho.md. + Plugins: Morpho lending protocol available via plugins/morpho.md. Moonwell lending on Base/Optimism via plugins/moonwell.md. --- # Base Account MCP -The Base Account MCP server gives your AI assistant direct access to the user's Base Account (smart wallet) on Base. Once connected at mcp.base.org, 9 tools are available with no additional setup. +The Base Account MCP server gives your AI assistant direct access to the user's Base Account (smart wallet) on Base. -## Connection +## Step 1 — Check if the MCP is installed -Server URL: `https://mcp.base.org` -Auth: OAuth via Coinbase Base Account (user must have a Coinbase account) +Before anything else, attempt to call `get_wallets`. If the tool is not available or the call fails with a connection error, the MCP server is not installed. Go to **Step 2**. If it succeeds, skip to **Step 3**. + +## Step 2 — Install the MCP server + +Tell the user the MCP is not connected and offer the right install command for their environment: + +**Claude Code (CLI)** +```bash +claude mcp add base-account --transport http https://mcp.base.org +``` + +**Claude Desktop** — add to `claude_desktop_config.json`: +```json +{ + "mcpServers": { + "base-account": { "url": "https://mcp.base.org" } + } +} +``` + +**Other MCP-compatible clients** — server URL: `https://mcp.base.org` + +After adding the server, the client will open an OAuth flow. The user authorizes via Base Account at mcp.base.org — no Coinbase account required. + +Once installed, re-run `get_wallets` to confirm the connection, then continue to Step 3. + +## Step 3 — Get wallets + +Call `get_wallets` immediately at the start of any session involving transactions. This returns: +- The user's Base Account address +- Any agent wallets and their delegation status +- `inSession: true/false` — determines whether approval mode is required + +**If `inSession: true`** on an agent wallet: transactions can execute without manual approval (M2 mode). Pass `agentWalletId` to send/swap. + +**If no wallet is `inSession: true`**: all write tools use approval mode — every transaction goes to keys.coinbase.com for the user to approve. + +Load [references/wallets.md](references/wallets.md) for full field reference. ## Tool Routing @@ -24,7 +61,7 @@ Read this table first. For the current task, load ONLY the matching reference fi | Task | Tool | Reference | |------|------|-----------| -| List wallets / check delegation | `get_wallets` | [references/wallets.md](references/wallets.md) | +| List wallets / check session status | `get_wallets` | [references/wallets.md](references/wallets.md) | | Check balance / portfolio / token lookup | `get_portfolio`, `search_tokens` | [references/portfolio.md](references/portfolio.md) | | Send ETH or ERC-20 | `send` | [references/send.md](references/send.md) | | Swap tokens | `swap` | [references/swap.md](references/swap.md) | @@ -33,18 +70,22 @@ Read this table first. For the current task, load ONLY the matching reference fi | View transaction history | `get_transaction_history` | [references/history.md](references/history.md) | | Check pending approval status | `get_request_status` | [references/approval-mode.md](references/approval-mode.md) | | Resolve token by symbol | `search_tokens` | [references/tokens.md](references/tokens.md) | +| Fetch protocol API calldata (Moonwell, etc.) | `web_request` | [references/web-request.md](references/web-request.md) | ## Approval Mode -All write tools (send, swap, sign, send_calls) operate in approval mode: the transaction is submitted to keys.coinbase.com and the response includes an `approvalUrl` the user must open and a `requestId` for polling. After the user approves, call `get_request_status` with the `requestId` to confirm completion. Load [references/approval-mode.md](references/approval-mode.md) for full details. +All write tools (send, swap, sign, send_calls) return an `approvalUrl` and `requestId`. Direct the user to open the URL to approve, then call `get_request_status` to confirm completion. Never report success before `get_request_status` returns confirmed. + +Load [references/approval-mode.md](references/approval-mode.md) for full details. ## Plugins -Additional protocol capabilities via plugin MCPs: +Additional protocol capabilities — no extra MCP server needed for Moonwell (uses `web_request`); Morpho requires its own MCP server. | Plugin | Protocol | Reference | |--------|---------|-----------| | Morpho | Lending / vaults on Base | [plugins/morpho.md](plugins/morpho.md) | +| Moonwell | Lending / borrowing on Base and Optimism | [plugins/moonwell.md](plugins/moonwell.md) | ## Installation diff --git a/skills/base-mcp/plugins/moonwell.md b/skills/base-mcp/plugins/moonwell.md new file mode 100644 index 0000000..89b934b --- /dev/null +++ b/skills/base-mcp/plugins/moonwell.md @@ -0,0 +1,155 @@ +# Moonwell Plugin + +Moonwell is a Compound v2 lending protocol on Base and Optimism. Use `web_request` to call the Moonwell HTTP API to read positions/rates and prepare unsigned calldata, then execute via `send_calls`. + +No additional MCP server required — everything goes through `web_request` + `send_calls`. + +**Prerequisite:** `api.moonwell.fi` must be in the MCP server's `web_request` allowlist. If requests to that hostname are rejected, inform the user that the Moonwell API is not yet whitelisted on this MCP instance. + +**Supported chains:** Base (8453), Optimism (10). + +--- + +## Orchestration Pattern + +``` +web_request(https://api.moonwell.fi/v1/prepare/?...) + → { data: { transactions: [ { to, data, value, chainId }, ... ] } } + ↓ +send_calls(chainId, calls mapped from transactions[]) + → approvalUrl + requestId + ↓ +User approves at keys.coinbase.com + ↓ +get_request_status(requestId) → confirmed +``` + +Steps in `transactions[]` are ordered — `approve` and `enter-market` come before the protocol action. Execute them as a single `send_calls` batch. + +--- + +## Read Endpoints (use web_request GET) + +``` +GET https://api.moonwell.fi/v1/markets?chain=base +GET https://api.moonwell.fi/v1/markets/USDC?chain=base +GET https://api.moonwell.fi/v1/rates?chain=base&asset=USDC +GET https://api.moonwell.fi/v1/yield?chain=base&sort=apy&min-tvl=1000000&limit=5 +GET https://api.moonwell.fi/v1/positions/
?chain=base +GET https://api.moonwell.fi/v1/health/
?chain=base +GET https://api.moonwell.fi/v1/rewards/
?chain=base +GET https://api.moonwell.fi/v1/token-balance/
?chain=base&asset=USDC +``` + +Market reads are edge-cached 30 s. User-scoped reads (`positions`, `health`, `rewards`, `token-balance`) are never cached. + +`/positions` returns an array — one entry per market. Use `?active=true` to filter out markets where both `suppliedUsd` and `borrowedUsd` are zero. + +--- + +## Prepare Endpoints (use web_request → send_calls) + +Verbs: `supply`, `withdraw`, `borrow`, `repay`. + +**GET form** (query params): + +``` +GET https://api.moonwell.fi/v1/prepare/supply?chain=base&asset=USDC&amountDecimal=100&from=
+``` + +**POST form** (JSON body — pass as the `body` object parameter to `web_request`): + +```json +{ + "url": "https://api.moonwell.fi/v1/prepare/supply", + "method": "POST", + "headers": { "content-type": "application/json" }, + "body": { "chain": "base", "asset": "USDC", "amountDecimal": "100", "from": "
" } +} +``` + +Both return identical response shapes. Use GET when simpler; use POST when the body is complex. + +### Key parameters + +| Field | Notes | +|-------|-------| +| `chain` | `base` (default), `optimism`, or chain ID | +| `asset` | Symbol: `USDC`, `WETH`, `ETH` (alias for WETH) | +| `amountDecimal` | Human-readable string, e.g. `"100"`. Use this **or** `amount` (base units), never both. | +| `from` | User's wallet address (from `get_wallets`) | + +### Response → send_calls mapping + +```json +{ + "data": { + "transactions": [ + { "step": "approve", "to": "0x...", "data": "0x...", "value": "0x0", "chainId": 8453 }, + { "step": "enter-market", "to": "0x...", "data": "0x...", "value": "0x0", "chainId": 8453 }, + { "step": "moonwell-supply", "to": "0x...", "data": "0x...", "value": "0x0", "chainId": 8453 } + ] + } +} +``` + +Pass all items as the `calls` array to `send_calls`, using `chainId` from any transaction item (`0x2105` for Base mainnet). + +--- + +## Example Flows + +### Supply 100 USDC on Base + +``` +1. get_wallets → address +2. web_request GET /token-balance/
?chain=base&asset=USDC → confirm balance ≥ 100 +3. web_request GET /prepare/supply?chain=base&asset=USDC&amountDecimal=100&from=
+4. send_calls(chainId=0x2105, calls from transactions[]) +5. User approves → get_request_status(requestId) +``` + +### Borrow USDC against collateral + +``` +1. get_wallets → address +2. web_request GET /health/
?chain=base → verify health > 1.5 +3. web_request GET /prepare/borrow?chain=base&asset=USDC&amountDecimal=50&from=
+4. send_calls(chainId=0x2105, calls from transactions[]) +5. User approves → get_request_status(requestId) +``` + +### Check positions and health + +``` +1. get_wallets → address +2. web_request GET /positions/
?chain=base&active=true → show per-market balances +3. web_request GET /health/
?chain=base → show health factor +``` + +--- + +## Protocol Notes + +- **mTokens** — ERC-20 receipt tokens (mUSDC, mWETH…); exchange rate accrues over time +- **WETH special-case** — borrow/withdraw deliver native ETH; supply/repay require ERC-20 WETH. Both `asset=ETH` and `asset=WETH` resolve to the same mWETH market +- **Compound v2 error codes** — `mint`, `borrow`, `repay` return non-zero codes for business-logic failures without reverting. Check the on-chain receipt after broadcast +- **Base has two mUSDC entries** — the current market and a deprecated bridged-USDC market. Disambiguate by `marketAddress` or `deprecated: true` + +### Health factor guide + +| Value | Status | +|-------|--------| +| `> 1.5` | Healthy | +| `1.1 – 1.5` | Caution | +| `< 1.1` | Liquidation risk | +| `null` | No borrows | + +--- + +## Chain IDs for send_calls + +| Chain | chainId param | +|-------|--------------| +| Base mainnet | `0x2105` | +| Optimism | `0xa` | diff --git a/skills/base-mcp/references/approval-mode.md b/skills/base-mcp/references/approval-mode.md index 7bcfcba..8f2be36 100644 --- a/skills/base-mcp/references/approval-mode.md +++ b/skills/base-mcp/references/approval-mode.md @@ -22,3 +22,4 @@ All write tools (send, swap, sign, send_calls) operate in approval mode. The use ## When approval is NOT needed Agent wallets marked `inSession: true` (from `get_wallets`) can transact without approval in M2 mode. The `agentWalletId` parameter on send/swap enables this. + diff --git a/skills/base-mcp/references/send.md b/skills/base-mcp/references/send.md index 8c2548f..a7724bf 100644 --- a/skills/base-mcp/references/send.md +++ b/skills/base-mcp/references/send.md @@ -12,7 +12,7 @@ Send native ETH or any ERC-20 token to an address. Operates in approval mode: th - `chain` — `base` or `base-sepolia` ## Optional parameters -- `decimals` — required when `asset` is a contract address (not a symbol) +- `decimals` — required when `asset` is a contract address (not a symbol); must be 0–18 - `agentWalletId` — scope to a specific agent wallet (M2 mode only) ## Approval mode flow diff --git a/skills/base-mcp/references/web-request.md b/skills/base-mcp/references/web-request.md new file mode 100644 index 0000000..221c6ef --- /dev/null +++ b/skills/base-mcp/references/web-request.md @@ -0,0 +1,45 @@ +# web_request + +Make an HTTP request to a whitelisted partner API. The hostname must be in the MCP server's configured allowlist — requests to unlisted domains are rejected outright. This is why the tool exists: AI assistants on Claude Desktop, ChatGPT, and similar environments can't autonomously fetch arbitrary URLs, but `web_request` gives controlled access to trusted protocol APIs so the agent can retrieve calldata and pass it to `send_calls`. + +## When to use + +- Fetching unsigned transaction calldata from a partner protocol API (e.g. Moonwell `/prepare/supply`) before passing it to `send_calls` +- Reading on-chain data from a whitelisted protocol HTTP API (positions, balances, rates, health factor) + +## Parameters + +- `url` — full HTTPS URL; hostname must be in the allowlist (required) +- `method` — `GET` or `POST` (required) +- `headers` — optional key/value map of custom headers. **Prohibited:** `Authorization`, `Cookie`, `Host`, `X-Forwarded-*` +- `body` — JSON object for POST requests; ignored for GET + +## Calldata pattern + +``` +web_request(GET or POST to whitelisted /prepare/* endpoint) + → { data: { transactions: [ { to, data, value, chainId }, ... ] } } + ↓ +send_calls(chainId, calls mapped from transactions[]) + → approvalUrl + requestId + ↓ +User approves at keys.coinbase.com + ↓ +get_request_status(requestId) → confirmed +``` + +## Mapping response transactions to send_calls + +Protocol `/prepare/*` responses return an ordered `transactions[]` array. Map each item directly: + +``` +transactions[i].to → calls[i].to +transactions[i].data → calls[i].data +transactions[i].value → calls[i].value (0x-prefixed hex) +``` + +Pass the `chainId` from any `transactions[i].chainId` to `send_calls`. Execute all calls in order — steps like `approve` and `enter-market` must confirm before later steps succeed. + +## Allowlist + +The allowlist is configured server-side on the MCP. If a request fails with a domain rejection error, the hostname is not whitelisted — inform the user rather than retrying. Currently whitelisted partner protocols are documented in the plugin references (e.g. `plugins/moonwell.md`). From f26e66a5b65b9257a08e4d761fe1e9558ae182e0 Mon Sep 17 00:00:00 2001 From: Youssef Date: Thu, 14 May 2026 23:15:19 +0100 Subject: [PATCH 5/6] update skill following tool routing --- skills/base-mcp/SKILL.md | 13 +-- skills/base-mcp/references/install.md | 112 ++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 skills/base-mcp/references/install.md diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md index 8886091..f7ab014 100644 --- a/skills/base-mcp/SKILL.md +++ b/skills/base-mcp/SKILL.md @@ -20,14 +20,14 @@ Before anything else, attempt to call `get_wallets`. If the tool is not availabl ## Step 2 — Install the MCP server -Tell the user the MCP is not connected and offer the right install command for their environment: +Tell the user the MCP is not connected and provide the right install method for their platform. See [references/install.md](references/install.md) for full platform-specific instructions including Cursor, troubleshooting, and OAuth details. -**Claude Code (CLI)** +**Claude Code (CLI · VS Code · JetBrains)** ```bash claude mcp add base-account --transport http https://mcp.base.org ``` -**Claude Desktop** — add to `claude_desktop_config.json`: +**Claude Desktop** (macOS / Windows) — add to `claude_desktop_config.json`: ```json { "mcpServers": { @@ -36,11 +36,11 @@ claude mcp add base-account --transport http https://mcp.base.org } ``` -**Other MCP-compatible clients** — server URL: `https://mcp.base.org` +**Claude.ai (web)** — Settings → Integrations → Add MCP server → enter `https://mcp.base.org` -After adding the server, the client will open an OAuth flow. The user authorizes via Base Account at mcp.base.org — no Coinbase account required. +**Any other MCP client** — HTTP server URL: `https://mcp.base.org` -Once installed, re-run `get_wallets` to confirm the connection, then continue to Step 3. +After adding the server, the client opens an OAuth flow at mcp.base.org — no Coinbase account required. Once installed, re-run `get_wallets` to confirm the connection, then continue to Step 3. ## Step 3 — Get wallets @@ -61,6 +61,7 @@ Read this table first. For the current task, load ONLY the matching reference fi | Task | Tool | Reference | |------|------|-----------| +| Install the MCP / platform-specific setup | — | [references/install.md](references/install.md) | | List wallets / check session status | `get_wallets` | [references/wallets.md](references/wallets.md) | | Check balance / portfolio / token lookup | `get_portfolio`, `search_tokens` | [references/portfolio.md](references/portfolio.md) | | Send ETH or ERC-20 | `send` | [references/send.md](references/send.md) | diff --git a/skills/base-mcp/references/install.md b/skills/base-mcp/references/install.md new file mode 100644 index 0000000..293864e --- /dev/null +++ b/skills/base-mcp/references/install.md @@ -0,0 +1,112 @@ +# Installing Base MCP + +Server URL: `https://mcp.base.org` + +--- + +## Claude Code (CLI · VS Code extension · JetBrains extension) + +All three use the same Claude Code MCP configuration. Run in any terminal (including the integrated terminal inside VS Code or JetBrains): + +```bash +claude mcp add base-account --transport http https://mcp.base.org +``` + +Or add manually to `~/.claude/settings.json`: + +```json +{ + "mcpServers": { + "base-account": { + "type": "http", + "url": "https://mcp.base.org" + } + } +} +``` + +No restart needed — the server is available in the next Claude Code session. + +--- + +## Claude Desktop + +**macOS** config file: `~/Library/Application Support/Claude/claude_desktop_config.json` +**Windows** config file: `%APPDATA%\Claude\claude_desktop_config.json` + +Add or merge the `mcpServers` key: + +```json +{ + "mcpServers": { + "base-account": { "url": "https://mcp.base.org" } + } +} +``` + +Restart Claude Desktop after saving. The server appears in the tool menu on next launch. + +--- + +## Claude.ai (web) + +1. Open **Settings** (top-right avatar → Settings) +2. Go to **Integrations** +3. Click **Add MCP server** +4. Enter the server URL: `https://mcp.base.org` +5. Click **Connect** + +The OAuth flow opens automatically in a new tab. + +--- + +## Cursor + +Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project-scoped): + +```json +{ + "mcpServers": { + "base-account": { + "type": "http", + "url": "https://mcp.base.org" + } + } +} +``` + +Restart Cursor after saving. + +--- + +## Any other MCP-compatible client + +Use the HTTP transport with server URL `https://mcp.base.org`. Consult your client's MCP documentation for the exact config format — the server URL is the only required field. + +--- + +## OAuth Authorization + +After adding the server, your client opens an OAuth flow: + +1. A browser tab opens to `mcp.base.org` +2. Sign in with your Base Account — no Coinbase account required +3. Authorize the requested permissions +4. Return to your AI client — the MCP is now connected + +--- + +## Verifying the connection + +Call `get_wallets`. A successful response lists your Base Account address and any agent wallets. An error or "tool not found" means the MCP is not connected — retry the install steps above. + +--- + +## Troubleshooting + +| Symptom | Fix | +|---------|-----| +| Tool not found / MCP not connected | Check config file syntax (valid JSON), ensure URL is `https://mcp.base.org`, restart client | +| OAuth window doesn't open | Open `https://mcp.base.org` manually in a browser and complete sign-in | +| `web_request` rejects a domain | The hostname is not in the MCP's allowlist — see plugin references for supported partner APIs | +| `get_wallets` returns no wallets | OAuth wasn't completed — re-run the auth flow | From a9ef2228527009f20cd84a65ebe98b021f37d89f Mon Sep 17 00:00:00 2001 From: Youssef Date: Fri, 15 May 2026 00:24:22 +0100 Subject: [PATCH 6/6] update skill instructions --- skills/base-mcp/SKILL.md | 34 +++----- skills/base-mcp/plugins/morpho.md | 6 +- skills/base-mcp/references/install.md | 115 +++++++++++++------------- skills/base-mcp/references/sign.md | 2 +- skills/base-mcp/references/wallets.md | 4 +- 5 files changed, 73 insertions(+), 88 deletions(-) diff --git a/skills/base-mcp/SKILL.md b/skills/base-mcp/SKILL.md index f7ab014..cbded52 100644 --- a/skills/base-mcp/SKILL.md +++ b/skills/base-mcp/SKILL.md @@ -1,7 +1,7 @@ --- name: base-mcp description: > - Base Account MCP — gives your AI assistant a wallet via the Base Account MCP server (mcp.base.org). + Base MCP — gives your AI assistant access to your Base account via the Base MCP server (mcp.base.org). Tools: get_wallets (list wallets), get_portfolio (balances, any address), send (ETH/ERC-20 transfers), swap (token swaps via Coinbase), sign (EIP-712/personal_sign), send_calls (EIP-5792 batch), get_transaction_history (paginated tx history), get_request_status (poll approval), search_tokens (token lookup), @@ -10,9 +10,9 @@ description: > Plugins: Morpho lending protocol available via plugins/morpho.md. Moonwell lending on Base/Optimism via plugins/moonwell.md. --- -# Base Account MCP +# Base MCP -The Base Account MCP server gives your AI assistant direct access to the user's Base Account (smart wallet) on Base. +The Base MCP server gives your AI assistant access to your Base account on Base. ## Step 1 — Check if the MCP is installed @@ -20,32 +20,20 @@ Before anything else, attempt to call `get_wallets`. If the tool is not availabl ## Step 2 — Install the MCP server -Tell the user the MCP is not connected and provide the right install method for their platform. See [references/install.md](references/install.md) for full platform-specific instructions including Cursor, troubleshooting, and OAuth details. +Tell the user the MCP is not connected and point them to [references/install.md](references/install.md) for step-by-step UI instructions. That file covers Claude Desktop, ChatGPT app, Claude.ai web, Claude Code CLI, and Cursor — with beginner-friendly walkthroughs for each. -**Claude Code (CLI · VS Code · JetBrains)** -```bash -claude mcp add base-account --transport http https://mcp.base.org -``` - -**Claude Desktop** (macOS / Windows) — add to `claude_desktop_config.json`: -```json -{ - "mcpServers": { - "base-account": { "url": "https://mcp.base.org" } - } -} -``` - -**Claude.ai (web)** — Settings → Integrations → Add MCP server → enter `https://mcp.base.org` - -**Any other MCP client** — HTTP server URL: `https://mcp.base.org` +Quick reference: +- **Claude Desktop** — Claude menu → Settings → Integrations → Add integration → `https://mcp.base.org` +- **ChatGPT app** — Settings → Connectors → Add connector → MCP server → `https://mcp.base.org` +- **Claude.ai web** — Settings → Integrations → Add integration → `https://mcp.base.org` +- **Claude Code CLI** — `claude mcp add base-account --transport http https://mcp.base.org` -After adding the server, the client opens an OAuth flow at mcp.base.org — no Coinbase account required. Once installed, re-run `get_wallets` to confirm the connection, then continue to Step 3. +After connecting, the user signs in to authorize via Base account — no Coinbase account required. Once installed, re-run `get_wallets` to confirm the connection, then continue to Step 3. ## Step 3 — Get wallets Call `get_wallets` immediately at the start of any session involving transactions. This returns: -- The user's Base Account address +- The user's Base account address - Any agent wallets and their delegation status - `inSession: true/false` — determines whether approval mode is required diff --git a/skills/base-mcp/plugins/morpho.md b/skills/base-mcp/plugins/morpho.md index 8e18314..f902f8e 100644 --- a/skills/base-mcp/plugins/morpho.md +++ b/skills/base-mcp/plugins/morpho.md @@ -1,12 +1,12 @@ # Morpho Plugin -Morpho is a lending protocol on Base. The Morpho MCP server prepares lending operations (deposit, borrow, withdraw, repay, supply collateral) which are then executed via Base Account MCP's `send_calls`. +Morpho is a lending protocol on Base. The Morpho MCP server prepares lending operations (deposit, borrow, withdraw, repay, supply collateral) which are then executed via Base MCP's `send_calls`. ## MCP Server URL: `https://mcp.morpho.org/` -## Installation (alongside Base Account MCP) +## Installation (alongside Base MCP) Add both servers to your MCP config: @@ -47,7 +47,7 @@ Claude Code: `claude mcp add morpho --transport http https://mcp.morpho.org/` ## Orchestration Pattern -Morpho `prepare_*` tools return unsigned call data. Pass the result to Base Account MCP's `send_calls` to execute. +Morpho `prepare_*` tools return unsigned call data. Pass the result to Base MCP's `send_calls` to execute. ``` morpho_prepare_deposit(vaultAddress, amount) → { calls: [...], chainId } diff --git a/skills/base-mcp/references/install.md b/skills/base-mcp/references/install.md index 293864e..40438f7 100644 --- a/skills/base-mcp/references/install.md +++ b/skills/base-mcp/references/install.md @@ -1,68 +1,62 @@ # Installing Base MCP -Server URL: `https://mcp.base.org` +Choose your app below. The whole process takes under two minutes. --- -## Claude Code (CLI · VS Code extension · JetBrains extension) - -All three use the same Claude Code MCP configuration. Run in any terminal (including the integrated terminal inside VS Code or JetBrains): - -```bash -claude mcp add base-account --transport http https://mcp.base.org -``` - -Or add manually to `~/.claude/settings.json`: +## Claude Desktop -```json -{ - "mcpServers": { - "base-account": { - "type": "http", - "url": "https://mcp.base.org" - } - } -} -``` +1. Open Claude Desktop +2. Click the **Claude** menu in the top menu bar → **Settings…** +3. Go to the **Integrations** tab +4. Click **Add integration** +5. Enter a name (e.g. `Base`) and the server URL: `https://mcp.base.org` +6. Click **Add** +7. A browser window opens — sign in to authorize (see [Authorization](#authorization) below) -No restart needed — the server is available in the next Claude Code session. +> If you don't see an Integrations tab, your Claude Desktop version may be older. Update to the latest version from [claude.ai/download](https://claude.ai/download). --- -## Claude Desktop +## ChatGPT (desktop app) -**macOS** config file: `~/Library/Application Support/Claude/claude_desktop_config.json` -**Windows** config file: `%APPDATA%\Claude\claude_desktop_config.json` +1. Open the ChatGPT app +2. Click your **profile picture** (top-right) → **Settings** +3. Go to the **Connectors** tab +4. Click **Add connector** → **MCP server** +5. Paste the server URL: `https://mcp.base.org` +6. Click **Connect** +7. A browser window opens — sign in to authorize (see [Authorization](#authorization) below) -Add or merge the `mcpServers` key: +--- -```json -{ - "mcpServers": { - "base-account": { "url": "https://mcp.base.org" } - } -} -``` +## Claude.ai (web) -Restart Claude Desktop after saving. The server appears in the tool menu on next launch. +1. Go to [claude.ai](https://claude.ai) and sign in +2. Click your **profile picture** (bottom-left) → **Settings** +3. Go to the **Integrations** tab +4. Click **Add integration** +5. Paste the server URL: `https://mcp.base.org` +6. Click **Connect** +7. A browser window opens — sign in to authorize (see [Authorization](#authorization) below) --- -## Claude.ai (web) +## Claude Code (CLI · VS Code · JetBrains) -1. Open **Settings** (top-right avatar → Settings) -2. Go to **Integrations** -3. Click **Add MCP server** -4. Enter the server URL: `https://mcp.base.org` -5. Click **Connect** +Run this in your terminal: -The OAuth flow opens automatically in a new tab. +```bash +claude mcp add base-account --transport http https://mcp.base.org +``` + +Then restart Claude Code and sign in when prompted. --- ## Cursor -Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project-scoped): +Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project): ```json { @@ -79,34 +73,37 @@ Restart Cursor after saving. --- -## Any other MCP-compatible client +## Authorization + +After connecting the server, a browser tab opens to mcp.base.org. Here's what to do: -Use the HTTP transport with server URL `https://mcp.base.org`. Consult your client's MCP documentation for the exact config format — the server URL is the only required field. +1. Click **Sign in with Base** +2. If you don't have a Base account yet, you can create one for free — no Coinbase account required +3. Review the permissions the app is requesting and click **Authorize** +4. The browser tab will close and you'll be taken back to your app + +That's it — the MCP is now connected. --- -## OAuth Authorization +## Did it work? -After adding the server, your client opens an OAuth flow: +Ask your AI assistant: **"Show me my wallets."** -1. A browser tab opens to `mcp.base.org` -2. Sign in with your Base Account — no Coinbase account required -3. Authorize the requested permissions -4. Return to your AI client — the MCP is now connected +If it replies with a wallet address, you're all set. If it says it doesn't have access to a wallet tool, the MCP isn't connected — try the install steps again or check the troubleshooting section below. --- -## Verifying the connection +## Troubleshooting -Call `get_wallets`. A successful response lists your Base Account address and any agent wallets. An error or "tool not found" means the MCP is not connected — retry the install steps above. +**The browser tab for sign-in never opened** +→ Try opening `https://mcp.base.org` in your browser directly and signing in there, then re-add the server in your app. ---- +**I see "Integration not found" or "Tool not available"** +→ The server may not have loaded yet. Restart your app and try again. -## Troubleshooting +**The Integrations / Connectors tab doesn't exist** +→ Your app version may be outdated. Update to the latest version and try again. -| Symptom | Fix | -|---------|-----| -| Tool not found / MCP not connected | Check config file syntax (valid JSON), ensure URL is `https://mcp.base.org`, restart client | -| OAuth window doesn't open | Open `https://mcp.base.org` manually in a browser and complete sign-in | -| `web_request` rejects a domain | The hostname is not in the MCP's allowlist — see plugin references for supported partner APIs | -| `get_wallets` returns no wallets | OAuth wasn't completed — re-run the auth flow | +**web_request fails with a domain error** +→ The website you're trying to reach isn't in the approved list. This is a security feature — see plugin references for supported partner APIs. diff --git a/skills/base-mcp/references/sign.md b/skills/base-mcp/references/sign.md index 76ce6d1..8aa82b7 100644 --- a/skills/base-mcp/references/sign.md +++ b/skills/base-mcp/references/sign.md @@ -1,6 +1,6 @@ # sign -Request a user-approved signature from the Base Account. Supports EIP-712 typed data and personal_sign. Operates in approval mode. +Request a user-approved signature from the Base account. Supports EIP-712 typed data and personal_sign. Operates in approval mode. ## When to use - "Sign this message", "Sign this typed data", agent needs a signature for authentication diff --git a/skills/base-mcp/references/wallets.md b/skills/base-mcp/references/wallets.md index 5cdad29..60c450d 100644 --- a/skills/base-mcp/references/wallets.md +++ b/skills/base-mcp/references/wallets.md @@ -1,6 +1,6 @@ # get_wallets -Returns all wallets in the user's wallet group: the Base Account (primary) plus any agent wallets. +Returns all wallets in the user's wallet group: the Base account (primary) plus any agent wallets. ## When to use - User asks "show me my wallets", "what wallets do I have", "which wallet is active" @@ -14,7 +14,7 @@ None. - `type` — `base-account` or `agent-wallet` - `address` — 0x address - `inSession` — boolean; only `true` wallets can be used with transactional tools -- `delegationStatus` — whether the agent wallet has delegated authority from the Base Account +- `delegationStatus` — whether the agent wallet has delegated authority from the Base account - `spendPolicy` — summary of spend limits (agent wallets only) ## Key patterns