AlgoScriptX

Seller integration & licensing

Integrate ASX license verification into your cTrader and MetaTrader 5 builds using the official AsxLicenseGuard snippets below. TradingView Pine scripts use invite-only access — see the TradingView section. cTrader uploads without a detectable license check are rejected automatically.

cTrader (v2): Paste the snippet, add a License Key parameter for buyers, and build your .algo — no product slug to edit. The server derives the product from the license key. MT5: still set your-product-slug in the snippet before upload.

Mandatory rules (cTrader v2)

  1. License key parameter — Expose an empty-by-default License Key field; buyers paste their key from My licenses.
  2. Verify on start — POST to https://id.algoscriptx.com/verify with key and Account.UserId (v2 signed protocol) — never per tick.
  3. Stop when invalid — Indicators render nothing while invalid; cBots call Stop(). Show the human-readable reason on chart.
  4. Web access — Use AccessRights.None and Http.SendAsync(HttpRequest) for verify (not HttpClient.PostAsync). Keep System.Net.Http + HMACSHA256 markers.

If a buyer owns multiple products, each license key runs against that key's product (server-authoritative). Account binding and sequence counters still prevent piracy.

Choose your integration

Minimal — quick start

Paste one verify method into your existing class. Verify on start only — fastest way to get protected.

  • ~60 lines to merge
  • Start-only verify
  • Same gatekeeper markers
MinimalFull (recommended)
Verify on start✓✓
12h re-validation—✓
Chart status messagesBasicFull
Test key support—✓
Passes .algo gatekeeper✓✓

Official snippets — Full (recommended)

Copy into cTrader Automate and merge the license logic into your indicator or cBot. No slug editing — compile and upload.

Full — cTrader Indicator (.cs)

Download
// AlgoScriptX — license guard v2 for cTrader Indicators (HMAC-SHA256, seq, monotonic grace).
//
// WHAT: Signed verify via id.algoscriptx.com/verify (protocol v2). 1h heartbeat, 24h offline grace.
// CHANGE: Buyers enter LicenseKey only — product is derived server-side from the key.
// BUYER SETUP: Run indicator with valid key; K_lic delivered once over TLS and cached locally.
// AI PROMPT: "Integrate AsxLicenseGuard v2 indicator: verify on init, block Calculate on fail, recalc bars after OK."
// HTTP: Use Http.SendAsync(HttpRequest) — HttpClient.PostAsync fails with AccessRights.None on many builds.
// Markers: id.algoscriptx.com | System.Net.Http | HMACSHA256 | Account.UserId | asx-ctrader-2026-01 | AccessRights.None
// License model: ONE-TIME purchase only (no subscription).
// .csproj: PackageReference cTrader.Automate only — no config.json, no SourceGenerators.

using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using cAlgo.API;

