Skip to content

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.

RevertCauseResolution
BlazpayRelayer: Transaction expireddeadline = now + 100 seconds elapsed between signing and miningRetry. Slow wallet prompt or congested chain
BlazpayRelayer: Invalid nonceA concurrent transaction from the same wallet consumed the nonceRetry
BlazpayRelayer: Invalid signatureWrong signer key for the chain (137 uses POL_TX_SIGNER), or the relayer's signer was rotatedRead signer on-chain and compare to TX_SIGNER
BlazpayRelayer: Provide correct valueNative swap with msg.value < nativeValue + feeThe client omitted the fee
ERC-20 transfer failureThe approval did not cover amount + feeThe fee is a second transferFrom
A router-specific revertSlippage, insufficient liquidity, expired provider quoteRe-quote
Any revert on an unsupported chainassertRelayerDeployed should have blocked it earlierCheck 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

CauseCheck
Output went to a different recipientWas a custom recipient set?
Partial fill, and the refund is what arrivedLook for a TokenRefunded event
Wrong token being checkedBridges 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 typeTypicalConcerning after
Across (optimistic relayer)seconds–minutes30 min
Liquidity network (deBridge, XY)minutes1 h
Intent solver (Relay, Nordstern)seconds–minutes30 min
Canonical / rollup bridgeminutes–hoursDepends on the chain's finality
Custodial (Changelly, ChangeNow)minutesContact 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

CauseFix
The PATCH with the real hash failedThe record still has the UUID. Patch it manually
Status not refreshedGET /api/defi/txn/{wallet}
Wrong history surfaceSwap and bridge histories are separate routes
Portfolio does not reflect itPortfolio is Zerion-backed and may lag; it also excludes vault and perp balances

Automation-specific: a CSIP order did not run

In order:

  1. CRON=false? Check the environment.
  2. Executor out of gas? node scripts/checkDeployerBalance.js. Roughly 0.00002 ETH per Arbitrum batch.
  3. lastError on the DcaInvestment document? The UI shows an amber "retrying" state. The field carries the actual error.
  4. Below the per-order minimum? Creation should have rejected it, but a price move can change the effective size.
  5. 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".
  6. Buys running too fast? interval was 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:

CheckDetail
Policy hash matches on-chain?A mismatch makes the cron skip the agent silently. POST /defi/autopilot/sync/:chainId/:agentId
active flagThe owner may have paused it on-chain
expiryPolicyExpired after the timestamp
Daily capsExceedsMaxPerDay on a rolling 24 h from first trade
Safety gateAn unscored token blocks — fail-closed by design
AUTOPILOT_ADDRESSES empty for the chain?The cron skips it (Base is inert)
OutputBelowMinimum in the activity feedAn 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.js

Reading the logs

bash
./scripts/deploy.gcp.sh logs bz-backend 500

Every request logs [timestamp] METHOD url - IP: … - Origin: …. Useful greps:

GrepFinds
/defi/quotesQuote requests, and per-aggregator failures around them
Route viaExecution-engine route rejections (Route via odos failed: …)
Not swap calldataCalldata-guard rejections
delivers no in-tx outputProbe rejections (solver providers)
/defi/signSigning requests
The user's wallet addressTheir whole session

Escalation

SituationAction
Bridge stuck past the expected windowProvider's support, with the source hash
Funds stranded in a vault by accountingOwner-key recovery. Precedent: CSIP recovery
A provider consistently returning bad routesComment out its registration in swap-sdk/src/index.ts with the reason inline, tag, release, bump both consumers
Relayer signer compromisedsetSigner(address) immediately — do not wait for confirmation
A contract needs pausingpause() 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

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