Skip to content

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())
TreeWhenContains
AiAppVITE_SITE=aiBlazAI chat, agent tools, share artifacts
MarketingAppA marketing path on the defi siteRouter + Helmet + MarketingLayout. No wallet stack
FullAppEverything else on the defi siteThe trading terminal

Adding a marketing route means editing three lists

  1. The MARKETING_ROUTE regex in src/index.tsx
  2. The route table in src/MarketingApp.tsx
  3. 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:

ExportPurpose
SITE_ID'defi' | 'ai'
IS_AI_SITEBoolean
AI_ORIGIN, DEFI_ORIGIN, SITE_ORIGINCross-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

LayoutSiteChrome
MainLayoutdefiMainNav + SiteFooter
MarketingLayoutdefiWallet-free nav
AiLayoutaiOnly the two top-right pills
AiContentLayoutaiA 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

FileCovers
src/routes/index.tsxThe defi app — all /defi/* plus SEO and share routes
src/routes/aiRoutes.tsxThe AI site
src/MarketingApp.tsxMarketing routes in the light tree
src/routes/path.tsxDashboardPath — the path constants
src/routes/RootErrorBoundary.tsxTop-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.

FamilyLibrary
EVM@web3modal/ethers5
Solana@solana/web3.js, @solana/spl-token
TRONtronweb, @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:

MethodPurpose
getSwapQuotesSingleQueryNon-streaming quote fetch
getAllowanceReads relayer fee config, then the token allowance
approveRelayerApproves amount + fee to the relayer
simulateTransactionBuild via /defi/swap/:id, then simulateTx
sendTransactionRawThe UUID-first record pattern plus triggerTransaction
sendTransactionKimaKima's server-side submission path
sendTransacionRawTronTRON path via setAllowanceTron (typo in source)
setAllowanceTronTRON 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:

  1. accent is strictly the brand duo, alternating down the rail. Never a third hue — the page's colour system is "blaze orange × signal blue on black".
  2. preview rows are illustrative, never presented as real balances. The panel is labelled "Preview".
  3. bullets carry verified claims only, sourced from PROOF_POINTS in src/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

ScriptRuns
buildvite buildprerender-seo.mjsgenerate-og.mjs
build:appvite build only
build:aivite build --mode ai --outDir dist-airewrite-ai-shell.mjs
check:routescheck-routes.mjs; -- --cutover for the AI cutover phase
prerenderprerender-seo.mjs alone
indexnowManual 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 --noEmit errors from node_modules. vite build is 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-size to ~500 px wide, so mobile screenshots look like overflow bugs when they are not. Verify at 500 px or above.
  • swap-sdk pin can lag the backend's. Both repos pin independently.

Internal + developer documentation. Usage figures are point-in-time snapshots — re-verify before publishing them.