namespace cAlgo
{
    [Indicator(IsOverlay = true, TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class AsxLicenseGuardIndicator : Indicator
    {
        private const string VerifyUrl = "https://id.algoscriptx.com/verify";
        private const string TestKey = "TEST-DEMO-DEMO-DEMO-DEMO";
        private const string PanelId = "asx_license_status";
        private const string KidTrusted = "asx-ctrader-2026-01";
        private const int BotVersion = 2;
        private const long GraceMs = 86_400_000L;

        [Parameter("License Key", DefaultValue = "")]
        public string LicenseKey { get; set; }

        // Marker only — do NOT call AsxHttp.PostAsync under AccessRights.None; use Http.SendAsync below.
        private static readonly HttpClient AsxHttp = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
        private const AccessRights AsxWebAccessMarker = AccessRights.None;

        private bool _licensed;
        private bool _gateOpen;
        private long _lastGoodTick;
        private int _lastSeenSeq;
        private byte[] _licenseSecret;

        protected override void Initialize()
        {
            AsxLoadCache();
            DrawStatus("Checking license...", Color.Gold);
            VerifyLicenseAsync();
            Timer.Start(TimeSpan.FromHours(1));
        }

        protected override void OnTimer()
        {
            VerifyLicenseAsync();
        }

        protected override void OnDestroy()
        {
            Timer.Stop();
        }

        public override void Calculate(int index)
        {
            if (!AsxRuntimeAllowed())
                return;
        }

        private bool AsxRuntimeAllowed()
        {
            if (!_licensed || !_gateOpen)
                return false;
            if (_lastGoodTick > 0 && !AsxWithinGrace(_lastGoodTick))
            {
                BlockLicense("License grace expired — verify server unreachable", false);
                return false;
            }
            return true;
        }

        private async void VerifyLicenseAsync()
        {
            try
            {
                string key = (LicenseKey ?? string.Empty).Trim().ToUpperInvariant();
                string userId = Account.UserId.ToString();

                if (string.IsNullOrEmpty(key))
                {
                    BlockLicense("No license key entered", true);
                    return;
                }

                if (key == TestKey)
                {
                    SetLicensed(true, "License OK [TEST] - user " + userId, Color.LimeGreen);
                    _lastGoodTick = Environment.TickCount64;
                    AsxRecalculateAllBars();
                    return;
                }

                string nonce = AsxNewNonce();
                var payload = new Dictionary<string, object>
                {
                    ["license_key"] = key,
                    ["account_user_id"] = userId,
                    ["client_nonce"] = nonce,
                    ["seq"] = _lastSeenSeq,
                    ["bot_version"] = BotVersion
                };

                string json = JsonSerializer.Serialize(payload);
                var response = await AsxHttpSendAsync(json).ConfigureAwait(false);
                int status = response.StatusCode;
                string body = response.Body ?? "";

                if (status == 429)
                {
                    if (!_licensed)
                        BlockLicense("Rate limit — try again later", true);
                    else
                        DrawStatus("Rate limit — will retry later", Color.Orange);
                    return;
                }

                if (status >= 500 || status == 0)
                {
                    if (_licensed && AsxWithinGrace(_lastGoodTick))
                    {
                        DrawStatus("Server unreachable — grace active", Color.Orange);
                        return;
                    }
                    BlockLicense("License server error (" + status + ")", false);
                    return;
                }

                if (status != 200)
                {
                    BlockLicense("Unexpected HTTP " + status, true);
                    return;
                }

                ParseSignedResponse(body, key, nonce, userId);
            }
            catch (Exception ex)
            {
                if (_licensed && AsxWithinGrace(_lastGoodTick))
                {
                    DrawStatus("Verify error — grace active", Color.Orange);
                    return;
                }
                BlockLicense("Verify failed: " + ex.Message, false);
                Print("ASX License verify error: {0}", ex);
            }
        }

        private void ParseSignedResponse(string body, string licenseKey, string sentNonce, string userId)
        {
            try
            {
                using (var doc = JsonDocument.Parse(body))
                {
                    var root = doc.RootElement;
                    if (!root.TryGetProperty("protocol", out var proto) || proto.GetInt32() != 2)
                    {
                        BlockLicense("Unsupported verify protocol", true);
                        return;
                    }

                    if (!root.TryGetProperty("signature", out var sigEl) || sigEl.ValueKind != JsonValueKind.String)
                    {
                        string reason = root.TryGetProperty("reason", out var r) && r.ValueKind == JsonValueKind.String
                            ? r.GetString() : "server_error";
                        if (_licensed && reason == "server_error" && AsxWithinGrace(_lastGoodTick))
                            return;
                        BlockLicense(ReasonMessage(reason, userId), true);
                        return;
                    }

                    if (!root.TryGetProperty("payload", out var payloadEl))
                    {
                        BlockLicense("Missing signed payload", true);
                        return;
                    }

                    if (root.TryGetProperty("license_secret", out var secEl) && secEl.ValueKind == JsonValueKind.String)
                    {
                        string b64 = secEl.GetString();
                        if (!string.IsNullOrEmpty(b64))
                        {
                            _licenseSecret = Convert.FromBase64String(b64);
                            AsxSaveCache(licenseKey);
                        }
                    }

                    if (_licenseSecret == null || _licenseSecret.Length != 32)
                    {
                        BlockLicense("License secret missing — contact support", true);
                        return;
                    }

                    string canonical = AsxCanonicalJson(payloadEl);
                    string signature = sigEl.GetString() ?? string.Empty;
                    if (!AsxVerifyHmac(_licenseSecret, canonical, signature))
                    {
                        BlockLicense("Invalid license signature", true);
                        return;
                    }

                    string echoed = payloadEl.TryGetProperty("client_nonce", out var cn) && cn.ValueKind == JsonValueKind.String
                        ? cn.GetString() : "";
                    if (!string.Equals(echoed, sentNonce, StringComparison.Ordinal))
                    {
                        BlockLicense("Nonce mismatch — possible replay", true);
                        return;
                    }

                    string status = payloadEl.TryGetProperty("status", out var st) && st.ValueKind == JsonValueKind.String
                        ? st.GetString() : "invalid";
                    int seq = payloadEl.TryGetProperty("seq", out var sq) && sq.ValueKind == JsonValueKind.Number
                        ? sq.GetInt32() : 0;

                    if (_lastSeenSeq > 0 && seq <= _lastSeenSeq)
                    {
                        BlockLicense("Stale sequence — possible clone", true);
                        return;
                    }

                    if (!string.Equals(status, "valid", StringComparison.Ordinal))
                    {
                        string reason = payloadEl.TryGetProperty("reason", out var rs) && rs.ValueKind == JsonValueKind.String
                            ? rs.GetString() : status;
                        BlockLicense(ReasonMessage(reason, userId), true);
                        return;
                    }

                    if (payloadEl.TryGetProperty("kid", out var kid) && kid.ValueKind == JsonValueKind.String
                        && !string.Equals(kid.GetString(), KidTrusted, StringComparison.Ordinal))
                    {
                        BlockLicense("Untrusted signing key id", true);
                        return;
                    }

                    _lastSeenSeq = seq;
                    _lastGoodTick = Environment.TickCount64;
                    _gateOpen = seq > 0;
                    AsxSaveCache(licenseKey);
                    string productLabel = payloadEl.TryGetProperty("product_id", out var pid) && pid.ValueKind == JsonValueKind.String
                        ? (pid.GetString() ?? "") : "";
                    SetLicensed(true, "License OK — " + productLabel + " — user " + userId, Color.LimeGreen);
                    AsxRecalculateAllBars();
                }
            }
            catch (Exception ex)
            {
                BlockLicense("Bad server response", true);
                Print("ASX License parse error: {0}", ex.Message);
            }
        }

        private Task<HttpResponse> AsxHttpSendAsync(string json)
        {
            var request = new HttpRequest(new Uri(VerifyUrl))
            {
                Method = HttpMethod.Post,
                Body = json,
                Timeout = TimeSpan.FromSeconds(15)
            };
            request.Headers.Add("Content-Type", "application/json");
            var tcs = new TaskCompletionSource<HttpResponse>();
            Http.SendAsync(request, response => tcs.TrySetResult(response));
            return tcs.Task;
        }

        private void AsxRecalculateAllBars()
        {
            BeginInvokeOnMainThread(() =>
            {
                for (int i = 0; i < Bars.Count; i++)
                    Calculate(i);
            });
        }

        private static byte[] AsxSha256Bytes(string text)
        {
            using var sha = SHA256.Create();
            return sha.ComputeHash(Encoding.UTF8.GetBytes(text));
        }

        private static string AsxNewNonce()
        {
            var bytes = new byte[32];
            using (var rng = RandomNumberGenerator.Create())
                rng.GetBytes(bytes);
            var sb = new StringBuilder(64);
            foreach (byte b in bytes)
                sb.Append(b.ToString("x2"));
            return sb.ToString();
        }

        private static string AsxCanonicalJson(JsonElement payload)
        {
            var fields = new SortedDictionary<string, string>(StringComparer.Ordinal);
            foreach (var prop in payload.EnumerateObject())
            {
                fields[prop.Name] = prop.Value.ValueKind switch
                {
                    JsonValueKind.Number => prop.Value.GetRawText(),
                    JsonValueKind.True => "true",
                    JsonValueKind.False => "false",
                    JsonValueKind.String => prop.Value.GetString() ?? "",
                    _ => prop.Value.GetRawText()
                };
            }

            var sb = new StringBuilder();
            sb.Append('{');
            bool first = true;
            foreach (var kv in fields)
            {
                if (!first) sb.Append(',');
                first = false;
                sb.Append('"').Append(AsxEscapeJson(kv.Key)).Append('"').Append(':');
                if (kv.Key == "grace_seconds" || kv.Key == "seq" || kv.Key == "server_time" || kv.Key == "v")
                    sb.Append(kv.Value);
                else if (kv.Value == "true" || kv.Value == "false")
                    sb.Append(kv.Value);
                else
                    sb.Append('"').Append(AsxEscapeJson(kv.Value)).Append('"');
            }
            sb.Append('}');
            return sb.ToString();
        }

        private static string AsxEscapeJson(string s)
        {
            if (string.IsNullOrEmpty(s)) return "";
            return s.Replace("\\", "\\\\").Replace("\"", "\\\"");
        }

        private static bool AsxVerifyHmac(byte[] secret, string canonical, string signatureHex)
        {
            using var hmac = new HMACSHA256(secret);
            byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical));
            var sb = new StringBuilder(64);
            foreach (byte bt in hash)
                sb.Append(bt.ToString("x2"));
            return AsxFixedTimeEquals(sb.ToString(), (signatureHex ?? "").ToLowerInvariant());
        }

        private static bool AsxFixedTimeEquals(string a, string b)
        {
            if (a == null || b == null || a.Length != b.Length) return false;
            int diff = 0;
            for (int i = 0; i < a.Length; i++)
                diff |= a[i] ^ b[i];
            return diff == 0;
        }

        private static bool AsxWithinGrace(long lastGoodTick)
        {
            if (lastGoodTick <= 0) return false;
            long delta = Environment.TickCount64 - lastGoodTick;
            if (delta < 0) delta += long.MaxValue;
            return delta < GraceMs;
        }

        private void AsxLoadCache()
        {
            try
            {
                string path = AsxCachePath();
                if (!System.IO.File.Exists(path)) return;
                string raw = System.IO.File.ReadAllText(path);
                using var doc = JsonDocument.Parse(raw);
                var root = doc.RootElement;
                if (root.TryGetProperty("seq", out var sq)) _lastSeenSeq = sq.GetInt32();
                if (root.TryGetProperty("tick", out var tk)) _lastGoodTick = tk.GetInt64();
                if (root.TryGetProperty("sec", out var sc) && sc.ValueKind == JsonValueKind.String)
                {
                    string obf = sc.GetString() ?? "";
                    _licenseSecret = AsxDeobfuscate(obf);
                }
            }
            catch { /* cache miss */ }
        }

        private void AsxSaveCache(string licenseKey)
        {
            try
            {
                if (_licenseSecret == null) return;
                string dir = Path.GetDirectoryName(AsxCachePath());
                if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
                    Directory.CreateDirectory(dir);
                var obj = new Dictionary<string, object>
                {
                    ["seq"] = _lastSeenSeq,
                    ["tick"] = _lastGoodTick,
                    ["sec"] = AsxObfuscate(_licenseSecret, licenseKey)
                };
                System.IO.File.WriteAllText(AsxCachePath(), JsonSerializer.Serialize(obj));
            }
            catch (Exception ex) { Print("ASX cache write: {0}", ex.Message); }
        }

        private string AsxCachePath()
        {
            string key = (LicenseKey ?? "").Trim().ToUpperInvariant();
            using var sha = SHA256.Create();
            byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(key + "|" + Account.UserId));
            var sb = new StringBuilder(16);
            for (int i = 0; i < 8; i++) sb.Append(hash[i].ToString("x2"));
            return "asx_lic_" + sb + ".dat";
        }

        private static string AsxObfuscate(byte[] secret, string licenseKey)
        {
            byte[] mask = AsxSha256Bytes(licenseKey);
            var buf = new byte[secret.Length];
            for (int i = 0; i < secret.Length; i++)
                buf[i] = (byte)(secret[i] ^ mask[i % mask.Length]);
            return Convert.ToBase64String(buf);
        }

        private byte[] AsxDeobfuscate(string obfB64)
        {
            string key = (LicenseKey ?? "").Trim().ToUpperInvariant();
            byte[] buf = Convert.FromBase64String(obfB64);
            byte[] mask = AsxSha256Bytes(key);
            var outBuf = new byte[buf.Length];
            for (int i = 0; i < buf.Length; i++)
                outBuf[i] = (byte)(buf[i] ^ mask[i % mask.Length]);
            return outBuf;
        }

