Why do we’d like Google Apps Script?
To alternate commerce knowledge between separate MetaTrader terminals, a easy relay server is required. Google Apps Script acts as a free middleman that transfers commerce occasions from the Grasp account to the Slave account. It ensures dependable supply of occasions even after web interruptions or terminal restarts and doesn’t require a VPS or devoted server.
The right way to create and deploy Google Apps Script
Go to https://script.google.com and Click on Begin scripting

Click on New challenge

Delete the default code and paste the script offered under the instruction steps

Press Ctrl + S to save lots of the challenge (the highest menu will change into energetic)

Click on Deploy within the top-right nook and choose New deployment

Within the opened window, click on Choose sort (⚙️) and select Internet app

In the Description subject, enter Model 1 (any textual content is okay). Set Who has entry to Anybody and depart Execute as unchanged

Click on Deploy – your Apps Script URL will likely be generated. Copy and paste this URL into the EA enter settings

const API_KEY  = 'I_AM_API_KEY'; const MAX_PRUNE = 200; const CONSUMER_TTL_MS = 6 * 60 * 60 * 1000; const MAX_CONSUMERS_PER_CHANNEL = 50; perform doPost(e) {   const lock = LockService.getScriptLock();   let locked = false;   attempt {     lock.waitLock(10000);     locked = true;     if (!e || !e.postData || !e.parameter) return _resp({ okay:false, error:'no knowledge' });     const key = (e.parameter.key || '').toString();     if (key !== API_KEY) return _resp({ okay:false, error:'forbidden' });     const channel  = (e.parameter.channel || 'default').toString();     const shopper = (e.parameter.shopper || '').toString();     const c        = shopper || 'single';     const retailer = PropertiesService.getScriptProperties();         let uncooked = e.postData.contents || '{}';     uncooked = uncooked.exchange(/[u0000-u001F]+$/g, '');     let physique;     attempt {       physique = JSON.parse(uncooked);     } catch (parseErr) {       return _resp({ okay:false, error:'unhealthy json', particulars:String(parseErr) });     }             _touchConsumerFast(retailer, channel, c);         if (physique && physique.motion === 'ack') {       const lastId = Quantity(physique.last_id || 0);       if (!lastId) return _resp({ okay:false, error:'unhealthy ack' });       retailer.setProperty(_ackKey(channel, c), String(lastId));             _pruneByMinAckFast(retailer, channel);       return _resp({ okay:true, ack:lastId });     }         const nextId = _nextSeq(retailer, channel);     physique.id = nextId;     physique.server_time_ms = Date.now();         retailer.setProperty(_evKey(channel, nextId), JSON.stringify(physique));         const minKey = _minKey(channel);     const curMin = Quantity(retailer.getProperty(minKey) || '0');     if (!curMin) retailer.setProperty(minKey, String(nextId));     return _resp({ okay:true, last_id: nextId });   } catch (err) {     return _resp({       okay:false,       error:'exception',       message:String(err),       stack:(err && err.stack) ? String(err.stack) : ''     });   } lastly {     if (locked) {       attempt { lock.releaseLock(); } catch(_) {}     }   } } perform doGet(e) {   const lock = LockService.getScriptLock();   let locked = false;   attempt {     lock.waitLock(10000);     locked = true;     if (!e || !e.parameter) return _resp({ okay:false, error:'no params' });     const key = (e.parameter.key || '').toString();     if (key !== API_KEY) return _resp({ okay:false, error:'forbidden' });     const channel  = (e.parameter.channel || 'default').toString();     const shopper = (e.parameter.shopper || '').toString();     const c        = shopper || 'single';     const restrict    = Math.max(1, Math.min(100, Quantity(e.parameter.restrict || 20)));     const retailer = PropertiesService.getScriptProperties();             _touchConsumerFast(retailer, channel, c);     const minId = Quantity(retailer.getProperty(_minKey(channel)) || '0');     const seq  = Quantity(retailer.getProperty(_seqKey(channel)) || '0');     const ackKey = _ackKey(channel, c);     let ack = Quantity(retailer.getProperty(ackKey) || '0');         if (minId > 0) {       const floorAck = Math.max(0, minId - 1);       if (ack < floorAck) {         ack = floorAck;         retailer.setProperty(ackKey, String(ack));       }     }         const mode = (e.parameter.mode || '').toString();     if (mode === 'well being' || mode === 'debug') {       const customers = _listActiveConsumersFast(retailer, channel);       const minAck = _minAckFast(retailer, channel, customers);       const out = { okay:true, channel, shopper:c, ack, seq, min_id:minId, active_consumers:customers, min_ack:minAck };       if (mode === 'debug') {                 const seen = {};         for (const cc of customers) '0');                 out.seen = seen;       }       return _resp(out);     }     const occasions = [];     let missing_id = 0;         for (let id = ack + 1; id <= seq && occasions.size < restrict; id++) {       const evStr = retailer.getProperty(_evKey(channel, id));       if (!evStr) {         missing_id = id;         break;       }       attempt {         occasions.push(JSON.parse(evStr));       } catch (parseErr) {         missing_id = id;         break;       }     }         if (missing_id && minId > 0 && missing_id < minId) {       const newAck = Math.max(0, minId - 1);       retailer.setProperty(ackKey, String(newAck));       const events2 = [];       let missing2 = 0;       for (let id = newAck + 1; id <= seq && events2.size < restrict; id++) {         const evStr = retailer.getProperty(_evKey(channel, id));         if (!evStr) { missing2 = id; break; }         attempt { events2.push(JSON.parse(evStr)); } catch (_) { missing2 = id; break; }       }       if (!missing2) {         return _resp({ okay:true, ack: newAck, seq: seq, occasions: events2 });       }             return _resp({         okay:false,         error:'gap_detected',         ack: newAck,         seq: seq,         missing_id: missing2       });     }     if (missing_id) {       return _resp({         okay:false,         error:'gap_detected',         ack: ack,         seq: seq,         missing_id: missing_id       });     }     return _resp({ okay:true, ack: ack, seq: seq, occasions: occasions });   } catch (err) {     return _resp({       okay:false,       error:'exception',       message:String(err),       stack:(err && err.stack) ? String(err.stack) : ''     });   } lastly {     if (locked) {       attempt { lock.releaseLock(); } catch(_) {}     }   } } perform _nextSeq(retailer, channel) perform _touchConsumerFast(retailer, channel, shopper) {   const now = Date.now();   retailer.setProperty(_seenKey(channel, shopper), String(now));   const listKey = _consumersKey(channel);   let arr = [];   attempt catch(_) { arr = []; }   if (arr.indexOf(shopper) < 0) {     arr.push(shopper);         if (arr.size > MAX_CONSUMERS_PER_CHANNEL) arr = arr.slice(arr.size - MAX_CONSUMERS_PER_CHANNEL);     retailer.setProperty(listKey, JSON.stringify(arr));   }   const ackKey = _ackKey(channel, shopper);   const ackStr = retailer.getProperty(ackKey);       if (ackStr === null || ackStr === undefined || ackStr === '') '0');     retailer.setProperty(ackKey, String(Math.max(0, seq)));     return;       const minId = Quantity(retailer.getProperty(_minKey(channel)) || '0');   const ack = Quantity(ackStr || '0');   if (minId > 0) {     const floorAck = Math.max(0, minId - 1);     if (ack < floorAck) retailer.setProperty(ackKey, String(floorAck));   } } perform _listActiveConsumersFast(retailer, channel) {   const now = Date.now();   const listKey = _consumersKey(channel);   let arr = [];   attempt catch(_) { arr = []; }   const energetic = [];   for (const c of arr) '0');     if (!seen) proceed;     if (now - seen <= CONSUMER_TTL_MS) energetic.push(c);     if (energetic.size === 0) energetic.push('single');   return energetic; } perform _minAckFast(retailer, channel, customers) {   let min = null;   for (const c of customers) '0');     if (min === null   return min === null ? 0 : min; } perform _pruneByMinAckFast(retailer, channel) {   const customers = _listActiveConsumersFast(retailer, channel);   const minAck = _minAckFast(retailer, channel, customers);   if (minAck <= 0) return;   _pruneAckedUpTo(retailer, channel, minAck); } perform _pruneAckedUpTo(retailer, channel, ackId) {   const minKey = _minKey(channel);   let minId = Quantity(retailer.getProperty(minKey) || '0');   if (!minId) return;   let eliminated = 0;   whereas (minId && minId <= ackId && eliminated < MAX_PRUNE) {     retailer.deleteProperty(_evKey(channel, minId));     minId++;     eliminated++;   }   const seq = Quantity(retailer.getProperty(_seqKey(channel)) || '0');   if (minId > seq) {     retailer.deleteProperty(minKey);   } else {     retailer.setProperty(minKey, String(minId));   } } perform _seqKey(channel) { return channel + '__seq'; } perform _minKey(channel) { return channel + '__min'; } perform _ackKey(channel, shopper) { return channel + '__ack__' + shopper; } perform _evKey(channel, id) { return channel + '__ev__' + id; } perform _seenKey(channel, shopper) { return channel + '__seen__' + shopper; } perform _consumersKey(channel) { return channel + '__consumers'; } perform _resp(obj) {         return ContentService     .createTextOutput(JSON.stringify(obj))     .setMimeType(ContentService.MimeType.JSON); }







