Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ const config = {
SENTRY_KEY: process.env.SENTRY_KEY,
LINEA_ENS_URL: process.env.LINEA_ENS_URL,
SEGMENT_ANALYTICS_KEY: process.env.SEGMENT_ANALYTICS_KEY,
DISCOURSE_API_KEY: process.env.DISCOURSE_API_KEY,
DISCOURSE_API_USERNAME: process.env.DISCOURSE_API_USERNAME,
DISCOURSE_CATEGORY_ID: process.env.DISCOURSE_CATEGORY_ID,
},

trailingSlash: true,
Expand Down
221 changes: 202 additions & 19 deletions src/components/DiscourseComment/index.jsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,214 @@
import { useEffect } from 'react'
import { useEffect, useState } from 'react'
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'

const DISCOURSE_URL = 'https://builder.metamask.io'

export default function DiscourseComment(props) {
// eslint-disable-next-line react/prop-types
const { postUrl } = props
useEffect(() => {
const url = window.location.href
if (!url.includes('https://metamask.io/')) {
return
} else {
window.DiscourseEmbed = {
discourseUrl: 'https://builder.metamask.io/',
discourseEmbedUrl: postUrl,
const { postUrl, discourseTopicId, metadata = {}, teaserContent } = props
const { siteConfig } = useDocusaurusContext()
const { customFields } = siteConfig

const DISCOURSE_API_KEY = customFields.DISCOURSE_API_KEY
const DISCOURSE_API_USERNAME = customFields.DISCOURSE_API_USERNAME || 'system'
const DISCOURSE_CATEGORY_ID = customFields.DISCOURSE_CATEGORY_ID || '11'

const [topicId, setTopicId] = useState(discourseTopicId)

// Utility function to ensure consistent URL formatting
const normalizeEmbedUrl = url => {
if (url.startsWith('http')) {
return url.replace('https://metamask.io', 'https://docs.metamask.io')
}
return `https://docs.metamask.io${url}`
}

// Search for existing topic by URL using Discourse search API
const searchExistingTopic = async url => {
try {
const embedUrl = normalizeEmbedUrl(url)

// Try direct embed URL lookup first (most reliable)
try {
const response = await fetch(
`${DISCOURSE_URL}/t/by-embed-url.json?embed_url=${encodeURIComponent(embedUrl)}&api_key=${DISCOURSE_API_KEY}&api_username=${DISCOURSE_API_USERNAME}`
)
if (response.ok) {
const topic = await response.json()
if (topic.id) return topic.id
}
} catch (e) {
// Silent failure, try search methods
}

// Fallback search methods
const searchMethods = [
// Method 1: Search by exact URL
() =>
fetch(
`${DISCOURSE_URL}/search.json?q=${encodeURIComponent(embedUrl)}&api_key=${DISCOURSE_API_KEY}&api_username=${DISCOURSE_API_USERNAME}`
),
// Method 2: Search by tutorial title + category
() =>
fetch(
`${DISCOURSE_URL}/search.json?q=${encodeURIComponent(metadata.title || '')}%20category:${DISCOURSE_CATEGORY_ID}&api_key=${DISCOURSE_API_KEY}&api_username=${DISCOURSE_API_USERNAME}`
),
// Method 3: Search by URL path only
() =>
fetch(
`${DISCOURSE_URL}/search.json?q=${encodeURIComponent(url.replace(/^https?:\/\/[^\/]+/, ''))}&api_key=${DISCOURSE_API_KEY}&api_username=${DISCOURSE_API_USERNAME}`
),
]

for (let i = 0; i < searchMethods.length; i++) {
try {
const response = await searchMethods[i]()

if (response.ok) {
const data = await response.json()

// Look for topics that match our criteria
const matchingTopic = data.topics?.find(topic => {
// Normalize titles for comparison (remove special chars, lowercase, trim)
const normalizeTitle = str =>
str?.toLowerCase().replace(/[|:]/g, ' ').replace(/\s+/g, ' ').trim() || ''

const topicTitleNorm = normalizeTitle(topic.title)
const metaTitleNorm = normalizeTitle(metadata.title)

// Check if titles share significant words (at least 50% overlap)
const topicWords = new Set(topicTitleNorm.split(' ').filter(w => w.length > 3))
const metaWords = new Set(metaTitleNorm.split(' ').filter(w => w.length > 3))
const commonWords = [...topicWords].filter(w => metaWords.has(w))
const wordOverlap =
topicWords.size > 0 && metaWords.size > 0
? commonWords.length / Math.min(topicWords.size, metaWords.size)
: 0
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Division by zero in word overlap calculation

The wordOverlap calculation can result in division by zero. If topicWords.size is greater than zero but metaWords.size is zero, Math.min(topicWords.size, metaWords.size) evaluates to zero. This causes commonWords.length / 0, which results in NaN and can affect the topic matching logic.

Fix in Cursor Fix in Web


const titleMatch =
wordOverlap > 0.5 ||
topicTitleNorm.includes(metaTitleNorm) ||
metaTitleNorm.includes(topicTitleNorm)
const excerptMatch = topic.excerpt?.includes(embedUrl) || topic.excerpt?.includes(url)
const categoryMatch = topic.category_id === parseInt(DISCOURSE_CATEGORY_ID)

return categoryMatch && (titleMatch || excerptMatch)
})

if (matchingTopic) {
return matchingTopic.id
}
}
} catch (e) {
// Silent failure
}
}

const d = document.createElement('script')
d.type = 'text/javascript'
d.async = true
d.src = `${window.DiscourseEmbed.discourseUrl}javascripts/embed.js`
;(
document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]
).appendChild(d)
return null
} catch (e) {
return null
}
}, [])
}

// Clean, minimal embed loading
const loadCleanEmbed = topicId => {
if (!topicId) return // Show nothing if no topic

// Clean up any existing embed
const existingScript = document.querySelector('script[src*="embed.js"]')
if (existingScript) {
existingScript.remove()
}

const existingComments = document.getElementById('discourse-comments')
if (existingComments) {
existingComments.innerHTML = ''
}

// Clean, minimal embed setup
window.DiscourseEmbed = {
discourseUrl: `${DISCOURSE_URL}/`,
discourseEmbedUrl: normalizeEmbedUrl(postUrl),
topicId: topicId,
embedContentSelector: '#discourse-embed-content',
}

// Load embed script without monitoring or error handling
const script = document.createElement('script')
script.type = 'text/javascript'
script.async = true
script.src = `${DISCOURSE_URL}/javascripts/embed.js`

const targetElement =
document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]
targetElement.appendChild(script)
}

// Find discussion topic (simplified)
const findDiscussionTopic = async () => {
// 1. Use provided topicId if available
if (discourseTopicId) return discourseTopicId

// 2. Search for existing topic (silent)
if (DISCOURSE_API_KEY) {
try {
return await searchExistingTopic(postUrl)
} catch (e) {
// Silent failure
return null
}
}

return null
}

// Main effect to handle topic loading
useEffect(() => {
const initializeEmbed = async () => {
const foundTopicId = await findDiscussionTopic()
setTopicId(foundTopicId)
loadCleanEmbed(foundTopicId)
}

initializeEmbed()
}, [postUrl, discourseTopicId, DISCOURSE_API_KEY])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Stale Closures from Incomplete Effect Dependencies

The useEffect dependency array is incomplete. The effect uses several variables that are not included in the dependencies: DISCOURSE_API_USERNAME, DISCOURSE_CATEGORY_ID, and metadata. Additionally, the functions findDiscussionTopic, loadCleanEmbed, searchExistingTopic, and normalizeEmbedUrl are defined in the component body and reference these variables, but React's exhaustive-deps rule would flag this. This can cause stale closures where the effect uses old values of these variables when they change, leading to incorrect API calls or search logic.

Fix in Cursor Fix in Web


// Render teaser content and comments div
return (
<>
<meta name="discourse-username" content="shahbaz"></meta>
{teaserContent && teaserContent.length > 0 && (
<div
className="discourse-teaser"
id="discourse-embed-content"
data-discourse-content="true"
style={{
marginBottom: '2rem',
padding: '1.5rem',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
border: '1px solid #e0e6ed',
}}>
{teaserContent.map((paragraph, index) => (
<p key={index} style={{ marginBottom: '1rem', lineHeight: '1.6' }}>
{paragraph}
</p>
))}
<p style={{ marginTop: '1.5rem', marginBottom: '0' }}>
<a
href={normalizeEmbedUrl(postUrl)}
style={{
color: '#1976d2',
textDecoration: 'none',
fontWeight: '500',
display: 'inline-flex',
alignItems: 'center',
gap: '0.5rem',
}}>
Continue reading the complete tutorial →
</a>
</p>
</div>
)}
<div id="discourse-comments" />
</>
)
Expand Down
9 changes: 8 additions & 1 deletion src/components/SEO/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,14 @@ export default function SEO(props) {
)}

