Manually Review Large Deposit Transactions

I’m looking for a way to review and validate crypto deposits on my exchange before they are fully processed.

Ideally, I’d like all deposits under $1,000 to be handled automatically, but for any deposit above that threshold, I want to introduce a manual review step so I can verify and approve the transaction myself.

Is there a recommended method or feature in HollaEx that allows setting up this kind of conditional deposit flow? Any guidance or best practices would be greatly appreciated.

Thanks!

1 Like

First, you’ll need to disable the automatic deposit processing feature in your Operator Control Panel (introduced in v2.17). You can find this setting under:

General → Security

Once auto-deposit is turned off, all incoming crypto deposits will remain in an “on hold” state, meaning they will require manual approval unless you automate the process yourself.

From there, you can create a simple plugin, similar to the example below that retrieves all deposits where onhold: true, checks their value, and processes them accordingly.
For example:

  • If the deposit amount is under $1,000, the plugin can automatically approve and process it.
  • If the amount is over $1,000, the deposit will remain on hold, allowing you to manually review and verify it through the Operator Control Panel.

This setup gives you full control while still allowing smaller deposits to be handled automatically.

'use strict';

const { publicMeta, meta } = this.configValues;
const {
	app,
	loggerPlugin,
	toolsLib
} = this.pluginLibraries;

const init = async () => {
	loggerPlugin.info(
		'/plugins/txanalysor/init/initializing'
	);
};

const parseNumber = (v, fallback = 0) => {
	const n = Number(v);
	return Number.isFinite(n) ? n : fallback;
};

const valueInQuote = async (currency, amount, quote) => {
	const sym = String(currency || '').toLowerCase();
	const tgt = String(quote || '').toLowerCase();
	if (!sym || !amount || !tgt) return null;
	try {
		if (sym === tgt) return parseNumber(amount, null);
		const prices = await toolsLib.getAssetsPrices([sym], tgt);
		const rate = prices && prices[sym];
		if (typeof rate !== 'number' || !(rate > 0)) return null;
		return parseNumber(amount, null) * rate;
	} catch (err) {
		loggerPlugin.warn('/plugins/txanalysor/valueInQuote price fetch failed', sym, '->', tgt, err.message);
		return null;
	}
};

const processOnHoldDeposits = async () => {
	const threshold = parseNumber(meta.THRESHOLD_USDT.value, 1000);
	const quote = (meta.QUOTE_CURRENCY.value || 'usdt').toLowerCase();

	let scanned = 0;
	let validated = 0;
	let skipped = 0;
	const res = await toolsLib.wallet.getExchangeDeposits(
		null,   // currency
		false,  // status (pending)
		false,  // dismissed
		false,  // rejected
		false,  // processing
		false,  // waiting
		null,   // limit
		null,      // page
		null,   // orderBy
		null,   // order
		null,   // startDate
		null,   // endDate
		null,   // transactionId
		null,   // address
		null,   // format
		{ onhold: true } // opts
	);
	const list = res && Array.isArray(res.data) ? res.data : [];
	if (!list.length) {
		loggerPlugin.info('/plugins/txanalysor/process complete', { scanned, validated, skipped });
		return { scanned, validated, skipped };
	}

	for (const d of list) {
		// Defensive parsing
		if (!d || !d.transaction_id) continue;

		// Only pending + onhold deposits
		const isOnHold = !!d.onhold;
		if (!isOnHold) {
			skipped += 1;
			continue;
		}

		const currency = String((d.currency || d.symbol) || '').toLowerCase();
		const amount = parseNumber(d.amount, null);
		if (!currency || !(amount > 0)) {
			skipped += 1;
			continue;
		}

		const valueQuote = await valueInQuote(currency, amount, quote);
		if (valueQuote === null) {
			skipped += 1;
			continue;
		}

		if (valueQuote < threshold) {
			try {
				await toolsLib.wallet.updatePendingMint(d.transaction_id, {
					status: true,
					onhold: false
				});
				loggerPlugin.info('/plugins/txanalysor/validated', {
					transaction_id: d.transaction_id,
					currency,
					amount,
					value_in_quote: valueQuote,
					quote,
					threshold
				});
				validated += 1;
			} catch (err) {
				loggerPlugin.error('/plugins/txanalysor/updatePendingMint failed', d.transaction_id, err.message);
				skipped += 1;
			}
		} else {
			skipped += 1;
		}
		scanned += 1;
	}

	loggerPlugin.info('/plugins/txanalysor/process complete', { scanned, validated, skipped });
	return { scanned, validated, skipped };
};

init()
	.then(() => {
		setTimeout(() => {
			processOnHoldDeposits().catch((err) => {
				loggerPlugin.error('/plugins/txanalysor/initial run error', err.message);
			});
		}, 10000);
			// Run every minute
			setInterval(() => {
				processOnHoldDeposits().catch((err) => {
					loggerPlugin.error('/plugins/txanalysor/scheduled run error', err.message);
				});
			}, 60 * 1000);
	})
	.catch((err) => {
		loggerPlugin.error(
			'/plugins/txanalysor/init/error during initialization',
			err.message
		);
	});


1 Like

very useful plugin, thanks