Email usBook a call (opens in a new tab)
Engineering

How to License and Protect MT5 Expert Advisors and Indicators

How to sell MT5 Expert Advisors and indicators without giving the strategy away, and without making life miserable for the customers who actually paid.

If you sell trading software, you will eventually find a copy of your Expert Advisor on a forum. MT5 EA licensing is not about making that impossible. It is about making unauthorised use inconvenient, making legitimate use effortless, and keeping control of who can run your code on a live account. This guide covers the mechanisms that work in practice: compiled distribution, account and broker locks, server-validated licence keys, activation limits, offline grace periods, revocation and automated key issuance from a checkout. It also covers TradingView invite-only scripts, and what no licensing scheme can stop.

Start with what you ship: .ex5, never .mq5

MetaTrader 5 programs are written in MQL5 (.mq5 source) and compiled by MetaEditor into .ex5 binaries. The terminal only needs the .ex5 to run an EA or indicator. Shipping source code means shipping your intellectual property: anyone can strip a licence check out of a .mq5 file in seconds.

Compiled builds are the baseline for every commercial product:

  • Distribute .ex5 only. Keep source in a private repository with tagged releases, so you can rebuild any version a customer reports a bug against.
  • Compile per customer when locks are hard-coded. If a build embeds an account number, the compile step belongs in your release pipeline, not on someone's laptop.
  • Keep secrets out of the binary. Anything inside an .ex5 (URLs, keys, salts) should be treated as recoverable by a determined reverse engineer. Use it to identify, not to authenticate.

Selling source is a legitimate product too, priced accordingly. Just treat it as a different product with a different licence agreement, not as the default.

The licensing ladder

Most vendors combine several of the controls below. Each step up adds protection and adds operational work.

Method How it works Strength Customer friction Operational cost
Account-number lock Build checks ACCOUNT_LOGIN against a hard-coded value Moderate High (new build for every account change) High at scale
Broker lock Build checks ACCOUNT_COMPANY or ACCOUNT_SERVER Low to moderate Low within that broker Low
Expiry date Build stops working after a date Low on its own Medium (renewal builds) Medium
Server-validated licence key EA sends key and account details to your API Strong Low once set up Requires a backend
Key plus activation limits Server tracks which accounts or terminals use each key Strongest practical option Low, with self-service resets Backend plus support tooling

Account-number locks

The EA reads AccountInfoInteger(ACCOUNT_LOGIN) in OnInit() and returns INIT_FAILED if it does not match. It is simple, needs no infrastructure and works offline. The cost is that every customer needs a unique build, and every account change (new prop firm challenge, new demo, broker migration) becomes a support ticket and a recompile. It suits small runs of high-value licences, not volume sales.

Broker locks

A broker lock compares AccountInfoString(ACCOUNT_COMPANY) or ACCOUNT_SERVER against an allow-list. It is common in free or partner-distributed builds, where a broker or introducing partner funds the product and wants it used on their platform. Match on the company string rather than a single server name where possible, because brokers add and rename servers.

Server-validated licence keys

This is where serious MT5 EA licensing lives. The customer enters a licence key as an input parameter. On start-up, and periodically afterwards, the EA calls your licence API with the key and a few identifiers. The server answers valid or invalid and the EA behaves accordingly. Keys can be issued, limited, extended and revoked without shipping a new build.

Validating a licence key with WebRequest

MQL5 provides WebRequest() for HTTP calls. Two platform rules shape the design:

  1. The URL must be on the terminal's allowed list. The user adds it under Tools, Options, Expert Advisors, "Allow WebRequest for listed URL". If it is missing, WebRequest() returns -1 and GetLastError() returns 4014. Your installation guide and your error message both need to say this plainly.
  2. WebRequest() cannot be called from indicators, and it does not run in the Strategy Tester. Indicators need a different approach (see below), and your EA must not depend on a network call during backtests.

An illustrative check, written for clarity rather than hardening:

input string InpLicenceKey = "";            // Licence key from your account area

