It automatically upgrades or downgrades users based on their trading volume over a rolling 30-day period.
1. Tiers are defined statically
const TIERS = [
{ level: 1, min: 0 },
{ level: 2, min: 1000 },
{ level: 3, min: 10000 },
{ level: 4, min: 50000 }
];
Each tier sets:
level → the verification level
min → minimum required volume (in native currency)
You can adjust these thresholds as needed.
2. Rolling window
const VOLUME_WINDOW_DAYS = 30;
The plugin calculates total trading volume for each user within this period.
3. Automatic update
For each user:
It sums their trades (converted to native currency).
It determines the correct tier.
If their current level differs, it updates it.
The cron job runs daily, but you can change the schedule if needed.
This provides a simple, fully customizable way to implement volume-based tiering.
'use strict';
/**
* Simple Static Volume-Based Tier Plugin
*
* - Calculates rolling 30-day trading volume
* - Converts to native currency using oracle prices
* - Automatically upgrades/downgrades users
* - Runs once per day
*/
module.exports = async function () {
const { loggerPlugin, toolsLib, moment, cron } = this;
// -----------------------------
// Static Configuration
// -----------------------------
const VOLUME_WINDOW_DAYS = 30;
// Define tier thresholds here
// Ordered from lowest to highest
const TIERS = [
{ level: 1, min: 0 },
{ level: 2, min: 1000 },
{ level: 3, min: 10000 },
{ level: 4, min: 50000 }
];
// -----------------------------
// Helpers
// -----------------------------
const determineLevelFromVolume = (volume) => {
let level = TIERS[0].level;
for (const tier of TIERS) {
if (volume >= tier.min) {
level = tier.level;
}
}
return level;
};
const getOracleIndex = async () => {
const coins = toolsLib.getKitCoins();
return toolsLib.getAssetsPrices(
coins,
toolsLib.getKitConfig().native_currency
);
};
const calculateUserTradeVolume = async (user_id, from, to) => {
let volume = 0;
const oracleIndex = await getOracleIndex();
const native = toolsLib.getKitConfig().native_currency;
const trades = await toolsLib.order.getAllUserTradesByKitId(
user_id,
null, null, null, null, null,
from,
to,
'all'
);
for (const trade of (trades.data || [])) {
let size = Number(trade.size) || 0;
const base = trade.symbol.split('-')[0];
if (base !== native) {
const price = oracleIndex[base];
if (!price || price <= 0) continue;
size = size * price;
}
volume += size;
}
return volume;
};
// -----------------------------
// Main Tier Logic
// -----------------------------
const runVolumeTiering = async () => {
try {
const now = moment().seconds(0).milliseconds(0);
const from = now.clone().subtract(VOLUME_WINDOW_DAYS, 'days').toISOString();
const to = now.toISOString();
const users = await toolsLib.database.findAll('user', {
where: { activated: true, flagged: false },
raw: true,
attributes: ['id', 'verification_level']
});
for (const user of users) {
const volume = await calculateUserTradeVolume(user.id, from, to);
const newLevel = determineLevelFromVolume(volume);
if (user.verification_level !== newLevel) {
loggerPlugin.info(
`volume-tier update: user=${user.id} volume=${volume} ${user.verification_level} -> ${newLevel}`
);
await toolsLib.user.changeUserVerificationLevelById(
user.id,
newLevel
);
}
}
} catch (err) {
loggerPlugin.error('runVolumeTiering error', err.message);
}
};
// -----------------------------
// Daily Execution (00:00 UTC)
// -----------------------------
cron.schedule('0 0 * * *', runVolumeTiering, {
timezone: 'UTC'
}).start();
loggerPlugin.info('static volume-based tier plugin initialized');
};
]]>Yes, we support backend-controlled app versioning for both iOS and Android without requiring a new app release.
Operators can configure version values directly from the Operator Control Panel → General → Apps section. Specifically, you can set:
Current Version
Minimum Version
Store URLs (iOS / Android)
These values are exposed publicly via the /kit endpoint under the apps object. The mobile application can read these values at runtime and apply different upgrade logics accordingly.
The platform provides the version values, and the mobile app implements the enforcement logic:
Hard upgrade: If the app version is below min_version, the app can block access and force the user to update.
Soft upgrade: If the app version is below current_version but above min_version, the app can display an optional update prompt.
This allows full flexibility in how strict the upgrade policy should be, without requiring a new app release.
Endpoint: GET /kit
Field: apps
Includes: current_version, min_version, and store URLs.
This configuration can be managed by the exchange operator (Admin role) via the Operator Control Panel.
]]>You can definitely use a root domain here but you’ll need to add the domain to Cloudflare first.
There’s an official document for this so please take a look.
TL;DR: Configure it in Cloudflare first and go through the dashboard setup.
Cheers!
]]>I want to use a root domain for my cloud exchange (something like example.com, without a subdomain), but the dashboard is showing me this popup and doesn’t let me continue. What should I do?
Thanks in advance.
]]>Is there a way to use different wallet for my exchange? For example, if we currently have users and wallets already but want to use the exchange with our existing system?
]]>Instead, plugins support a prescript field in the plugin’s .json file. This is the recommended and supported way to install dependencies.
You can define multiple npm packages there, and they’ll be installed automatically when the plugin is built/loaded.
Example:
"prescript": {
"run": null,
"install": [
"aws-sdk",
"awesome-phonenumber"
]
}
If you need a specific version, you can specify it directly:
"install": [
"[email protected]",
"[email protected]"
]
prescript.Below is a list of libraries that are already available in the plugins:
[
"express",
"morgan",
"cors",
"latest-version",
"npm-programmatic",
"sequelize",
"eval",
"hollaex-tools-lib",
"lodash",
"express-validator",
"multer",
"moment",
"mathjs",
"bluebird",
"umzug",
"request-promise",
"uuid",
"jsonwebtoken",
"moment-timezone",
"json2csv",
"flat",
"ws",
"node-cron",
"random-string",
"bcryptjs",
"expect-ct",
"validator",
"otp",
"geoip-lite",
"nodemailer",
"ws-heartbeat",
"winston",
"elastic-apm-node",
"winston-elasticsearch-apm",
"triple-beam",
"uglify-js",
"body-parser",
"express-limiter"
]
]]>Go question on installing npm dependencies for my plugins. How do you do it? I know HollaEx plugins include some pre-installed dependencies, but what is best way to use them?
Also, how to add other libraries beyond the above?
]]>I just want to highlight that in the code snippet posted by @Ella the lines highlighted below is quite important. It tries to run the function 0.5 second after the site is loaded in order to avoid site rerendering on ReactJS to override this.
]]><script>
function hideFooterRowBottom() {
var footers = document.getElementsByClassName('footer-row-bottom');
for (var i = 0; i < footers.length; i++) {
footers[i].style.setProperty('display', 'none', 'important');
}
}
// run once
hideFooterRowBottom();
// keep enforcing (in case of re-render)
setInterval(function () {
hideFooterRowBottom();
}, 500);
</script>
]]>I’m wondering if there are other plugins or modules that can automatically upgrade or downgrade a user’s account based on their trading volume for example, how much a user has traded in a month, and then dynamically adjust their account level accordingly.
Specifically:
verification_level, while KYC status is stored in id_data.status.
id_data.status values are typically:
0: no status1: pending (KYC is being processed)2: rejected3: completed (approved)There are two approaches to do this using Event-driven and Cron-based setup.
Plugins can subscribe to the internal events channel:
toolsLib.database.subscriber.subscribe('channel:events');
toolsLib.database.subscriber.on('message', async (channel, message) => {
try {
if (channel === 'channel:events') {
const { type, data } = JSON.parse(message);
// For tier changes:
if (type === 'user' && data.action === 'update') {
loggerPlugin.info('/plugins/tier-upgrade-kyc/events update',
data.user_id,
data.email,
data.id_data);
// upgrade user's verification_level if all conditions are met
}
}
} catch (err) {
loggerPlugin.error('/plugins/tier-upgrade-kyc/events', err);
}
});
From there, you can implement your logic to upgrade the user automatically (e.g., when you confirm id_data.status === 3, or when your KYC provider marks the user approved), using:
await toolsLib.user.changeUserVerificationLevelById(userId, 2);
You can periodically scan for users who:
activated: true)flagged: false)verification_level: 1)id_data.status: 3)Example query:
const users = await toolsLib.database.findAll('user', {
where: {
activated: true,
flagged: false,
verification_level: 1,
id_data: {
status: 3
}
},
raw: true,
attributes: [
'id',
'email',
'network_id',
'id_data',
'verification_level',
'email_verified',
'activated',
'flagged',
'created_at'
]
});
Then loop through the matches and upgrade them:
await toolsLib.user.changeUserVerificationLevelById(user.id, 2);
'use strict';
const {
toolsLib,
loggerPlugin,
cron
} = this;
const EVENTS_CHANNEL = 'channel:events';
// Event-based approach
toolsLib.database.subscriber.subscribe(EVENTS_CHANNEL);
toolsLib.database.subscriber.on('message', async (channel, message) => {
try {
if (channel !== EVENTS_CHANNEL) return;
const { type, data } = JSON.parse(message);
// For tier changes:
if (type === 'user' && data && data.action === 'update') {
loggerPlugin.info(
'/plugins/tier-upgrade-kyc/events update',
data.user_id,
data.email,
data.id_data,
data.previous_user
);
// Extra guard: only upgrade if the user is still eligible (prevents loops / bad events)
const user = await toolsLib.database.findOne('user', {
where: { id: data.user_id },
raw: true,
attributes: ['id', 'email', 'id_data', 'verification_level', 'activated', 'flagged']
});
if (
user &&
user.activated === true &&
user.flagged === false &&
user.verification_level === 1 &&
user.id_data &&
user.id_data.status === 3
) {
loggerPlugin.verbose(
'/plugins/tier-upgrade-kyc/events',
`Upgrading user ${user.id} (${user.email}) -> level 2`
);
await toolsLib.user.changeUserVerificationLevelById(user.id, 2);
}
}
} catch (err) {
loggerPlugin.error('/plugins/tier-upgrade-kyc/events', err);
}
});
// Cron-based approach
const run = async () => {
loggerPlugin.verbose('/plugins/tier-upgrade-kyc/run', 'Running tier upgrade KYC plugin');
const users = await toolsLib.database.findAll('user', {
where: {
activated: true,
flagged: false,
verification_level: 1,
id_data: {
status: 3
}
},
raw: true,
attributes: [
'id',
'email',
'network_id',
'id_data',
'verification_level',
'email_verified',
'activated',
'flagged',
'created_at'
]
});
loggerPlugin.verbose('/plugins/tier-upgrade-kyc/run', `Found ${users.length} users to upgrade`);
if (!users.length) return;
for (const user of users) {
loggerPlugin.verbose('/plugins/tier-upgrade-kyc/run', `Upgrading user ${user.id} (${user.email}) -> level 2`);
await toolsLib.user.changeUserVerificationLevelById(user.id, 2);
loggerPlugin.verbose('/plugins/tier-upgrade-kyc/run', `User ${user.id} upgraded successfully`);
}
};
run();
// Runs at minute 0 of every hour (UTC)
const task = cron.schedule(
'0 * * * *',
() => run(),
{ timezone: 'Etc/UTC' }
);
task.start();
]]>internal_system_id on a user and use it inside plugins, you can use the user meta system in HollaEx.
First, you need to define the meta key so it’s supported in the operator panel:
internal_system_idOnce this is configured, you can manually set internal_system_id for users from the admin UI if needed.
If you want to do this programmatically from outside (e.g., from a backend service), you can use the Admin APIs, for example via hollaex-node-lib with admin keys and updateExchangeUser.
Example:
// Pseudo code using hollaex-node-lib
await updateExchangeUser(userId, {
meta: {
internal_system_id: 'ABC123XYZ'
}
});
This will persist the value in the user’s meta and can be used for other operations later.
If you are doing this inside a plugin, you can update the user meta directly using toolsLib.user.updateUserMeta.
Example:
async function updateMeta() {
const userId = 123; // HollaEx user ID
const metaUpdate = {
internal_system_id: 'ABC123XYZ'
};
const auditInfo = {
userEmail: '[email protected]',
sessionId: 'sess-123',
apiPath: '/admin/user/meta',
method: 'put'
};
const user = await toolsLib.user.updateUserMeta(
userId,
metaUpdate,
{ overwrite: false }, // set true if you want to overwrite existing meta fields
auditInfo
);
console.log(user.meta); // will include internal_system_id
}
overwrite: false will merge the provided meta fields with existing ones, instead of wiping them.auditInfo is used for logging/auditing so the system knows who performed the change and via which path.I want to add a new field to each user — for example, something like:
internal_system_id: “ABC123XYZ”
This value needs to be:
From my understanding, there is a meta field in the system but I am not sure how I can customize that.
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:
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
);
});
]]>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!
]]>Does the plugin system provide a way to trigger these tier updates automatically when KYC verification is confirmed? Any guidance or examples on how to implement this would be greatly appreciated.
Thanks in advance!
]]>Thanks for reporting the issue.
I just set up a new testnet exchange from scratch on my computer, but I was not able to reproduce the issue.
Could you please provide some more information?
Thank you.
]]>I went through the testnet setup flow on my own computer (macOS) and found no issues.
Additionally, could you please tell me which OS you are using too?
Thank you.
]]>503 Service Unavailable error. In the logs I also see repeated 404 Not Found requests to the /api/v2/announcements endpoint. As a result, the server never becomes fully operational on testnet.
Has anyone encountered this before or found a solution?

