Troubleshooting
Symptom-first index of the failures this stack actually produces.
Ordered by symptom, because that is what you have when something breaks.
Reads
DATABASE_URL is unset or wrong. Reads return empty lists by design rather than throwing.
curl -s http://localhost:4000/readyz | jq .dbCheck the port (5433, not 5432) and the user (titanadmin). A wrong connection string reads
as "quiet market", not as an error.
The api was deployed before the indexer migrations. Drizzle emits an explicit column list, so this fails on the first request.
Apply 0014_tpsl_v2 and 0015_fired_limit, then redeploy the api. Always indexer first.
Phantom open orders. The live open-order read
(GET /v1/admin/users/{party}/orders?market=all) failed and fell back to non-terminal mirror rows.
Cause: an engine older than c879f7f, where market=all is a 400 market_unsupported. Silent —
no 5xx. Grep for:
ordersForUser: live open-orders fetch failedIndexer lag, not a UI bug. Live frames come from SSE and bypass the indexer; REST reads go through Postgres.
Compare bff_engine_max_seq against the indexer's high-water mark.
Orders
It was rejected. The engine answers HTTP 200 with ok: false — code branching on res.ok
reports it as filled.
if (!res.ok) { /* this is the rejection path */ }The value is off the market grid. Size must be an exact multiple of 10^-szDecimals; price must be
an integer, or ≤5 significant figures and ≤6 - szDecimals decimals.
Snap before deriving margin or fees, or the preview disagrees with the fill.
minNotional is being enforced on a reduce. It applies to the opening leg only — reduce-only
orders are exempt engine-side. Remove the client-side gate on Close.
The canonical bytes and the JSON body disagree, or the encoding is wrong.
VITE_SIGN_MESSAGE_ENCODING:base64for most extensions,rawfor the Splice reference signer.- 5N Loop ignores it entirely —
SIGN_RAW_MESSAGEalways sends literal text.
Both produce a well-formed signature over the wrong bytes.
The engine rejects a reused nonce even for an identical payload. Increment before a retry — a timeout on a request the engine processed leaves the nonce consumed.
WebSocket
ENGINE_ADMIN_TOKEN is unset, so the SSE bridge never started. Clients connect fine and sit
silent. /readyz shows engineMaxSeq: 0.
The markets= filter did not match. The BFF compares it exactly, and an unmatched filter is
ignored rather than treated as empty.
Use the engine symbol verbatim: BTC-USDCX, never BTC/USDCx.
Mixed content — an https:// page opening ws://. streamWsUrl() guards it; a hand-built URL
does not.
Positions are being keyed by market instead of by positionId. A flip closes one epoch (size: "0") and opens another under a new id with realizedPnl restarting at zero. Keying by market
merges them.
Wallet
DappSDK.connect() is being used instead of DiscoveryClient.connect(providerId). Its failure
path reads a window opened by the SDK's own picker popup — one we never open — so the real error
is posted into an absent window and dropped.
A dead gateway reports Failed to fetch through DiscoveryClient. That one is debuggable.
Extensions share the window.canton slot. WindowTransport routes by target but matches the
response on the JSON-RPC id alone, so an untargeted adapter can be answered by the wrong wallet.
The RPC client must be built from the active session, carrying the announced target.
Reown AppKit and the Canton WalletConnect adapter both initialise WC Core with the same project id. Harmless today; the fix if it ever misbehaves is a separate project id for the Canton adapter.
Its <dialog> is opened by attribute, not showModal(), so Escape is inert and it ships no
close control. installEscapeHatch() adds Escape + backdrop-click and calls loop.logout(), whose
clearLocalSession() tears it down.
VITE_LOOP_NETWORK defaults to mainnet — Loop accounts live there — so a devnet build still
gets a mainnet party id, which a devnet engine has never seen.
Network
Very often an upstream 5xx in disguise. On staging, a duplicate party hint produced an engine 502; Cloudflare replaced it with an HTML error page carrying no CORS headers, and the browser reported CORS.
Check the origin's real status before touching CORS config.
IPv6 is disabled on Wi-Fi and Ethernet on this machine on purpose, for ProtonVPN. Re-enabling it brings the stalls back — do not "fix" it.
Build
go build ./cmd/a ./cmd/b with multiple packages only compiles as a check and discards the
output. Build one package at a time:
go build -o ./engine ./cmd/engineThe Vite mode is derived from CF_PAGES_BRANCH, not a local .env. Locally:
BRANCH=main npm run buildWrong tree. The deployed indexer is server/indexer; the standalone titan/indexer at the
repo root is stale.