Reliability
Two things shape any serious integration: you learn a wallet’s state by asking for it, and a /send is only safe to repeat if you repeat it correctly. Design for both.
Detecting deposits
You learn that a user received funds by checking for it: poll /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.
Confirming a send
A 200 from /send means the network accepted the transaction, not that it succeeded. Check it by polling /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:
Escalate a transfer that stays pending past those windows to your operator — treat it as stuck or dropped, and resolve it outside this API rather than by sending again.
/transactions is a feed, not a ledger
/transactions takes userId and assetId, and returns roughly the last 10 transactions for that pair. The window is set server-side.
Persist transactions on your side as you see them, keyed on hash. Once a transaction falls off the end of that window, you will not be able to read it back from this endpoint — so if you need statements, reconciliation or an audit trail, your own store has to be the record.
Treat /transactions as “what happened recently”, and your own database as the record.
Retrying a /send safely
/send is idempotent on the X-Request-Id header, which it requires you to supply. That header is what separates “retry the transfer I already authorised” from “authorise a second transfer” — and the service cannot tell those apart on your behalf. It only knows what you put in the header.
One id per logical transfer. The same id on every retry of it. A fresh id only for a genuinely new transfer.
Mint the id before the first attempt and store it with your intent record. If you generate a new one when retrying, you have asked for a second transfer, and you will get one — idempotency cannot save you, because you never told the service it was the same transfer.
Repeat a request with the same id and the service resolves it against the original rather than re-broadcasting:
Which failures are safe to retry
That last row used to be the dangerous one. It no longer is, provided you reuse the id: the transfer stays claimed, so a same-id retry either replays the original result or returns 409 while the outcome is still unknown. It will not double-send.
The claim expires after 48 hours. Within that window a same-id retry is always safe; after it, the ledger row is gone and a repeat is treated as a new transfer.
When the outcome stays unknown
A 409 that persists means the service is holding an ambiguous attempt open — the transaction may be on-chain. Don’t fight it:
- Write an intent record before you call
/send— user, asset, recipient, amount, and theX-Request-Idyou generated. If your process dies mid-call, this row is the only evidence the attempt existed. - Retry with the same id, with backoff. A completed original replays its
txHash; an unresolved one keeps returning409. - If it is still
409after several minutes, poll/transactionsand watch/balancefor a matching drop — but read the warning below before you conclude anything from an absence. - Escalate to a human, quoting the
requestId, rather than re-sending under a new id.
“Not found in /transactions” does not mean “not sent.” On EVM chains history lags a fresh broadcast, so a transfer sent seconds ago is routinely absent. And a transaction row carries only hash, type, amount, fee, status and date, so you cannot positively match a specific transfer to a specific recipient. Re-sending under a new id because you didn’t find it is still how you send twice.
When a read fails
Failures are per-asset: a read failing for one asset says nothing about the others.
- A
500means the read failed on our side. Treat it as transient and retry with backoff. - A
503is not transient. That asset is unavailable on your endpoint, permanently — retrying will never help. Contact your operator. /infodegrades rather than failing: it can return the address and balance withtransactions: []. An empty list there is not proof the user has no history, so don’t treat it as one.