Is LoveDeals: Automatic Coupons & Deals safe?

Medium risk

LoveDeals reports the host of nearly every site you visit to lovedeals.ai and scrapes your Google search result URLs.

A content script running on all sites sends the main domain of each page you open to lovedeals.ai/deal/api/v1/info on navigation (re-reported every 5 minutes), building a server-side browsing profile; an allowlist skips some banking, search, and major sites. On Google search pages it collects all result URLs from the page and posts them to lovedeals.ai/deal/api/v1/g-search. The server's responses control on-page behavior including coupon popups, banner/ad injection, and silent affiliate-redirect tabs that route purchases through lovedeals.ai/deal/api/v1/transit.

LoveDealsv2.7.0Chrome Web Store
52Risk
Who publishes it

LoveDeals.ai - no other listings under this identity, 3 shared hostnames

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
LoveDeals
Declared legal entity
LoveDeals.ai
Registered contact
LoveDeals.ai

Shared hosts - 3 hostnames

Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.

lovedeals.ai
Also called by 2 other listings, including LoveDeals: AI-Powered Coupon Finder
pikbest.com
Also called by 2 other listings, including LoveDeals: AI-Powered Coupon Finder
lovepik.com
Also called by 3 other listings, including LoveDeals: AI-Powered Coupon Finder

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

Server Response Controls LoveDeals Page UI

MediumunwantedCWE-829Dynamic sandbox

LoveDeals requested config from lovedeals.ai/deal/api/v1/info and got route=base with domainAd for en.wikipedia.org.

What actually happens

You did this

You visit a site where the extension is allowed to run.

The manifest registers content scripts for all HTTP and HTTPS pages.

The extension did this

The extension asks lovedeals.ai which deal UI or page action to activate.

A route=base response can lead to coupon UI, domain ads, hot-deal data, page ads, transit tabs, or an update prompt.

Captured request

POSThttps://lovedeals.ai/deal/api/v1/info

Observed during dynamic analysis: the response returned route=base with domainAd for en.wikipedia.org, and the extension immediately sent tabs.sendMessage cmd rHot/domainAd carrying that data to the tab.

Server fields that drive page behavior

  • Route selector
    route: "base"

    This tells the extension which branch of page behavior to run for the site you are viewing.

  • Domain ad payload
    domainAd for en.wikipedia.org with img, url, and ballClose fields

    This can place a domain-specific ad banner into the page and open a transit URL when it is clicked.

  • Coupon popup data
    coupons[0].code with couponTotal, brand, worked, and successRate

    This fills the coupon UI shown on the page, including a code, brand, total count, and success-rate labels.

  • Transit tab target
    https://lovedeals.ai/deal/api/v1/transit?type=1&cid=42&goto=https%3A%2F%2Fwww.example-store.com%2Fdeal

    This can open a background transit tab that routes through lovedeals.ai before going to a deal URL.

  • Update prompt flag
    update: true

    This can show an alert asking you to update the extension while you are browsing.

How the route response maps to page messages

  1. immediate

    You did

    The server returns route=base with domainAd data for the current site.

    The extension did

    The service worker sends a domainAd message into the active tab.

  2. same response

    You did

    The server returns coupon data in the base response.

    The extension did

    The service worker sends rBase data that fills the coupon popup.

  3. same response

    You did

    The server returns pageAd data and brand context.

    The extension did

    The service worker sends adDisplay data to draw a page ad.

  4. same response

    You did

    The server includes an update flag in the base response.

    The extension did

    The content script shows an extension update alert in the page.

The code that does this

Service worker route handling and tab messages

Readable version

The network request builder and JSON route dispatch

