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.
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.
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.
Related
- Network and RPC — why detection slows down before it stops
- Wallets and key custody — where these addresses come from
- Withdrawals and fees — the other direction
- Troubleshooting — deposits that never credited