        private static string ReasonMessage(string reason, string userId)
        {
            if (string.IsNullOrEmpty(reason)) return "License invalid";
            switch (reason)
            {
                case "license_not_found": return "License not found";
                case "license_revoked": return "License revoked (refunded or cancelled)";
                case "license_expired": return "License expired";
                case "license_suspended": return "License suspended";
                case "account_mismatch":
                    return "License bound to another account (UserId). One purchase = one account.";
                case "product_mismatch":
                case "invalid_product_slug":
                    return "License/product mismatch — contact support.";
                case "seq_stale": return "License sequence conflict — possible clone";
                case "invalid_license_format": return "Invalid license key format";
                case "invalid_account_id": return "Invalid account id";
                case "rate_limit_exceeded": return "Rate limit — try again later";
                case "server_error": return "License server error";
                default:
                    if (reason.StartsWith("license_")) return "License " + reason.Substring("license_".Length);
                    return "License invalid: " + reason;
            }
        }

        private void BlockLicense(string message, bool immediate)
        {
            if (!immediate && _licensed && AsxWithinGrace(_lastGoodTick))
                return;
            SetLicensed(false, message, Color.OrangeRed);
        }

        private void SetLicensed(bool licensed, string text, Color color)
        {
            _licensed = licensed;
            if (!licensed) _gateOpen = false;
            try { BeginInvokeOnMainThread(() => DrawStatus(text, color)); }
            catch (Exception ex) { Print("ASX License UI error: {0}", ex); }
            Print("ASX License: {0} | {1}", licensed ? "VALID" : "INVALID", text);
        }

        private void DrawStatus(string text, Color color)
        {
            Chart.DrawStaticText(PanelId, "AlgoScriptX License: " + text, VerticalAlignment.Top, HorizontalAlignment.Left, color);
        }
    }
}

Full — cTrader cBot (Robot)

Download
// AlgoScriptX — license guard v2 for cTrader cBots (HMAC-SHA256, seq, monotonic grace).
//
// WHAT: Signed verify via id.algoscriptx.com/verify (protocol v2). 1h heartbeat, 24h offline grace.
// CHANGE: Buyers enter LicenseKey only — product is derived server-side from the key.
// BUYER SETUP: Run cBot with valid key; K_lic delivered once over TLS and cached locally.
// AI PROMPT: "Integrate AsxLicenseGuard v2: verify on start, gate OnTick, Stop() on fail."
// HTTP: Use Http.SendAsync(HttpRequest) — HttpClient.PostAsync fails with AccessRights.None on many builds.
// Markers: id.algoscriptx.com | System.Net.Http | HMACSHA256 | Account.UserId | asx-ctrader-2026-01 | AccessRights.None
// License model: ONE-TIME purchase only (no subscription).
// .csproj: PackageReference cTrader.Automate only — no config.json, no SourceGenerators.

using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using cAlgo.API;

