Skip to main content

Discord Activity Integration Implementation Plan

Status: COMPLETE (as of 2026-04-09)

Original plan covered Tasks 1–9 (Discord SDK + badge). Tasks 10–14 were added during implementation to handle wallet connection, production server, and CSP constraints.


Phase 1 — Discord SDK Integration (✅ Done)

Task 1: Install dependencies ✅

  • @discord/embedded-app-sdk, express, @types/express, tsx, qrcode.react added to package.json

Task 2: Discord environment detection ✅

  • src/utils/discordEnv.tsisDiscordActivity() checks for frame_id + instance_id in URL

Task 3: Discord Provider ✅

  • src/providers/DiscordProvider.tsx — SDK init, OAuth (authorize → token exchange → authenticate), React context
  • Exposes useDiscord() with { sdk, user, channelId, guildId, isReady, error }

Task 4: Express OAuth backend ✅

  • api/server.tsPOST /token exchanges Discord OAuth code for access_token
  • Uses DISCORD_CLIENT_ID + DISCORD_CLIENT_SECRET (server-side only)

Task 5: Vite proxy ✅ (updated)

  • Originally proxied only /.proxy/api/*
  • Updated: now proxies ALL /.proxy/*http://localhost:3001 (required for session polling and external-url endpoint)

Task 6: Conditional DiscordProvider in main.tsx ✅

  • src/main.tsx wraps app with DiscordProvider only when isDiscordActivity() is true
  • Added ErrorBoundary for crash display

Task 7: DiscordBadge component ✅

  • src/components/DiscordBadge.tsx — shows Discord user/channel info, errors, loading state
  • Returns null in standalone mode

Task 8: Environment variables documentation ✅

  • .env.example created with all required vars

Task 9: Final build and regression check ✅


Phase 2 — Wallet Connection in Discord (✅ Done)

Discord's iframe blocks Privy popups. Solution: external wallet login page + session token system.

Task 10: External wallet login page ✅

  • src/components/WalletLoginPage.tsx — Privy login → POST /session { token, address }
  • src/App.tsx — detects ?session=TOKEN and renders WalletLoginPage instead of PlayerMapView

Task 11: Session store + polling ✅

  • api/server.ts + api/unified-server.ts: POST /session, GET /session/:token, GET /external-url
  • src/components/PlayerMapView.tsx: generates UUID token, shows QR code, polls /.proxy/session/:token every 2s
  • Address stored in localStorage for persistence across reloads

Task 12: Production unified server ✅

  • api/unified-server.ts — serves dist/ static files + all API routes on port 5173
  • serve:discord script: tsx api/unified-server.ts
  • EXTERNAL_URL env var passes Cloudflare tunnel URL to the iframe

Phase 3 — Discord CSP & Network Compatibility (✅ Done)

Discord's iframe CSP blocks all external connections except *.discordsays.com.

Task 13: Fix network check (wagmi/publicClient) ✅

  • discordPublicClient — static viem client with getChainId() hardcoded to 1155
  • Transport: /.proxy/rpc (goes through Discord proxy → our server → RPC)
  • discordUseAccount — mock useAccount hook returning chain: intuitionMainnet, isConnected: true
  • discordWalletProxy — minimal wallet client for address display and read operations

Task 14: CSP bypass via fetch monkey-patch ✅

  • src/main.tsx — before React renders, if isDiscordActivity():
    • https://mainnet.intuition.sh/v1/graphql/.proxy/graphql
    • https://rpc.intuition.systems/.proxy/rpc
  • api/unified-server.ts + api/server.ts: /graphql and /rpc proxy routes added
  • This allows player-map's internal API calls (positions, atomDetails) to work inside Discord

Current Architecture Summary

Discord iframe
├─ window.fetch intercepted → external URLs → /.proxy/* → unified-server → real endpoints
├─ DiscordProvider (SDK OAuth)
└─ PlayerMapView
├─ discordAddress (from session polling)
├─ discordPublicClient (viem, transport /.proxy/rpc, getChainId=1155)
└─ discordWalletProxy (fake wallet client, read-only)

External browser (wallet login)
└─ WalletLoginPage
└─ Privy login → POST /session → unified-server stores address

unified-server (port 5173)
├─ Serves dist/ (SPA)
├─ POST /token (Discord OAuth)
├─ POST/GET /session[/:token] (wallet session store)
├─ GET /external-url (Cloudflare URL)
├─ */graphql (proxy → mainnet.intuition.sh)
└─ POST /rpc (proxy → rpc.intuition.systems)

Known Limitations / Future Work

  • In-memory session store → sessions lost on server restart (consider Redis or file-based)
  • Transactions not supported in Discord (wallet proxy throws on writeContract/signTypedData)
  • Cloudflare tunnel URL changes each run → must re-update Discord portal URL mapping
  • WalletConnect blocked by CSP (not needed since Privy handles it on external page)