Permissions, background jobs and Redis keys

Every permission key the Binance spot surface checks, the five cron jobs that keep the ledger reconciled with their real periods, and the Redis keys that cache or mute it.

7 min readUpdated 6 August 2026permissions, roles, cron, redis, reference

Three things decide whether this integration works for somebody other than you: which permission keys their role holds, whether the cron process is running, and whether a Redis key is quietly telling every spot path to do nothing. All three are listed here.

The connection itself — clock sync, instance caching, rate limits, the proxy — is in Connection, rate limits and bans.

Permissions

Two different layers read these keys and they read them for different reasons.

  • access.* keys decide what an operator can see. frontend/config/menu.ts hides a menu entry whose permission the role does not hold, and the admin data tables refuse to render without their access key. No backend route is gated on an access.* key.
  • view.* / create.* / edit.* / delete.* / manage.* keys are what the API enforces. A missing one answers 403 Forbidden - You do not have permission to access this.

Super Admin bypasses every check. The role gate short-circuits on userRole.name === "Super Admin", so a permission problem is invisible while you are testing as the owner account. Test role changes on a real staff account.

The exchange surface

Permission What it gates
access.exchange The Trading Infrastructure → Exchange Providers menu entry and /admin/finance/exchange
view.exchange Reading the provider list, and the active-provider check the exchange hub runs on load
edit.exchange Enable/disable a provider, activate a licence, save or test the proxy, Verify Credentials
access.exchange.market The markets table
view.exchange.market Listing markets, and reading one
create.exchange.market Import Markets
edit.exchange.market Editing a market's precision, limits and fees
delete.exchange.market Removing a market row
edit.ecosystem.market Enabling or disabling a market — see the trap below
access.spot.currency The spot currency table
view.spot.currency Listing currencies, and the missing currencies check
create.spot.currency Import on the spot currency screen
edit.spot.currency Editing a currency, and toggling its status
view.exchange.balance /admin/finance/exchange/balance — the live account balances
view.exchange.fee /admin/finance/exchange/fee — the fee comparison
view.exchange.chart /admin/finance/exchange/chart, and the cache settings response
manage.exchange.chart Build, Fix gaps, Clean, and saving cache settings
access.exchange.order The Order Management → Spot Orders menu entry and the table
view.exchange.order Listing spot orders, and opening one
edit.exchange.order Updating an order, and the single and bulk status routes
delete.exchange.order Deleting spot orders, single and bulk
view.exchange.watchlist Reading one watchlist row

The two routes that flip a market's status live under /api/admin/finance/exchange/market but are gated on edit.ecosystem.market, not edit.exchange.market:

Enables or disables one market. Gated on the ECOSYSTEM key.
Bulk enables or disables markets. Gated on the ECOSYSTEM key.

The switch in the markets table renders regardless — the toggle cell is not permission-aware — so the operator flips it, it snaps back, and nothing on screen says why. The backend logged a 403.

Grant edit.ecosystem.market to any role that curates the market list, even on an install that has never had Ecosystem. It is the only key that controls which pairs your customers can trade.

The money surface

Withdrawals, wallets and the transaction ledger are core screens, not exchange-provider screens, so their keys are unrelated to exchange.*. An operator who is meant to handle customer payouts needs these as well.

Permission What it gates
access.withdraw The Withdrawal Management → Withdrawal Records menu entry and /admin/finance/withdraw/log
view.withdraw Listing withdrawals and opening one
edit.withdraw The bulk approve/reject route on the withdrawal queue
delete.withdraw Deleting withdrawal rows
edit.wallet The single-row approve and reject routes — the ones that actually pay
access.wallet / view.wallet /admin/finance/wallet
delete.wallet Deleting a wallet row
access.transaction / view.transaction /admin/finance/transaction
access.cron / view.cron /admin/system/cron
manage.cron Triggering a job by hand