namespace cAlgo
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class AsxLicenseGuardCbot : Robot
    {
        private const string VerifyUrl = "https://id.algoscriptx.com/verify";
        private const string TestKey = "TEST-DEMO-DEMO-DEMO-DEMO";
        private const string PanelId = "asx_license_status";
        private const string KidTrusted = "asx-ctrader-2026-01";
        private const int BotVersion = 2;
        private const long GraceMs = 86_400_000L;

        [Parameter("License Key", DefaultValue = "")]
        public string LicenseKey { get; set; }

        // Marker only — do NOT call AsxHttp.PostAsync under AccessRights.None; use Http.SendAsync below.
        private static readonly HttpClient AsxHttp = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
        private const AccessRights AsxWebAccessMarker = AccessRights.None;

        private bool _licensed;
        private bool _gateOpen;
        private long _lastGoodTick;
        private int _lastSeenSeq;
        private byte[] _licenseSecret;

        protected override void OnStart()
        {
            AsxLoadCache();
            DrawStatus("Checking license...", Color.Gold);
            VerifyLicenseAsync();
            Timer.Start(TimeSpan.FromHours(1));
        }

        protected override void OnTimer()
        {
            VerifyLicenseAsync();
        }

        protected override void OnStop()
        {
            Timer.Stop();
        }

        protected override void OnTick()
        {
            if (!AsxRuntimeAllowed())
                return;
        }

        private bool AsxRuntimeAllowed()
        {
            if (!_licensed || !_gateOpen)
                return false;
            if (_lastGoodTick > 0 && !AsxWithinGrace(_lastGoodTick))
            {
                Invalidate("License grace expired — verify server unreachable", false);
                return false;
            }
            return true;
        }

        private async void VerifyLicenseAsync()
        {
            try
            {
                string key = (LicenseKey ?? string.Empty).Trim().ToUpperInvariant();
                string userId = Account.UserId.ToString();

                if (string.IsNullOrEmpty(key))
                {
                    Invalidate("No license key entered", true);
                    return;
                }

                if (key == TestKey)
                {
                    SetLicensed(true, "License OK [TEST] - user " + userId, Color.LimeGreen);
                    _lastGoodTick = Environment.TickCount64;
                    return;
                }

                string nonce = AsxNewNonce();
                var payload = new Dictionary<string, object>
                {
                    ["license_key"] = key,
                    ["account_user_id"] = userId,
                    ["client_nonce"] = nonce,
                    ["seq"] = _lastSeenSeq,
                    ["bot_version"] = BotVersion
                };

                string json = JsonSerializer.Serialize(payload);
                var response = await AsxHttpSendAsync(json).ConfigureAwait(false);
                int status = response.StatusCode;
                string body = response.Body ?? "";

                if (status == 429)
                {
                    if (!_licensed)
                        Invalidate("Rate limit — try again later", true);
                    else
                        DrawStatus("Rate limit — will retry later", Color.Orange);
                    return;
                }

                if (status >= 500 || status == 0)
                {
                    if (_licensed && AsxWithinGrace(_lastGoodTick))
                    {
                        DrawStatus("Server unreachable — grace active", Color.Orange);
                        return;
                    }
                    Invalidate("License server error (" + status + ")", false);
                    return;
                }

                if (status != 200)
                {
                    Invalidate("Unexpected HTTP " + status, true);
                    return;
                }

                ParseSignedResponse(body, key, nonce, userId);
            }
            catch (Exception ex)
            {
                if (_licensed && AsxWithinGrace(_lastGoodTick))
                {
                    DrawStatus("Verify error — grace active", Color.Orange);
                    return;
                }
                Invalidate("Verify failed: " + ex.Message, false);
                Print("ASX License verify error: {0}", ex);
            }
        }

        private void ParseSignedResponse(string body, string licenseKey, string sentNonce, string userId)
        {
            try
            {
                using (var doc = JsonDocument.Parse(body))
                {
                    var root = doc.RootElement;
                    if (!root.TryGetProperty("protocol", out var proto) || proto.GetInt32() != 2)
                    {
                        Invalidate("Unsupported verify protocol", true);
                        return;
                    }

                    if (!root.TryGetProperty("signature", out var sigEl) || sigEl.ValueKind != JsonValueKind.String)
                    {
                        string reason = root.TryGetProperty("reason", out var r) && r.ValueKind == JsonValueKind.String
                            ? r.GetString() : "server_error";
                        if (_licensed && reason == "server_error" && AsxWithinGrace(_lastGoodTick))
                            return;
                        Invalidate(ReasonMessage(reason, userId), true);
                        return;
                    }

                    if (!root.TryGetProperty("payload", out var payloadEl))
                    {
                        Invalidate("Missing signed payload", true);
                        return;
                    }

                    if (root.TryGetProperty("license_secret", out var secEl) && secEl.ValueKind == JsonValueKind.String)
                    {
                        string b64 = secEl.GetString();
                        if (!string.IsNullOrEmpty(b64))
                        {
                            _licenseSecret = Convert.FromBase64String(b64);
                            AsxSaveCache(licenseKey);
                        }
                    }

                    if (_licenseSecret == null || _licenseSecret.Length != 32)
                    {
                        Invalidate("License secret missing — contact support", true);
                        return;
                    }

                    string canonical = AsxCanonicalJson(payloadEl);
                    string signature = sigEl.GetString() ?? string.Empty;
                    if (!AsxVerifyHmac(_licenseSecret, canonical, signature))
                    {
                        Invalidate("Invalid license signature", true);
                        return;
                    }

                    string echoed = payloadEl.TryGetProperty("client_nonce", out var cn) && cn.ValueKind == JsonValueKind.String
                        ? cn.GetString() : "";
                    if (!string.Equals(echoed, sentNonce, StringComparison.Ordinal))
                    {
                        Invalidate("Nonce mismatch — possible replay", true);
                        return;
                    }

                    string status = payloadEl.TryGetProperty("status", out var st) && st.ValueKind == JsonValueKind.String
                        ? st.GetString() : "invalid";
                    int seq = payloadEl.TryGetProperty("seq", out var sq) && sq.ValueKind == JsonValueKind.Number
                        ? sq.GetInt32() : 0;

                    if (_lastSeenSeq > 0 && seq <= _lastSeenSeq)
                    {
                        Invalidate("Stale sequence — possible clone", true);
                        return;
                    }

                    if (!string.Equals(status, "valid", StringComparison.Ordinal))
                    {
                        string reason = payloadEl.TryGetProperty("reason", out var rs) && rs.ValueKind == JsonValueKind.String
                            ? rs.GetString() : status;
                        Invalidate(ReasonMessage(reason, userId), true);
                        return;
                    }

                    if (payloadEl.TryGetProperty("kid", out var kid) && kid.ValueKind == JsonValueKind.String
                        && !string.Equals(kid.GetString(), KidTrusted, StringComparison.Ordinal))
                    {
                        Invalidate("Untrusted signing key id", true);
                        return;
                    }

                    _lastSeenSeq = seq;
                    _lastGoodTick = Environment.TickCount64;
                    _gateOpen = seq > 0;
                    AsxSaveCache(licenseKey);
                    string productLabel = payloadEl.TryGetProperty("product_id", out var pid) && pid.ValueKind == JsonValueKind.String
                        ? (pid.GetString() ?? "") : "";
                    SetLicensed(true, "License OK — " + productLabel + " — user " + userId, Color.LimeGreen);
                }
            }
            catch (Exception ex)
            {
                Invalidate("Bad server response", true);
                Print("ASX License parse error: {0}", ex.Message);
            }
        }

        private Task<HttpResponse> AsxHttpSendAsync(string json)
        {
            var request = new HttpRequest(new Uri(VerifyUrl))
            {
                Method = HttpMethod.Post,
                Body = json,
                Timeout = TimeSpan.FromSeconds(15)
            };
            request.Headers.Add("Content-Type", "application/json");
            var tcs = new TaskCompletionSource<HttpResponse>();
            Http.SendAsync(request, response => tcs.TrySetResult(response));
            return tcs.Task;
        }

        private static byte[] AsxSha256Bytes(string text)
        {
            using var sha = SHA256.Create();
            return sha.ComputeHash(Encoding.UTF8.GetBytes(text));
        }

        private static string AsxNewNonce()
        {
            var bytes = new byte[32];
            using (var rng = RandomNumberGenerator.Create())
                rng.GetBytes(bytes);
            var sb = new StringBuilder(64);
            foreach (byte b in bytes)
                sb.Append(b.ToString("x2"));
            return sb.ToString();
        }

        private static string AsxCanonicalJson(JsonElement payload)
        {
            var fields = new SortedDictionary<string, string>(StringComparer.Ordinal);
            foreach (var prop in payload.EnumerateObject())
            {
                fields[prop.Name] = prop.Value.ValueKind switch
                {
                    JsonValueKind.Number => prop.Value.GetRawText(),
                    JsonValueKind.True => "true",
                    JsonValueKind.False => "false",
                    JsonValueKind.String => prop.Value.GetString() ?? "",
                    _ => prop.Value.GetRawText()
                };
            }

            var sb = new StringBuilder();
            sb.Append('{');
            bool first = true;
            foreach (var kv in fields)
            {
                if (!first) sb.Append(',');
                first = false;
                sb.Append('"').Append(AsxEscapeJson(kv.Key)).Append('"').Append(':');
                if (kv.Key == "grace_seconds" || kv.Key == "seq" || kv.Key == "server_time" || kv.Key == "v")
                    sb.Append(kv.Value);
                else if (kv.Value == "true" || kv.Value == "false")
                    sb.Append(kv.Value);
                else
                    sb.Append('"').Append(AsxEscapeJson(kv.Value)).Append('"');
            }
            sb.Append('}');
            return sb.ToString();
        }

        private static string AsxEscapeJson(string s)
        {
            if (string.IsNullOrEmpty(s)) return "";
            return s.Replace("\\", "\\\\").Replace("\"", "\\\"");
        }

        private static bool AsxVerifyHmac(byte[] secret, string canonical, string signatureHex)
        {
            using var hmac = new HMACSHA256(secret);
            byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical));
            var sb = new StringBuilder(64);
            foreach (byte bt in hash)
                sb.Append(bt.ToString("x2"));
            return AsxFixedTimeEquals(sb.ToString(), (signatureHex ?? "").ToLowerInvariant());
        }

        private static bool AsxFixedTimeEquals(string a, string b)
        {
            if (a == null || b == null || a.Length != b.Length) return false;
            int diff = 0;
            for (int i = 0; i < a.Length; i++)
                diff |= a[i] ^ b[i];
            return diff == 0;
        }

        private static bool AsxWithinGrace(long lastGoodTick)
        {
            if (lastGoodTick <= 0) return false;
            long delta = Environment.TickCount64 - lastGoodTick;
            if (delta < 0) delta += long.MaxValue;
            return delta < GraceMs;
        }

        private void AsxLoadCache()
        {
            try
            {
                string path = AsxCachePath();
                if (!System.IO.File.Exists(path)) return;
                string raw = System.IO.File.ReadAllText(path);
                using var doc = JsonDocument.Parse(raw);
                var root = doc.RootElement;
                if (root.TryGetProperty("seq", out var sq)) _lastSeenSeq = sq.GetInt32();
                if (root.TryGetProperty("tick", out var tk)) _lastGoodTick = tk.GetInt64();
                if (root.TryGetProperty("sec", out var sc) && sc.ValueKind == JsonValueKind.String)
                {
                    string obf = sc.GetString() ?? "";
                    _licenseSecret = AsxDeobfuscate(obf);
                }
            }
            catch { /* cache miss */ }
        }

        private void AsxSaveCache(string licenseKey)
        {
            try
            {
                if (_licenseSecret == null) return;
                string dir = Path.GetDirectoryName(AsxCachePath());
                if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
                    Directory.CreateDirectory(dir);
                var obj = new Dictionary<string, object>
                {
                    ["seq"] = _lastSeenSeq,
                    ["tick"] = _lastGoodTick,
                    ["sec"] = AsxObfuscate(_licenseSecret, licenseKey)
                };
                System.IO.File.WriteAllText(AsxCachePath(), JsonSerializer.Serialize(obj));
            }
            catch (Exception ex) { Print("ASX cache write: {0}", ex.Message); }
        }

        private string AsxCachePath()
        {
            string key = (LicenseKey ?? "").Trim().ToUpperInvariant();
            using var sha = SHA256.Create();
            byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(key + "|" + Account.UserId));
            var sb = new StringBuilder(16);
            for (int i = 0; i < 8; i++) sb.Append(hash[i].ToString("x2"));
            return "asx_lic_" + sb + ".dat";
        }

        private static string AsxObfuscate(byte[] secret, string licenseKey)
        {
            byte[] mask = AsxSha256Bytes(licenseKey);
            var buf = new byte[secret.Length];
            for (int i = 0; i < secret.Length; i++)
                buf[i] = (byte)(secret[i] ^ mask[i % mask.Length]);
            return Convert.ToBase64String(buf);
        }

        private byte[] AsxDeobfuscate(string obfB64)
        {
            string key = (LicenseKey ?? "").Trim().ToUpperInvariant();
            byte[] buf = Convert.FromBase64String(obfB64);
            byte[] mask = AsxSha256Bytes(key);
            var outBuf = new byte[buf.Length];
            for (int i = 0; i < buf.Length; i++)
                outBuf[i] = (byte)(buf[i] ^ mask[i % mask.Length]);
            return outBuf;
        }

        private static string ReasonMessage(string reason, string userId)
        {
            if (string.IsNullOrEmpty(reason)) return "License invalid";
            switch (reason)
            {
                case "license_not_found": return "License not found";
                case "license_revoked": return "License revoked (refunded or cancelled)";
                case "license_expired": return "License expired";
                case "license_suspended": return "License suspended";
                case "account_mismatch":
                    return "License bound to another account (UserId). One purchase = one account.";
                case "product_mismatch":
                case "invalid_product_slug":
                    return "License/product mismatch — contact support.";
                case "seq_stale": return "License sequence conflict — possible clone";
                case "invalid_license_format": return "Invalid license key format";
                case "invalid_account_id": return "Invalid account id";
                case "rate_limit_exceeded": return "Rate limit — try again later";
                case "server_error": return "License server error";
                default:
                    if (reason.StartsWith("license_")) return "License " + reason.Substring("license_".Length);
                    return "License invalid: " + reason;
            }
        }

        private void Invalidate(string message, bool immediate)
        {
            if (!immediate && _licensed && AsxWithinGrace(_lastGoodTick))
                return;
            SetLicensed(false, message, Color.OrangeRed);
            try { BeginInvokeOnMainThread(() => Stop()); }
            catch (Exception ex) { Print("ASX License stop error: {0}", ex); }
        }

        private void SetLicensed(bool licensed, string text, Color color)
        {
            _licensed = licensed;
            if (!licensed) _gateOpen = false;
            try { BeginInvokeOnMainThread(() => DrawStatus(text, color)); }
            catch (Exception ex) { Print("ASX License UI error: {0}", ex); }
            Print("ASX License: {0} | {1}", licensed ? "VALID" : "INVALID", text);
        }

        private void DrawStatus(string text, Color color)
        {
            Chart.DrawStaticText(PanelId, "AlgoScriptX License: " + text, VerticalAlignment.Top, HorizontalAlignment.Left, color);
        }
    }
}

