Repair, reconcile and diagnostic scripts
The command-line repair tools that ship with the platform — what each one fixes, which are dry-run by default, which write immediately, and which need the site stopped first.
About thirty command-line tools ship in the repo for jobs the admin panel cannot do: repairing rows a fixed bug left behind, reconciling locked balances against open orders, diagnosing a geo lockout from outside the HTTP stack, draining a mail queue. None of them are wired to a button anywhere, and most of them exist because the panel would have to be running for a button to work — which is exactly the situation several of these are for.
They all run from the project root on the server, with .env in place.
The convention: report first, write second
Almost every writing script here is a dry run by default and only changes
anything when you pass --apply. Run it, read the report, then run it again
with the flag. That is the shape of the whole set, and it is deliberate: a
report you disagree with costs nothing, and several of these scripts move
customer balances.
pnpm --filter backend repair:settings # report
pnpm --filter backend repair:settings --apply # writeThree exceptions you must know before you type anything:
pnpm fix:eco-orders and pnpm fix:eco-candles parse no flags at all. They
report what they found and then immediately write — refunding wallet balances,
cancelling orders in ScyllaDB and rewriting candles. There is no preview and no
undo. Take a database and ScyllaDB backup first.
pnpm rebuild:eco-orderbook uses --execute, not --apply. Passing --apply
to it does nothing at all, which reads as "the script did not work".
Where a script needs a flag that npm run would swallow, the repo ships a
second script name instead — verify:binary:fix, geo:doctor:fix,
eco:index:repair, icons:sync, staking:audit:repair, eco:mm:orders:clean.
Use those rather than trying to pass the flag.
Settings and permissions
repair:settings — numeric settings coerced to booleans
pnpm --filter backend repair:settings — backend/scripts/repair-coerced-settings.ts
The settings PUT route used to run every submitted value through a validator
that could not describe the settings body. Untyped values were coerced, so 1
in a number field was stored as the literal text "true". The route was
fixed; the rows were not. On a live install this was measured on
spotWithdrawFee, where the withdrawal route's parseFloat produced NaN — and
because x < NaN is false, both balance guards on that route went dead.
The script reads which keys are numbers from frontend/config/settings.ts (the
screen that writes them), maps "true" back to "1" and "false" back to
"0", and leaves switch-typed keys alone. It also reports a second bucket:
undeclared keys from extension settings screens whose name looks numeric
(…Fee, …Percent, …Amount, …Limit) and that hold a boolean. Those are
reported only — check each against its own screen and fix it there.
Safe on a live install. It writes through the cache manager, so the row, the Redis hash and every process's in-memory copy are all updated; no restart needed.
sync:permissions — permissions no role can be granted
pnpm --filter backend sync:permissions — backend/scripts/sync-route-permissions.ts
A route's permission gate passes when the caller's role holds the declared
permission or the caller is Super Admin. If that permission is not a row in
the permission table it can never be attached to any role, so the route is
Super-Admin-only, permanently and silently. Nothing creates those rows from
route metadata.
Inserting a permission grants nobody anything — it only makes the key available
to tick in Admin → Users → Roles & Permissions. That is why this is safe to
run. Report by default, --apply to insert.
Pair it with pnpm check:permission from the root, which reports drift between
the route metadata, the frontend manifest and the seeder without writing
anything. See Roles and permissions.
Compliance and infrastructure
geo:doctor — a geo rule that is blocking everyone, including you
backend/scripts/geo-doctor.mjs, exposed as three scripts:
| Command | What it does |
|---|---|
pnpm --filter backend geo:doctor |
Diagnose. Read-only, the default. |
pnpm --filter backend geo:doctor:fix |
Repair the minimum needed to make the platform reachable again. |
pnpm --filter backend geo:doctor:disable |
Switch geographic restrictions off entirely. |
The failure it exists for: Block when the country cannot be determined switched on for an install that has no way to determine anyone's country — no CDN country header, no IP lookup provider, or a reverse proxy hiding every visitor's real address. The country rules are then never consulted at all. Everyone gets the compliance notice, including you, and the notice looks identical to the feature working correctly.
It talks straight to the database, so it works when nothing else does: no
backend boot, no login, no HTTP, no admin session. Add a permanent escape hatch
for your own address with node backend/scripts/geo-doctor.mjs --allow-ip 203.0.113.4 --fix.
Two caches sit in front of the settings table. Each process holds them in
memory, and the shared Redis settings hash has no expiry — so an emptied
in-memory map refills from the stale hash, and even a full restart re-serves the
old values. The script writes the rows, drops the Redis hash and then announces
the invalidation, in that order. If it cannot reach Redis it says so and prints
what to run by hand rather than claiming a fix that has not landed.
Money and market data
Everything in this section touches customer funds, order books or candles. Take backups first, and read Backup and restore if you have not set up a ScyllaDB copy — the built-in backup screen covers MySQL only, and the ecosystem order book, trades and candles are not in MySQL.
| Command | Default | What it does |
|---|---|---|
pnpm fix:eco-orders |
dry run, --execute |
Finds ecosystem orders whose funds were never properly locked, releases the held amount to the wallet balance, marks the order CANCELED, removes its size from the aggregated order-book level and deletes its row from the matching engine's index. Read the dry-run list first: it compares one order against the whole wallet's inOrder, so it is deliberately conservative rather than precise. Restart the backend afterwards. |
pnpm reconcile:eco-inorder |
dry run, --apply |
Recomputes each ECO wallet's correct inOrder as the sum of holds attributable to that user's OPEN orders, and releases the surplus back to spendable balance. Never debits balance and never raises inOrder, so it cannot over-credit. --user <userId> scopes it to one account. Restart the backend afterwards. |
pnpm fix:eco-candles |
writes immediately | Merges duplicate candles and repairs open prices that do not match the previous candle's close, for both the ecosystem and futures keyspaces. |
pnpm rebuild:eco-orderbook |
dry run, --execute |
Recomputes every aggregated order-book price level from the actual open orders — deleting ghost levels, inserting missing ones, correcting mismatched amounts. Orders, trades, candles and funds are untouched. Takes an optional symbol (ETH/USDT). Symbols with an ACTIVE AI market maker are skipped unless --include-ai, because the AI maintains that book without real order rows. Restart the backend afterwards. |
pnpm eco:index:check |
read-only | Verifies open_orders_by_market — the index the matching engine reads by default — against the orders ledger, and exits non-zero on any disagreement, so it works as a deployment gate. MISSING rows are orders the matcher cannot see; STALE rows are the opposite and the worse-looking symptom, because the engine loads them as resting orders and the order book then displays depth that no customer can cancel. |
pnpm eco:index:repair |
writes (--apply --prune) |
Backfills and repairs that index, in both directions: re-inserts open orders that are missing from it and prunes rows the ledger has cancelled, closed or deleted. Run it before rebuild:eco-orderbook when a market is showing depth nobody can cancel — the engine rebuilds the book from this index, so repairing the book alone puts the level straight back. Restart the backend afterwards. |
pnpm eco:index:mark |
writes a marker | Marks the index as verified. |
pnpm eco:mm:orders |
survey only | Counts accumulated AI market-maker orders resting in the ecosystem book. --symbol=MASH/USDT scopes it. |
pnpm eco:mm:orders:clean |
writes (--apply) |
Cancels them. --keep=200 leaves the newest 200 per side. |
pnpm --filter backend p2p:reconcile |
dry run, --apply |
Normalises double-encoded JSON columns on p2p_offers and backfills p2pOffer.escrowAmount plus p2pTrade.escrowAmount/escrowStatus. |
pnpm --filter backend repair:currency-precision |
dry run, --apply |
Repairs exchange_currency.precision rows written by the old spot-currency import, which read a tick size like 1e-8 with parseInt and stored 1. Floors at 8; rows that already look like a plausible digit count are left alone. |
pnpm --filter backend verify:binary |
read-only | Audits binary options against your real database: deployed schema, market configuration, ScyllaDB reachability, and every settled order and admin-profit row already on the box. |
pnpm --filter backend verify:binary:fix |
writes | The above, plus backfilling unambiguous binaryMarket.source rows. |
pnpm --filter backend verify:binary:live |
writes and deletes | Creates a throwaway user, wallet, market and binary market, places and settles real orders through the production service, asserts the money moved correctly, then deletes everything. Refuses to run when NODE_ENV=production unless --force is also passed. |
pnpm --filter backend staking:audit:check |
dry run | Reports whether staking_admin_activities.userId is still NOT NULL in the database while the model declares it nullable. |
pnpm --filter backend staking:audit:repair |
writes (--apply) |
Drops the foreign key, makes the column nullable, re-adds the key. Schema auto-sync will never do this itself — the fingerprint manifest already records the column as nullable, so the ALTER is skipped on every boot forever. |
eco:mm:orders:clean cancels orders written with userId = botId and
marketMakerId/botId set. Those hold no customer funds and are settled from
the market-maker pool, so cancelling one moves no wallet balance and the script
performs zero MySQL writes. A real user's order looks identical apart from
those two columns being null, and cancelling one of those without releasing its
hold would strand a customer's funds in inOrder — which is why every row is
re-read and re-checked individually before it is touched. Do not reach for a
hand-written DELETE.
Mail and content
mail:purge — drain the outbound email queues
pnpm --filter backend mail:purge — backend/scripts/purge-email-queues.ts
Both email paths are Bull queues persisted in Redis, so a backlog survives a
backend restart and a code change. Every retry is a real SMTP login against your
production mail account, and enough of them gets the account throttled — Gmail
answers 454 4.7.0 Too many login attempts — which then blocks mail to genuine
users until it lifts.
The two queues are independent: emailQueue (transactional templates) and
notification-emails (the notification service). Report by default, --apply
to remove every waiting, delayed and failed job.
Read the summary before you pass --apply. If you are purging because the
provider has throttled you, set MAIL_DISABLED=true in .env and restart the
backend first, so nothing refills the queue while you work.
repair:faq-escapes — entity-escaped FAQ text
pnpm --filter backend repair:faq-escapes — backend/scripts/repair-faq-entity-escapes.mjs
Until 6.0.9 the FAQ addon escaped plain-text fields on the way into the
database, so what got stored was the entity soup itself — What's the fee?
became What's the fee? — and re-saving escaped the previous pass's
ampersands too, compounding the damage. The code path is fixed, but nothing
repairs the existing rows and nothing will: those fields are only rewritten when
an admin edits that exact FAQ.
It classifies rather than assumes, because a single-level escape is genuinely
ambiguous (R&D is what the bug produces from R&D, and also what an
author legitimately typing R&D would leave):
| Bucket | Meaning | Repaired |
|---|---|---|
COMPOUND |
Nested entity, depth ≥ 2. Only repeated machine escaping produces this. | Automatically |
UNAMBIGUOUS |
Depth 1 and contains one of ' / \ `, none of which any input path in this addon produces. |
Automatically |
AMBIGUOUS |
Depth 1 with only & < > ". |
Never, without --interactive and a human answering per row |
Flags: --apply, --apply --interactive, --table=faq_feedbacks, --json.
After a successful --apply a re-run must report 0 COMPOUND and 0 UNAMBIGUOUS;
the AMBIGUOUS bucket is allowed to stay non-zero forever.
icons:report / icons:sync — missing currency icons
backend/scripts/sync-crypto-icons.mjs, exposed as three scripts:
| Command | What it does |
|---|---|
pnpm --filter backend icons:report |
Report the gaps. Changes nothing. |
pnpm --filter backend icons:sync |
Fill them, using local files first and the network only where it must. |
pnpm --filter backend icons:sync:offline |
Fill only what can be resolved without any network call. |
Every currency renders as /img/crypto/<symbol>.webp, and a symbol with no file
renders as a broken image or a meaningless gold-coin placeholder. A file counts
as missing when it is absent or byte-identical to generic.webp, because an
earlier bulk fill copied the placeholder over about 88 symbols and a plain
existence check reports those as present.
Most of the work needs no network: an ecosystem token usually already carries
its own logo on disk under /blockchains/<chain>/assets/<key>/logo.webp, and
exchange derivative names (ACE3L → ACE, 1000KQUACK → QUACK) resolve to a
base icon you already own. --enabled-only restricts the run to currencies that
are actually switched on, which on a typical install is a tiny fraction of the
gaps.
Lifecycle helpers
graceful-stop — drain withdrawals before maintenance
node backend/scripts/graceful-stop.mjsA blind pnpm stop can kill the backend mid-withdrawal-broadcast. This polls
for in-flight withdrawals to finish and only then performs the normal stop. The
ceiling is GRACEFUL_STOP_TIMEOUT_MS in .env, default 120000 (two
minutes); when in-flight state cannot be observed at all it falls back to a
fixed 10 second grace window so HTTP requests can finish, which is still
strictly safer than an instant kill.
It is a soft drain. It waits on withdrawals, not on the open-order matcher —
there is no read-only mode that stops accepting new orders first. Use it before
any deliberate restart that is not already going through pnpm update-all,
which runs it for you.
Headless and managed operations
env-manager — edit .env without breaking it
node scripts/env-manager.mjs get [--json]
node scripts/env-manager.mjs set KEY=VALUE [KEY2=VALUE2 ...] [--restart]Targeted line replacement that preserves comments, section headers, ordering and
quoting — rather than round-tripping through a parser and serialiser, which
strips all of that. Every write snapshots a timestamped .env.bak and writes
atomically (temp file, then rename).
set --restart restarts the backend through the graceful drain above,
health-checks it, and automatically rolls back to the snapshot if the process
does not come back healthy.
Two guardrails you cannot switch off:
ENCRYPTED_ENCRYPTION_KEYandENCRYPTION_KEY_PASSPHRASEcan never be edited through this tool. Altering either permanently bricks every encrypted wallet on the install.- Secret-looking keys are redacted on read — anything matching
SECRET,PASSWORD,PASSPHRASE,PRIVATE,MNEMONIC,SEED,TOKEN,API_KEY,WEBHOOKand similar is reported as set or unset, never printed.
Headless product activation lives on the licensing page:
pnpm activate-product <productId> <purchaseCode> [clientName].
What needs the platform stopped
| Script | Requirement |
|---|---|
pnpm eco:mm:orders:clean |
Stop the backend first. Cancelling underneath a live matching engine races its settlement — the engine can fill an order between this script reading it and cancelling it. The script probes NEXT_PUBLIC_BACKEND_PORT and warns, but that check is advisory: it cannot see a backend running on another host. |
pnpm fix:eco-orders --execute |
Restart the backend afterwards; the script says so on completion. |
pnpm reconcile:eco-inorder --apply |
Restart the backend afterwards so cached wallet rows refresh. |
pnpm rebuild:eco-orderbook --execute |
Restart the backend afterwards so the matching engine reloads the book. |
pnpm eco:index:repair |
Restart the backend afterwards. |
pnpm --filter backend staking:audit:repair |
Restart the backend afterwards so the models see the new column definition. |
pnpm --filter backend verify:binary:live |
Not for production. It refuses to run with NODE_ENV=production unless --force is passed. |
Everything else on this page is safe on a live install: repair:settings,
sync:permissions, geo:doctor (all three modes), mail:purge,
p2p:reconcile, repair:currency-precision, repair:faq-escapes,
verify:binary in its default read-only mode, icons:*, and every --check or
report-only run of the rest.
fix:eco-orders, reconcile:eco-inorder, fix:eco-candles,
rebuild:eco-orderbook, eco:index:repair and eco:mm:orders:clean all write
to ScyllaDB, and the first two also write wallet balances in MySQL. The
platform's own backup screen dumps MySQL only — the ecosystem and futures
keyspaces are yours to copy. See Backup and restore.