Both surfaces end up in the same two handlers, but they arrive by different routes with different keys. The row buttons and the bulk menu on /admin/finance/withdraw/log send the queue-decision route; the buttons on /admin/finance/withdraw/log/{id} call the wallet routes directly:

Pays out one spot withdrawal against the exchange.
Rejects and refunds one withdrawal. Requires a reason.
The queue decision route behind both the row buttons and the bulk menu. Delegates each id to the two handlers above.

A role with edit.withdraw but not edit.wallet can decide every row from the queue and cannot decide a single one from a row's own detail page. The reverse is also true. Grant both to anyone who handles payouts.

Keys that are seeded but gate nothing

The permission seeder writes these; no route or screen in the Binance surface checks them. Granting them changes nothing, and their absence is not your bug.

Key Why it is inert
create.exchange.order The spot order table declares it, but canCreate is false and there is no admin order-create route. Orders are placed by customers
delete.spot.currency There is no delete route under /api/admin/finance/currency/spot. Currencies are removed by the import, not by hand

The one grant to think twice about

edit.exchange is not "can edit some settings". It is two very consequential powers behind one key:

  • It can switch the whole spot back end to another exchange. PUT /api/admin/finance/exchange/provider/{id}/status disables every other provider in the same transaction. So does the licence activation route. The markets, currencies, open orders and customer balances all stay pointing at Binance — see Switching the active exchange provider for what that costs.
  • It can route every exchange call through a host of the holder's choosing. The proxy field on the exchange hub's Settings tab is saved by PUT /api/admin/finance/exchange/provider/{productId}, which also evicts the cached connection so the next request rebuilds through the proxy. Credentials embedded in the URL are masked on read (//***:***@), so the value cannot be read back out of the panel — but everything the platform sends to Binance, signed with your API key, goes through whatever was saved.

view.exchange is safe to hand out. edit.exchange belongs to the same small group of people who hold the .env file.

Background jobs

All of these run in the cron process, not the web process. production.config.js starts backend with CRON_MODE=off and a separate cron app with CRON_MODE=only; a single-process install sets CRON_MODE=inline instead. If the cron app is not running, none of the reconciliation below happens and nothing on any screen says so.

Job Period What it does
processPendingSpotOrders 60 s Reconciles OPEN spot orders against Binance — credits fills, refunds cancellations, handles partials
processCurrenciesPrices 2 min Rewrites exchangeCurrency.price for every enabled currency
reconcileSpotWithdrawals 5 min Crash recovery: finalises PROCESSING withdrawals whose send committed but whose status update was lost
processSpotPendingDeposits 15 min Catch-up deposit detection for anything the live WebSocket missed
processPendingWithdrawals 30 min General sweep of pending withdrawals against Binance's withdrawal history
cacheExchangeCurrencies 60 min Warms the Redis exchangeCurrencies blob

Two of these are load-bearing in ways their names do not suggest.

reconcileSpotWithdrawals is the one that prevents a customer being left short. It only looks at PROCESSING + WITHDRAW rows older than 5 minutes, scoped away from Ecosystem rows, and it caps itself: 200 rows per run, at most 2,000 rows scanned while paging past ones already in review backoff. A large backlog therefore clears over several runs rather than one. Rows older than 24 hours become eligible for an evidence-based refund after a 1,000-record lookback against Binance's own withdrawal history.

processPendingWithdrawals skips a row with no referenceId. That is deliberate — a row with no exchange id has not been sent, so this job has nothing to look up. Those rows are reconcileSpotWithdrawals's job.

cacheExchangeCurrencies, and the cache that is usually cold

The job writes the enabled-currency list to Redis under exchangeCurrencies with a 120-second TTL, tied to the 120-second price cadence so the cached price can never be more than one tick behind the row.

The job itself runs hourly. The arithmetic is unavoidable: the key exists for two minutes out of every sixty, and for the rest of the hour /api/exchange/currency and /api/exchange/currency/{id} miss and read the database instead. The endpoints never repopulate the key — they fall back and return, they do not write. So the cache is a small hourly optimisation, not a dependency, and a Redis outage does not take the currency endpoints down.

What it does mean: nothing else warms this key. If you flush Redis, the currency endpoints are on the database until the next hourly tick, which is fine, and if you were expecting the cache to absorb load it is not doing that.

processPendingWithdrawals is explicit about it — "Exchange unavailable; skipping pending withdrawal run" — and the others follow the same shape: they ask ExchangeManager.startExchange() for a connection, get null, log, and return. The admin cron screen shows them as completed. See the ban key below.

Watching them

Admin → System → Cron (/admin/system/cron) lists every job with its last run, last error and next scheduled run, and streams live progress over a WebSocket.

Lists every registered job with its schedule and last run.
Runs one job immediately, out of schedule.

A job that throws is recorded with lastRunError and a failed status; processPendingSpotOrders deliberately rethrows so a dead reconciler is distinguishable from a healthy one on that screen.

Backend log tags worth grepping: EXCHANGE (connection, credentials, clock, ban), SPOT_RECON (order reconciliation), WITHDRAW and WALLET (payouts), SPOT_DEPOSIT (the live deposit verifier), CRON (the jobs themselves).

Redis keys

Key Written by TTL Cleared by
exchange:ban_status The rate-limit handler, and any error text containing IP banned until matches the remaining ban, max 24 h its own TTL, or a manual DEL
exchange:tickers The ticker WebSocket, once a second none overwritten each flush; filtered to enabled markets
ohlcv:<symbol>:<interval> Build charts and Fix gaps, and the public chart read-through path, alongside data/chart/<base>/<quote>/<interval>.json.gz 24 h from the two admin actions, none from the public path its own TTL, or the chart Clean action
exchangeCurrencies cacheExchangeCurrencies 120 s its own TTL
ecosystem_token_icon:<currency> The public market list, per currency 1 h its own TTL

Note what has no TTL. An ohlcv:* key written by Build charts or Fix gaps self-expires after 24 hours, so the keys an operator creates from that screen do clear themselves. The same key written by the public chart read-through path does not, and the gzipped files behind it never expire at all — so a miss just reloads the old candles from disk into Redis with no TTL, which is why candles from a previous provider keep being served after a switch. Clean on /admin/finance/exchange/chart is what removes both. exchange:tickers is a single key rewritten in place.

exchange:ban_status is a kill switch

The value is an epoch-millisecond unblock time, stored as a string, with the key's TTL set to the remaining seconds so it clears itself.

It is written in two places:

  • The rate-limit handler. A ccxt.RateLimitExceeded, or Binance code -1003, while the connection is being built records a one-minute ban and sleeps a minute before retrying.
  • Any error message containing IP banned until <epoch>. That number is scraped out of the free-form text and used as the expiry.

Both go through the same clamp: a time already in the past, or one that does not parse, is ignored; a time more than 24 hours away is clamped to 24 hours with a warning naming the original value. That guard exists because the number comes from error text — a units mismatch would otherwise park a permanent outage in a key nothing routinely inspects.

startExchange() checks the key first and returns null before it looks at anything else. Order placement, the ticker stream, the price cron, the deposit verifier, the withdrawal reconciler, the balance screen, the fee screen and the chart build all receive nothing, log a warning and return. No screen turns red. The platform reports itself healthy and stops moving.

The one place it surfaces in the panel is the chart settings response, which reports whether a ban is active and how many seconds remain — and the chart page uses it to disable the Build charts button.

Reads the chart cache settings and the current ban state.

Deleting the key early is what unwedges the stack if you know Binance has lifted the block. Deleting it while Binance is still throttling you simply earns a longer one.

What we could not determine

  • There is no admin screen that lists Redis keys or lets you delete one. Clearing exchange:ban_status means a redis-cli DEL on the server.
  • Nothing records which permission key a 403 was raised for, on the client side. The response is the generic "Forbidden - You do not have permission to access this"; identifying the missing key means reading the route's metadata or the backend log.