Official snippets — Minimal (quick start)

Paste the verify method and constants into your own class. Use AccessRights.None on your class attribute.

Minimal — cTrader Indicator paste-in

Download
// AlgoScriptX MINIMAL license verify v2 — cTrader Indicator (.cs)
//
// WHAT: HMAC-SHA256 signed verify (protocol v2). Account.UserId binding. 1h heartbeat, 24h monotonic grace.
// CHANGE: Call VerifyAsxLicenseAsync() in Initialize; gate Calculate with AsxRuntimeOk(). No slug to set.
// HTTP: Use Http.SendAsync(HttpRequest) — HttpClient.PostAsync fails with AccessRights.None on many builds.
// Markers: id.algoscriptx.com | System.Net.Http | HMACSHA256 | Account.UserId | asx-ctrader-2026-01 | AccessRights.None
// AI PROMPT: "Integrate this v2 license guard indicator: verify Initialize, block Calculate until licensed, recalculate bars after OK."

using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using cAlgo.API;

private const string AsxVerifyUrl = "https://id.algoscriptx.com/verify";
private const string AsxKid = "asx-ctrader-2026-01";
private const int AsxBotVersion = 2;
private const long AsxGraceMs = 86_400_000L;
private const AccessRights AsxWebAccessMarker = AccessRights.None;
// Marker only — do NOT call AsxHttp.PostAsync under AccessRights.None; use Http.SendAsync below.
private static readonly HttpClient AsxHttp = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };

private bool _asxLicensed;
private bool _asxGate;
private int _asxSeq;
private long _asxLastGoodTick;
private byte[] _asxSecret;

[Parameter("License Key", DefaultValue = "")]
public string LicenseKey { get; set; }

private bool AsxRuntimeOk()
{
    if (!_asxLicensed || !_asxGate) return false;
    if (_asxLastGoodTick > 0 && !AsxWithinGrace(_asxLastGoodTick)) { AsxBlock("Grace expired", false); return false; }
    return true;
}

private async void VerifyAsxLicenseAsync()
{
    try
    {
        AsxLoadState();
        string key = (LicenseKey ?? "").Trim().ToUpperInvariant();
        string userId = Account.UserId.ToString();
        if (string.IsNullOrEmpty(key)) { AsxBlock("No license key", true); return; }

        string nonce = AsxNonce();
        var req = new Dictionary<string, object>
        {
            ["license_key"] = key,
            ["account_user_id"] = userId,
            ["client_nonce"] = nonce,
            ["seq"] = _asxSeq,
            ["bot_version"] = AsxBotVersion
        };
        string json = JsonSerializer.Serialize(req);
        var response = await AsxHttpSendAsync(json).ConfigureAwait(false);
        int status = response.StatusCode;
        string body = response.Body ?? "";
        if (status >= 500) { if (_asxLicensed && AsxWithinGrace(_asxLastGoodTick)) return; AsxBlock("Server error", false); return; }
        if (status != 200) { AsxBlock("HTTP " + status, true); return; }
        AsxParse(body, key, nonce);
    }
    catch (Exception ex)
    {
        if (_asxLicensed && AsxWithinGrace(_asxLastGoodTick)) return;
        AsxBlock(ex.Message, false);
    }
}

private Task<HttpResponse> AsxHttpSendAsync(string json)
{
    var request = new HttpRequest(new Uri(AsxVerifyUrl))
    {
        Method = HttpMethod.Post,
        Body = json,
        Timeout = TimeSpan.FromSeconds(15)
    };
    request.Headers.Add("Content-Type", "application/json");
    var tcs = new TaskCompletionSource<HttpResponse>();
    Http.SendAsync(request, response => tcs.TrySetResult(response));
    return tcs.Task;
}

private void AsxParse(string body, string key, string nonce)
{
    using var doc = JsonDocument.Parse(body);
    var root = doc.RootElement;
    if (!root.TryGetProperty("signature", out var sig)) { AsxBlock("Unsigned response", true); return; }
    if (!root.TryGetProperty("payload", out var payload)) { AsxBlock("No payload", true); return; }
    if (root.TryGetProperty("license_secret", out var sec) && sec.ValueKind == JsonValueKind.String)
        _asxSecret = Convert.FromBase64String(sec.GetString() ?? "");
    if (_asxSecret == null || _asxSecret.Length != 32) { AsxBlock("No license secret", true); return; }
    string canonical = AsxCanonical(payload);
    if (!AsxHmacOk(_asxSecret, canonical, sig.GetString())) { AsxBlock("Bad signature", true); return; }
    if ((payload.TryGetProperty("client_nonce", out var cn) ? cn.GetString() : "") != nonce) { AsxBlock("Nonce mismatch", true); return; }
    int seq = payload.TryGetProperty("seq", out var sq) ? sq.GetInt32() : 0;
    if (_asxSeq > 0 && seq <= _asxSeq) { AsxBlock("Stale seq", true); return; }
    if ((payload.TryGetProperty("status", out var st) ? st.GetString() : "") != "valid") { AsxBlock("License invalid", true); return; }
    _asxSeq = seq; _asxLastGoodTick = Environment.TickCount64; _asxGate = seq > 0; _asxLicensed = true;
    AsxSaveState(key);
    string productLabel = payload.TryGetProperty("product_id", out var pid) && pid.ValueKind == JsonValueKind.String ? (pid.GetString() ?? "") : "";
    Print("ASX: license OK — " + productLabel + " — user " + Account.UserId);
    AsxRecalculateAllBars();
}

private void AsxRecalculateAllBars()
{
    BeginInvokeOnMainThread(() =>
    {
        for (int i = 0; i < Bars.Count; i++)
            Calculate(i);
    });
}

private static string AsxNonce()
{
    var b = new byte[32]; using var r = RandomNumberGenerator.Create(); r.GetBytes(b);
    var sb = new StringBuilder(64); foreach (var x in b) sb.Append(x.ToString("x2")); return sb.ToString();
}

private static string AsxCanonical(JsonElement payload)
{
    var map = new SortedDictionary<string, string>(StringComparer.Ordinal);
    foreach (var p in payload.EnumerateObject())
        map[p.Name] = p.Value.ValueKind == JsonValueKind.Number ? p.Value.GetRawText() : (p.Value.GetString() ?? "");
    var sb = new StringBuilder("{");
    bool f = true;
    foreach (var kv in map)
    {
        if (!f) sb.Append(','); f = false;
        sb.Append('"').Append(kv.Key).Append('"').Append(':');
        if (kv.Key is "grace_seconds" or "seq" or "server_time" or "v") sb.Append(kv.Value);
        else sb.Append('"').Append(kv.Value.Replace("\\", "\\\\").Replace("\"", "\\\"")).Append('"');
    }
    return sb.Append('}').ToString();
}