#define LICENCE_URL "https://licence.example.com/v1/validate"

bool ValidateLicence()
{
   // Let customers backtest and optimise without a network call
   if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION))
      return(true);

   string login  = IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN));
   string server = AccountInfoString(ACCOUNT_SERVER);
   string body   = "{\"key\":\"" + InpLicenceKey + "\",\"login\":" + login +
                   ",\"server\":\"" + server + "\"}";

   char   data[], result[];
   string responseHeaders;
   int    len = StringToCharArray(body, data, 0, WHOLE_ARRAY, CP_UTF8);
   ArrayResize(data, len - 1);               // drop the trailing null character

   ResetLastError();
   int status = WebRequest("POST", LICENCE_URL,
                           "Content-Type: application/json\r\n",
                           5000, data, result, responseHeaders);

   if(status == -1)
   {
      if(GetLastError() == 4014)
         Print("Add ", LICENCE_URL, " to Tools > Options > Expert Advisors > Allow WebRequest");
      return(CheckGraceCache());              // offline fallback, see below
   }

   string response = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8);
   if(status == 200 && StringFind(response, "\"valid\":true") >= 0)
   {
      WriteGraceCache();
      return(true);
   }
   return(false);
}

int OnInit()
{
   if(!ValidateLicence())
   {
      Alert("Licence could not be validated. See the Experts tab for details.");
      return(INIT_FAILED);
   }
   EventSetTimer(3600);                       // re-check hourly
   return(INIT_SUCCEEDED);
}

In production you would parse the JSON properly, include a build version so the server can force upgrades, and handle a failed re-check in OnTimer() gracefully: stop opening new positions, keep managing open ones, and tell the user why. Never close a customer's open trades because your licence server had a bad minute.

Licensing indicators

Because indicators cannot call WebRequest(), the usual patterns are:

  • An account or broker lock compiled into the indicator.
  • A companion EA that validates the key and writes a short-lived status to a terminal global variable, which the indicator reads.
  • Selling indicators as part of a bundle with an EA that carries the online check.

Terminal fingerprinting and activation limits

A key on its own can be shared. Activation limits stop one purchase becoming a hundred installs. The server records each activation against the key and refuses new ones beyond the limit.

What you bind an activation to is a trade-off:

  • Account login plus server is stable, meaningful to the customer and easy to explain ("this licence covers two live accounts").
  • A terminal fingerprint (for example a hash of the data path from TerminalInfoString(TERMINAL_DATA_PATH) combined with the login, via CryptEncode() with CRYPT_HASH_SHA256) distinguishes installations, but changes when a customer reinstalls or moves to a new VPS.

Account-based limits are usually the better default for trading products. Whatever you choose, give customers a self-service way to release an activation from their account area. Support teams that reset activations by hand become the bottleneck within months.

Offline grace and caching

Licence servers go down. Customer VPS hosts lose connectivity. A good scheme tolerates both. On each successful validation, store a timestamp (a terminal global variable or a file in the common data folder works) and allow the EA to keep running for a defined grace period, typically 24 to 72 hours, if the server cannot be reached. A server returning "invalid" is different from a server not answering: only the first should stop the EA.

Be realistic about the cache. Without built-in public-key verification in the standard library, a locally stored grace token is a convenience for honest users, not a security boundary.

Let customers backtest

Serious buyers backtest before and after they buy. MQLInfoInteger(MQL_TESTER) returns true inside the Strategy Tester, and since WebRequest() is unavailable there anyway, skipping the online check in the tester is both necessary and sensible. The tester cannot place live trades, so this costs you very little. If you want to restrict backtests for unpaid users, a demo build with a date range or symbol limit is cleaner than a broken tester experience.

Revocation, refunds and chargebacks

Revocation is the main reason to run a licence server at all. Mark the key revoked and the next hourly check fails. Typical triggers:

  • Subscription cancelled or payment failed after retries.
  • Refund issued or chargeback opened.
  • Key found published publicly.

