Appearance
Errors & failure modes
Symptom → cause → fix, ordered by how often each comes up. Use this as the first stop when something is broken.
Trading
"Fewer routes than expected"
Cause. One or more provider APIs failing. AggregatorFactory.getQuotes wraps each aggregator in a try/catch, so a failure removes that route silently.
Diagnosis. Check the backend logs during a quote request — each failure is logged. Compare against the 21 registered providers.
This is the platform's biggest observability gap
A permanently-broken provider disappears with no alert. Nothing tracks per-provider success rate. See monitoring.
Chain not found
404 from GET /defi/quotes. No Network record with that id, or the record has show: false.
Fix. Seed the network, or flip show.
From token not found / To token not found
404. No Token record for that (address, chainId).
Fix. Seed the token. If it exists but is invisible in the picker, that is a different problem — see below.
"The token exists but is not in the picker"
The picker slices the top 200 by Token.order. A token ranked below 200 appears absent.
Fix. scripts/curate-token-order.js. See token curation.
"Quote not found or expired" on confirm
The Redis key under meta.id has a 300-second TTL. A user who leaves the confirm screen open longer sees this.
Fix. Re-quote. Re-quoting on window focus is the correct product fix and is not implemented.
BlazpayRelayer: Transaction expired
deadline = now + 100 seconds, set in swap-sdk/src/relayer.ts. A slow wallet prompt or a congested chain exceeds it.
Fix. Retry. Widening the window trades safety for convenience.
BlazpayRelayer: Invalid nonce
nonces[msg.sender] moved between signing and sending — usually a concurrent transaction from the same wallet.
Fix. Retry; the nonce is re-read.
BlazpayRelayer: Invalid signature
Three possible causes:
| Cause | Check |
|---|---|
| Wrong signer key for the chain | Chain 137 uses POL_TX_SIGNER, everything else TX_SIGNER |
The relayer's stored signer was rotated | Read signer on-chain and compare |
Malformed signature recovering to address(0) | v2 rejects this explicitly |
BlazpayRelayer: Provide correct value
Native swap where msg.value < nativeValue + fee.
Fix. Include the fee. txValue = enableFees ? value + fee : value.
Insufficient allowance
The approval must cover amount + fee, because an ERC-20 swap pulls the fee as a second transferFrom.
Fix. TradeManager.getAllowance reads inPercentFee and enableFees first for exactly this reason — use it rather than approving amount.
relayerAddresses throws / assertRelayerDeployed throws
No relayer on that chain. Ethereum (1) and Etherlink (42793) are deliberate.
This is a safety feature
An earlier version fell back to a default address, which on an undeployed chain has no code — so the transaction would succeed at the EVM level while doing nothing, after the user's tokens were already approved.
Transaction succeeds but no output token arrives
The route's calldata was an approve() step, not a swap. Some providers return one.
Diagnosis. tx.to === fromToken, or calldata ≤ 68 bytes.
Fix. The automation path guards against this (calldata guard). The interactive path relies on sortQuotes gas simulation, which does not catch it — the approval succeeds.
Platform fee displays as $0
usdAmount is 0 from OKX and Nordstern, and the price enrichment in swap.service.js also failed.
Fix. Check getTokenPrice and the Binance price cache. Enrichment is best-effort by design.
Automation
OutputBelowMinimum(actual, minimum)
Both vaults revert with this. Two causes:
| Cause | Fix |
|---|---|
| The route was an intent/solver provider (Relay, Nordstern) delivering nothing in-transaction | The route probe should have filtered it — check that the caller passes probe |
| Genuine slippage past the floor | Micro orders (<$5) use a 1.5% tolerance; larger use 0.5% |
Nordstern's signature
Calldata selector 0x57f96440… and an asynchronous fill. Seeing this revert with a Nordstern route means the guard is working, not failing.
RouterNotAllowed(router)
Autopilot only. The route's targetContract or spender is not in the owner-curated allow-list.
Until setRouters is called, no trade executes at all
The vault is fail-safe. This is the current state on Robinhood Chain (4663). Use contract-deployment/scripts/probe-autopilot-routers.cjs to find routers that deliver in-transaction output on that chain, then allow-list them.
Autopilot agent silently stops trading
The Mongo policy's hash differs from AgentAccount.policyHash on-chain, so autopilotCron skips the agent.
Cause. An update that never landed on-chain, or a policy edited directly in the database.
Fix. Re-sign and re-submit updatePolicy, or POST /defi/autopilot/sync/:chainId/:agentId to resync.
ExceedsMaxPerTrade / ExceedsMaxPerDay / ExceedsMaxTradesPerDay
On-chain limits. maxPerDay uses a rolling 24 hours from the first trade, not a calendar day.
Only budgetToken outflows are metered
A token-to-token trade not involving budgetToken is not counted against maxPerTrade/maxPerDay — audit finding H-1, accepted so the contract stays oracle-free.
PolicyExpired / AgentInactive / TokenNotAllowed
Self-explanatory. expiry is capped at 180 days from creation.
CSIP position created but never executes
Checklist, in order:
CRON=falsein the environment?- Executor wallet out of native gas? (~0.00002 ETH per Arbitrum batch)
- Per-order value below the minimum? Creation should have rejected it
lastErroron theDcaInvestmentdocument — the UI shows an amber "retrying" state- No route available for that pair
CSIP buys running too fast
interval was sent as the per-order gap instead of the total window. The contract computes spacing as interval / frequency.
Fix. duration = perOrderGap × orders. See semantics.
CSIP claim is less than the displayed balance
V1-era phantom credits mean accounting can exceed actual holdings. The frontend clamps the claim to the contract's payable balance.
This is working as intended. A partial claim keeps the remainder credited; the user claims twice rather than losing anything.
PositionNotFound(positionId)
positionId is unique per wallet (userPosition[user][positionId]). A global lookup is wrong.
BlazAI
A card was expected, plain text arrived
| Cause | Detail |
|---|---|
| No handler for the intent | LIMIT_ORDER classifies but has no backend handler |
Extraction returned follow_up | A required field was missing; the reply is a clarifying question |
| The action has no renderer | An unknown action falls through to ResponseCard |
A card renders empty
The data field is missing from the Mongoose schema in v1/models/blazMessage.js, so Mongoose silently drops it on write.
Fix. Add the field. Also add it to IChatMessage['data'] in defi-dex/src/types/ai-chat.type.ts.
Empty chat sidebar for a returning user
History is keyed by wallet. No wallet connected means no history — which is why the AI shell keeps a persistent WalletControl pill despite having no other chrome.
Previous wallet's history visible after disconnect
enabled: !!address only stops the query; React Query keeps serving cached data.
Fix. queryClient.removeQueries({ queryKey: ['address'] }) plus local state reset, gated on a connected→disconnected transition. The effect must be declared after const queryClient or the dependency array throws a temporal dead-zone error.
?q= in the URL with an empty composer
The effect was keyed on mount instead of location.search. The palette navigates while the chat is already mounted.
Fix. Key on location.search, and strip with react-router navigate(path, { replace: true }) — not history.replaceState, which leaves the router's location stale so picking the same action twice does nothing.
AI-site user bounced to defi.blazpay.com
A hardcoded /defi/blazai path hit the catch-all.
Fix. Use CHAT_ROOT, chatPath(id) and isChatPath(pathname) from src/config/site.ts.
Conversation answers worse than expected
Partial conversation-provider configuration. getConversationLLM() needs all three of LLM_GOOGLE_PROJECT_ID, LLM_GOOGLE_CLIENT_EMAIL and LLM_GOOGLE_PRIVATE_KEY; with any missing it silently falls back to the cheaper extraction model instead of the conversation tier.
Agent unreachable
AGENT_URL wrong, or bz-agent down on :4000. Check ./scripts/deploy.gcp.sh status.
Safety suite
| Symptom | Cause |
|---|---|
| Certificate 404s | certificateEligible is false — there are critical or high findings |
| Audit by address fails | Source is not verified on that explorer. The user must paste source |
| Partial audit | 40,000-character clamp; the truncation flag is on the document |
| Weak token score on a fine token | Unverified source — the score reflects uncertainty |
| Badge will not embed | X-Frame-Options: SAMEORIGIN on /* in netlify.toml. Fixed only in netlify.ai.toml for /token/*/embed |
| Approval list shows a spent allowance | The scan reads Approval logs, not current allowances |
| Approval list looks short | Capped at the top 25 by allowance size |
| Wallet card shows zero balance | Zerion does not index that chain |
| Shared report unfurls generically | No server-side OG rendering |
Deployment
"The code deployed but the feature does not work"
Almost always a missing environment variable. .env is excluded from the rsync, so a new key does not travel with the code. UNISWAP_API_KEY is the canonical example.
Fix. Add it on the VM, then restart.
"The old swap-sdk version is still installed"
package-lock.json pins by resolved commit, and npm install obeys it even after rm -rf node_modules/swap-sdk.
Fix.
bash
npm install github:blazpay/swap-sdk#vX.Y.Z --package-lock-only
# commit BOTH package.json and package-lock.json"My uncommitted work is in production"
The backend deploy is rsync mode, copying the entire local working tree (excluding only .git/, node_modules/, .env, dist/, build/, logs/, *.log) and without --delete.
Fix. Make the VM's app directory a git repo so deploy switches to git-pull mode.
All 114 SEO pages 404
force = true was added to the SPA rewrite in netlify.toml. Non-forced rewrites let Netlify serve prerendered files first; forced ones shadow them.
AI-site pages carry canonical=defi.blazpay.com
scripts/rewrite-ai-shell.mjs did not run. It is wired into build:ai and fails the build if any defi.blazpay.com string survives — so this means the wrong build command was used.
IndexNow returns 403
The key file was not deployed before submission. IndexNow caches failed key validations for hours or days.
Fix. Wait. Do not rotate the key and submit in the same deploy.
Build & tooling
| Symptom | Reality |
|---|---|
npx tsc --noEmit shows ~3,300 errors | Pre-existing, from node_modules. vite build is the gate |
| Mobile screenshots look like overflow bugs | Headless Chrome on macOS clamps --window-size to ~500 px wide. Verify at 500 px or above |
npm test in bz-backend exits 1 | There are no backend tests. Contract tests live in contract-deployment/test/ |
| A marketing route loads the 7 MB bundle | The MARKETING_ROUTE regex in src/index.tsx was not updated |
| A marketing route 404s inside the light tree | src/MarketingApp.tsx's route list was not updated |
| Web3Modal hook throws on a marketing page | MainNav cannot be used there — createWeb3Modal() never ran |
Contract error selectors
For decoding a raw revert:
Relayer (string reverts)
"BlazpayRelayer: Transaction expired"
"BlazpayRelayer: Invalid nonce"
"BlazpayRelayer: Invalid signature"
"BlazpayRelayer: Provide correct value"
"BlazpayRelayer: Refund failed"
"Transaction execution failed"DCA vault (custom errors)
OutputBelowMinimum(uint256 actual, uint256 minimum)
NotAuthorized(address caller)
NothingToClaim(address user, uint256 positionId)
PrepaidExhausted(address user, uint256 positionId)
PositionNotFound(uint256 positionId)Autopilot vault (custom errors)
AgentNotFound(uint256) NotAgentOwner(uint256, address)
NotAuthorized(address) AgentInactive(uint256)
PolicyExpired(uint256) InvalidTrade()
TokenNotAllowed(uint256, address) InvalidAmount()
InsufficientAgentBalance(uint256, address)
RouterNotAllowed(address) InvalidTarget(address)
InvalidCalldata() ExceedsMaxPerTrade(uint256, uint256)
ExceedsMaxPerDay(uint256, uint256) ExceedsMaxTradesPerDay(uint256)
OutputBelowMinimum(uint256, uint256) RouterCallFailed()