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.

7 min readUpdated 6 August 2026scripts, repair, reconcile, cli, maintenance

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    # write

Three 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:settingsbackend/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:permissionsbackend/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:purgebackend/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-escapesbackend/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&#x27;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&amp;D is what the bug produces from R&D, and also what an author legitimately typing R&amp;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 &#x27; &#x2F; &#x5C; &#96;, none of which any input path in this addon produces. Automatically
AMBIGUOUS Depth 1 with only &amp; &lt; &gt; &quot;. 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 (ACE3LACE, 1000KQUACKQUACK) 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.mjs

A 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_KEY and ENCRYPTION_KEY_PASSPHRASE can 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, WEBHOOK and 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.