Appearance
Frontend architecture
One repository, three application trees, two deployed sites. The interesting part is the dispatcher that decides which tree loads.
Repo: defi-dex · Stack: React 18, Vite, Tailwind, ethers v5, Redux (legacy), React Query
The entry dispatcher
src/index.tsx is roughly 3 KB and is the only code every visitor downloads.
ts
const MARKETING_ROUTE =
/^\/(about|chains|blazai|rug-check|wallet-checker|buy-crypto-inr|perpetuals|
compare\/[^/]+|swap\/[^/]+|bridge\/[^/]+)\/?$/
document.documentElement.dataset.site = SITE_ID // lets CSS branch without prop drilling
initAnalytics() // no-op without VITE_GA_MEASUREMENT_ID
if (IS_AI_SITE) import('./AiApp').then(m => m.mount())
else if (MARKETING_ROUTE.test(location.pathname)) import('./MarketingApp').then(m => m.mount())
else import('./FullApp').then(m => m.mount())| Tree | When | Contains |
|---|---|---|
AiApp | VITE_SITE=ai | BlazAI chat, agent tools, share artifacts |
MarketingApp | A marketing path on the defi site | Router + Helmet + MarketingLayout. No wallet stack |
FullApp | Everything else on the defi site | The trading terminal |
Adding a marketing route means editing three lists
- The
MARKETING_ROUTEregex insrc/index.tsx - The route table in
src/MarketingApp.tsx src/routes/index.tsx, for in-app navigation
Miss the regex and the page loads the 7 MB FullApp. Miss MarketingApp.tsx and it 404s inside the light tree.
MainNav cannot be used on marketing pages
It calls Web3Modal hooks, which throw without createWeb3Modal() — and MarketingApp deliberately omits the wallet stack. Marketing pages use MarketingLayout's wallet-free nav, and its catch-all HardRedirect does a full page load into the app.
Why the split exists
The full app is roughly 7 MB. Marketing pages load about 234 KB.
The 114 prerendered landing pages exist to rank in search, and Core Web Vitals is a ranking factor. Shipping the wallet stack, TronWeb, Solana web3.js, ethers, ApexCharts and Recharts to a page whose only job is to describe swapping on Berachain would have made the SEO investment self-defeating.
The AI app is also ~7.2 MB, and that is unavoidable
BlazAI genuinely needs the trading core — every chat action that quotes, signs or executes uses the same wallet and aggregator code. A separate repository would not reduce it. FullApp and MarketingApp are tree-shaken out of dist-ai (VITE_SITE is inlined, so Rollup folds the branch), so there is no dead-chunk problem.
Manual chunking
vite.config.ts's manualChunks has two non-obvious pins, both discovered empirically:
preload
ts
if (id.includes('vite/preload-helper') || id.includes('vite/modulepreload-polyfill'))
return 'preload'Vite's dynamic-import helper must live in its own tiny chunk. Left to auto-chunking it gets colocated with a big vendor chunk, which the bootstrap entry then statically imports on every page — defeating the entire dual-entry split. The symptom was every marketing page paying 595 KB.
vendor-react
ts
if (id.includes('node_modules/react-dom/') || .../react/ || .../scheduler/
|| .../react-router || .../@remix-run/ || .../react-helmet-async/)
return 'vendor-react'Without this pin Rollup colocates react-dom inside vendor-charts, dragging roughly 1 MB of chart libraries onto marketing pages.
Safe to split, verified
React has no imports back into the web3/wallet/TRON cluster, so no cycle is cut and there is no temporal-dead-zone risk. It was verified via headless Chrome on /defi/swap and /.
Site configuration
src/config/site.ts is the single place VITE_SITE is read:
| Export | Purpose |
|---|---|
SITE_ID | 'defi' | 'ai' |
IS_AI_SITE | Boolean |
AI_ORIGIN, DEFI_ORIGIN, SITE_ORIGIN | Cross-linking |
CHAT_ROOT | '/' |
chatPath(id) | `/c/${id}` |
isChatPath(pathname) | Predicate |
Branch on IS_AI_SITE, never on window.location
The Node prerender script must emit exactly the links React renders. Sniffing location produces server/client divergence, which Google reads as cloaking.
Layouts
| Layout | Site | Chrome |
|---|---|---|
MainLayout | defi | MainNav + SiteFooter |
MarketingLayout | defi | Wallet-free nav |
AiLayout | ai | Only the two top-right pills |
AiContentLayout | ai | A single "Back to BlazAI" bar |
AiNav and AiFooter were created and then deleted
The AI site is chat-only by directive. Do not re-add them.
Route tables
| File | Covers |
|---|---|
src/routes/index.tsx | The defi app — all /defi/* plus SEO and share routes |
src/routes/aiRoutes.tsx | The AI site |
src/MarketingApp.tsx | Marketing routes in the light tree |
src/routes/path.tsx | DashboardPath — the path constants |
src/routes/RootErrorBoundary.tsx | Top-level error boundary |
Every page is lazy()-imported in routes/index.tsx.
Share artifacts (/report/:id, /certificate/:id, /token/:address, /token/:address/embed, /wallet/:wallet) are routed outside MainLayout so they render unauthenticated on a public host.
Providers
src/providers/AppProviders.tsx holds the provider tree extracted from App.tsx, shared by both shells: React Query, Redux, ChainContext, wallet adapters, Helmet, toasts.
src/hooks/useWalletConnect.ts is the wallet-connection hook used by WalletControl.
Wallet families
ChainContext holds the active family; connecting one disconnects the others.
| Family | Library |
|---|---|
| EVM | @web3modal/ethers5 |
| Solana | @solana/web3.js, @solana/spl-token |
| TRON | tronweb, @tronweb3/tronwallet-adapters, @tronweb3/walletconnect-tron |
Trading wrapper
src/utils/trade.ts is the largest file in the repo. It wraps swap-sdk's TradeManager and adds:
| Method | Purpose |
|---|---|
getSwapQuotesSingleQuery | Non-streaming quote fetch |
getAllowance | Reads relayer fee config, then the token allowance |
approveRelayer | Approves amount + fee to the relayer |
simulateTransaction | Build via /defi/swap/:id, then simulateTx |
sendTransactionRaw | The UUID-first record pattern plus triggerTransaction |
sendTransactionKima | Kima's server-side submission path |
sendTransacionRawTron | TRON path via setAllowanceTron (typo in source) |
setAllowanceTron | TRON approvals with '41' + hex address encoding |
It also holds legacy provider base URLs (mostly unused, kept for parity) and the iZiSwap limit-order imports for the unshipped spot product.
Product model
src/pages/defi/home/products.ts drives the home hero rail and the launcher grid.
ts
type Product = {
slug, title, tab, tagline, blurb, href, icon,
accent: '#FE652B' | '#3588F0', // STRICTLY the brand duo, alternating
bullets: string[], // verified claims only
preview: { k, v }[], // illustrative, labelled "Preview"
badge?: string,
media?: ProductMedia, // optional Higgsfield loop
}Three rules encoded in the file's own comments:
accentis strictly the brand duo, alternating down the rail. Never a third hue — the page's colour system is "blaze orange × signal blue on black".previewrows are illustrative, never presented as real balances. The panel is labelled "Preview".bulletscarry verified claims only, sourced fromPROOF_POINTSinsrc/data/chainSeo.mjs.
media is optional; without it an animated CSS fallback renders. Asset specs and generation prompts are in defi-dex/HERO_ASSETS.md.
A copy bug worth fixing
liquidity carries badge: 'Soon' but limit does not — even though both routes render ComingSoon. See limit orders.
User feedback already applied to this page: no per-page font class (inherit the app's global font — the app does not set Kodchasan globally despite loading it), and BlazAI sits last in both MainNav and Hamburger.
Legacy Redux
src/state/swap/ holds Redux state for the older native on-chain DEX path backed by blazpay-sdk. It predates the aggregator architecture.
Do not extend src/state/swap/
New work goes through swap-sdk and utils/trade.ts. The Redux path exists because removing it has not been worth the risk.
Styling
Tailwind, with tailwind-scrollbar, tailwind-scrollbar-hide and tailwindcss-scrollbar. Some styled-components and rebass remain from an earlier era.
Theme files in src/theme/, including site-ai.css which branches on :root[data-site='ai'].
The chrome-height variable
css
:root { --blaz-chrome-h: 64px } /* responsive 64 / 96 / 112 */
:root[data-site='ai'] { --blaz-chrome-h: 0 } /* specificity beats media queries */The chat column is absolutely positioned. It used to hardcode defi navbar offsets, leaving ~96 px of dead space on the AI site. Never hardcode a navbar offset in the chat again.
Menus and palette panels must be opaque (bg-[#0d0d12], not /95) — the chat headline bled through and muddied labels.
Build scripts
| Script | Runs |
|---|---|
build | vite build → prerender-seo.mjs → generate-og.mjs |
build:app | vite build only |
build:ai | vite build --mode ai --outDir dist-ai → rewrite-ai-shell.mjs |
check:routes | check-routes.mjs; -- --cutover for the AI cutover phase |
prerender | prerender-seo.mjs alone |
indexnow | Manual IndexNow submission |
rewrite-ai-shell.mjs is mandatory
The shared index.html head is hardcoded to defi. Without the rewrite, every AI URL ships canonical=defi.blazpay.com — telling Google the whole domain is a duplicate — and unfurls as "Blazpay DEX" on social. The script fails the build if any defi.blazpay.com string survives.
Patching
patch-package is a dependency, so some node_modules are patched. Check patches/ before assuming a library behaves as published.
Known limits
- ~3,300 pre-existing
tsc --noEmiterrors fromnode_modules.vite buildis the gate. - Three route lists to keep in sync for marketing routes.
- 7 MB app bundle, structurally.
- Legacy Redux swap path still present.
- Headless Chrome on macOS clamps
--window-sizeto ~500 px wide, so mobile screenshots look like overflow bugs when they are not. Verify at 500 px or above. swap-sdkpin can lag the backend's. Both repos pin independently.