Appearance
Domains & surfaces
Blazpay serves several hostnames from a small number of deployments. Knowing which surface owns which route saves a lot of confusion — particularly since two of them are built from the same frontend repository.
Hostname map
| Host | What lives there | Built from |
|---|---|---|
blazpay.com | Brand hub, entry pass, marketing | Separate marketing site |
defi.blazpay.com | Blazpay DEX — trading only. Swap, bridge, buy/sell, perps, limit, CSIP, Autopilot, portfolio, plus the per-chain SEO corpus | defi-dex, VITE_SITE=defi → dist/ |
ai.blazpay.com | BlazAI — chat only, A-to-Z. All AI tools are chat intents, not routes | defi-dex, VITE_SITE=ai → dist-ai/ |
rewards.blazpay.com | Rewards, staking, GameFi, marketplace, DAO, quests | Separate rewards frontend |
api.blazpay.com | The bz-backend REST + SSE + Socket.io API | bz-backend on the GCP VM, port 5000 |
audit.blazpay.ai | Public share artifacts — audit reports, certificates, token scans, wallet cards | Historically defi-dex; migrating to ai.blazpay.com |
kima.blazpay.com | Kima bridge backend (provider currently disabled) | Server-only |
qube.blazpay.com | Qubetics-specific surface | Server-only |
The AI domain split
A CEO-level decision on 2026-07-26 moved BlazAI entirely to ai.blazpay.com, leaving defi.blazpay.com as a trading-only product. The plan document is defi-set/AI_SPLIT_PLAN.md; the setup and cutover runbook is defi-set/AI_SITE_SETUP.md.
The split is by product, not by interface:
- The AI site owns chat plus every AI-native tool — contract auditor, rug check, wallet reputation, approval revoker, dApp builder — and all public share artifacts (
/report/:id,/certificate/:id,/token/:address,/wallet/:wallet). - The per-chain SEO pages (
/swap/:chain,/bridge/:chain,/chains) stay on defi. They carry trading intent, and fragmenting that corpus across two hosts would cost rankings.
One repo, two app shells
defi-dex builds both sites. The switch is a single env var.
VITE_SITE=defi → src/FullApp.tsx + src/MarketingApp.tsx → dist/
VITE_SITE=ai → src/AiApp.tsx → dist-ai/src/config/site.ts is the only place that reads it, exporting SITE_ID, IS_AI_SITE, AI_ORIGIN, DEFI_ORIGIN and SITE_ORIGIN.
Branch on IS_AI_SITE, never on window.location
The Node prerender script has to emit exactly the links React renders. Sniffing window.location produces server/client divergence, which Google reads as cloaking. VITE_SITE is inlined at build time, so Rollup also tree-shakes the unused shell out of each bundle — FullApp and MarketingApp do not appear in dist-ai at all.
The AI site is chat-only
A follow-up directive on the same day removed all chrome from the AI shell: no nav bar, no footer, no in-app tool pages. AiNav.tsx and AiFooter.tsx were created and then deleted — do not re-add them.
Two layouts exist:
| Layout | Routes | Chrome |
|---|---|---|
AiLayout | / (chat), /c/:id | Only a top-right cluster of two pills |
AiContentLayout | Share artifacts + the two 301-target landing pages | A single "Back to BlazAI" bar |
The two pills are both load-bearing:
WalletControl— chat history is keyed by wallet, so without a persistent connect affordance a returning user sees an empty sidebar and no explanation. The in-chatconnect_walletcard is EVM-only and only appears via a suggestion chip or the five-message gate. A disconnected pill connects straight to EVM/Web3Modal with no family dropdown; the family menu (EVM / Solana / TRON, each disconnecting the others) only exists once connected.PaletteTrigger— opens a ⌘K command palette that fuzzy-searches ten agent actions plus past chats. Every action resolves to a prompt seeded into the composer via?q=— never a route, never auto-sent. That is how capability discovery works without a nav bar.
Both pills share src/component/ai/pill.ts (AI_PILL_BASE pins h-8 and leading-none). They sit side by side, so any padding or text-metric difference reads as a bug.
Chat route constants are not optional
CHAT_ROOT (/), chatPath(id) (/c/:id) and isChatPath(pathname) live in src/config/site.ts. The chat used to hardcode /defi/blazai in handleActiveChat, newChat and SidebarBlazAi's active check — on the AI site those hit the catch-all and bounced the user to defi.blazpay.com. Always use the constants.
Gotchas worth internalising
| Gotcha | Consequence if ignored |
|---|---|
--blaz-chrome-h CSS var sets the chat column's top offset (64/96/112 responsive, forced to 0 by :root[data-site='ai']) | Hardcoding a navbar offset leaves ~96px of dead space on the AI site |
scripts/rewrite-ai-shell.mjs is wired into build:ai and fails the build if any defi.blazpay.com string survives | Without it every AI URL ships canonical=defi.blazpay.com and unfurls as "Blazpay DEX" on social |
npm run check:routes is phase-aware; pass -- --cutover only at runbook step 5 | Share artifacts must keep working on defi until ai.blazpay.com resolves |
The 301 map is staged commented out in netlify.toml | Enabling it early 301s live public share links into a dead domain |
netlify.toml sets X-Frame-Options: SAMEORIGIN for /* with no override | The rug-check embed badge cannot be iframed by third parties from defi. netlify.ai.toml fixes it for /token/*/embed via CSP frame-ancestors * |
The chat shell adds no sidebar — swapAI/SidebarBlazAi already renders one | A shell sidebar collides with it |
Disconnect must clear the chat
enabled: !!address only stops the chat-list query; React Query keeps serving the last data. Without an explicit clear, the previous wallet's history stayed in the sidebar after disconnect — someone else's data on a shared machine. The fix in swapAI/index.tsx calls queryClient.removeQueries({ queryKey: ['address'] }), resets messages/chatId/input/count, and navigates to CHAT_ROOT, gated on a prevAddressRef connected→disconnected transition so guests are not reset on mount or during auto-connect.
Effect ordering
That effect must be declared after const queryClient — the dependency array evaluates at render time, so an earlier declaration throws a temporal dead-zone error.
Backend CORS allowlist
Every origin above must be present in bz-backend/index.js. The current list:
https://rewards.blazpay.com https://blazpay.com
https://uat.blazpay.com https://qube.blazpay.com
https://galxe.com https://defi.blazpay.com
https://admin.blazpay.com https://nfts.qubetics.com
https://blazpay.ai https://www.blazpay.ai
https://audit.blazpay.ai https://ai.blazpay.com
http://localhost:3000 http://localhost:3001
http://localhost:3002 http://localhost:3039
http://localhost:5000 http://localhost:5173
http://localhost:5174http://localhost:5173 (defi dev) and http://localhost:5174 (AI dev) are both allowlisted, so you can run both shells locally against production or staging API without touching CORS.
There is no cookie or bearer session for BlazAI
Every BlazAI endpoint keys off the wallet request parameter. The same wallet on a new origin therefore gets the same chat history with zero auth work — which is exactly why the domain split needed almost no backend change. Socket.io CORS was the one real gap (bz-backend/services/socket.js), and it has been fixed.
Canonical and trailing-slash rule
Netlify serves prerendered directories at {path}/ and 301s the slash-less form. Therefore:
- Prerendered pages (
/swap/hemi/,/bridge/base-to-sei/,/chains/,/about/) — canonicals and sitemap<loc>s use a trailing slash. - SPA-only routes (
/defi/swap,/defi/csip) — stay slash-less.
Do not add force = true to the SPA rewrite
netlify.toml's from = "/*" to = "/index.html" status = 200 rewrite must stay non-forced. Non-forced rewrites let Netlify serve the existing prerendered files first; force = true shadows them and silently breaks all 114 SEO pages.