Doc routing: see docs/INDEX.md.
Flaim is an MCP (Model Context Protocol) service that connects ESPN, Yahoo, and Sleeper fantasy leagues to Flaim Fantasy in ChatGPT and Claude and, as an advanced option, to compatible AI platforms through a manual custom connector. It handles authentication, credential management, and real-time data fetching. The web app also includes a homepage live demo.
git clone https://github.com/jdguggs10/flaim
cd flaim
corepack pnpm install
cp web/.env.example web/.env.local # add keys
corepack pnpm run devPrerequisites: Node 24+ with Corepack. Wrangler is installed through workspace dependencies and can be run with corepack pnpm exec wrangler.
Root, web, and workers use pnpm via Corepack. The Chrome extension is intentionally npm-isolated under extension/ with its own lockfile and Chrome Web Store release flow.
- Chrome Extension (
/extension): Captures ESPN cookies (SWID, espn_s2) and syncs them to Flaim using Clerk Sync Host (no pairing codes). - Next.js web app (
/web): Site pages (discovery-first landing page, setup + management hub at/leagues, privacy policy), OAuth consent screens, and homepage live demo. - Auth worker (
/workers/auth-worker): Supabase credential + league storage, JWT verification, OAuth token management, extension APIs, durable ESPN history workflows, and the Svix-verified Clerkuser.deletedwebhook that drives account deletion. Uses Hono for routing. - Unified Gateway (
/workers/fantasy-mcp): Single MCP endpoint exposing unified tools for all platforms and sports. Routes to platform-specific workers via service bindings. For most tools the gateway is a pure conduit; where a tool's provider envelopes diverge enough to confuse consumers, the gateway may layer a canonical, additive normalization on the routed result (per-tool normalizer modules applied at the route seam —get_free_agentsis the template). Canonical gateway fields use camelCase; the older provider-sideownership_scopefields emitted insideget_playerspredate this pattern and are a legacy variant to converge in a future reviewed version. Legacy provider fields are never removed or renamed by normalization — published clients pin old schemas. - ESPN Client (
/workers/espn-client): Internal worker handling all ESPN API calls for all sports (football, baseball, basketball, hockey). Called by fantasy-mcp gateway. - Yahoo Client (
/workers/yahoo-client): Internal worker handling all Yahoo Fantasy API calls for all sports (football, baseball, basketball, hockey). Called by fantasy-mcp gateway. - Sleeper Client (
/workers/sleeper-client): Internal worker handling all Sleeper API calls for NFL and NBA (public API, no auth required). Called by fantasy-mcp gateway. - Shared package (
/workers/shared): Common utilities (CORS middleware, auth-fetch helper, types) used by all workers. - Supabase Postgres:
espn_credentials,espn_leagues,espn_history_jobs,yahoo_credentials,yahoo_leagues,sleeper_connections,sleeper_leagues,archived_leagues(manual league archive),user_preferences(defaults),oauth_tokens,oauth_codes, andaccount_deletions(permanent deletion tombstones). Seedocs/DATABASE.mdfor the full model.
- API routes run on the Node.js runtime (default). We removed Edge runtime flags because these routes are simple proxies/handlers and don't need Edge-specific features.
- This avoids Edge limitations (no ISR, tighter API compatibility) and keeps behavior predictable for Node APIs like
Buffer.
web/ # Next.js app (see web/README.md)
workers/ # Cloudflare Workers (see workers/README.md)
auth-worker/ # Auth, OAuth, credentials, leagues
fantasy-mcp/ # Unified MCP gateway (routes to platform workers)
espn-client/ # ESPN API client (called by fantasy-mcp)
yahoo-client/ # Yahoo API client (called by fantasy-mcp)
sleeper-client/ # Sleeper API client (called by fantasy-mcp; public API)
shared/ # @flaim/worker-shared package
extension/ # Chrome extension; npm-isolated (see extension/README.md)
docs/ # Documentation
Flaim is an authentication and data service, not a chatbot:
- MCP Server: Exposes fantasy league data to ChatGPT, Claude, and optional manual MCP clients via Model Context Protocol
- OAuth Provider: Handles secure authentication between AI clients and ESPN data
- Credential Manager: Securely stores ESPN session cookies captured by the Chrome extension
The public live showcase lives on the homepage, with /chat retained as a redirect to /#live-demo. The live demo is backed by a dedicated demo account and server-owned auth. The interactive dev console has been extracted to a separate flaim-chat repo (chat.flaim.app).
Extension path (automatic on sync):
- Sign in — Create an account at
flaim.app - Connect ESPN — Install extension → sync credentials
- Auto-discover leagues + past seasons: Current leagues save immediately; past seasons continue in the background
- Set defaults — Manage at
/leagues(extension v1.4.0 no longer handles defaults)
Connect AI:
- Open Flaim Fantasy in ChatGPT or Claude, or copy the MCP URL from
/leaguesand add it as an optional custom connector in a compatible AI platform.
Season year defaults are deterministic and use America/New_York time. The canonical form always stores the start year of the season.
| Sport | Rollover Date | Rationale |
|---|---|---|
| Baseball | Feb 1 | ~10 weeks before Opening Day (late March) |
| Football | Jun 1 | Longer pre-draft window before NFL kickoff (early September) |
| Basketball | Aug 1 | ~10 weeks before NBA opening night (late October) |
| Hockey | Aug 1 | ~10 weeks before NHL opening night (early October) |
ESPN normalization: ESPN uses the END year for NBA/NHL seasons (e.g., 2025 for the 2024-25 season). Flaim normalizes this to the start year internally via the shared toCanonicalYear()/toPlatformYear() helpers in @flaim/worker-shared (workers/shared/src/season.ts).
Defaults are stored centrally in user_preferences:
default_sport- User's preferred sport (football, baseball, etc.)default_football- Default football league:{ platform, leagueId, seasonYear }default_baseball- Default baseball leaguedefault_basketball- Default basketball leaguedefault_hockey- Default hockey league
Each per-sport column is nullable JSONB. Cross-platform exclusivity is automatic (one column per sport = one value).
The extension simplifies ESPN credential capture. See extension/README.md for full documentation.
Extension Popup → Clerk Sync Host → POST /api/extension/sync → Auth Worker → Supabase
↓
ESPN Cookies → POST /api/extension/sync → Auth Worker → Supabase
Sync Host flow:
- User signs in at
flaim.app(Clerk session) - Extension popup detects the session via Sync Host
- Extension reads ESPN cookies and syncs to Flaim with Clerk JWT
Extension APIs (via Next.js proxy → auth-worker):
| Endpoint | Auth | Purpose |
|---|---|---|
POST /extension/sync |
Clerk JWT | Sync ESPN credentials |
POST /extension/discover |
Clerk JWT | Save current leagues and start historical discovery |
GET /extension/history |
Clerk JWT | Read the latest historical discovery status |
GET /extension/status |
Clerk JWT | Check connection status |
GET /extension/connection |
Clerk | Web UI status check |
Security:
- Clerk JWT verification in auth-worker
- Extension never stores long-lived custom tokens
ChatGPT, Claude, and optional manual MCP clients connect to Flaim's MCP servers:
- MCP URL:
https://api.flaim.app/mcp(unified gateway - handles all sports;/fantasy/mcpalso works as legacy alias) - Opt-in authenticated discovery: The exact path
/mcp?auth=requiredrequires OAuth during connector discovery in production and preview. The canonical resource remains/mcp, public widget resources remain available, and the default unauthenticated/mcpdiscovery handshake is unchanged. - OAuth Flow: Full OAuth 2.1 with PKCE, Dynamic Client Registration (RFC 7591), Protected Resource Metadata (RFC 9728)
- Endpoints:
/auth/register(DCR),/auth/authorize,/auth/token,/auth/revoke - Metadata:
/.well-known/oauth-authorization-server,/.well-known/oauth-protected-resource - Token lifetime: MCP access tokens are short-lived (1 hour). Refresh tokens rotate on each successful refresh and use a 1-year inactivity window by default (
OAUTH_REFRESH_TOKEN_TTL_SECONDS, default31536000, clamped to 1 hour minimum and 1 year maximum).
The exact observed Grok OAuth callback, https://grok.com/connectors-oauth-exchange-code/, is accepted for registration and authorization. With the opt-in authenticated-discovery path, Grok completes the Flaim authorization flow and can use the authenticated get_user_session tool.
Gemini Spark registers six callbacks together: /r/ and /a/ user-bound paths on each of its production, test, and sandbox Google redirect hosts. Flaim accepts only those exact hosts and path forms with Gemini's numeric identifier and Flaim production-host suffix; sibling hosts and structural URI variations remain rejected.
Cursor's docs publish two fixed OAuth callbacks: http://localhost:8787/callback for the desktop app, already covered by the generic loopback rule (/callback is an allowed loopback path), and https://www.cursor.com/agents/mcp/oauth/callback for web and Cloud/Background Agents, accepted as an exact match. Separately, Flaim also matches Cursor's older cursor://anysphere.cursor-*/oauth/{id}/callback custom-scheme redirect structurally, for continuity with earlier desktop-IDE versions.
User flow: Open Flaim Fantasy in ChatGPT or Claude, or add the MCP URL as an optional custom connector in a compatible AI platform → 401 triggers OAuth → user consents at flaim.app/oauth/consent → token exchange → tools available.
The unified gateway exposes tools with explicit parameters (platform, sport, league_id, season_year). See the Unified Gateway Architecture section below for the full tool list. Legacy per-sport workers are still functional but deprecated.
The unified gateway (fantasy-mcp) provides a single MCP endpoint for all platforms and sports, replacing the per-sport workers.
ChatGPT / Claude / manual MCP clients → fantasy-mcp (gateway) → espn-client → ESPN API
→ yahoo-client → Yahoo API
→ sleeper-client → Sleeper API (public)
→ auth-worker → Supabase
Key benefits:
- Single MCP URL for all sports:
https://api.flaim.app/mcp - Explicit tool parameters:
platform,sport,league_id,season_year - Easier multi-platform support (ESPN + Yahoo + Sleeper)
- Service bindings for worker-to-worker communication (no 522 timeouts)
Unified tools:
get_user_session— Current-season leagues only withstructuredContentfor ChatGPT widget renderingrefresh_leagues— Re-discover connected leagues and update Flaim's league records (mcp:write; non-destructive)get_ancient_history— Past seasons and historical leagues (everything not in the current season)get_league_info— Baseline league context: settings, roster config, teams/owners (requires platform, sport, league_id, season_year)get_draft: Confirmed draft results and provider-grounded draft-pick ownership (optional round, team_id, and Sleeper draft_id)get_standings— League standingsget_matchups— Current/specified week matchupsget_roster— Team roster with player detailsget_free_agents— Players available in the selected league, with a canonical gateway-normalized envelope (capabilities, ordering, ownership scope) layered over each platform's legacy fieldsget_players— Player lookup across roster statuses with market/global ownership context (market_percent_owned,ownership_scope) and league ownership when availableget_transactions— Recent transactions (adds, drops, waivers, trades)- ESPN treats the public
weekselector as a matchup period for every sport and returns source/window/limitation metadata; the structuredmTransactions2view is the primary source (failed bids, trade lifecycle, FAAB, directional trade sides) with an activity-feed fallback where structured-only filters fail explicitly. - Week semantics are platform-specific: ESPN accepts matchup periods starting at 0 (preseason); Sleeper accepts positive matchup weeks starting at 1; Yahoo uses a recent 14-day timestamp window and ignores explicit week.
- Yahoo
type=waiverandtype=pending_tradereturn pending items for the authenticated user's own team; other supported types use Yahoo's recent league transaction feed.
- ESPN treats the public
Status:
- Unified gateway is the sole MCP endpoint (Jan 2026)
- JWKS-based Clerk JWT verification in auth-worker (5m cache). Prod rejects spoofed headers.
- MCP workers forward
Authorization; auth-worker alone validates tokens. - Per-user isolation via verified
sub; credentials never sent back to client after setup. - Self-service account deletion (FLA-311): Clerk's native delete-account flow fires a dedicated Svix-verified
user.deletedwebhook on auth-worker, which calls an atomic Postgres purge (permanentaccount_deletionstombstone + per-user advisory lock, all connected-platform credentials and league data removed in one transaction). Guard triggers on every user-keyed table reject later writes for a deleted account. Usage telemetry is retained per the privacy policy. - Rate limiting: Cloudflare Workers native
rate_limitsbindings — 10 req/60s per IP on token endpoint, 15 req/60s per user on credentials endpoint, and 60 req/60s per user on OAuth/Clerk-authenticated MCP requests (internal eval and demo API keys are exempt). - Public demo cache: the homepage reads precomputed answers from
demo_answer_cache. The live-turn public-chat path has been removed. - Public demo refresh pipeline: an external private runner uses static MCP bearer auth to populate
demo_answer_cacheout of band on a scheduled cadence. The website only reads cached answers and never triggers refresh runs. The legacy ESPN demo contract ispublic-demo-answer:{presetId}:{sport}:v7:v2. Platform-aware targets usepublic-demo-answer:{presetId}:{platform}:{sport}:v8:v3, with matchingplatform,sport,prompt_version, andcontext_versioncolumns. The platform-aware reader requires the target to be enabled and fully warmed through/api/public-chat/capabilities; during the current transition, only ESPN baseball may fall back from a missing v8/v3 target row to its legacy v7/v2 row. Version tags and homepage preset metadata live inweb/lib/public-chat.ts, while LLM system prompts and per-preset generation instructions remain outside this public repository. - Public demo phone client: the homepage phone reads
/api/public-chat/capabilitieson mount and derives its selectable platforms, sports, and presets from that response. When at least one target is advertised it sendsplatform,sport, andpresetIdon every cache read, shows only the selected target's advertised presets, and switches sport automatically when the chosen platform has no demo for the current one. An empty or failed capabilities response keeps the legacy ESPN baseball behavior and omitsplatform, which is what keeps the one-release fallback lane reachable. Prepared prompts stay inert until that response lands, so manual and deep-link runs both wait and the client never issues a read whose identity it is about to change. Activating a target discards any run that predates the response instead of adopting it. Because that request gates the prompts, it carries an 8s client deadline, and a stalled response falls back to legacy mode rather than leaving the demo unusable. The client state model lives inweb/lib/public-demo-client.ts: capability parsing, target selection, request construction, and the run-token reducer that discards stale responses. - Public demo prompt lifecycle: add or remove homepage demo prompts in both codebases. Update
web/lib/public-chat.tshere for site metadata and keep the external runner aligned on preset IDs, prompt versions, and context versions. If a prompt is removed from the site, remove it from the runner too so the Pi stops refreshing an unreachable cache key. - OAuth tokens stored in Supabase with expiration tracking.
- ESPN credentials: AES-256 encrypted at rest (Supabase default).
See workers/README.md for worker-to-worker communication requirements.
- User syncs ESPN credentials via the Chrome extension → stored in Supabase via auth-worker.
- User confirms and manages discovered leagues at
/leagues(per-season rows) → stored in Supabase. - User connects through ChatGPT, Claude, or an optional manual MCP client → OAuth flow → token stored in Supabase.
- ChatGPT, Claude, or the manual MCP client calls an MCP tool after connection → MCP worker fetches creds from auth-worker → calls ESPN → returns data.
For allowlisted web and extension users, current ESPN leagues are discovered inside the request and historical seasons can be processed by a Cloudflare Workflow. This durable path is rollout-gated and disabled by default in every environment; it requires both ESPN_DURABLE_HISTORY_ENABLED=true and an exact Clerk user ID in ESPN_DURABLE_HISTORY_USERS. When enabled, the request and workflow share an exact ESPN sync lease, and every league write is checked against its current owner. Changing or removing ESPN credentials, or deleting or replacing saved ESPN leagues, takes over that lease before changing rows, so an in-flight refresh cannot restore stale data. The workflow checkpoints each historical league-season through a fenced Supabase RPC and can resume after worker retries. A versioned full-repair marker makes the first scan exhaustive; later scans skip existing historical rows. get_ancient_history remains a fast read-only index over the rows already committed by that workflow. MCP-triggered refresh remains synchronous until the separately gated MCP behavior change ships.
Legacy ESPN repair uses a separate, default-off backend migration lane. Every five minutes, auth-worker may atomically claim one account from a fixed pre-deployment cohort, seed the durable job from the newest saved row for each league root, and transfer that account's exact ESPN lease to the same Workflow. Database constraints allow only one active scheduled job globally. Candidate selection excludes completed full repairs, hidden roots, active leases, recent failed attempts, exhausted credential snapshots, and accounts with no saved league roots. The interactive rollout gate does not enable or select these scheduled jobs.
The gateway emits one best-effort telemetry event per MCP tool call (FLA-156), independent of the tool-call path — it cannot slow or break a tool call.
fantasy-mcp tool call → waitUntil(POST /internal/usage-event) → auth-worker → Supabase mcp_tool_events
↓ nightly pg_cron rollup (05:15 UTC)
mcp_user_daily + mcp_tool_daily (permanent)
- Fire-and-forget: emitted in
ctx.waitUntilwith swallowed errors — never awaited, adds no latency, and a logging failure can never break a tool call. - Tagged for filtering: every event carries
env(prod/preview/dev) andauth_type(oauth/clerk/eval-api-key/demo-api-key). Real-user metrics filterenv='prod' AND auth_type='oauth', which excludes preview traffic, the demo runner (demo-api-key), and eval runs (eval-api-key). - Two tiers: raw
mcp_tool_eventsis pruned after 90 days;pg_cronrolls each UTC day into the permanent, tinymcp_user_daily/mcp_tool_dailyrollups. - ET history is the dashboard source: the database contract contains an owner-only
mcp_user_daily_etaggregate and serialized close/backfill function. The canonical dashboard payload delegates to an owner-only implementation that combines closed ET summaries with raw days after the marker. Migrations create no close cron; local synthetic seed data initializes the marker before its first snapshot refresh, while hosted backfill and scheduling remain explicit operations. - Health stays exact: the history implementation keeps latency and error health on raw events, using a disclosed 30-day window for the historical health keys and the existing seven-day window for recent health. It does not combine stored percentiles.
- Attribution retained: ET summaries also preserve nullable platform and sport from each event. Missing attribution stays unknown; it is not inferred from current connections. Existing dashboard metrics sum across these dimensions.
- Telemetry only: tool, platform, sport, status, latency, and a hashed league id — never rosters, players, or question text.
Schema is summarized in docs/DATABASE.md; the reviewed, secret-free
deployable contract lives in supabase/. Clerk cannot see this usage (it only
observes website-session activity), so these metrics — not Clerk's dashboard —
are the source of truth for DAU/WAU/MAU and retention.
| I want to... | Do this | What happens |
|---|---|---|
| Deploy to prod | Push/merge to main |
Workers + Frontend auto-deploy (~1-2 min) |
| Test in preview | Open a PR | Workers + Frontend deploy to preview URLs |
| Test locally | corepack pnpm run dev |
Nothing deploys, runs on localhost |
| Check deploy status | gh run list --limit 5 |
Shows recent GitHub Actions runs |
| Fix broken prod | Revert commit, push to main |
Auto-redeploys with fix |
| Instant worker rollback | wrangler rollback --env prod in worker dir |
Reverts to previous version without git commit |
| Update extension | Manual CWS upload | See extension/README.md |
Everything deploys automatically on merge to main:
| Component | Platform | Trigger | Environment |
|---|---|---|---|
| Workers (auth-worker, espn-client, yahoo-client, sleeper-client, fantasy-mcp) | Cloudflare | Push to main |
--env prod |
| Workers (auth-worker, espn-client, yahoo-client, sleeper-client, fantasy-mcp) | Cloudflare | PR opened/updated | --env preview |
Frontend (/web) |
Vercel | Push to main |
Production |
Frontend (/web) |
Vercel | PR opened/updated | Preview |
| Extension | Chrome Web Store | Manual | N/A |
GitHub Actions workflows (.github/workflows/):
deploy-workers.yml— Tests + deploys all 5 workers on push/PR (paths-filtered toworkers/**)check-web.yml— Lint + type-check for Next.js app (paths-filtered toweb/**)claude.yml— Claude Code bot responds to@claudementions in issues/PRsclaude-code-review.yml— Auto-reviews PRs with Claude
| Env | ENVIRONMENT | NODE_ENV | Notes |
|---|---|---|---|
| dev | dev | development | Local corepack pnpm run dev |
| preview | preview | production | PR deploys (auto) |
| prod | prod | production | main branch (auto) |
Usually not needed since CI/CD handles it, but available for debugging:
- Workers (manual fallback, per worker):
corepack pnpm --dir workers/auth-worker exec wrangler deploy --env preview(or--env prod)corepack pnpm --dir workers/fantasy-mcp run deploy:preview(ordeploy:prod)corepack pnpm --dir workers/espn-client run deploy:preview(ordeploy:prod)corepack pnpm --dir workers/yahoo-client run deploy:preview(ordeploy:prod)corepack pnpm --dir workers/sleeper-client run deploy:preview(ordeploy:prod)
- Frontend: Push to
mainor PR (Vercel auto-deploys) - Extension: See
extension/README.mdfor Chrome Web Store update process
- Frontend:
web/.env.local(seeweb/README.md) - Workers: Via
wrangler devlocally, Cloudflare Dashboard in prod (seeworkers/README.md) - GitHub Actions:
CLOUDFLARE_ACCOUNT_ID,CLOUDFLARE_API_TOKENin repo secrets
Opening a PR triggers a full preview stack: Vercel preview deploy + all 5 Cloudflare Workers in --env preview. The preview chain is fully isolated from production at the worker level (preview workers bind to each other via service bindings).
Key differences from production:
| Layer | Production | Preview |
|---|---|---|
| Frontend | flaim.app (Vercel) |
flaim-git-{branch}-gerald-guggers-projects.vercel.app |
| Clerk | Production instance (pk_live_) |
Development instance (pk_test_), separate user pool |
| Workers | auth-worker, fantasy-mcp, etc. |
auth-worker-preview, fantasy-mcp-preview, etc. |
| Supabase | Isolated production project | Isolated preview project with synthetic data |
| Worker URLs | Custom domains (api.flaim.app/*) |
.workers.dev URLs |
Vercel env vars are scoped by environment. NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEY, AUTH_WORKER_URL, NEXT_PUBLIC_AUTH_WORKER_URL, and NEXT_PUBLIC_FANTASY_MCP_URL each have separate Production and Preview values. Preview points to dev Clerk and preview worker URLs. Server-only web routes should use AUTH_WORKER_URL with the direct .workers.dev worker URL; browser-visible configuration uses the NEXT_PUBLIC_* names and may point at the public custom gateway.
Auth-worker's MCP OAuth consent redirect (/authorize) uses a static preview FRONTEND_URL (https://preview.flaim.app, the domain pinned to the staging branch below) rather than resolving dynamically per PR branch. This is deliberate: an MCP client (ChatGPT, Claude) opens the user's browser directly at /authorize with no preceding Flaim webpage in that navigation, so there is no flaim-*.vercel.app Origin/Referer header to recover the calling branch from — a dynamic lookup here would have nothing to read and fall through to production. Auth-worker does resolve the frontend dynamically for the Yahoo-connect flow specifically (stored redirect_after, captured from the Origin/X-Forwarded-Origin header on the initiating request), since that flow starts from a user actively browsing a specific PR's Vercel preview URL.
Supabase is isolated. Preview Workers use the dedicated preview database; production Workers use the production database. Preview verification uses synthetic or preview-created rows and must not copy production credentials or production user data.
Triggering preview deploys: Workers only deploy on PRs (not bare branch pushes). A PR must exist for GitHub Actions to run deploy-workers.yml with --env preview. Vercel deploys on any push.
Staging integration preview. staging is a disposable integration-preview
branch: it is rebuilt from main plus selected in-flight branches whenever
composition changes, so one continuously deployed Vercel preview always shows
the combined upcoming site (a team preview domain may be pinned to it). Rules:
- Nothing ever merges from
stagingintomain. Every real change lands through its own PR againstmainwith normal review and CI. - The branch may be force-pushed or rebuilt at any time; it carries no review or deployment authority, and no PR is opened for it.
- Preview deployments read the isolated preview database, so the homepage demo
shows its unavailable state on
stagingby design; demo answers are only proven in production.
See Notion (Platform + Infrastructure) for decision rationale.
Cloudflare DNS: A record, name api, IPv4 192.0.2.1, proxied (orange).
Verify: curl https://api.flaim.app/auth/health
- Local:
curl http://localhost:8786/health(auth-worker),localhost:3000(frontend) - Remote: Deployed worker URL +
/health - Check deploy status:
gh run list --limit 5
| Symptom | Cause | Fix |
|---|---|---|
| Double slashes in URLs | Trailing slash in env vars | Remove trailing slashes |
| Extension "Failed to fetch" | Production build loaded locally | Rebuild from extension/ with npm run build:dev |
| Extension not signed in | Clerk session not syncing | Close/reopen extension popup, confirm flaim.app sign-in |
| MCP error 424 "Failed Dependency" | AI client can't reach localhost MCP URLs | Deploy workers to preview, update .env.local with preview URLs |
| Node.js v25 localStorage warning | Known Node v25 regression | Harmless; suppressed via --no-webstorage in dev script |
See workers/README.md for worker-specific troubleshooting (522s, 404s, 500s).