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.
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)
- License key parameter — Expose an empty-by-default
License Keyfield; buyers paste their key from My licenses. - Verify on start — POST to
https://id.algoscriptx.com/verifywith key andAccount.UserId(v2 signed protocol) — never per tick. - Stop when invalid — Indicators render nothing while invalid; cBots call
Stop(). Show the human-readable reason on chart. - Web access — Use
AccessRights.NoneandHttp.SendAsync(HttpRequest)for verify (notHttpClient.PostAsync). KeepSystem.Net.Http+HMACSHA256markers.
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
Recommended
Full — AsxLicenseGuard
Complete guard class with 12-hour re-validation, chart status messages, test-key support, and detailed error reasons.
- Timer re-check every 12h
- Chart feedback for buyers
- Rate-limit handling
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
| Minimal | Full (recommended) | |
|---|---|---|
| Verify on start | ✓ | ✓ |
| 12h re-validation | — | ✓ |
| Chart status messages | Basic | Full |
| 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)
// 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)
// 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
// 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
// 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)
// 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
//+------------------------------------------------------------------+
//| 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.
- When a buyer activates, they enter their TradingView username in My licenses.
- You receive an email and an ops dashboard task: Buyer X requests access for username Y.
- Add that username to your script’s invite list on TradingView.
- 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
| Reason | Meaning | Buyer action |
|---|---|---|
license_not_found | Key does not exist | Check key from My licenses |
license_revoked | Refunded or revoked | Contact support |
license_expired | Trial ended or expiry date passed | Purchase a full license |
account_mismatch | License already bound to a different trading account | Use the bound account, contact support for a reset, or buy another license |
product_mismatch | Legacy — client slug did not match license product | Update to v2 snippet (no slug in bot) |
invalid_product_slug | Legacy — missing slug in verify request | Update to v2 snippet (no slug in bot) |
account_not_activated | Legacy edge case — account not active yet | Contact support |
invalid_license_format | Malformed key | Copy key exactly from My licenses |
invalid_account_id | Bad account ID | Use your platform account number (cTrader top-right / MT login) |
rate_limit_exceeded | Too many verify requests | Wait and retry; re-validate every 12–24h, not every tick |
server_error | License service unavailable | Retry 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
- Purchase — License key appears in My licenses.
- 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.
- TradingView — Buyer requests access with their TradingView username; you add them to the invite-only script; mark complete in ops.
- 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
- 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. - Test key test — Load with
TEST-DEMO-DEMO-DEMO-DEMO. Tool must run and show license OK. - 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.