Connecting a spot exchange provider
How to licence, credential, enable and verify a spot exchange provider, and how markets, currencies, balances, fees and chart data all follow from that one connection.
Spot trading on Bicrypto is not a venue you run. It is a connection to somebody else's exchange: your customers' orders, the currencies they can hold, the pairs they can trade, the deposit networks and the price of everything are all read from one provider over its public API. Until that connection is up, the spot half of the platform has no data to show.
Exactly one provider is active at a time. Three are supported:
| Provider | name in the database |
Product id | Extra credential |
|---|---|---|---|
| Binance | binance |
38650585 | — |
| KuCoin | kucoin |
37179816 | API passphrase |
| XT | xt |
54510301 | — |
All three are separately licensed products. The rows are seeded on install, so all three appear in the Extension Manager whether or not you own them.
The Ecosystem (on-chain) and Futures engines have their own market data and do
not use this connection. Fiat rates come from the FX providers configured in
.env. What breaks without a provider is spot markets, spot currency listings,
spot deposits and spot withdrawals.
The order things have to happen in
Each step depends on the one before it, and three of them are easy to do out of order and then spend an afternoon on.
-
Activate the licence. Open the hub at Finance → Trading Infrastructure → Exchange Providers. If the provider shows Unlicensed, the Activate License button deep-links to
/admin/system/licensewith the product id already filled in. Activation writeslic/<productId>.licin the project root, and that file is what everything else checks. -
Put the API credentials in
.env. The names are built from the provider alias — see below. This is a file edit, not a screen. -
Restart the backend.
.envis read at boot. A key you pasted while the server was running is not inprocess.env, and the hub will keep reporting API credentials are missing from environment variables until you restart. -
Enable the provider. The on/off switch is on System → Extension Manager, filtered to exchanges (
/admin/system/extension?type=exchange) — not on the exchange hub. Enabling one provider disables every other in the same transaction. -
Verify the credentials. Back on the hub, press Verify Credentials. This loads markets and fetches your balance from the exchange; a green result means the keys are real, accepted and permitted.
-
Import markets, then import currencies, then build chart data — in that order, because currencies are derived from the markets you enabled and charts are built per market.
The credentials, and why you cannot grep for them
The backend never hardcodes a variable name. It builds them at runtime from the
active provider's name, uppercased:
APP_${PROVIDER}_API_KEY
APP_${PROVIDER}_API_SECRET
APP_${PROVIDER}_API_PASSPHRASESo the real names are:
| Provider | Variables |
|---|---|
| Binance | APP_BINANCE_API_KEY, APP_BINANCE_API_SECRET |
| KuCoin | APP_KUCOIN_API_KEY, APP_KUCOIN_API_SECRET, APP_KUCOIN_API_PASSPHRASE |
| XT | APP_XT_API_KEY, APP_XT_API_SECRET |
The passphrase is read for every provider but only KuCoin issues one; the other two ignore it. Key and secret are both mandatory — an empty string counts as missing, and the connection is refused before a request is made.
When verification fails, the hub prints the exact variable names it expects for
the provider you have active. That block is generated from the alias, not from a
translation, so it is always the literal name to paste into .env.
With the variables absent or empty, no connection is built at all: the
loader logs API credentials for <provider> are missing. and returns
nothing, so every spot caller — public data included — has no exchange to talk
to. That failure is at least total and obvious.
The quiet one is a key that is present but rejected. When market loading fails on it, the platform falls back to an unauthenticated connection to the same exchange and logs a warning. Public data — markets, tickers, candles — keeps working, so the site looks healthy. Authenticated data does not: deposit and withdrawal networks come back empty and balances are unavailable. A "currency not found" report from a customer trying to deposit is usually this.
See Environment variables for the full list and for
NEXT_PUBLIC_EXCHANGE, which selects chart symbols in the frontend and has
no effect on which provider the backend connects to.
The hub
/admin/finance/exchange is the only exchange screen in the menu. It shows the
provider that is currently enabled, its version, its status and its licence
state, over three tabs.
| Tab | What is on it |
|---|---|
| Overview | The provider's description. Informational. |
| Regions | The countries the exchange supports or restricts. Binance links to its own region selector; KuCoin and XT list their restricted regions inline. Read this before you pick a provider — it is the exchange's own eligibility rule, and it applies to your server's location as much as to your customers'. |
| Settings | Status, licence and version tiles, plus the proxy configuration. |
Two buttons sit in the header on every tab: Verify Credentials, and Activate License when the licence file is absent.
If no provider is enabled at all, the page replaces itself with a "No active exchange provider" panel that links to the Extension Manager. That is the expected state on a fresh install, not a fault.
Enabling, and the licence check
Enabling is refused with a 403 and licenseRequired: true when
lic/<productId>.lic does not exist. Licence first, always.
Enabling also runs UPDATE exchange SET status = false for every other row in
the same transaction, so you never have two providers half-connected. Switching
provider is one action, and it takes effect for new connections immediately.
The hub cross-checks the stored licence flag against the file on disk each time you open it and repairs the row if they disagree, which is why a provider can go from Unlicensed to Licensed on a refresh with no action from you.
Verifying
Verification synchronises the clock, loads markets and calls fetchBalance. It
is deliberately not written to the audit trail — it reads, it changes
nothing, and saving the key is the action that gets audited.
The message it returns is the diagnosis. The ones worth recognising:
| Message | What it means |
|---|---|
| API credentials are missing from environment variables | The variables are absent or empty in the running process. Check the spelling against the table above, then restart the backend. |
| Invalid API credentials | The exchange rejected the key. Usually a copy/paste truncation or a key that has been revoked. |
| Access denied: your server's location is blocked | HTTP 451. The exchange will not serve your server's country. Configure a proxy, below. |
| Insufficient API permissions | The key exists but the exchange has not granted it the rights the platform needs. |
| Server time synchronization failed | Clock skew. See the last section. |
The proxy
If the exchange blocks your server's region, the Settings tab takes a proxy URL
in http://, https://, socks4:// or socks5:// form, with credentials
embedded if needed. Two buttons, and the order matters:
Test Proxy builds a throwaway connection using the URL in the box — not the saved one — and asks the exchange for its server time. Save Proxy Settings then persists it to the provider row. Test first: saving a proxy that does not answer takes the whole spot connection down until you clear it.
A proxy that connects but is itself in a blocked region reports that distinctly, which is the common case with cheap datacentre proxies.
The four screens with no menu entry
The hub's Quick Actions grid is the only way into these, other than typing the URL. None of them appear in the admin navigation.
| Screen | Path | What it is |
|---|---|---|
| Markets | /admin/finance/exchange/market |
The trading pairs you list |
| Charts | /admin/finance/exchange/chart |
The OHLCV cache console |
| Balances | /admin/finance/exchange/balance |
What your exchange account actually holds |
| Fees | /admin/finance/exchange/fee |
Fees charged versus fees the exchange took |
Balances and Fees are greyed out while credential verification is failing —
both are authenticated calls and would only show you an error. Currencies is
also on that grid, but it points at /admin/finance/currency/spot, which does
have a menu entry under Currency Management.
Markets
Import Markets is preview-first: without confirm=true it reports what
would change and writes nothing, so you see the delete count before agreeing to
it. Delisted markets that still carry OPEN orders are never removed — the funds
those orders hold would be stranded.
You cannot create a market by hand; the button is off. What you decide per row
is status (listed or not), isTrending, isHot, and the price/amount
precision and maker/taker figures held in the row's metadata.
Currencies
Same principle, one screen along at Finance → Currency Management → Cryptocurrencies.
The import is also preview-first, and it refreshes name, precision and fee for
currencies you already hold. It deliberately does not touch status:
enabling a currency is your decision, and an earlier version of this import
switched every enabled currency off on each run.
The missing currencies alert at the top of the screen is the one to act on. It compares the currencies used by your enabled markets against the currencies you have enabled, and a pair whose base or quote is not enabled will not trade. The Activate Missing Currencies button enables the lot in one call.
You cannot create a currency by hand on either screen. The provider supplies the list; you enable, disable and price what it offers.
Chart data
/admin/finance/exchange/chart needs view.exchange.chart to open and
manage.exchange.chart for all three actions. Candles live in two places at
once: gzipped files under data/chart/<base>/<quote>/<interval>.json.gz, and
Redis keys ohlcv:<symbol>:<interval> with a 24-hour TTL.
Build takes the markets to cover (all enabled markets if you select none), the intervals, Historical Days, and Rate Limit (ms) — the delay between requests to the exchange. It runs as a background job, reports progress over a WebSocket, and returns an estimate up front — markets × intervals × delay. On a hundred markets across thirteen intervals that is hours, not minutes, so start it when nobody needs the connection.
Clean removes both the Redis keys and the files, per symbol and interval. Fix finds holes in one cached series and re-fetches up to ten of them.
This exists because a customer opening a chart on a cold cache makes the platform fetch that history from the exchange live, one page of candles at a time, while they wait. Do it for a hundred customers on a hundred symbols and the exchange rate-limits you — which does not just slow charts down. It sets a platform-wide ban key.
The console has no tabs. The Settings button in its header opens a Chart
Cache Settings dialog, and saving it writes cacheDays, rateLimit,
intervals and autoUpdate into the chart_cache settings row. Only two of
them are consumed: Cache Duration (days) is the figure on the header tile
labelled Cache Days, and Rate Limit (ms) is the delay the per-row Fix
action uses (it posts the saved rateLimit, or 500 ms if you never saved one).
There is no background job that rebuilds charts on a schedule, so Auto
Update changes nothing today, and the Build dialog starts from the built-in
defaults rather than from the Default Intervals you saved here. Set the
days, delay and intervals in the Build dialog itself.
The console also reports the exchange ban status with a countdown — not in
that dialog, but as a red Exchange API Rate Limited banner across the top of
the page, above the stat cards, shown only while the ban is live. It is backed
by a Redis key (exchange:ban_status) set when the provider rate-limits or bans
the server, and while it is set the Build Charts button is disabled.
Every spot path — the price cron, order reconciliation, deposits, withdrawals
— quietly does nothing while reporting success. It carries a TTL and clears
itself; nothing you press makes it clear sooner.
The currency cache job
cacheExchangeCurrencies runs hourly from the cron process and writes the
whole currency list into the Redis key exchangeCurrencies. The key's own TTL
is 120 seconds, tied to the price refresh cadence so a cached price can never be
more than one tick behind the database row.
The public currency endpoints fall back to the database on a miss but never repopulate the cache themselves, so with cron stopped they keep answering correctly and slightly slower. Confirm the job is running on System → System Monitoring → Scheduled Tasks; see Processes and ports for what else stops when the cron process is down.
When the clock is the problem
Signed exchange requests carry a timestamp, and Binance rejects any request whose timestamp is more than a second ahead of its own clock. The platform measures the offset itself before it loads markets, brackets the round trip so network latency is not counted as drift, and deliberately signs half a second behind the server so ordinary jitter cannot push it over the line. The offset is refreshed in the background every five minutes on a long-lived connection.
The direction of that measurement is the thing to know if you ever read the
logs: timeDifference is local minus server — a positive value means your
server's clock is running ahead of the exchange. It is logged as "Clock drift:
Nms, signing offset: Nms", and a drift above five seconds is logged as a
warning in its own right.
Symptoms of real skew: verification failing with server time synchronization failed, or repeated timestamp for this request was outside of the recvWindow errors in the backend log. The platform retries these automatically with a fresh sync, so a handful in the log is the mechanism working. A continuous stream is your box.
The fix is NTP on the host, not a setting in the panel. Once the system clock is disciplined, restart the backend so a new connection is built with a clean offset.
Permissions
| Key | Grants |
|---|---|
access.exchange |
Opens the hub |
view.exchange |
Reads the active provider and its status |
edit.exchange |
Enable/disable, verify, proxy save and test, licence activation |
access.exchange.market · view.exchange.market |
The Markets screen |
create.exchange.market |
Import markets |
edit.exchange.market · delete.exchange.market |
Change or remove a market row |
view.exchange.chart |
Opens the Charts console |
manage.exchange.chart |
Build, clean and fix — all three write actions |
view.exchange.balance · view.exchange.fee |
The Balances and Fees screens |
access.spot.currency · view.spot.currency |
The Cryptocurrencies screen |
create.spot.currency |
Import currencies |
edit.spot.currency |
Enable or disable a currency |
There is no bare create. or delete. key for providers themselves — only for
the markets and orders beneath them. Providers are seeded rows; you enable one,
you do not add one. See
Roles and permissions for how these keys are
granted.