I got to the point of creating an admin account and got an error:
Welcome to HollaEx Server Setup!
Checking docker-compose availability...
Docker Compose version v2.29.1
Select the network
Before you continue, You need to select the network that you want to use with the exchange.
HollaEx Kit by default, will be connected to the official mainnet HollaEx Network (api.hollaex.network).
You can also run HollaEx Kit with official testnet HollaEx Network for testing purpose (api.testnet.hollaex.network).
If you want to connect to a different custom network, you can type the URL.
1) Mainnet HollaEx Network
2) Testnet HollaEx Network
3) Custom HollaEx Network
Please select the network: 2
{"name":"HollaEx Testnet","version":"2.15.0","host":"api.testnet.hollaex.network","basePath":"/v2"}
Successfully reached to the network health page.
https://api.testnet.hollaex.network ✔
Create Admin
Please type your email address and password that you are going to use for your admin account.
Email:
[email protected]
Password:
-Password must contain at least 8 characters and one digit.
Create Exchange
Please type in the name of your new exchange.
- Alphanumeric, Dash (-), Underscore Only (_). No space or special character allowed.
ADEX
ADEX ✔
Error 1001 - relation "deposits" does not exist
Error: Failed to create an account on HollaEx Network.
Please review the logs and try it again.
I tried to run the setup again but got different error:
ubuntu@ip-172-31-7-77:~/hollaex-kit$ sudo hollaex server --setup
Checking docker-compose availability...
Docker Compose version v2.29.1
Checking docker-compose availability...
Docker Compose version v2.29.1
/usr/local/bin/hollaex: line 1115: /root/.hollaex-cli/tools_generator.sh: No such file or directory
/usr/local/bin/hollaex: line 1121: hollaex_setup_existing_exchange_check: command not found
/usr/local/bin/hollaex: line 1125: hollaex_setup_initialization: command not found
/usr/local/bin/hollaex: line 1164: system_dependencies_check: command not found
/usr/local/bin/hollaex: line 1166: check_docker_daemon_status: command not found
Checking docker-compose availability...
Docker Compose version v2.29.1
Target hollaex docker registry : bitholla/my-hollaex-server.
/usr/local/bin/hollaex: line 4222: /root/.hollaex-cli/tools_generator.sh: No such file or directory
/usr/local/bin/hollaex: line 4223: load_config_variables: command not found
/usr/local/bin/hollaex: line 4236: build_user_hollaex_core: command not found
Error: The image (bitholla/my-hollaex-server:2.15.10) is currently not available.
This could be an temporary issue, especially when there is a recent Kit release came up just a while ago.
Please try it again after a while, or run 'hollaex server --setup --local_build' to force build local image.
Please help
]]>While it’s technically possible to build your own app from scratch, we generally advise against it. Doing so can require substantial development time and resources. Instead, we strongly recommend leveraging the existing, production-ready solution offered within the Enterprise Plan. It’s optimized for speed, reliability, and ease of deployment, allowing you to focus on customization and growth rather than foundational engineering.
]]>Please note that this is a public technical forum and not a marketing platform. If your goal is to advertise your services or portfolio, we recommend reaching out to HollaEx directly and making open-source contributions that demonstrate your skills. That way, you may have the opportunity to be acknowledged or vouched for by the team if your work aligns with the project’s standards.
Let’s keep this space focused on relevant and constructive discussions.
]]>Frontend Interface: Admin uploads a CSV file (React + role-based access).
Backend:
Queue System: We used Celery + Redis
Audit Trail: All processed rows were stored in a MongoDB audit log
Can’t share the exact code due to NDA
Sample code:
@app.post("/upload")
async def upload_csv(file: UploadFile = File(...), user: User = Depends(get_current_user)):
df = pd.read_csv(file.file)
for index, row in df.iterrows():
if not validate_row(row):
log_error(row, reason="Invalid data format")
continue
enqueue_transaction(row) # sends to Celery worker
return {"status": "Processing initiated"}
]]>key considerations
This is necessary because I currently have a large volume of fiat transactions that need to be handled manually, and automating this process would significantly streamline operations and reduce the risk of human error.
]]>Now a days many companies say they do Flutter, but few optimize for both platforms equally, not just about compiling for Android and iOS but fine-tuning performance (animation smoothness, native APIs, device-specific bugs) for both OS.
We built a wellness app for both iOS and Android. The challenge? Complex BLE (Bluetooth) integrations with wearables. Our team used Flutter with native bridges (via platform channels) to make sure real-time health tracking worked flawlessly on both platforms.
you should ask how they handle OTA (Over-The-Air) updates using services like CodePush (via third-party plugins), when your app needs rapid iteration.
A lot of firms disappear post-launch. Look for those who offer maintenance SLAs, when OS upgrades & Flutter version shifts.
At Impero IT Services, we recently managed a project that had an outdated Flutter 2.x codebase. We updated it to Flutter 3.22 with Dart 3, optimized the widget tree for better performance & integrated Firebase’s latest SDKs for analytics and crash reporting ==> all without downtime for the app
]]>Note:
Dynamic values can be added to your template using the ${value} format. For example, in the case of a login email, using ${name} will display the user’s email address.
Here’s an example of an email I created using a tool called Stripo.
<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'>
<html dir='ltr' xmlns='http://www.w3.org/1999/xhtml' xmlns:o='urn:schemas-microsoft-com:office:office' lang='en'>
<head>
<meta charset='UTF-8'>
<meta content='width=device-width, initial-scale=1' name='viewport'>
<meta name='x-apple-disable-message-reformatting'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<meta content='telephone=no' name='format-detection'>
<title>Important: Recent Login Alert for Your Account</title>
<style type='text/css'>.rollover:hover .rollover-first {
max-height:0px!important;
display:none!important;
}
.rollover:hover .rollover-second {
max-height:none!important;
display:block!important;
}
.rollover span {
font-size:0px;
}
u + .body img ~ div div {
display:none;
}
#outlook a {
padding:0;
}
span.MsoHyperlink,
span.MsoHyperlinkFollowed {
color:inherit;
mso-style-priority:99;
}
a.q {
mso-style-priority:100!important;
text-decoration:none!important;
}
a[x-apple-data-detectors],
#MessageViewBody a {
color:inherit!important;
text-decoration:none!important;
font-size:inherit!important;
font-family:inherit!important;
font-weight:inherit!important;
line-height:inherit!important;
}
.g {
display:none;
float:left;
overflow:hidden;
width:0;
max-height:0;
line-height:0;
mso-hide:all;
}
@media only screen and (max-width:600px) {.bf { padding-bottom:20px!important } *[class='gmail-fix'] { display:none!important } p, a { line-height:150%!important } h1, h1 a { line-height:120%!important } h2, h2 a { line-height:120%!important } h3, h3 a { line-height:120%!important } h4, h4 a { line-height:120%!important } h5, h5 a { line-height:120%!important } h6, h6 a { line-height:120%!important } .bc p { } .bb p { } h1 { font-size:40px!important; text-align:left; margin-bottom:0px!important } h2 { font-size:28px!important; text-align:left; margin-bottom:0px!important } h3 { font-size:20px!important; text-align:left; margin-bottom:0px!important } h4 { font-size:24px!important; text-align:left } h5 { font-size:20px!important; text-align:left } h6 { font-size:16px!important; text-align:left } .bd p, .bd a { font-size:14px!important } .bc p, .bc a { font-size:14px!important } .bb p, .bb a { font-size:14px!important } .w .rollover:hover .rollover-second, .x .rollover:hover .rollover-second, .y .rollover:hover .rollover-second { display:inline!important } .v { display:inline-table } .p, .p .q, .r, .r td, .e { display:inline-block!important } .m table, .n, .o { width:100%!important } .j table, .k table, .l table, .j, .l, .k { width:100%!important; max-width:600px!important } .adapt-img { width:100%!important; height:auto!important } table.d, .esd-block-html table { width:auto!important } .h-auto { height:auto!important } .img-6307 { height:36px!important } .a .c, .a .c * { font-size:14px!important; line-height:150%!important } .a .b, .a .b * { font-size:12px!important; line-height:150%!important } }
@media screen and (max-width:384px) {.mail-message-content { width:414px!important } }</style>
</head>
<body class='body' style='width:100%;height:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;padding:0;Margin:0'>
<span style='display:none !important;font-size:0px;line-height:0;color:#ffffff;visibility:hidden;opacity:0;height:0;width:0;mso-hide:all'>Review the details of a recent login to ensure your account's security.</span>
<div dir='ltr' class='es-wrapper-color' lang='en' style='background-color:#E6F9FE'><!--[if gte mso 9]>
<v:background xmlns:v='urn:schemas-microsoft-com:vml' fill='t'>
<v:fill type='tile' color='#f6f6f6'></v:fill>
</v:background>
<![endif]-->
<table width='100%' cellspacing='0' cellpadding='0' class='es-wrapper' role='none' style='mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;padding:0;Margin:0;width:100%;height:100%;background-repeat:repeat;background-position:center top;background-color:#E6F9FE'>
<tr>
<td valign='top' style='padding:0;Margin:0'>
<table cellspacing='0' cellpadding='0' align='center' background class='k' role='none' style='mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;width:100%;table-layout:fixed !important;background-color:transparent;background-repeat:repeat;background-position:center top'>
</table>
<table cellpadding='0' cellspacing='0' align='center' class='k' role='none' style='mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;width:100%;table-layout:fixed !important;background-color:transparent;background-repeat:repeat;background-position:center top'>
<tr>
<td align='center' bgcolor='transparent' style='padding:0;Margin:0'>
<table align='center' cellpadding='0' cellspacing='0' bgcolor='#ffffff' class='bd' role='none' style='mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;background-color:transparent;width:600px'>
<tr>
<td align='left' bgcolor='#fbfafa' style='padding:0;Margin:0;padding-bottom:15px;background-color:#fbfafa'>
<table cellspacing='0' width='100%' cellpadding='0' role='none' style='mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px'>
<tr>
<td align='left' style='padding:0;Margin:0;width:600px'>
<table cellpadding='0' cellspacing='0' width='100%' role='presentation' style='mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px'>
<tr>
<td align='center' bgcolor='#0b5394' style='padding:0;Margin:0;font-size:0'>
<table border='0' width='100%' height='100%' cellpadding='0' cellspacing='0' class='v' role='presentation' style='mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px'>
<tr>
<td style='padding:0;Margin:0;border-bottom:3px solid #0b5394;background:none;height:0px;width:100%;margin:0px'></td>
</tr>
</table></td>
</tr>
<tr>
<td align='center' style='padding:0;Margin:0;padding-top:30px;padding-bottom:20px;font-size:0px'><a target='_blank' href='https://viewstripo.email' class='companyWebsite' style='mso-line-height-rule:exactly;text-decoration:underline;color:#0D4259;font-size:14px'><img alt='' height='33' src='https://yscnzh.stripocdn.email/content/guids/CABINET_8263422e3fe53147a17e98df62c92ed0c8dacb60d4e047ec18914664ea1108cc/images/hollaexblack.png' class='companyLogo img-6307' style='display:block;font-size:14px;border:0;outline:none;text-decoration:none;border-radius:0' width='107'></a></td>
</tr>
</table></td>
</tr>
</table></td>
</tr>
</table></td>
</tr>
</table>
<table cellspacing='0' cellpadding='0' align='center' class='j' role='none' style='mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;width:100%;table-layout:fixed !important'>
</table>
<table cellpadding='0' cellspacing='0' align='center' class='j' role='none' style='mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;width:100%;table-layout:fixed !important'>
<tr>
<td align='center' style='padding:0;Margin:0'>
<table bgcolor='#ffffff' align='center' cellpadding='0' cellspacing='0' class='bc' role='none' style='mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;background-color:#FFFFFF;width:600px'>
<tr>
<td align='left' style='Margin:0;padding-top:10px;padding-right:30px;padding-bottom:10px;padding-left:30px'>
<table cellpadding='0' cellspacing='0' width='100%' role='none' style='mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px'>
<tr>
<td align='center' valign='top' style='padding:0;Margin:0;width:540px'>
<table cellpadding='0' cellspacing='0' width='100%' role='presentation' style='mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px'>
<tr>
<td align='left' class='generalTextHtml' style='padding:0;Margin:0'><p style='Margin:0;mso-line-height-rule:exactly;font-family:Lexend, Arial, sans-serif;line-height:16.8px;letter-spacing:0;color:#0D4259;font-size:14px;margin-bottom:10px'><br></p><p style='Margin:0;mso-line-height-rule:exactly;font-family:Lexend, Arial, sans-serif;line-height:16.8px;letter-spacing:0;color:#0D4259;font-size:14px;margin-bottom:10px'>Dear ${name},</p><p style='Margin:0;mso-line-height-rule:exactly;font-family:Lexend, Arial, sans-serif;line-height:16.8px;letter-spacing:0;color:#0D4259;font-size:14px;margin-bottom:10px'><br></p><p style='Margin:0;mso-line-height-rule:exactly;font-family:Lexend, Arial, sans-serif;line-height:16.8px;letter-spacing:0;color:#0D4259;font-size:14px;margin-bottom:10px'>We would like to inform you of a recent login to your account. Please review the details below to ensure this activity was authorized:</p>
<ul style='font-family:Lexend, Arial, sans-serif;padding:0px 0px 0px 40px;margin-top:15px;margin-bottom:15px'>
<li style='color:#0D4259;margin:0px 0px 15px;font-size:14px'><strong style='line-height:120%'>Time:</strong><span style='line-height:120%'> ${time}</span></li>
<li style='color:#0D4259;margin:0px 0px 15px;font-size:14px'><strong style='line-height:120%'>IP Address:</strong><span style='line-height:120%'> ${ip}</span></li>
<li style='color:#0D4259;margin:0px 0px 15px;font-size:14px'><strong style='line-height:120%'>Location:</strong><span style='line-height:120%'> ${country}</span></li>
</ul><p style='Margin:0;mso-line-height-rule:exactly;font-family:Lexend, Arial, sans-serif;line-height:16.8px;letter-spacing:0;color:#0D4259;font-size:14px;margin-bottom:10px'><br></p><p style='Margin:0;mso-line-height-rule:exactly;font-family:Lexend, Arial, sans-serif;line-height:16.8px;letter-spacing:0;color:#0D4259;font-size:14px;margin-bottom:10px'>If you did not initiate this login, we strongly recommend changing your password immediately and enabling two-factor authentication for enhanced security. Should you have any concerns, please contact our support team without delay.</p><p style='Margin:0;mso-line-height-rule:exactly;font-family:Lexend, Arial, sans-serif;line-height:16.8px;letter-spacing:0;color:#0D4259;font-size:14px;margin-bottom:10px'><br></p><p style='Margin:0;mso-line-height-rule:exactly;font-family:Lexend, Arial, sans-serif;line-height:16.8px;letter-spacing:0;color:#0D4259;font-size:14px;margin-bottom:10px'><em>-- Thank you for your attention to this matter.</em></p><p style='Margin:0;mso-line-height-rule:exactly;font-family:Lexend, Arial, sans-serif;line-height:16.8px;letter-spacing:0;color:#0D4259;font-size:14px;margin-bottom:10px'><br></p></td>
</tr>
</table></td>
</tr>
</table></td>
</tr>
</table></td>
</tr>
</table>
<table cellpadding='0' cellspacing='0' align='center' class='j' role='none' style='mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;width:100%;table-layout:fixed !important'>
<tr>
<td align='center' style='padding:0;Margin:0'>
<table cellspacing='0' cellpadding='0' align='center' class='bb' role='none' style='mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;background-color:#BCF0FF;width:600px'>
<tr>
<td align='left' bgcolor='#efefef' style='Margin:0;padding-bottom:20px;padding-right:30px;padding-left:30px;padding-top:20px;background-color:#efefef'><!--[if mso]><table style='width:540px' cellpadding='0' cellspacing='0'><tr><td style='width:346px' valign='top'><![endif]-->
<table cellspacing='0' cellpadding='0' align='left' class='n' role='none' style='mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;float:left'>
<tr>
<td align='left' class='bf' style='padding:0;Margin:0;width:346px'>
<table width='100%' cellspacing='0' cellpadding='0' role='presentation' style='mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px'>
<tr>
<td esdev-links-color='#666666' align='left' class='a' style='padding:0;Margin:0'><p class='c' style='Margin:0;mso-line-height-rule:exactly;font-family:Lexend, Arial, sans-serif;line-height:15.4px;letter-spacing:0;color:#0D4259;font-size:14px;margin-bottom:0px'><strong style='color:#0a3142'>HollaEx® </strong><em>— White-label Crypto Software Solutions </em></p><p class='b' style='Margin:0;mso-line-height-rule:exactly;font-family:Lexend, Arial, sans-serif;line-height:13.2px;letter-spacing:0;color:#1b5670;font-size:12px;margin-bottom:0px'><strong><br></strong></p><p class='b' style='Margin:0;mso-line-height-rule:exactly;font-family:Lexend, Arial, sans-serif;line-height:13.2px;letter-spacing:0;color:#1b5670;font-size:12px;margin-bottom:0px'><strong>Exchange:</strong> <a href='https://pro.hollaex.com' target='_blank' class='unsubscribe' style='mso-line-height-rule:exactly;text-decoration:underline;color:#1b5670;font-size:12px;line-height:13.2px'>https://pro.hollaex.com</a></p><p class='b' style='Margin:0;mso-line-height-rule:exactly;font-family:Lexend, Arial, sans-serif;line-height:13.2px;letter-spacing:0;color:#1b5670;font-size:12px;margin-bottom:0px'><strong>Business:</strong> <a href='https://www.hollaex.com' target='_blank' style='mso-line-height-rule:exactly;text-decoration:underline;color:#1b5670;font-size:12px;line-height:13.2px'>https://www.hollaex.com</a></p></td>
</tr>
</table></td>
</tr>
</table><!--[if mso]></td><td style='width:20px'></td><td style='width:174px' valign='top'><![endif]-->
<table cellspacing='0' cellpadding='0' align='right' class='o' role='none' style='mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;float:right'>
<tr>
<td align='left' style='padding:0;Margin:0;width:174px'>
<table width='100%' cellspacing='0' cellpadding='0' role='presentation' style='mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px'>
<tr>
<td esdev-links-color='#666666' align='left' style='padding:0;Margin:0'><p style='Margin:0;mso-line-height-rule:exactly;font-family:Lexend, Arial, sans-serif;line-height:28px;letter-spacing:0;color:#0D4259;font-size:14px;margin-bottom:0px'><a target='_blank' href='https://www.hollaex.com/resources' style='mso-line-height-rule:exactly;text-decoration:underline;color:#0D4259;font-size:14px'>Questions?</a></p><p style='Margin:0;mso-line-height-rule:exactly;font-family:Lexend, Arial, sans-serif;line-height:28px;letter-spacing:0;color:#0D4259;font-size:14px;margin-bottom:0px'><a target='_blank' href='https://www.hollaex.com/contact#:~:text=contacting%20us%20at%20support%40hollaex.com%E2%80%8D' class='email_us_text' style='mso-line-height-rule:exactly;text-decoration:underline;color:#0D4259;font-size:14px'>Contact Us</a></p><p style='Margin:0;mso-line-height-rule:exactly;font-family:Lexend, Arial, sans-serif;line-height:28px;letter-spacing:0;color:#0D4259;font-size:14px;margin-bottom:0px'><a target='_blank' href='https://www.hollaex.com/about' style='mso-line-height-rule:exactly;text-decoration:underline;color:#0D4259;font-size:14px;line-height:28px'>About Us</a></p></td>
</tr>
<tr>
<td align='left' style='padding:0;Margin:0;padding-top:5px;font-size:0'>
<table cellspacing='0' cellpadding='0' class='d r' role='presentation' style='mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px'>
<tr>
<td valign='top' align='center' style='padding:0;Margin:0;padding-right:10px'><a href='https://x.com/hollaex' style='mso-line-height-rule:exactly;text-decoration:underline;color:#0D4259;font-size:14px'><img title='X' src='https://yscnzh.stripocdn.email/content/assets/img/social-icons/rounded-gray/x-rounded-gray.png' alt='X' width='24' height='24' style='display:block;font-size:14px;border:0;outline:none;text-decoration:none'></a></td>
<td valign='top' align='center' style='padding:0;Margin:0;padding-right:10px'><a href='https://www.youtube.com/@HollaEx' style='mso-line-height-rule:exactly;text-decoration:underline;color:#0D4259;font-size:14px'><img title='Youtube' src='https://yscnzh.stripocdn.email/content/assets/img/social-icons/rounded-gray/youtube-rounded-gray.png' alt='Yt' width='24' height='24' style='display:block;font-size:14px;border:0;outline:none;text-decoration:none'></a></td>
<td valign='top' align='center' style='padding:0;Margin:0;padding-right:10px'><a href='https://t.me/s/hollaex' style='mso-line-height-rule:exactly;text-decoration:underline;color:#0D4259;font-size:14px'><img title='Telegram' src='https://yscnzh.stripocdn.email/content/assets/img/messenger-icons/rounded-gray/telegram-rounded-gray.png' alt='Telegram' width='24' height='24' style='display:block;font-size:14px;border:0;outline:none;text-decoration:none'></a></td>
</tr>
</table></td>
</tr>
</table></td>
</tr>
</table><!--[if mso]></td></tr></table><![endif]--></td>
</tr>
</table></td>
</tr>
</table></td>
</tr>
</table>
</div>
</body>
</html>
]]>Can I also update the html template and make something of my own?
]]>In case you don’t have cloudflare setup already checkout this doc.
]]>You simply go to the OTC broker and create a new one. Then you select the Assets as BTC and ARS
You then select your spread. Its a percentage added to the quote the broker gives the user and it can have your profit margin includes. So 1 would be 1%. I put 0.1% here which is. pretty small spread margin to be very competitive.
You then click Advanced and set the formula for it to be binance_btc-usdt*binance_usdt-ars
This means to calculate the price it should take the BTC-USDT price from Binance then multiply that by the USDT-ARS price from Binance. Note that you can set this to anything you want. You can even set USDT-ARS price to be static like you requested here by putting a static value so for example binance_btc-usdt*1070. and 1070 here is the exchange rate between USDT-ARS I put here statically. I just checked and alternatively I could even get BTC-ARS exchange rate directly from Binance since Binance has that market listed.
Once that is all set, you then proceed and create the broker. Now each time a user gets the price it would automatically get the prices dynamically and display the correct value with the spread you selected as you can see in the image below.
Helpful Resources:
]]>Can you also give me a sample code using the HollaEx Admin APIs?
]]>