Skip to content

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:

CauseCheck
Wrong signer key for the chainChain 137 uses POL_TX_SIGNER, everything else TX_SIGNER
The relayer's stored signer was rotatedRead 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:

CauseFix
The route was an intent/solver provider (Relay, Nordstern) delivering nothing in-transactionThe route probe should have filtered it — check that the caller passes probe
Genuine slippage past the floorMicro 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:

  1. CRON=false in the environment?
  2. Executor wallet out of native gas? (~0.00002 ETH per Arbitrum batch)
  3. Per-order value below the minimum? Creation should have rejected it
  4. lastError on the DcaInvestment document — the UI shows an amber "retrying" state
  5. 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

CauseDetail
No handler for the intentLIMIT_ORDER classifies but has no backend handler
Extraction returned follow_upA required field was missing; the reply is a clarifying question
The action has no rendererAn 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

SymptomCause
Certificate 404scertificateEligible is false — there are critical or high findings
Audit by address failsSource is not verified on that explorer. The user must paste source
Partial audit40,000-character clamp; the truncation flag is on the document
Weak token score on a fine tokenUnverified source — the score reflects uncertainty
Badge will not embedX-Frame-Options: SAMEORIGIN on /* in netlify.toml. Fixed only in netlify.ai.toml for /token/*/embed
Approval list shows a spent allowanceThe scan reads Approval logs, not current allowances
Approval list looks shortCapped at the top 25 by allowance size
Wallet card shows zero balanceZerion does not index that chain
Shared report unfurls genericallyNo 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

SymptomReality
npx tsc --noEmit shows ~3,300 errorsPre-existing, from node_modules. vite build is the gate
Mobile screenshots look like overflow bugsHeadless Chrome on macOS clamps --window-size to ~500 px wide. Verify at 500 px or above
npm test in bz-backend exits 1There are no backend tests. Contract tests live in contract-deployment/test/
A marketing route loads the 7 MB bundleThe MARKETING_ROUTE regex in src/index.tsx was not updated
A marketing route 404s inside the light treesrc/MarketingApp.tsx's route list was not updated
Web3Modal hook throws on a marketing pageMainNav 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()

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