Appearance
BlazAI — the agent
Type or speak what you want. BlazAI classifies the intent, extracts the parameters, routes to the best provider, and hands back a one-click confirmation card. It executes; it does not merely advise.
Home: ai.blazpay.com · Chat routes: / and /c/:id · Cost: free
What it is
A conversational front door to everything in the platform. 27 intents covering trading, safety, automation, discovery and dApp building. Replies stream token-by-token over SSE, so it reads like a messaging app rather than a form submission.
The interaction contract is deliberately narrow: the agent never silently spends money. Anything with financial consequence resolves to a card the user signs.
The interaction model
Three repos, three responsibilities:
| Repo | Does | Does not |
|---|---|---|
bz-agent | Classify intent, extract parameters | Touch a chain, execute anything |
bz-backend | Persist chat, execute the intent, produce { text, data, action } | Decide what the user meant |
defi-dex | Stream, render the right card, drive the wallet | Interpret free text |
The model stack
The intent engine is Blazpay's own: a LangGraph state machine in bz-agent with one prompt template and one Zod schema per intent, a Redis checkpointer for multi-turn state, and a PGVector knowledge base behind the fallback path. That graph — not any single model — is what turns a sentence into a structured, validated intent.
Underneath it, models are addressed by tier, never by name. A node asks for a tier; which model serves it is an env var.
| Tier | Role | Used for |
|---|---|---|
extraction | Fast, deterministic, cheap. temperature: 0 | Every intent node — parameter extraction |
conversation | Richer prose | The conversation node — DeFi answers |
complex | Most capable | dApp creation, contract audit |
A separate JSON-mode drafting model backs Autopilot policy generation, and a self-hosted Rasa NLU service plus a wit.ai integration back the legacy swap-ai chat path.
Every node's output is validated by a Zod schema, so a malformed model response fails structurally rather than propagating.
Why the tier indirection matters
Swapping a provider is an env change, not a code change — LLM_MODEL_EXTRACTION, LLM_MODEL_CONVERSATION, LLM_MODEL_COMPLEX. Nothing in the graph names a model. See agent graph.
Why temperature: 0 everywhere
Intent classification and parameter extraction are not creative tasks. The same sentence should produce the same intent every time, because users retry and support needs to reproduce. Creativity lives only in the conversation node's prose.
Capability discovery without a nav bar
The AI site has no navigation. Users find capabilities three ways:
- Suggestion chips —
GET /v1/blaz/suggestionsandGET /v1/blaz/home-chips. - The ⌘K command palette —
src/component/ai/CommandPalette.tsxfuzzy-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. Openable anywhere via theblazai:palette-openwindow event. - Just asking. The
CONVERSATIONintent answers "what can you do".
The ?q= effect must key on location.search
The palette navigates while the chat is already mounted, so a mount-only effect never fires — leaving ?q= in the URL with an empty composer. Strip the param with react-router navigate(path, { replace: true }), not history.replaceState: replaceState leaves the router's location stale, so picking the same action twice registers as the same location and silently does nothing.
Voice
Voice input uses the Web Speech API and auto-submits after a pause. A user can speak "swap half my eth to usdc" and get the same card. Voice confirmations and hands-free navigation are roadmap, not shipped.
Session and history
| Concern | Detail |
|---|---|
| Session model | bz-backend/v1/models/blazSession.js — { wallet, name, pending, activeDappProjectId } |
| Message model | v1/models/blazMessage.js — { session, senderType, text, action, data } |
| Identity | Keyed by wallet address. There is no cookie or bearer session |
| Chat list | GET /v1/blaz/sessions |
| One chat | GET /v1/blaz/chat/:chatId |
Because history is keyed by wallet, a returning user with no wallet connected sees an empty sidebar — which is exactly why the AI shell keeps a persistent WalletControl pill despite having no other chrome. See domains.
Disconnect must clear the chat
enabled: !!address only stops the chat-list query; React Query keeps serving the last data, so the previous wallet's history stayed visible after disconnect — someone else's data on a shared machine. The fix clears via queryClient.removeQueries({ queryKey: ['address'] }) and resets local state, gated on a connected→disconnected transition so guests are not reset on mount.
Streaming protocol
POST /v1/blaz/message/stream emits three event types:
| Event | Payload |
|---|---|
meta | The resolved { action, data } — arrives first so the UI can start rendering the card |
token | Successive text chunks |
done | Stream complete |
POST /v1/blaz/message is the non-streaming equivalent, returning the whole { text, data, action } at once.
Client: defi-dex/src/apis/ai.api.ts → sendAIMessageStream.
Accuracy
The measured non-fallback rate is 99.9%+ — roughly one unresolved reply in 18,000. That number is a proxy for accuracy, not a direct measure: it counts messages that reached a real intent rather than the fallback path, so it does not catch a message classified as the wrong intent.
Source: PROOF_POINTS in defi-dex/src/data/chainSeo.mjs, all-time snapshot.
Training-data loop
bz-backend/v1/routes/training.routes.js plus models/trainingDataModel.js provide a review pipeline: conversations get labelled, an operator approves or rejects, and approved examples export for prompt refinement.
| Endpoint | Purpose |
|---|---|
GET /v1/training | List candidates |
GET /v1/training/stats | Counts by status |
GET /v1/training/export | Export approved examples |
PATCH /v1/training/:id | Approve / reject one |
POST /v1/training/bulk | Bulk update |
Under the hood
| Concern | Where |
|---|---|
| Intent list | bz-agent/src/utils/constant.ts → AVAILABLE_INTENTS |
| Entry prompt | bz-agent/src/template/entry.ts → getInitSystemTemplate(intents) |
| Per-intent templates | bz-agent/src/template/*.ts |
| Nodes | bz-agent/src/agent/node.ts (+ bzTokenNode, createDappNode, goOnchainNode, modifyDappNode) |
| Graph | bz-agent/src/agent/graph.ts |
| Compilation + checkpointer | bz-agent/src/agent/index.ts, src/utils/redisCheckPointer.ts |
| Vector store | bz-agent/src/agent/vector.ts — PGVector on PostgreSQL |
| Dispatcher | bz-backend/v1/services/agent.service.js → getAgentResponse |
| Controller | bz-backend/v1/controllers/blaz.controller.js |
| Renderer | defi-dex/src/component/defi/swapAI/BotMessage.tsx |
Deep dives: agent graph, chat request path, action renderer.
Known limits
- No perpetuals intent. The agent cannot open or manage a perp position.
LIMIT_ORDERclassifies but cannot execute — the spot limit product is not shipped. The user gets a text reply, which reads as a failure. See limit orders.- The DCA
amountsemantic mismatch — the agent template says per-cycle, the app deposits it as total. See CSIP. connect_walletis EVM-only and only appears via a suggestion chip or the five-message gate. Solana and TRON connect through the pill.- No message-count quota exists. The only per-wallet limit in the platform is the audit quota (
SystemSetting.audit_free_limit_per_wallet, default 3). If a chat quota is needed, the pattern to copy is that one. - Voice is input-only.