Appearance
Debugging a stuck trade
A user says their swap or bridge did not arrive. Work through this in order — the cheap checks eliminate most cases.
Triage in 60 seconds
1. No on-chain hash
The record exists but txHash still looks like a UUID.
That is the expected shape for a trade that never got signed
sendTransactionRaw creates the record with a UUID as txHash, pushes ?hash=<uuid> into the URL, then patches in the real hash. A UUID-shaped txHash means the wallet prompt was rejected, the tab closed, or PATCH /defi/transaction/hash/<uuid> failed.
Check:
GET /api/defi/transaction/{wallet}Nothing is stuck. No funds moved. If the user insists they confirmed, the patch may have failed — ask them to check the wallet's own activity list for a transaction to the relayer address.
2. The transaction reverted
Read the revert reason on the explorer. The relayer bubbles the router's own error via inline assembly, so you usually get something specific.
| Revert | Cause | Resolution |
|---|---|---|
BlazpayRelayer: Transaction expired | deadline = now + 100 seconds elapsed between signing and mining | Retry. Slow wallet prompt or congested chain |
BlazpayRelayer: Invalid nonce | A concurrent transaction from the same wallet consumed the nonce | Retry |
BlazpayRelayer: Invalid signature | Wrong signer key for the chain (137 uses POL_TX_SIGNER), or the relayer's signer was rotated | Read signer on-chain and compare to TX_SIGNER |
BlazpayRelayer: Provide correct value | Native swap with msg.value < nativeValue + fee | The client omitted the fee |
| ERC-20 transfer failure | The approval did not cover amount + fee | The fee is a second transferFrom |
| A router-specific revert | Slippage, insufficient liquidity, expired provider quote | Re-quote |
| Any revert on an unsupported chain | assertRelayerDeployed should have blocked it earlier | Check the chain is in RELAYER_DEPLOYED_CHAINS |
Funds are safe. A revert is atomic — the approval may have been consumed but nothing moved.
3. Transaction succeeded, no output token
The nastiest case, and it has a specific signature.
Check whether the route was an approve() step
Some providers return an approval as their transaction data. tx.to is the sell token and the calldata is 68 bytes. The relayer dutifully executes it — which does nothing — and the user paid gas for an approval they did not want.
Diagnosis: decode the input data on the explorer. If the selector is 0x095ea7b3 (approve) and to is the input token, this is it.
Resolution: the funds were never moved, so nothing is lost beyond gas. Re-quote and retry; the aggregator that produced it should be noted.
The automation path guards against this; the interactive path does not
executionEngine.js rejects tx.to == fromToken or data.length <= 68. The interactive swap path relies on sortQuotes gas simulation, which passes for an approval because an approval succeeds. Adding the same calldata guard to the interactive path is an obvious improvement.
Other reasons for no output
| Cause | Check |
|---|---|
| Output went to a different recipient | Was a custom recipient set? |
| Partial fill, and the refund is what arrived | Look for a TokenRefunded event |
| Wrong token being checked | Bridges can deliver a wrapped variant |
4. Bridge — source confirmed, destination pending
Bridges are not atomic. Two lifecycles.
Check the provider's status
GET /api/defi/txn/{wallet}This re-polls each provider's status endpoint via getTxStatus(chainId, hash), using the routers map in swap-sdk/src/utils/constants.ts.
Per-provider status endpoints also exist directly:
GET /api/defi/across/status?…
GET /api/defi/relay/status?…
GET /api/defi/squidrouter/status?…
GET /api/defi/gas-zip/status?…
GET /api/defi/near-1click/status?…Expected times
| Provider type | Typical | Concerning after |
|---|---|---|
| Across (optimistic relayer) | seconds–minutes | 30 min |
| Liquidity network (deBridge, XY) | minutes | 1 h |
| Intent solver (Relay, Nordstern) | seconds–minutes | 30 min |
| Canonical / rollup bridge | minutes–hours | Depends on the chain's finality |
| Custodial (Changelly, ChangeNow) | minutes | Contact the provider |
Blazpay cannot force a fill
Once the source transaction confirms, delivery is the bridge's responsibility. Blazpay surfaces status but has no lever. For a genuinely stuck bridge the escalation path is the provider's support, with the source hash.
Background reconciliation
bz-backend/services/bridgeStatusCron.js polls pending records so a user who closed the tab still gets a resolved record. DISABLE_BRIDGE_STATUS_CRON turns it off — check it is not set.
5. It worked but the UI does not show it
| Cause | Fix |
|---|---|
The PATCH with the real hash failed | The record still has the UUID. Patch it manually |
| Status not refreshed | GET /api/defi/txn/{wallet} |
| Wrong history surface | Swap and bridge histories are separate routes |
| Portfolio does not reflect it | Portfolio is Zerion-backed and may lag; it also excludes vault and perp balances |
Automation-specific: a CSIP order did not run
In order:
CRON=false? Check the environment.- Executor out of gas?
node scripts/checkDeployerBalance.js. Roughly 0.00002 ETH per Arbitrum batch. lastErroron theDcaInvestmentdocument? The UI shows an amber "retrying" state. The field carries the actual error.- Below the per-order minimum? Creation should have rejected it, but a price move can change the effective size.
- No route available? The engine tries the top 5 quotes; if all fail the position skips the tick with no distinction between "no liquidity" and "five flaky endpoints".
- Buys running too fast?
intervalwas sent as the per-order gap rather than the total window.
Automation-specific: an Autopilot agent is not trading
Check the router allow-list first
setRouters([...], true) must have been called by the owner. The vault is fail-safe — with an empty allowedRouter map every trade reverts with RouterNotAllowed. This is the current state on Robinhood Chain.
Then:
| Check | Detail |
|---|---|
| Policy hash matches on-chain? | A mismatch makes the cron skip the agent silently. POST /defi/autopilot/sync/:chainId/:agentId |
active flag | The owner may have paused it on-chain |
expiry | PolicyExpired after the timestamp |
| Daily caps | ExceedsMaxPerDay on a rolling 24 h from first trade |
| Safety gate | An unscored token blocks — fail-closed by design |
AUTOPILOT_ADDRESSES empty for the chain? | The cron skips it (Base is inert) |
OutputBelowMinimum in the activity feed | An intent/solver route slipped past the probe |
Activity feed: GET /api/defi/autopilot/:chainId/:agentId/activity.
Diagnostic scripts
bz-backend/scripts/ — read-only unless noted:
bash
node scripts/analyze-failed-tx.js # decode a failed transaction
node scripts/analyze-latest-fail.js
node scripts/analyze-error.js
node scripts/debug-signature-onchain.js # signature verification
node scripts/diagnose-signature.js
node scripts/debug_signature.cjs
node scripts/diagnose-transfer-error.js # ERC-20 transfer failures
node scripts/checkRelayerDeployed.js # relayer presence per chain
node scripts/check-contract-state.js # fee config, pause state
node scripts/check-contract-owner.js
node scripts/fetchAllChainBalances.jsReading the logs
bash
./scripts/deploy.gcp.sh logs bz-backend 500Every request logs [timestamp] METHOD url - IP: … - Origin: …. Useful greps:
| Grep | Finds |
|---|---|
/defi/quotes | Quote requests, and per-aggregator failures around them |
Route via | Execution-engine route rejections (Route via odos failed: …) |
Not swap calldata | Calldata-guard rejections |
delivers no in-tx output | Probe rejections (solver providers) |
/defi/sign | Signing requests |
| The user's wallet address | Their whole session |
Escalation
| Situation | Action |
|---|---|
| Bridge stuck past the expected window | Provider's support, with the source hash |
| Funds stranded in a vault by accounting | Owner-key recovery. Precedent: CSIP recovery |
| A provider consistently returning bad routes | Comment out its registration in swap-sdk/src/index.ts with the reason inline, tag, release, bump both consumers |
| Relayer signer compromised | setSigner(address) immediately — do not wait for confirmation |
| A contract needs pausing | pause() on the relevant contract, owner only |
What to collect before escalating
□ Wallet address
□ Transaction hash (or the UUID if there is none)
□ Chain ids — source and destination
□ Token addresses — in and out
□ Which provider (meta.aggregator on the record)
□ Approximate timestamp
□ Screenshot of what the user saw
□ The backend log window around that timestamp