Decide in advance what a revoked EA does with open positions. The defensible answer is: stop opening new trades, leave existing positions and their stops in place, and log a clear message.

From Stripe checkout to licence key

Manual key issuance does not scale and it delays the customer at the moment they are most motivated. A typical automated flow on Stripe and Supabase:

  1. Customer pays through Stripe Checkout (one-off price or subscription).
  2. Stripe sends a checkout.session.completed webhook to an edge function.
  3. The function verifies the webhook signature, checks idempotency (Stripe can deliver an event more than once), generates a random key and stores it against the customer and product.
  4. The key appears in the customer's account area and in a confirmation email.
  5. Further webhooks (invoice.paid, customer.subscription.deleted, charge.refunded, charge.dispute.created) extend, suspend or revoke the key.

Store only what you need: key, product, status, expiry, activation records and a validation log. Row-level security on the database means a customer can only ever read their own keys.

TradingView invite-only scripts

TradingView takes a different approach. Pine Script source runs on TradingView's servers, and an invite-only script hides the code while letting the author choose who can add it to a chart. Access is granted per TradingView username, optionally with an expiry date, through the script's access management panel. Publishing invite-only scripts requires a paid TradingView plan, and vendors must follow TradingView's House Rules on selling scripts.

The operational gap is automation. TradingView does not offer a public API for managing invite-only access, so many vendors grant access by hand or with semi-automated tooling. Before automating, read the platform's terms. Whatever you do, keep your own record of who should have access and when it expires, driven by the same payment webhooks as your MT5 keys, so access reviews take minutes rather than an afternoon.

What you cannot prevent

Honesty matters here:

  • A customer can share their screen, signals or trade copier output. No licence check stops someone copying trades from an account running your EA.
  • Binaries can be patched. A skilled reverse engineer can find and disable a licence check in a compiled program. Server-side checks raise the effort; they do not make it impossible.
  • Strategies can be reverse engineered from behaviour. Enough trade history reveals a lot about a simple strategy.
  • Leaked keys get used until you notice. Activation limits and validation logs help you notice quickly.

Why UX beats unbreakable DRM

The people who crack your EA were never going to pay. The people who paid will judge you on how often your protection gets in their way. Aggressive schemes (hardware locks, frequent forced revalidation, killing trades on a network blip) punish exactly the wrong group and generate support load, refunds and bad reviews.

Good MT5 EA licensing feels invisible: a key pasted once, a clear message if the WebRequest URL is missing, backtesting that just works, self-service activation resets and a grace period that survives a VPS reboot. Spend your effort there, and on shipping updates that make the paid version worth staying current with.

Key takeaways

  • Ship compiled .ex5 builds only and treat anything inside them as recoverable.
  • Use account or broker locks for simple cases, server-validated licence keys for anything sold at volume.
  • WebRequest() needs the URL on the terminal's allowed list, does not run in the Strategy Tester and cannot be called from indicators.
  • Bind activations to accounts, not fragile fingerprints, and let customers reset them themselves.
  • Build in an offline grace period and never close customer positions because a check failed.
  • Automate issuance and revocation from Stripe webhooks; keep the same source of truth for TradingView invite-only access.
  • Optimise for paying customers. No scheme stops a determined cracker.

How we apply this

We operate our own trading software as well as building it for clients, so licensing is something we run, not just design. Our MT5 and TradingView work typically pairs compiled builds with a Supabase-backed licence API, Stripe webhooks handled in edge functions and an account area where customers manage keys and activations. See our algorithmic trading services for how we approach EA and indicator engineering.

If you sell trading tools under your own brand, we can build and run the licensing layer for you, from checkout to revocation, as part of a white-label development engagement.


This article is for information and education only and is not investment advice. See our risk disclaimer.

Start a project

Need this engineered, not just explained?

We turn research like this into production software for trading and investment businesses.

Prefer email or phone? hello@aurionlabs.io · +44 7832 617626