private static bool AsxHmacOk(byte[] secret, string canon, string sig)
{
    using var h = new HMACSHA256(secret);
    var hash = h.ComputeHash(Encoding.UTF8.GetBytes(canon));
    var sb = new StringBuilder(); foreach (var bt in hash) sb.Append(bt.ToString("x2"));
    string a = sb.ToString(), sigHex = (sig ?? "").ToLowerInvariant();
    if (a.Length != sigHex.Length) return false;
    int d = 0; for (int i = 0; i < a.Length; i++) d |= a[i] ^ sigHex[i];
    return d == 0;
}

private static bool AsxWithinGrace(long tick)
{
    if (tick <= 0) return false;
    long d = Environment.TickCount64 - tick; if (d < 0) d += long.MaxValue;
    return d < AsxGraceMs;
}

private void AsxLoadState() { try { if (!System.IO.File.Exists(AsxStatePath())) return; using var d = JsonDocument.Parse(System.IO.File.ReadAllText(AsxStatePath()));
    _asxSeq = d.RootElement.TryGetProperty("seq", out var s) ? s.GetInt32() : 0;
    _asxLastGoodTick = d.RootElement.TryGetProperty("tick", out var t) ? t.GetInt64() : 0;
    if (d.RootElement.TryGetProperty("sec", out var x)) _asxSecret = Convert.FromBase64String(x.GetString() ?? ""); } catch { } }

private void AsxSaveState(string key) { try { System.IO.File.WriteAllText(AsxStatePath(), JsonSerializer.Serialize(new Dictionary<string, object>{{"seq",_asxSeq},{"tick",_asxLastGoodTick},{"sec",Convert.ToBase64String(_asxSecret)}})); } catch { } }

// Rename prefix per product (e.g. asx_sob_pro_v54_) so multiple indicators do not share state.
private string AsxStatePath() => "asx_min_ind_" + Account.UserId + ".dat";

private void AsxBlock(string msg, bool immediate)
{
    if (!immediate && _asxLicensed && AsxWithinGrace(_asxLastGoodTick)) return;
    _asxLicensed = false; _asxGate = false; Print("ASX: " + msg);
}

// Initialize: Timer.Start(TimeSpan.FromHours(1)); VerifyAsxLicenseAsync();
// OnTimer: VerifyAsxLicenseAsync();
// OnDestroy: Timer.Stop();
// Calculate: if (!AsxRuntimeOk()) return;
// .csproj: PackageReference cTrader.Automate only — no config.json, no SourceGenerators.

Minimal — cTrader cBot paste-in

Download
// AlgoScriptX MINIMAL license verify v2 — cTrader cBot (.cs)
//
// WHAT: HMAC-SHA256 signed verify (protocol v2). Account.UserId binding. 1h heartbeat, 24h monotonic grace.
// CHANGE: Call VerifyAsxLicenseAsync() in OnStart; gate OnTick with AsxRuntimeOk(). No slug to set.
// HTTP: Use Http.SendAsync(HttpRequest) — HttpClient.PostAsync fails with AccessRights.None on many builds.
// Markers: id.algoscriptx.com | System.Net.Http | HMACSHA256 | Account.UserId | asx-ctrader-2026-01 | AccessRights.None
// AI PROMPT: "Integrate this v2 license guard: verify OnStart, block OnTick until licensed."

using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using cAlgo.API;

private const string AsxVerifyUrl = "https://id.algoscriptx.com/verify";
private const string AsxKid = "asx-ctrader-2026-01";
private const int AsxBotVersion = 2;
private const long AsxGraceMs = 86_400_000L;
private const AccessRights AsxWebAccessMarker = AccessRights.None;
// Marker only — do NOT call AsxHttp.PostAsync under AccessRights.None; use Http.SendAsync below.
private static readonly HttpClient AsxHttp = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };

private bool _asxLicensed;
private bool _asxGate;
private int _asxSeq;
private long _asxLastGoodTick;
private byte[] _asxSecret;

[Parameter("License Key", DefaultValue = "")]
public string LicenseKey { get; set; }

private bool AsxRuntimeOk()
{
    if (!_asxLicensed || !_asxGate) return false;
    if (_asxLastGoodTick > 0 && !AsxWithinGrace(_asxLastGoodTick)) { AsxBlock("Grace expired", false); return false; }
    return true;
}

private async void VerifyAsxLicenseAsync()
{
    try
    {
        AsxLoadState();
        string key = (LicenseKey ?? "").Trim().ToUpperInvariant();
        string userId = Account.UserId.ToString();
        if (string.IsNullOrEmpty(key)) { AsxBlock("No license key", true); return; }

        string nonce = AsxNonce();
        var req = new Dictionary<string, object>
        {
            ["license_key"] = key,
            ["account_user_id"] = userId,
            ["client_nonce"] = nonce,
            ["seq"] = _asxSeq,
            ["bot_version"] = AsxBotVersion
        };
        string json = JsonSerializer.Serialize(req);
        var response = await AsxHttpSendAsync(json).ConfigureAwait(false);
        int status = response.StatusCode;
        string body = response.Body ?? "";
        if (status >= 500) { if (_asxLicensed && AsxWithinGrace(_asxLastGoodTick)) return; AsxBlock("Server error", false); return; }
        if (status != 200) { AsxBlock("HTTP " + status, true); return; }
        AsxParse(body, key, nonce);
    }
    catch (Exception ex)
    {
        if (_asxLicensed && AsxWithinGrace(_asxLastGoodTick)) return;
        AsxBlock(ex.Message, false);
    }
}

private Task<HttpResponse> AsxHttpSendAsync(string json)
{
    var request = new HttpRequest(new Uri(AsxVerifyUrl))
    {
        Method = HttpMethod.Post,
        Body = json,
        Timeout = TimeSpan.FromSeconds(15)
    };
    request.Headers.Add("Content-Type", "application/json");
    var tcs = new TaskCompletionSource<HttpResponse>();
    Http.SendAsync(request, response => tcs.TrySetResult(response));
    return tcs.Task;
}

private void AsxParse(string body, string key, string nonce)
{
    using var doc = JsonDocument.Parse(body);
    var root = doc.RootElement;
    if (!root.TryGetProperty("signature", out var sig)) { AsxBlock("Unsigned response", true); return; }
    if (!root.TryGetProperty("payload", out var payload)) { AsxBlock("No payload", true); return; }
    if (root.TryGetProperty("license_secret", out var sec) && sec.ValueKind == JsonValueKind.String)
        _asxSecret = Convert.FromBase64String(sec.GetString() ?? "");
    if (_asxSecret == null || _asxSecret.Length != 32) { AsxBlock("No license secret", true); return; }
    string canonical = AsxCanonical(payload);
    if (!AsxHmacOk(_asxSecret, canonical, sig.GetString())) { AsxBlock("Bad signature", true); return; }
    if ((payload.TryGetProperty("client_nonce", out var cn) ? cn.GetString() : "") != nonce) { AsxBlock("Nonce mismatch", true); return; }
    int seq = payload.TryGetProperty("seq", out var sq) ? sq.GetInt32() : 0;
    if (_asxSeq > 0 && seq <= _asxSeq) { AsxBlock("Stale seq", true); return; }
    if ((payload.TryGetProperty("status", out var st) ? st.GetString() : "") != "valid") { AsxBlock("License invalid", true); return; }
    _asxSeq = seq; _asxLastGoodTick = Environment.TickCount64; _asxGate = seq > 0; _asxLicensed = true;
    AsxSaveState(key);
    string productLabel = payload.TryGetProperty("product_id", out var pid) && pid.ValueKind == JsonValueKind.String ? (pid.GetString() ?? "") : "";
    Print("ASX: license OK — " + productLabel + " — user " + Account.UserId);
}

private static string AsxNonce()
{
    var b = new byte[32]; using var r = RandomNumberGenerator.Create(); r.GetBytes(b);
    var sb = new StringBuilder(64); foreach (var x in b) sb.Append(x.ToString("x2")); return sb.ToString();
}