{image ? (
<meta property="og:image" content={image} />
<>
<meta property="og:image" content={image} />
<meta property="og:image:secure_url" content={image} />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content={title || 'MetaMask Tutorial'} />
</>
) : (
<meta
property="og:image"
Expand Down
1 change: 1 addition & 0 deletions src/pages/tutorials/android-wallet.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ description: Empower your Android app with an Ethereum web3 wallet using the Web
tags: [embedded wallets, android, evm, kotlin, secp256k1, web3auth]
date: May 27, 2024
author: MetaMask Developer Relations
discourseTopicId: 2603
---

import SEO from '@site/src/components/SEO'
Expand Down
1 change: 1 addition & 0 deletions src/pages/tutorials/create-custom-caveat-enforcer.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ tags: [smart accounts kit, delegation toolkit, caveat enforcer, smart contracts]
keywords: [delegation, smart accounts kit, create, custom, caveat enforcer, smart contracts]
date: Aug 27, 2025
author: MetaMask Developer Relations
discourseTopicId: 2613
---

import Tabs from "@theme/Tabs";
Expand Down
1 change: 1 addition & 0 deletions src/pages/tutorials/create-invite-link.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ keywords: [delegation, smart accounts kit, social, invite, referral, link]
date: Sep 8, 2025
author: MetaMask Developer Relations
toc_max_heading_level: 4
discourseTopicId: 2601
---

This tutorial walks you through creating an invite link so users can refer their friends to your dapp with minimal friction.
Expand Down
1 change: 1 addition & 0 deletions src/pages/tutorials/create-wallet-ai-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ description: Create a wallet AI agent using MetaMask SDK and Vercel's AI SDK.
tags: [metamask sdk, AI agent, Vercel, Wagmi, Next.js, OpenAI]
date: May 2, 2025
author: MetaMask Developer Relations
discourseTopicId: 2609
---

import Tabs from "@theme/Tabs";
Expand Down
5 changes: 3 additions & 2 deletions src/pages/tutorials/design-server-wallets.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ tags: [AI agent, ERC-8004, server wallet, embedded wallets, ethereum, solana, no
date: October 28, 2025
author: MetaMask Developer Relations
toc_max_heading_level: 2
discourseTopicId: 2600
---

MetaMask has specialized in creating fast and secure non-custodial wallets across the web3 ecosystem.
With [ERC‑8004](https://eips.ethereum.org/EIPS/eip-8004) standardizing how AI agents express what they intend to do, there is a clear need for a fast, safe, multichain signing backend, more commonly known as a *server wallet*.
With [ERC‑8004](https://eips.ethereum.org/EIPS/eip-8004) standardizing how AI agents express what they intend to do, there is a clear need for a fast, safe, multichain signing backend, more commonly known as a _server wallet_.
In the near future, even non-web3-native developers will require server wallets, so it's important to outline an ideal architecture.

This page describes a production‑oriented architecture you can adopt today and hints at where MetaMask is going next with a one-click ERC‑8004 server wallet experience.
Expand All @@ -23,7 +24,7 @@ The signing key lives in a secure environment, so it is never exposed directly t

The following is a high-level server wallet architecture:

- The client holds an *agent key* used to authenticate who is asking to sign; this is separate from the onchain *account key* that controls funds.​
- The client holds an _agent key_ used to authenticate who is asking to sign; this is separate from the onchain _account key_ that controls funds.​
- The backend exposes minimal APIs that forward requests to a trusted execution environment (TEE) — for example, [AWS Nitro Enclaves](https://aws.amazon.com/ec2/nitro/nitro-enclaves/).
- The TEE enclave is the only environment allowed to verify agent keys, generate account keys, decrypt keys, apply policy, and produce signatures.
It has no external networking and no persistent storage.
Expand Down
1 change: 1 addition & 0 deletions src/pages/tutorials/erc20-paymaster.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ description: Enable users to pay gas fees with ERC-20 tokens using a paymaster w
tags: [embedded wallets, account abstraction, erc-20 paymaster, erc-4337, web3auth]
date: October 29, 2024
author: MetaMask Developer Relations
discourseTopicId: 2602
---

import SEO from '@site/src/components/SEO'
Expand Down
1 change: 1 addition & 0 deletions src/pages/tutorials/flutter-wallet.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ description: Empower your Flutter app with a chain-agnostic web3 wallet using th
tags: [embedded wallets, flutter, android, ios, evm, solana, web3auth]
date: April 22, 2024
author: MetaMask Developer Relations
discourseTopicId: 2607
---

import SEO from '@site/src/components/SEO'
Expand Down
1 change: 1 addition & 0 deletions src/pages/tutorials/pnp-no-modal-multichain.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ description: Create a chain-agnostic web3 wallet with Web3Auth, connecting embed
tags: [embedded wallets, multichain, polkadot, evm, cosmos, web3auth]
date: February 9, 2024
author: MetaMask Developer Relations
discourseTopicId: 2604
---

import SEO from '@site/src/components/SEO'
Expand Down
1 change: 1 addition & 0 deletions src/pages/tutorials/sending-gasless-transaction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ description: Sponsor user's gas fees using a paymaster with Web3Auth Native Acco
tags: [embedded wallets, account abstraction, gasless paymaster, erc-4337, web3auth]
date: October 22, 2024
author: MetaMask Developer Relations
discourseTopicId: 2608
---

import SEO from '@site/src/components/SEO'
Expand Down
1 change: 1 addition & 0 deletions src/pages/tutorials/upgrade-eoa-to-smart-account.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ image: 'img/tutorials/tutorials-banners/upgrade-eoa-to-smart-account.png'
tags: [metamask sdk, wagmi, EOA, smart account, EIP-7702, EIP-5792]
date: Aug 22, 2025
author: MetaMask Developer Relations
discourseTopicId: 2614
---

This tutorial walks you through upgrading a MetaMask externally owned account (EOA) to a [MetaMask smart account](/delegation-toolkit/concepts/smart-accounts) via [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702), and sending an [atomic batch transaction](/wallet/how-to/send-transactions/send-batch-transactions/#about-atomic-batch-transactions) via [EIP-5792](https://eips.ethereum.org/EIPS/eip-5792).
Expand Down
1 change: 1 addition & 0 deletions src/pages/tutorials/use-erc20-paymaster.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ tags: [delegation toolkit, smart accounts kit, ERC-20 paymaster, smart accounts]
keywords: [delegation, smart accounts kit, ERC-20 paymaster, smart accounts, USDC, ERC-4337]
date: Sep 2, 2025
author: MetaMask Developer Relations
discourseTopicId: 2602
---

This tutorial walks you through using an ERC-20 paymaster with [MetaMask Smart Accounts](/delegation-toolkit/concepts/smart-accounts), enabling users to pay gas fees in USDC.
Expand Down
7 changes: 4 additions & 3 deletions src/pages/tutorials/use-passkey-as-backup-signer.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ tags: [delegation toolkit, smart accounts kit, passkey, backup signer, smart acc
keywords: [delegation, smart accounts kit, passkey, webauthn, P-256, backup signer, smart account]
date: Aug 27, 2025
author: MetaMask Developer Relations
discourseTopicId: 2612
---

This tutorial walks you through using a passkey as a backup signer for your [MetaMask smart account](/delegation-toolkit/concepts/smart-accounts).
Expand All @@ -26,7 +27,7 @@ This tutorial walks you through adding a passkey signer to an already deployed s

- Install [Node.js](https://nodejs.org/en/blog/release/v18.18.0) v18 or later.
- Install [Yarn](https://yarnpkg.com/),
[npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm), or another package manager.
[npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm), or another package manager.

## Steps

Expand Down Expand Up @@ -90,8 +91,8 @@ const smartAccount = await toMetaMaskSmartAccount({

// Deploy the smart account by sending a user operation.
// Appropriate fee per gas must be determined for the bundler being used.
const maxFeePerGas = 1n;
const maxPriorityFeePerGas = 1n;
const maxFeePerGas = 1n
const maxPriorityFeePerGas = 1n

const userOperationHash = await bundlerClient.sendUserOperation({
account: smartAccount,
Expand Down
Loading
Loading