static/background/index.js
request(e, t = 1, r = !1) {  let i = Math.floor(new Date().getTime() / 1e3),    n = this.requestOption[t],    o = this.origin + "/deal/api/v1/",    a = {},    s = !1;  switch (a.domain = r ? r.urlInfo.mainhost : "", a.time = i, a.type = e, a.sendType = t, e) {    case "base":      o += "info", !this.pageAdExp || new Date().getTime() / 1e3 - this.pageAdExp > 43200 ? a.pageAd = 0 : a.pageAd = this.pageAd ? 1 : 0;      break;    case "site":      o += "site";      break;    case "ping":      o += "ping";      break;    case "search":      o += "search", a.text = r.text;      break;    case "hot":      o += "hot", s = !0;      break;    case "g-search":      o += "g-search", a.urls = r.urls, s = !0;      break;    case "make":      o += "make", s = !0;      break;    case "user-info":      o += "user-info", n.headers["Auth-Cookie"] = r.cookie, s = !0  }  if (o != this.origin + "/deal/api/v1/" && (s || a.domain)) {    let e = {      ...a    };    delete e.urls, delete e.pageAd;    let t = this.toEncrypt(e);    n.url = o, n.headers[this.encryptField] = t, n.headers.Authorization = void 0, n.headers.Version = "2.7.0", n.headers["Content-Type"] = "application/x-www-form-urlencoded", n.headers.Authorization || delete n.headers.Authorization, Object.keys(a).length > 0 ? n.body = new URLSearchParams(a).toString() : delete n.body, this.goRequest(n, r)  }}goRequest(e, t = !1) {  fetch(e.url + "?guid=" + this.guid, e).then(e => (e.ok || console.log("net connect error!"), e.json())).then(e => {    e && e.route && this.responseRoute(e, t)  })}

The base route decides which tab messages to send

static/background/index.js
responseRoute(e = {}, t = !1) {  let r = this.tabs();  switch (e.route) {    case "base":      e.pageAd && (this.pageAd = e.pageAd, this.pageAdExp = new Date().getTime() / 1e3), e.domainAd && (this.domainAd = e.domainAd, this.domainAdList[t.urlInfo.mainhost] = 1, this.sendDomainAd(t).then()), e.coupons && e.coupons.length > 0 ? (this.domainInfo[t.urlInfo.mainhost] = e, this.domainInfo[t.urlInfo.mainhost].time = Math.floor(new Date().getTime() / 1e3), this.setExtInfo(t).then()) : (this.pageAd && !e.platform && e.brand && this.adSearch(t.urlInfo.host, t), this.domainInfo[t.urlInfo.mainhost] = !1, e.hotDeals ? (e.hotVersion > this.hotDeals.hotVersion && (this.hotDeals = {        route: "hot",        hotDeals: e.hotDeals,        hotVersion: e.hotVersion      }, this.storage.set("hotDeals", this.hotDeals).then()), this.getTabById(t.tab.id).then(i => {        i && r.sendMessage(t.tab.id, {          cmd: "rHot",          data: e        }).then()      }).catch(e => {        console.log(e.message)      })) : this.hotDeals.hotVersion && this.getTabById(t.tab.id).then(e => {        e && r.sendMessage(t.tab.id, {          cmd: "rHot",          data: this.hotDeals        }).then()      }).catch(e => {        console.log(e.message)      })), e.update && this.getTabById(t.tab.id).then(e => {        e && r.sendMessage(t.tab.id, {          cmd: "update"        }).then()      }).catch(e => {        console.log(e.message)      });      break;    case "hot":      e.hotVersion && (this.hotDeals = e, this.storage.set("hotDeals", e).then(), t && this.sendHot(t).then());      break;    case "search":      this.runtime().sendMessage(t.id, {        cmd: "rSearch",        data: e.data      }).then();      break;    case "g-search":      e.data.ad ? this.imageToBase64(e.data.ad.img).then(r => {        e.data.ad.img = r, this.tabs().sendMessage(t.tab.id, {          cmd: "gSearch",          data: e.data        }).then()      }) : this.tabs().sendMessage(t.tab.id, {        cmd: "gSearch",        data: e.data      }).then();      break;    case "installed":      this.setInstalled(e.token);      break;    case "user-info":      this.storage.set("user_info", e.data).then(), this.runtime().sendMessage(t.id, {        cmd: "userInfo",        data: e.data      }).then()  }}

Server data forwarded into the tab

