Currencies and markets
The two importers that turn XT's listings into your tradable pairs — what they write, what they delete, how XT's data differs from other providers, and where the fees come from.
Nothing on your platform is tradable until two importers have run. They are separate, they write to different tables, and they have different failure modes.
- Spot currencies (
exchange_currency) — the assets a SPOT wallet can hold. Admin → Finance → Currency → Spot. - Exchange markets (
exchange_market) — the tradable pairs, with precision, order limits and maker/taker rates. Admin → Finance → Exchange → Markets.
Both are preview-first. Run without confirming and they return a plan and write nothing. That is not decoration: both importers delete rows.
Importing currencies
The import calls loadMarkets() and reads exchange.currencies, then runs each
one through the XT-specific normaliser before storing it.
The plan it returns names four numbers — toCreate, toUpdate, toDelete and
enabledCount — plus the first 25 currency codes queued for deletion. Read the
delete count before you confirm. A currency XT has stopped listing is removed
from your platform.
Five columns are persisted per currency: the code, name, precision, status and
fee. status is deliberately not written on an update — re-importing to
refresh precision or fees does not switch off the currencies you had enabled. New
currencies arrive disabled, and enabling them is a separate, explicit action.
After a confirmed import the price job runs immediately so the new currencies are not sitting at a null price.
exchange_currency has no column for chains. The importer normalises XT's
per-network data and then keeps only the five fields above. The network list a
customer sees on the deposit or withdrawal screen is fetched from XT at the
moment the screen loads, via fetchCurrencies().
Two consequences: a network XT adds or suspends appears or disappears without a re-import, and every network list is an authenticated call — so on a connection that has fallen back to a public instance, every asset shows no networks at all.
How XT's currency data differs
The normaliser for XT is not the same code that handles Binance or KuCoin, and the differences show up wherever network data is read:
withdrawStatus: data.info.withdrawStatus === "1",
depositStatus: data.info.depositStatus === "1",
withdrawMemo: false, // XT data doesn't have memo information
chainId: networkKey.toUpperCase(),Deposit and withdraw flags are string comparisons. XT reports "1" / "0"
in the raw payload rather than booleans. Anything that is not the string "1"
reads as disabled.
active is always undefined. XT publishes no explicit enable flag on either
currencies or networks. The platform's rule is therefore that only an explicit
false counts as disabled, and deposit/withdraw are the real signal. Do not
interpret a blank active flag as an XT suspension.
Memo support is always reported as false. There is nothing in XT's payload to derive it from. For assets that genuinely require a destination tag or memo, this flag will not tell you — treat it as unknown rather than as "no memo needed".
The chain id is the raw network key, uppercased. XT's own network naming is what the platform carries, which is the root of the translation problem covered in Deposits and withdrawals.
A currency whose fee will not parse as a number is skipped entirely — no row, no warning in the plan. If an asset you expect is simply absent after an import, that is the likeliest reason.
The ticker can be in either field. XT puts the lowercase exchange id in id
and the ticker in code — btc against BTC — where Binance, KuCoin and OKX
put the ticker in both. Currency lookups match on either field,
case-insensitively, because matching on id alone made every XT currency
lookup return a 404.
Importing markets
The market import is more selective than it looks. A symbol is only imported if all of these hold:
- The market is
activeon XT. - It has both a price precision and an amount precision.
- It is a spot market —
market.spot === trueormarket.type === "spot". - Its symbol contains no colon.
The last two exist because ccxt loads swap and perpetual markets alongside spot
for several exchanges, and importing them poisons exchange_market with symbols
like BTC/USDT:USDT that break multi-symbol spot calls.
Each imported market stores a metadata blob:
{
"taker": 0.002,
"maker": 0.002,
"precision": { "price": 6, "amount": 4 },
"limits": {
"amount": { "min": 0.0001, "max": null },
"price": { "min": 0, "max": null },
"cost": { "min": 0.0001, "max": 9000000 },
"leverage": {}
}
}New markets are created with status: false. They will not appear on /market
until you enable them.
What the market import will not delete
Markets XT no longer lists are queued for deletion — with one exception that is worth understanding, because it is the difference between a tidy import and stranded customer funds.
Every open order holds inOrder against a customer's wallet balance. Deleting
the market out from under it strands that money with nothing in the database
pointing at it. The importer counts open orders per delisted symbol, keeps those
markets, and reports them as keptForOpenOrders. Cancel the orders, then
re-import.
Order history is never deleted. The importer reports retainedOrderCount — how
many order rows survive on the markets being removed — because a delisted market
does not un-happen the trades that occurred on it, and in most jurisdictions this
product is sold into, that record is one the operator is required to retain.
Watchlist entries pointing at a removed market are deleted. They are a user preference, not a record.
Fees are yours, not XT's
The import copies XT's maker and taker rates into the market metadata, but from that point on the numbers are editable and the platform charges what your row says:
const feeRate = side === "BUY" ? Number(metadata.taker) : Number(metadata.maker);
const feeCurrency = side === "BUY" ? currency : pair;Two consequences.
The taker rate applies to buys and the maker rate to sells, regardless of whether the order was actually a maker or taker fill on XT's book. If you set them to different values, that asymmetry is what your customers pay.
Your fee and XT's fee are independent. XT charges your account its own rate on every fill. If your stored rate is lower than XT's, each trade costs you the difference. Set your rates from your XT fee tier, and revisit them when that tier changes — nothing re-imports them for you.
Edit a market at Admin → Finance → Exchange → Markets → the row's edit action.
Precision and limits are validated against on order entry before the order ever reaches XT, which is why a wrong value here shows up as "Amount is too low" on a perfectly normal order rather than as an exchange error.
Chart data
Live candles stream from XT over watchOHLCV. Historical candles are fetched
with fetchOHLCV and cached, and the cache is managed at Admin → Finance →
Exchange → Chart.
XT publishes these timeframes: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 6h,
8h, 1d, 3d, 1w, 1M. A chart interval outside that set has no source.
Build jobs pace themselves with a configurable delay between requests. Leave it alone unless you are being rate-limited — a build that trips XT's limiter sets the platform-wide ban switch and takes the whole spot stack down with it, not just the chart builder.
Re-importing safely
After a provider switch, always. Otherwise: when XT lists an asset or pair you want, when XT delists one, and when your XT fee tier changes and you want the defaults refreshed. There is no scheduled job that does this — both imports are manual.
No. The currency importer deliberately omits status from its update path,
because it used to write false there and silently switched off the entire
listed set on an unconfirmed GET.
Yes, for markets. The market importer only creates rows it has not seen before, so an existing market keeps your edited metadata — but a market that was deleted and re-created by a later import comes back with XT's rates. If you run custom fees, record them outside the database.
Three candidates, in order of likelihood: XT returned a fee that will not parse
as a number and the row was skipped; the currency has no precision and was
filtered out; or the connection fell back to an unauthenticated instance, in
which case fetchCurrencies returns far less than it should. Check the backend
log for the fallback warning before assuming XT delisted it.
That is not an import problem — networks are never stored. The deposit screen calls XT live, and that call is authenticated. An unauthenticated fallback instance, a key without deposit-address permission, or an active ban switch all produce the same empty list.
Because the previous import ran against a public, unauthenticated instance. Once credentials work the payload changes shape. Always re-run both imports after fixing a credential fault, and read the delete counts carefully.