Deposits

How a native SOL transfer and an SPL token transfer are each detected, what has to be running for either to be credited, why every currency has its own Solana address, and the deposit that will never arrive.

6 min readUpdated 6 August 2026deposits, spl, monitoring, crediting, associated-token-account

Detection credits. When a Solana monitor sees a deposit it hands the transaction to storeAndBroadcastTransaction, which runs the credit inline and broadcasts the new balance to the open session — there is no queue in front of it. Only a deposit whose inline credit failed is parked in Redis for a 60-second watchdog to retry. That is most of the diagnosis: a deposit sitting in the pending set is not one waiting its turn, it is one that has already gone wrong once.

Every currency has its own Solana address

An ECO wallet is one row per user per currency, and each one gets its own Solana keypair. A user who holds SOL and USDC therefore has two different Solana addresses, both belonging to them, both with keys held by the platform.

Send USDC to the address shown on the SOL deposit page and it lands at an address the platform controls, under a wallet whose currency is SOL. Nothing watches that address for USDC and nothing will credit it.

The funds are recoverable — the key exists — but only through manual operator intervention against the database. Tell customers to use the address shown on the deposit page for the asset they are sending, every time.

The native SOL path

SOL is carried as a NATIVE token. Deposits go to the user's own address.

While the deposit page is open, the platform holds a logs subscription on that account. Before subscribing it also replays the address's last 25 signatures, so a deposit that arrived while nothing was watching — the tab was closed, the backend restarted, a previous monitor had already fired — is picked up rather than missed.

When a signature appears, the transaction is fetched at finalized commitment, retried up to 30 times roughly every 5 seconds while the cluster catches up. The credited amount is the increase in that account's lamport balance across the transaction, not a parsed transfer instruction. Two consequences worth knowing:

  • A transaction that touches the address without increasing its balance is skipped as terminal — it will not be retried and it is not an error.
  • Transactions that use address lookup tables, which is how most exchange withdrawals are built, are handled: account keys are resolved including the looked-up entries. An exchange-originated deposit is an ordinary deposit here.

The monitor is one-shot by design. After it credits a deposit it tears itself down, and the next time the user opens the deposit page a fresh one is armed. It also self-cancels after one hour of no activity.

The SPL token path

SPL tokens are carried as PERMIT tokens. They also go to the user's own address — there is no custodial contract on Solana — but they physically land in the associated token account derived from that address and the token's mint.

While the deposit page is open, the platform subscribes to token-program account changes filtered to that exact mint and owner. When one fires, it reads the block at that slot and looks for a transfer into the monitored account, falling back to the change in the account's token balance for that mint when the instruction cannot be parsed directly. Either way, the credited figure is the token amount received, and the record is stored as a PERMIT deposit.

In the background, the same detection runs against the last 25 signatures of the associated token account, skipping anything already recorded against this wallet.

The mint address is used verbatim to build the subscription filter. If it is not a valid Solana address the monitor refuses to start and logs a message naming the token and telling you to fix its contract in the ecosystem token settings.

An invalid mint used to make the RPC reject the subscription and the client retry it forever, flooding the logs. It now fails fast instead — but it still fails, and no SPL deposit for that token will ever be detected until the mint is correct.

The background scanner

Both paths above only run while a user has the deposit page open. The background scanner is what catches the far more common case: the customer copies the address, closes the tab, and sends the funds twenty minutes later.

An address is registered in the scanner's working set every time a deposit page subscribes, and stays there for 72 hours. Each entry is re-scanned roughly every two minutes, paced by a token bucket at 0.5 Solana scans per second, so detection latency grows with the number of active addresses while the request rate stays bounded. Only one backend process runs the loop, coordinated through a Redis lock.

It uses the same detection code as the live monitors, so there is no second set of crediting rules to reason about.

Set to "false" to disable the background scanner entirely. Deposits then depend on a live session being open at the right moment, which is not a reasonable production posture.

Crediting

Crediting is not a separate stage. storeAndBroadcastTransaction, in backend/src/api/(ext)/ecosystem/utils/redis/deposit.ts, calls handleEcosystemDeposit synchronously; when that returns a transaction ID it broadcasts the new balance, raises the notification and returns without writing anything to Redis. The balance has moved before the detection call finishes.

Redis only sees a deposit the inline credit could not complete. A thrown 409 — "already processed", or an output the platform produced itself — is a permanent rejection and is dropped on the spot. Anything else, an RPC blip or a database error, falls through to the pending set:

verifyPendingEcoDeposits runs every 60 seconds and re-runs the same credit against everything parked there. For Solana — as for Tron, TON and Monero — a parked record already marked complete is taken as confirmed and credited on that pass with no further on-chain check, because the native path already read the transaction at finalized commitment before recording it. There is no confirmation-depth wait on this chain. A record that keeps failing gets 30 attempts and is then moved to a dead-letter hash, ecosystem:pendingDeposits:dead, rather than deleted, so it can still be replayed by hand.

So the expected shape of a deposit is: detected and credited within seconds if a session is open, or within a couple of minutes if not. A balance that has not moved a minute after the platform says it saw the transaction means the credit threw — read the log lines tagged DEPOSIT rather than waiting for another pass.

A deposit in ecosystem:pendingDeposits has been credited to nobody, and losing Redis loses that record. The coins are still at the customer's address and will be found again the next time that address is scanned — but nothing happens until then, and if the address has aged out of the 72-hour working set, nothing happens until the customer opens the deposit page again.

Double-crediting is not possible

The live monitor, the start-up rescan and the background scanner can all observe the same transaction, and are expected to. Crediting is idempotent twice over: a prior deposit row with the same signature and wallet makes the second attempt a no-op, and the balance credit itself carries an idempotency key derived from the transaction hash and wallet ID.

That is why repeated scans are safe, and why "I can see the deposit was processed three times in the logs" is not a symptom.

What has to be true for a deposit to credit

Work down this list before anything else — it is ordered by how often each one is the answer.

Requirement Symptom when it is wrong
SOL_NETWORK resolves to the cluster the customer sent on Nothing arrives, ever, and everything else looks healthy
The chain licence is valid and the row is enabled Every Solana call fails with "Solana service not available"
An active ecosystem token exists for that currency on SOL The deposit page cannot issue an address at all
For SPL, the token's contract is the correct mint address The monitor refuses to start and logs the token name
The customer used the address for that currency Funds sit at a platform-controlled address, uncredited
Redis is healthy Detection still credits, but a credit that fails has nowhere to be retried from, and the scanner's working set is gone
The Ecosystem cron process is running Only matters once a credit has failed — those deposits then sit in Redis and are never retried

What the transaction list will and will not show

The Solana transaction history view fetches the last 50 signatures for an address and parses System Program transfers only. An address whose activity is entirely SPL therefore renders rows with an empty counterparty and a zero amount.

That is a limitation of that view, not of detection. The deposit itself is recorded as an ordinary platform transaction with its signature as the reference, and the balance is correct. Check the signature on explorer.solana.com rather than reading this view as authoritative.

Results are cached in Redis for 30 minutes per address, so a freshly credited deposit may not appear in the list immediately even though the balance has already moved.