static/background/index.js
async setExtInfo(e = !1) {  let t = this.domainInfo[e.urlInfo.mainhost];  if (!t) return !1;  t = await this.checkOpen(t, e), this.autoOpenTrack(t);  let r = this.browserAction(),    i = this.tabs();  r.setIcon({    path: {      19: v.default,      38: _.default    },    tabId: e.tab.id  }).then(), r.setBadgeBackgroundColor({    color: "#242c3c",    tabId: e.tab.id  }).then(), r.setBadgeText({    text: t.couponTotal + "",    tabId: e.tab.id  }).then(), t.noDisplayFirst = !1, t.noDisplay3h = !1, t.noDisplay1h = !1;  let n = Math.floor(new Date().getTime() / 1e3);  if (t.popupCanOpen) {    let r = parseInt(await this.storage.get("noDisplay3h_" + e.urlInfo.mainhost)),      i = parseInt(await this.storage.get("noDisplay1h_" + e.urlInfo.mainhost)),      o = parseInt(await this.storage.get("noDisplayFirst_" + e.urlInfo.mainhost));    (r || i || o) && (r > 0 && (t.noDisplay3h = n - r < 10800, t.noDisplay3h || this.storage.remove("noDisplay3h_" + e.urlInfo.mainhost).then()), i > 0 && (t.noDisplay1h = n - i < 86400, t.noDisplay1h || this.storage.remove("noDisplay1h_" + e.urlInfo.mainhost).then()), o > 0 && (t.noDisplayFirst = n - o < 86400, t.noDisplayFirst || this.storage.remove("noDisplayFirst_" + e.urlInfo.mainhost).then())), o || this.storage.set("noDisplayFirst_" + e.urlInfo.mainhost, n).then()  }  let o = parseInt(await this.storage.get("ballNoDisplay24h"));  if (o > 0) {    let e = n - o < 86400;    e ? t.ball_open = 0 : this.storage.remove("ballNoDisplay24h").then()  }  this.getTabById(e.tab.id).then(r => {    r && (i.sendMessage(e.tab.id, {      cmd: "rBase",      data: t    }).then(), this.pageAd && !t.platform && t.brand && this.adSearch(e.urlInfo.host, e))  }).catch(e => {    console.log(e.message)  })}async sendDomainAd(e) {  if (this.domainAd && 1 == this.domainAdList[e.urlInfo.mainhost]) {    let t = parseInt(await this.storage.get("ld_bottom_ad")),      r = parseInt(await this.storage.get("ld_bottom_ad_ball")),      i = Math.floor(new Date().getTime() / 1e3),      n = {        ad: !1,        ball: !1,        info: this.domainAd      };    (!t || t - i > 86400) && (n.ad = !0), (!r || r - i > 86400) && (n.ball = !0), this.domainAd.ballClose && (n.ball = !1), n.ad ? this.imageToBase64(n.info.img).then(t => {      t && (n.info.img = t, this.getTabById(e.tab.id).then(t => {        t && this.tabs().sendMessage(e.tab.id, {          cmd: "domainAd",          data: n        }).then()      }).catch(e => {        console.log(e.message)      }))    }) : this.getTabById(e.tab.id).then(t => {      t && this.tabs().sendMessage(e.tab.id, {        cmd: "domainAd",        data: n      }).then()    }).catch(e => {      console.log(e.message)    })  }}adSearch(e, t) {  if (this.pageAd) {    let r = (0, a.default).getNowDate();    this.storage.get("ad_display_none").then(i => {      if (i) {        let t = JSON.parse(i),          n = new Set(t[r]);        if (n.has(e)) return !1      }      this.tabs().sendMessage(t.tab.id, {        cmd: "adDisplay",        data: this.pageAd      }).then()    })  }}

The code that does this

Content script handlers for server-selected messages

Readable version

Message switch for rBase, adDisplay, domainAd, rHot, and update