private static string AsxCanonical(JsonElement payload)
{
    var map = new SortedDictionary<string, string>(StringComparer.Ordinal);
    foreach (var p in payload.EnumerateObject())
        map[p.Name] = p.Value.ValueKind == JsonValueKind.Number ? p.Value.GetRawText() : (p.Value.GetString() ?? "");
    var sb = new StringBuilder("{");
    bool f = true;
    foreach (var kv in map)
    {
        if (!f) sb.Append(','); f = false;
        sb.Append('"').Append(kv.Key).Append('"').Append(':');
        if (kv.Key is "grace_seconds" or "seq" or "server_time" or "v") sb.Append(kv.Value);
        else sb.Append('"').Append(kv.Value.Replace("\\", "\\\\").Replace("\"", "\\\"")).Append('"');
    }
    return sb.Append('}').ToString();
}

private static bool AsxHmacOk(byte[] secret, string canon, string sig)
{
    using var h = new HMACSHA256(secret);
    var hash = h.ComputeHash(Encoding.UTF8.GetBytes(canon));
    var sb = new StringBuilder(); foreach (var bt in hash) sb.Append(bt.ToString("x2"));
    string a = sb.ToString(), sigHex = (sig ?? "").ToLowerInvariant();
    if (a.Length != sigHex.Length) return false;
    int d = 0; for (int i = 0; i < a.Length; i++) d |= a[i] ^ sigHex[i];
    return d == 0;
}

private static bool AsxWithinGrace(long tick)
{
    if (tick <= 0) return false;
    long d = Environment.TickCount64 - tick; if (d < 0) d += long.MaxValue;
    return d < AsxGraceMs;
}

private void AsxLoadState() { try { if (!System.IO.File.Exists(AsxStatePath())) return; using var d = JsonDocument.Parse(System.IO.File.ReadAllText(AsxStatePath()));
    _asxSeq = d.RootElement.TryGetProperty("seq", out var s) ? s.GetInt32() : 0;
    _asxLastGoodTick = d.RootElement.TryGetProperty("tick", out var t) ? t.GetInt64() : 0;
    if (d.RootElement.TryGetProperty("sec", out var x)) _asxSecret = Convert.FromBase64String(x.GetString() ?? ""); } catch { } }

private void AsxSaveState(string key) { try { System.IO.File.WriteAllText(AsxStatePath(), JsonSerializer.Serialize(new Dictionary<string, object>{{"seq",_asxSeq},{"tick",_asxLastGoodTick},{"sec",Convert.ToBase64String(_asxSecret)}})); } catch { } }

// Rename prefix per product (e.g. asx_sob_pro_v54_) so multiple cBots do not share state.
private string AsxStatePath() => "asx_min_cbot_" + Account.UserId + ".dat";

private void AsxBlock(string msg, bool immediate)
{
    if (!immediate && _asxLicensed && AsxWithinGrace(_asxLastGoodTick)) return;
    _asxLicensed = false; _asxGate = false; Print("ASX: " + msg); Stop();
}

// OnStart: Timer.Start(TimeSpan.FromHours(1)); VerifyAsxLicenseAsync();
// OnTimer: VerifyAsxLicenseAsync();
// OnStop: Timer.Stop();
// OnTick: if (!AsxRuntimeOk()) return;
// .csproj: PackageReference cTrader.Automate only — no config.json, no SourceGenerators.

MetaTrader 5 integration

MT5 EAs and indicators use the same JSON contract as cTrader. Use WebRequest in MQL5 — buyers must whitelist https://id.algoscriptx.com under Tools → Options → Expert Advisors → Allow WebRequest for listed URL.

Pass AccountInfoInteger(ACCOUNT_LOGIN) as ctrader_account_id in the JSON body (field name is shared across platforms).

Full — MT5 Indicator (.mq5)

Download
// AlgoScriptX — canonical license guard for MT5 Indicators.
//
// WHAT: One-time license verify via id.algoscriptx.com/verify; 12h re-validation timer included.
// CHANGE: Set ProductSlug (REQUIRED). Buyers enter LicenseKey — account auto-binds on first verify.
// BUYER SETUP: No manual website activation — whitelist https://id.algoscriptx.com in WebRequest settings.
// AI PROMPT: "Integrate this AsxLicenseGuard into my MT5 indicator: verify in OnInit, block until licensed."
// ProductSlug MUST match marketplace slug (enforced — wrong slug = product_mismatch).
// License model: ONE-TIME purchase only (no subscription).

#property copyright "AlgoScriptX"
#property link      "https://algoscriptx.com"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots   0

input string LicenseKey = "";
input string Broker     = "";
input string ProductSlug = "your-product-slug"; // REQUIRED: marketplace product slug

static const string VERIFY_URL = "https://id.algoscriptx.com/verify";
static const string TEST_KEY   = "TEST-DEMO-DEMO-DEMO-DEMO";
static bool g_licensed = false;

//+------------------------------------------------------------------+
int OnInit()
{
   Comment("Checking license...");
   if(!VerifyLicense())
      return INIT_FAILED;
   EventSetTimer(12 * 60 * 60);
   return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   EventKillTimer();
   Comment("");
}

//+------------------------------------------------------------------+
void OnTimer()
{
   VerifyLicense();
}

//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
   if(!g_licensed)
      return 0;
   // Your indicator logic runs only when licensed.
   return rates_total;
}

//+------------------------------------------------------------------+
bool VerifyLicense()
{
   string key = LicenseKey;
   StringTrimLeft(key);
   StringTrimRight(key);
   StringToUpper(key);

   string accountId = IntegerToString((long)AccountInfoInteger(ACCOUNT_LOGIN));

   if(StringLen(key) == 0)
   {
      SetResult(false, "No license key entered");
      return false;
   }

   if(key == TEST_KEY)
   {
      SetResult(true, "License OK [TEST] - account " + accountId);
      return true;
   }

   string body = StringFormat(
      "{\"license_key\":\"%s\",\"ctrader_account_id\":\"%s\",\"product_id\":\"%s\",\"broker\":\"%s\"}",
      key, accountId, ProductSlug, Broker
   );

   char post[];
   char result[];
   string resultHeaders;
   StringToCharArray(body, post, 0, StringLen(body));

   ResetLastError();
   int status = WebRequest("POST", VERIFY_URL, "Content-Type: application/json\r\n", 15000, post, result, resultHeaders);

   if(status == -1)
   {
      int err = GetLastError();
      SetResult(false, "WebRequest failed (" + IntegerToString(err) + "). Whitelist " + VERIFY_URL);
      return false;
   }

   if(status == 429)
   {
      SetResult(g_licensed, "Rate limit — will retry later");
      return g_licensed;
   }

   if(status < 200 || status >= 300)
   {
      SetResult(false, "License server HTTP " + IntegerToString(status));
      return false;
   }

   string response = CharArrayToString(result);
   return ParseAndApply(response, accountId);
}

//+------------------------------------------------------------------+
bool ParseAndApply(const string body, const string accountId)
{
   bool valid = (StringFind(body, "\"valid\":true") >= 0 || StringFind(body, "\"valid\": true") >= 0);
   if(valid)
   {
      bool isTest = StringFind(body, "\"is_test\":true") >= 0;
      SetResult(true, "License OK" + (isTest ? " [TEST]" : "") + " - account " + accountId + " (auto-bound)");
      return true;
   }

   string reason = "invalid";
   int p = StringFind(body, "\"reason\"");
   if(p >= 0)
   {
      int q1 = StringFind(body, "\"", p + 8);
      int q2 = StringFind(body, "\"", q1 + 1);
      if(q1 >= 0 && q2 > q1)
         reason = StringSubstr(body, q1 + 1, q2 - q1 - 1);
   }

   SetResult(false, ReasonMessage(reason, accountId));
   return false;
}

//+------------------------------------------------------------------+
string ReasonMessage(const string reason, const string accountId)
{
   if(reason == "license_not_found") return "License not found";
   if(reason == "license_revoked") return "License revoked (refunded or cancelled)";
   if(reason == "license_expired") return "License expired";
   if(reason == "license_suspended") return "License suspended";
   if(reason == "account_not_activated") return "License not yet active — contact support";
   if(reason == "account_mismatch")
      return "This license is already bound to another trading account. One purchase = one account. Contact support to reset, or buy another license for this account.";
   if(reason == "product_mismatch" || reason == "invalid_product_slug")
      return "License/product mismatch — check ProductSlug matches this product's marketplace slug (seller integration error).";
   if(reason == "invalid_license_format") return "Invalid license key format";
   if(reason == "invalid_account_id") return "Invalid account id";
   if(reason == "rate_limit_exceeded") return "Rate limit — try again later";
   if(reason == "server_error") return "License server error — try again later";
   if(StringFind(reason, "license_") == 0) return "License " + StringSubstr(reason, 8);
   return "License invalid: " + reason;
}

