Is LoveDeals: Automatic Coupons & Deals safe?

High 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
75Risk

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

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

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

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.

01EvidenceCAUSE EFFECT
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.

02EvidenceNETWORK CAPTURE
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
Version2.7.0
Auth-TokenTUFaVE9OLkFNQVpPTi5DT00uMTcxNzUwMDAwMC5CQVNFLjE=
Content-Typeapplication/x-www-form-urlencoded
Body
domain=https%3A%2F%2Fwww.amazon.com&time=1717500000&type=base&sendType=1&pageAd=0
03EvidenceFIELD TABLE
Fields transmitted on every navigation
FieldValueWhy it matters
Visited domain
https://www.amazon.comThe full origin (scheme + hostname + port) of every site you visit, sent on every page load.
Device identifier (guid)
a3f82c1d9e4b0756a3f82c1d9e4b0756A persistent random token set on first install, stored locally and in a lovedeals.ai cookie, tying visits across sessions to one device.
Unix timestamp
1717500000The current time in seconds, included with every request.
Extension version
2.7.0The installed extension version, sent in the Version header on every request.
04EvidenceCODE COMPARE
The code that does this

Navigation handler and domain-reporting logic

What it actually does
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
  }
}
05EvidenceTHIRD PARTY LIST
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.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-829
SourceAI SANDBOX

Server Response Controls LoveDeals Page UI

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

The worker uses that to pick which messages to send the active tab: popups, domain ads, hot-deal data, page ads, prompts.

01EvidenceCAUSE EFFECT
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.

02EvidenceNETWORK CAPTURE
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.
03EvidenceFIELD TABLE
Server fields that drive page behavior
FieldValueWhy it matters
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 fieldsThis 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 successRateThis 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%2FdealThis can open a background transit tab that routes through lovedeals.ai before going to a deal URL.
Update prompt flag
update: trueThis can show an alert asking you to update the extension while you are browsing.
04EvidenceCORRESPONDENCE
How the route response maps to page messages
WhenYou didExtension did
immediate
remote
The server returns route=base with domainAd data for the current site.
service_worker
The service worker sends a domainAd message into the active tab.
same response
remote
The server returns coupon data in the base response.
service_worker
The service worker sends rBase data that fills the coupon popup.
same response
remote
The server returns pageAd data and brand context.
service_worker
The service worker sends adDisplay data to draw a page ad.
same response
remote
The server includes an update flag in the base response.
content_script
The content script shows an extension update alert in the page.
05EvidenceCODE COMPARE
The code that does this

Service worker route handling and tab messages

What it actually does
The network request builder and JSON route dispatchstatic/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 sendstatic/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 tabstatic/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()
    })
  }
}
06EvidenceCODE COMPARE
The code that does this

Content script handlers for server-selected messages

What it actually does
Message switch for rBase, adDisplay, domainAd, rHot, and updatecontents.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 bannercontents.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)
  }
}
07EvidenceTHIRD PARTY LIST
External hosts involved in the observed route
  • lovedeals.ai

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

SeverityLOW
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Install Token Written as a Cookie on Four Vendor Domains

LoveDeals requests an install token from lovedeals.ai and writes it into a loveDeal_installed cookie on lovedeals.ai, lovepik.com, pikbest.com, and pngtree.com.

All four sites can recognize your browser as the same install; no disclosure.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You install or reload the LoveDeals extension with no local token yet stored.

The extension's background script runs its init routine on chrome.runtime.onInstalled.

The extension did this

The extension writes the same install token into a cookie on four separate websites.

It sets a loveDeal_installed cookie on lovedeals.ai, lovepik.com, pikbest.com, and pngtree.com, each carrying the identical token value.

02EvidenceNETWORK CAPTURE
Captured request
POSThttps://lovedeals.ai/deal/api/v1/make?guid=7af5ead42c58b1623366cd9ae0c18204
Observed during dynamic analysis: the response carries an install token that is then written verbatim into the loveDeal_installed cookie on all four vendor domains.
03EvidenceCODE COMPARE
The code that does this

setInstalled() and setInstallCookie() in the background service worker

What it actually does
Beautified equivalentextracted/static/background/index.js:221
setInstalled(e = "") {
  e
    ? (this.storage.set("token", e).then(), this.setInstallCookie(e))
    : this.storage.get("token").then((e) => {
        e ? this.setInstallCookie(e) : this.request("make", 1);
      });
}

setInstallCookie(e) {
  let t = this.cookie();
  for (let r of [
    "https://lovepik.com",
    "https://pikbest.com",
    "https://pngtree.com",
    "https://lovedeals.ai",
  ])
    t.get({ url: r, name: "loveDeal_installed" }).then((i) => {
      i ||
        t.set({
          url: r,
          name: "loveDeal_installed",
          domain: r.replace("https://", "."),
          value: e,
        }).then();
    });
}
04EvidenceFIELD TABLE
The cookie written on each of the four domains
FieldValueWhy it matters
Cookie name
loveDeal_installedThe name used to store the install token in your browser's cookie jar for this domain.
Cookie value
a4f9c7e2-91b3-4d5a-8f6e-1c2d3e4f5a6b (illustrative)The same install token returned by the extension's token-creation request, reused as a cross-domain identifier.
Cookie domain
.lovepik.comThe site the cookie is attached to; the extension repeats this write for all four vendor domains.
05EvidenceTHIRD PARTY LIST
Domains that receive the loveDeal_installed cookie
  • lovedeals.ai

    Issues the install token via /deal/api/v1/make and receives the same cookie back.

  • lovepik.com

    Stock-content marketplace; receives the install token as a cookie for cross-site recognition.

  • pikbest.com

    Stock-content marketplace; receives the install token as a cookie for cross-site recognition.

  • pngtree.com

    Stock-content marketplace; receives the install token as a cookie for cross-site recognition.

Data recipients

lovedeals.ai
Updated 17 September 2026ocallppfcfgngjhoplfhlmekcdjaeapl