Reliability
Three properties of this API shape any serious integration. None of them is a bug; all of them are things you must build around.
There are no webhooks
The service never calls you. There are no callbacks, no subscriptions, no push notifications of any kind. Everything you learn about a wallet, you learn by asking.
That has two consequences.
Incoming deposits are your problem to detect. Nobody will tell you a user received funds. You find out by polling /balance or /transactions for that user and asset. Note the shape of that cost: it is one call per user, per asset. With 16 assets and 10,000 users, a naive full sweep is 160,000 requests. Poll only the assets you actually support, stagger users across the interval, and back off hard on wallets that never change.
Outgoing transfers must be followed up. A 200 from /send means the network accepted the transaction, not that it succeeded.
Confirming a send
Poll /txid with the txHash until status leaves pending:
status is pending, success or failed. While pending, date is null.
Sensible intervals, given how fast each chain settles:
A transfer that stays pending past those windows is stuck or dropped. There is no /cancel, no replace-by-fee and no rebroadcast endpoint — you cannot fix it through this API. Escalate to your operator.
/transactions is a feed, not a ledger
/transactions takes only userId and assetId. There is no pagination, no cursor, no date range, no page size — it returns roughly the last 10 transactions, and that number is set server-side, not by you.
Older history is unreachable. Once a transaction falls off the end of that window, this API will never show it to you again. If you need statements, reconciliation or an audit trail, you must persist transactions on your side as you see them, keyed on hash.
Treat /transactions as “what happened recently”, and your own database as the record.
Retrying a /send safely
There is no idempotency key. The same /send sent twice sends the funds twice.
Most failures are unambiguous and safe — the transfer was rejected before anything was signed:
That last row is the dangerous one: the transaction may have been signed and broadcast before the failure reached you.
The reconciliation procedure
“Not found in /transactions” does not mean “not sent.” On EVM chains, history is served by an indexer that lags the mempool — a transfer broadcast seconds ago is routinely absent. And a transaction row carries no counterparty address (just hash, type, amount, fee, status, date), so you cannot positively match a specific transfer to a specific recipient. Retrying because you didn’t find it is exactly how you send twice.
Do this instead:
- Write an intent record before you call
/send— user, asset, recipient, amount, stateSENDING. If your process dies mid-call, this row is the only evidence the attempt existed. - Send your own
X-Request-Idand store it on that row. It is echoed on every response and written to the service’s audit log, so it is the key your operator can search on. - On a timeout or
500, do not retry. Mark the rowUNKNOWN. - Poll
/transactionswith backoff for several minutes, and watch/balancefor a matching drop. - If it still hasn’t appeared, leave it
UNKNOWNand escalate to a human, quoting therequestId. Do not let an automated job resolveUNKNOWNinto “retry.”
Only ever auto-retry the failures in the “No” rows of the table above.
If exactly-once transfers matter to you, say so — an idempotency key on /send is a server-side change, and the service already carries X-Request-Id end to end, so it is a small one. Hand-rolling exactly-once semantics on the client is not a good place to be.
Chain outages
Chains fail independently. A Bitcoin node being down does not affect an Ethereum balance check.
- A node or RPC provider that is down, erroring or timing out surfaces as a
500. Reads are safe to retry with backoff. - A
503is not an outage. It meansChain is not configured: <CHAIN>— this deployment does not run that chain at all. It is permanent, and retrying will never help. /infodegrades rather than failing: if history lookup breaks, it still returns the address and balance, withtransactions: []. An empty list there means “we couldn’t fetch history”, not necessarily “no history”.