//+------------------------------------------------------------------+
void SetResult(const bool licensed, const string text)
{
   g_licensed = licensed;
   color c = licensed ? clrLimeGreen : clrOrangeRed;
   Comment("AlgoScriptX License: " + text);
   Print("ASX License: ", licensed ? "VALID" : "INVALID", " | ", text);
}

MT5 — Minimal

Minimal — MT5 paste-in

Download
//+------------------------------------------------------------------+
//| AlgoScriptX MINIMAL license verify — MT5 (MQL5)                   |
//+------------------------------------------------------------------+
// WHAT: One-time purchase license check. POSTs buyer key + account to id.algoscriptx.com/verify.
//       First successful verify AUTO-BINDS this trading account (no website activation step).
// CHANGE (you): (1) ProductSlug — REQUIRED exact marketplace slug (enforced; wrong slug = product won't run)
//               (2) LicenseKey input (3) call AsxVerifyLicense() in OnInit.
// WHERE: OnInit → if (!AsxVerifyLicense()) return INIT_FAILED;
//        OnCalculate/OnTick → if (!g_asxLicensed) return 0;
// BUYER SETUP: Enter license key when prompted — account binds automatically on first run.
// BUYER SETUP: MT5 → Tools → Options → Expert Advisors → Allow WebRequest for https://id.algoscriptx.com
// AI PROMPT: "Integrate this license guard into my MT5 EA/indicator: verify in OnInit, block logic until licensed, keep my existing code."
// NOTE: Full AsxLicenseGuard (download) adds 12h re-validation — recommended for production.
// License model: ONE-TIME purchase only (no subscription).

input string LicenseKey  = "";                  // buyers paste key from algoscriptx.com/account/licenses/
input string ProductSlug = "your-product-slug"; // REQUIRED: marketplace product slug from product URL

static const string ASX_VERIFY_URL = "https://id.algoscriptx.com/verify";
static bool g_asxLicensed = false;

string AsxExtractJsonReason(const string json)
{
   int p = StringFind(json, "\"reason\"");
   if(p < 0) return "invalid";
   int q1 = StringFind(json, "\"", p + 8);
   if(q1 < 0) return "invalid";
   int q2 = StringFind(json, "\"", q1 + 1);
   if(q2 <= q1) return "invalid";
   return StringSubstr(json, q1 + 1, q2 - q1 - 1);
}

string AsxReasonMessage(const string reason)
{
   if(reason == "license_not_found") return "License not found";
   if(reason == "license_revoked") return "License revoked (refunded or cancelled)";
   if(reason == "license_expired") return "License expired";
   if(reason == "license_suspended") return "License suspended";
   if(reason == "account_not_activated") return "License not yet active — contact support";
   if(reason == "account_mismatch")
      return "This license is already bound to another trading account. One purchase = one account. Contact support to reset, or buy another license for this account.";
   if(reason == "product_mismatch" || reason == "invalid_product_slug")
      return "License/product mismatch — check ProductSlug matches this product's marketplace slug (seller integration error).";
   if(reason == "invalid_license_format") return "Invalid license key format";
   if(reason == "invalid_account_id") return "Invalid account id";
   if(reason == "rate_limit_exceeded") return "Rate limit — try again later";
   if(reason == "server_error") return "License server error — try again later";
   if(StringFind(reason, "license_") == 0) return "License " + StringSubstr(reason, 8);
   return "License invalid: " + reason;
}

bool AsxVerifyLicense()
{
   g_asxLicensed = false;
   string key = LicenseKey;
   StringTrimLeft(key);
   StringTrimRight(key);
   StringToUpper(key);
   if(StringLen(key) == 0) { Comment("ASX: enter license key"); return false; }

   string accountId = IntegerToString((long)AccountInfoInteger(ACCOUNT_LOGIN));
   string body = StringFormat(
      "{\"license_key\":\"%s\",\"ctrader_account_id\":\"%s\",\"product_id\":\"%s\",\"broker\":\"\"}",
      key, accountId, ProductSlug);

   char post[], result[];
   string headers;
   StringToCharArray(body, post, 0, StringLen(body));
   ResetLastError();
   int status = WebRequest("POST", ASX_VERIFY_URL, "Content-Type: application/json\r\n", 15000, post, result, headers);
   if(status == -1) { Comment("ASX: WebRequest failed — whitelist " + ASX_VERIFY_URL); return false; }
   if(status != 200) { Comment("ASX: HTTP " + IntegerToString(status)); return false; }

   string json = CharArrayToString(result);
   bool valid = (StringFind(json, "\"valid\":true") >= 0 || StringFind(json, "\"valid\": true") >= 0);
   if(!valid)
   {
      Comment("ASX: " + AsxReasonMessage(AsxExtractJsonReason(json)));
      return false;
   }

   g_asxLicensed = true;
   Comment("ASX: license OK — account auto-bound");
   return true;
}

TradingView (Pine Script)

Publish as invite-only on TradingView. Pine cannot call external URLs for live license verification — this is a TradingView platform limitation, not an AlgoScriptX choice.

  1. When a buyer activates, they enter their TradingView username in My licenses.
  2. You receive an email and an ops dashboard task: Buyer X requests access for username Y.
  3. Add that username to your script’s invite list on TradingView.
  4. Mark Added on TradingView in the ops dashboard — we record the activation and notify the buyer.

Verify API contract (cTrader v2)

POST https://id.algoscriptx.com/verify

Request JSON (bot does not send product_id):

{
  "license_key": "ASX-XXXX-XXXX-XXXX",
  "account_user_id": "12345678",
  "client_nonce": "64-char-hex",
  "seq": 0,
  "bot_version": 2
}

Response: signed payload + signature (HMAC-SHA256). The payload includes server-derived product_id (marketplace slug). Verify nonce echo, seq, signature, and status before running.

Verify API contract (MT4/MT5 legacy)

POST https://id.algoscriptx.com/verify

JSON body:

{
  "license_key": "XXXX-XXXX-XXXX-XXXX",
  "ctrader_account_id": "12345678",
  "product_id": "your-product-slug",
  "broker": ""
}

Check order: HTTP status → valid boolean → reason when invalid. HTTP 429 = rate limit (60/h per key) — re-validate every 12–24h via timer, not every tick.

Error codes

ReasonMeaningBuyer action
license_not_foundKey does not existCheck key from My licenses
license_revokedRefunded or revokedContact support
license_expiredTrial ended or expiry date passedPurchase a full license
account_mismatchLicense already bound to a different trading accountUse the bound account, contact support for a reset, or buy another license
product_mismatchLegacy — client slug did not match license productUpdate to v2 snippet (no slug in bot)
invalid_product_slugLegacy — missing slug in verify requestUpdate to v2 snippet (no slug in bot)
account_not_activatedLegacy edge case — account not active yetContact support
invalid_license_formatMalformed keyCopy key exactly from My licenses
invalid_account_idBad account IDUse your platform account number (cTrader top-right / MT login)
rate_limit_exceededToo many verify requestsWait and retry; re-validate every 12–24h, not every tick
server_errorLicense service unavailableRetry later; contact support if it persists

Development test key

TEST-DEMO-DEMO-DEMO-DEMO — always valid locally without calling the API. Use only while building; remove hard-coded test keys from production builds.

How your buyer uses the license

  1. Purchase — License key appears in My licenses.
  2. cTrader / MT5 / MT4 — Buyer pastes the license key in product inputs and allows network/WebRequest. On first run, the trading account binds automatically — no manual activation on the website.
  3. TradingView — Buyer requests access with their TradingView username; you add them to the invite-only script; mark complete in ops.
  4. Start the tool — On success, chart shows “License OK” (cTrader/MT) or script appears in invite-only list (TV). One purchase = one account.

Test your build

  1. Invalid key test — Load your .algo with a nonsense key (e.g. AAAA-BBBB-CCCC-DDDD). Tool must refuse to run and show a clear error.
  2. Test key test — Load with TEST-DEMO-DEMO-DEMO-DEMO. Tool must run and show license OK.
  3. Upload — Submit the .algo from Seller → Products → Versions. Native cTrader exports are encrypted — we validate the container and verify paid products manually in cTrader (invalid key must block; TEST or real key must pass). Rare ZIP-package .algo files are also scanned for v2 markers.