contents.04ff201a.js
eX = (e, n) => {  let r = [];  switch (e) {    case "rBase": {      if (eo = n, !n.coupons || !n.coupons[0]) break;      i(n.logo), j(n.coupons[0]), l = n.coupons[0], m(n.coupons[0].code), t(n.couponTotal), v(n.successRate), b(et[n.successRateAvg]), I(n.brand), f(n.worked), B(n.name), H(1 === parseInt(n.win_open)), J(1 === parseInt(n.ball_open)), ek(n.noDisplay3h);      let e = !(n.noDisplay3h || n.noDisplay1h || n.noDisplayFirst);      if (L(e), n.active) {        let e = {          ...eC        };        n.active.content && (e.content.box = n.active.content.style?.box, e.content.img = n.active.content.style?.img), n.active.icon && (e.icon.box = n.active.icon.style?.box, e.icon.img = n.active.icon.style?.img), n.active.popup && (e.popup.box = n.active.popup.style.box, e.popup.img = n.active.popup.style.img), n.active.logo && (1 == n.active.logo ? (e.logo.in = er + P.default, e.logo.out = er + D.default) : n.active.logo.in && n.active.logo.out && (e.logo.in = n.active.logo.in, e.logo.out = n.active.logo.out)), eE(e)      }      n.codeTest && (n = eJ(n)), eb(n), o = n, n.popupCanOpen || (L(!1), J(!1));      break    }    case "gData":      eo || (eo = (0, d.default).sendCmd("hot", {})), r = eo;      break;    case "gSearch":      (0, p.default).setGCoupons(n.list, n.className), n.ad && (0, p.default).rightAd(n.ad);      break;    case "adDisplay":      (0, X.default).displayAd(n);      break;    case "domainAd":      eK(n);      break;    case "rHot":      eo = n;      break;    case "update":      alert("Lovedeals Extension Version is too old, please update!");      break;    case "codeTest":      e5();      return  }  return r}

Domain ad renderer inserts a fixed page banner

contents.04ff201a.js
static displayAd(e) {  if (this.isSetAd) return !1;  this.isSetAd = !0;  let t = document.getElementById("LdAd_456");  if (!t) {    let t = document.createElement("div");    t.id = "LdAd_456", t.style.width = "100%", t.style.zIndex = "2147483647", t.style.boxShadow = "0 1px 6px rgba(0, 0, 0, .2)", t.style.position = "fixed", t.style.bottom = "0", t.style.left = "0", t.style.cursor = "pointer", t.onclick = n => {      n.stopPropagation(), t.remove(), (0, l.default).sendCmd("domainAdClose", {        ad: !1      }), (0, l.default).sendCmd("openNewTab", {        url: e.url,        cid: 0,        type: 5,        transit: !0      })    };    let n = t.attachShadow({        mode: "closed"      }),      r = document.createElement("link");    r.href = u + i.default, r.type = "text/css", r.rel = "stylesheet";    let a = document.createElement("div");    a.className = "ld-ad-body";    let o = document.createElement("img");    o.style.width = "100%", o.src = e.img, a.appendChild(o), n.appendChild(r), n.appendChild(a), document.body.prepend(t)  }}

External hosts involved in the observed route

    • lovedeals.ai

    Receives the configuration request and returns route data that selects the extension's page messages.

LoveDeals reports every visited domain to lovedeals.ai on each page load

MediumunwantedCWE-359Dynamic sandbox

Each http/https visit, LoveDeals POSTs the hostname to lovedeals.ai/deal/api/v1/info with a guid and timestamp. ~40 domains are excluded; others, incl. banking sites, are reported every 5 min. 8 requests seen across amazon, ebay, bestbuy.

What actually happens

You did this

You navigate to any website not on the extension's built-in exclusion list.

The exclusion list covers approximately 40 domain substrings (e.g. google., facebook., chatgpt.com) but does not exclude the vast majority of http/https origins.

The extension did this

The extension POSTs the site's hostname and your device's persistent identifier to lovedeals.ai without prompting you.

The same domain is re-reported every five minutes if you stay on the page.

Captured request

POSThttps://lovedeals.ai/deal/api/v1/info?guid=a3f82c1d9e4b0756a3f82c1d9e4b0756

Server returns coupon data for the domain or an empty response; the extension uses this to decide whether to show a deals badge.

Headers
Version
2.7.0
Auth-Token
TUFaVE9OLkFNQVpPTi5DT00uMTcxNzUwMDAwMC5CQVNFLjE=
Content-Type
application/x-www-form-urlencoded
Body
domain=https%3A%2F%2Fwww.amazon.com&time=1717500000&type=base&sendType=1&pageAd=0

