A customer paid and the balance did not move
Four deposit pipelines fail in four different places — how to tell which one you are in, what evidence each leaves, which cron has to be running, and when it is safe to credit by hand.
The customer has paid. The balance is unchanged. Before you touch anything, work out which pipeline you are in, because they share almost nothing: different detectors, different cadences, different failure states, and the evidence lives in a different place for each.
A hand adjustment does not stop the pipeline. If you credit the customer and the deposit then completes on its own, they are credited twice — the adjustment and the deposit carry different idempotency keys and neither knows about the other. Diagnose first; credit by hand only at the end, and only under the conditions at the bottom of this page.
Which pipeline
| Pipeline | The customer did this | Where the row appears |
|---|---|---|
| FIAT gateway | Paid on a hosted checkout — Stripe, Paystack, PayPal, … | PENDING at checkout, completed by webhook or return |
| SPOT | Sent crypto to an exchange-hosted address and submitted the tx hash | PENDING with amount: 0, hash in referenceId |
| ECO | Sent crypto to an ecosystem-chain address the platform generated | No row at all until confirmed, then already COMPLETED with the hash in trxId |
| BTC / UTXO | Sent BTC (or LTC, DOGE, DASH) to a platform address | As above — the scanner credits, then the row exists |
| Manual method | Filled in one of your own deposit methods (bank transfer, cash) | PENDING, waiting for a human. Not a fault |
The quickest tell is the wallet type on the row, or — if there is no row — the currency and how the customer got the address.
The order to work in
-
Find the row.
/admin/finance/deposit/log, clear the defaultPENDINGfilter, and search by the customer's email or the amount. If the deposit reached any pipeline at all there is a row; if there is none, you are in ECO or BTC and the deposit was never detected. -
Read the status and the metadata. The status alone answers most cases (the table below). The metadata block on the view dialog carries whatever the gateway or the chain supplied — the vendor's own status, a channel, a payer reference, a chain and address.
-
Check the relevant scheduled job actually ran.
/admin/system/cron. Each pipeline depends on a different one, and a stopped scheduler does not stop the website — nothing else makes it obvious. -
Check the pipeline's own configuration. For a gateway that is
/admin/finance/deposit/gateway; for a chain it is the ecosystem blockchain screen and the RPC the scanner uses.
What each status means
| Status | Meaning | Usual cause |
|---|---|---|
PENDING |
Nothing has been credited | Waiting on a confirmation, a webhook, or you |
COMPLETED |
Credited — check the wallet, not the row | The customer is looking at the wrong wallet or currency |
FAILED |
The pipeline gave up deliberately | Usually a deposit that nets nothing after the exchange's fee |
TIMEOUT |
The deposit aged out of its verification window | Deposit Expiration is on and the payment fell outside the window |
REJECTED |
An operator refused it, with a reason | Look at metadata.message |
Both present as "my deposit did not arrive". They are different faults with
different fixes: FAILED on a spot deposit means the exchange's own network fee
was equal to or larger than the deposit, so there was nothing to credit — the
row's description says so in words. TIMEOUT means the deposit was found but
fell outside the expiry window that Admin → System → Settings → Wallet →
Deposit Expiration turns on. Read the description before you answer the ticket.
SPOT — exchange-hosted addresses
A spot deposit is verified by polling the exchange for the transaction the customer named.
How it runs. When the deposit page is open, a WebSocket handler starts a
setInterval for that transaction, held in memory in spotVerificationIntervals
keyed by transaction id. It polls every 15 seconds and cancels itself after 30
minutes.
Those intervals are process memory. Restart the backend and they are gone — nothing in the database remembers that a deposit was being watched.
The recovery is processSpotPendingDeposits, on /admin/system/cron, every
15 minutes. It re-arms the schedule for pending rows younger than 25 minutes
and verifies older ones directly, once per run. If that job is not running, a
spot deposit interrupted by a restart is never verified at all — and the row just
sits PENDING with amount: 0.
Three deliberate terminal outcomes, all of which the operator sees as "it did not work":
- Nets nothing after the exchange fee →
FAILED, with a description naming the amount and the fee. This is correct behaviour, not a bug: crediting zero is rejected by the wallet service, and without the explicit failure the row would be retried forever. - Outside the expiry window (only when
Deposit Expirationis on) →TIMEOUT, descriptionDeposit expired. Please try again. - Currency mismatch — the deposit arrived in a different asset than the row claims → the transaction is soft-deleted and disappears from the queue's default view. If a customer insists they submitted a deposit and no row exists on a spot wallet, this is the case to check for.
A spot row still at amount: 0 has never been matched to an exchange deposit.
Do not approve it from the queue — approval refuses with Cannot approve: deposit amount is invalid (0), which is the correct answer.
ECO — ecosystem chains
Ecosystem deposits are not pending rows. A detected but unconfirmed deposit
lives in a Redis hash; the transaction row is written only when confirmations
land, already COMPLETED, with the chain hash in trxId. So "there is no row"
is the normal appearance of an unconfirmed ecosystem deposit — it is not evidence
that nothing was detected.
How it runs. One thing drains that Redis set: verifyPendingEcoDeposits,
the cron, every 60 seconds. It runs whether or not anybody has the deposit
page open, and it is the only path. The deposit WebSocket used to start a second,
faster in-process worker over the same set, but that worker was never in the cron
registry — no admin screen could list, trigger or stop it, and it kept crediting
after the ecosystem extension was disabled. It has been removed. If deposits are
not finalizing, this one job is what to check.
If a customer's ecosystem deposit has not landed several minutes after the chain
shows enough confirmations, the confirmations are not reaching the platform.
Check that verifyPendingEcoDeposits is running and not erroring on
/admin/system/cron, then check the chain's RPC on Extensions → Ecosystem →
Blockchains (/admin/ecosystem/blockchain).
Two aging rules are worth knowing before you promise a customer anything:
- A transaction that repeatedly fails verification (a provider error, not "not yet confirmed") is moved to a dead-letter hash after 30 attempts rather than being deleted — a real on-chain deposit is never destroyed, and support can replay it. Attempt counters reset after 30 idle minutes.
- A pending transaction that has produced no receipt for 48 hours is treated as dropped — never mined, or replaced — and removed from the pending set.
Deposits that arrive after the customer closes the page are picked up by
backgroundDepositScanner, which only watches addresses seen recently:
registered when the customer opens their deposit page, with a 72-hour TTL,
rescanned about every two minutes, rate-limited per chain. It is on by default and
switched off with ECOSYSTEM_BACKGROUND_SCAN=false in .env. A customer who
funded an address they last visited a week ago is outside that window.
BTC and the other UTXO chains
btcDepositScanner is BTC only. Its wallet query is hardcoded to
type: "ECO", currency: "BTC" — there is no LTC, DOGE or DASH branch in it, so
checking it for those chains tells you nothing. It runs every 60 seconds and
credits at 3 confirmations. Its provider is chosen by BTC_NODE in .env — node (a local
Bitcoin Core), mempool (the default, free, no configuration) or blockcypher
(needs BLOCKCYPHER_TOKEN) — with an automatic fallback chain if the first
choice cannot be reached. BlockCypher is only usable on mainnet and legacy
testnet3.
It is deliberately rate-limited against the public APIs: at most 100 wallets per cycle, spaced 250 ms apart, with a round-robin cursor so the remainder are picked up on later cycles. On an install with thousands of BTC wallets, a deposit is not late because it took several minutes to be reached.
LTC, DOGE and DASH are credited by the ecosystem path instead — the UTXO
deposit monitor while the customer has their deposit page open, and
backgroundDepositScanner afterwards, which carries its own per-chain rate
buckets for those three because BlockCypher's anonymous tier is shared and slow.
Their confirmation depth is 6, not BTC's 3, so they are legitimately slower
to land. For one of those chains, check backgroundDepositScanner and the
72-hour address window described above, not btcDepositScanner.
Both this scanner and the ecosystem jobs are extension-gated — they must not credit while the ecosystem extension is disabled. If the extension was turned off on System → Extension Manager, no chain deposit is credited at all.
FIAT gateways
A gateway deposit creates a PENDING row at checkout and is completed by the
vendor's webhook, or by the verification the customer's return triggers. A row
still PENDING minutes later means neither arrived.
/admin/finance/deposit/gateway is the screen that explains why. It is a
readiness console, not a table: for each gateway it reports which environment
variable is missing, the leading marker of the keys that are set (sk_live
versus pk_test — enough to tell you that you shipped test keys to production,
and nothing more of the secret), whether it resolves to test or live, and the
exact webhook URL the vendor should be calling.
The three failures that produce this symptom, in order of frequency:
- The webhook was never registered with the vendor, or was registered on the test side only. Several vendors keep a separate webhook URL per mode, so going live with only the test webhook set means every real payment succeeds at the vendor and is never credited here. Copy the URL from the gateway page and paste it into the vendor dashboard.
- A credential is unset, so the platform cannot verify the payment even when it is told about it. The page names the variable.
- The signing secret is missing on the gateways that verify a signature — notifications arrive, fail verification and are dropped silently, so the payment succeeds at the vendor and is never credited.
Press Test credentials on the gateway before you conclude anything. It is a read-only call to the vendor and creates nothing.
Once you have confirmed at the vendor's dashboard that the payment really settled, the right fix is to approve the pending row from the deposit queue — that credits the wallet, books your fee and emails the customer. See Working the deposit queue.
How late is late
The deposit SLA is 72 hours. That is what the Age column and the dashboard health card are measured against — amber at half, red past it. It is a queue budget, not a customer expectation: a gateway payment should credit in seconds and a chain deposit within its confirmation depth, so a customer complains long before anything turns amber. Do not use the SLA chip to decide whether a deposit is stuck.
Crediting by hand
Last resort, when the money genuinely arrived and no pipeline will ever settle the row.
It credits the balance and nothing else. It does not mark the deposit row
COMPLETED, it does not book the platform fee, and it does not stop a
scanner or a webhook from later crediting the same payment again.
-
Make sure nothing can still settle it. The row must be in a terminal state —
FAILED,TIMEOUTorREJECTED— or you must reject it yourself first, with a reason that says you are crediting manually. APENDINGrow is still live. -
Adjust the balance.
/admin/finance/wallet, find the customer's wallet — check the type as well as the currency — and use Adjust balance → Add. -
Record the external reference in the description. The on-chain transaction hash, or the gateway's reference. That description is written onto the permanent balance-ledger row, and it is the only thing that will ever explain the adjustment to an auditor, to your accountant, or to you in six months.
-
Decide whether the customer is notified. The dialog's notify switch is on by default. Leave it on unless you are also writing to them yourself.
Full detail on what the adjustment writes is in Customer wallets and balance adjustments.
Related
- Working the deposit queue — the queue, the gateway console, and what Approve does.
- A withdrawal is stuck — the same problem in the other direction.
- Troubleshooting — a backend that will not start, a dead scheduler, or an exchange provider with bad credentials.