Fields transmitted on every navigation

  • Visited domain
    https://www.amazon.com

    The full origin (scheme + hostname + port) of every site you visit, sent on every page load.

  • Device identifier (guid)
    a3f82c1d9e4b0756a3f82c1d9e4b0756

    A persistent random token set on first install, stored locally and in a lovedeals.ai cookie, tying visits across sessions to one device.

  • Unix timestamp
    1717500000

    The current time in seconds, included with every request.

  • Extension version
    2.7.0

    The installed extension version, sent in the Version header on every request.

The code that does this

Navigation handler and domain-reporting logic

Readable version

Content script sends docStart on page load

static start(listener) {  this.addListener(listener);  this.sendCmd('docStart', {});}static sendCmd(cmd, options = {}) {  try {    chrome.runtime.sendMessage({ cmd, options }, () => {});  } catch (e) {}  return true;}

Service worker routes docStart to loadInit

router(message, tab) {  tab.urlInfo = new URL(tab.url);  tab.urlInfo.mainhost = tab.urlInfo.origin;  switch (message.cmd) {    case 'docStart':      this.loadInit(tab);      break;  }}

loadInit — 300-second re-report gate

loadInit(tab) {  if (!tab?.tab?.id || this.returnTabIds[tab.tab.id]) return false;  const now = Math.floor(Date.now() / 1000);  let forceRefresh = false;  if (this.domainInfo[tab.urlInfo.mainhost] &&      now - parseInt(this.domainInfo[tab.urlInfo.mainhost].time) > 300) {    forceRefresh = true;  }  if (this.domainInfo[tab.urlInfo.mainhost] === false ||      this.domainInfo[tab.urlInfo.mainhost] === undefined ||      forceRefresh) {    this.getGuid().then(guid => {      if (guid) {        if (forceRefresh) this.domainInfo[tab.urlInfo.mainhost] = undefined;        this.getBaseConf(tab);      }    });  }}

getBaseConf — skip excluded domains, otherwise POST to /info

getBaseConf(tab) {  if (this.domainInfo[tab.urlInfo.mainhost] == null) {    if (this.noPassDomain(tab.urlInfo.mainhost)) {      this.sendHot(tab);      return false;    }    this.request('base', 1, tab); // POSTs domain to /deal/api/v1/info  } else {    this.sendDomainAd(tab);    this.sendHot(tab);  }}

request() — builds the POST with visited domain in body

request(type, method = 1, tab = false) {  const now = Math.floor(Date.now() / 1000);  const opts = Object.assign({}, this.requestOption[method]);  let url = this.origin + '/deal/api/v1/';  const payload = {};  payload.domain = tab ? tab.urlInfo.mainhost : '';  payload.time = now;  payload.type = type;  payload.sendType = method;  switch (type) {    case 'base': url += 'info'; break;  }  if (url !== this.origin + '/deal/api/v1/' && payload.domain) {    const authToken = this.toEncrypt({ ...payload });    opts.url = url;    opts.headers['Auth-Token'] = authToken;    opts.headers['Version'] = '2.7.0';    opts.body = new URLSearchParams(payload).toString();    this.goRequest(opts, tab); // fetch to url + '?guid=' + this.guid  }}

Where the browsing data is sent

    • lovedeals.ai

    Receives the visited hostname, device guid, and timestamp on every page navigation. Operated by the LoveDeals extension developer.

What it can do

Permissions this extension asks for, as declared in version 2.7.0. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.

  • Read and change your data on every secure site you visit

    https://*/*

  • Read and change your data on every site you visit

    http://*/*

  • Store data in your browser

    storage

  • Run its own code inside the pages you visit

    scripting

  • Read and change cookies, including the ones that keep you signed in

    cookies

  • See the address and title of every tab you have open

    tabs

Where it sends data

Destinations our analysis observed LoveDeals: Automatic Coupons & Deals contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.

  • lovedeals.ai

    LoveDeals: Automatic Coupons & Deals sends data to lovedeals.ai. One other extension we have analyzed sends data here.

Updated 2 November 2026ocallppfcfgngjhoplfhlmekcdjaeapl