Is Shopilo: Automatic Coupon Finder by DontPayFull safe?

Medium risk

Shopilo is medium risk. When you visit a tracked store such as target.com or sephora.com, the extension reports the hostname to DontPayFull's events.dpf.cloud, gated by remote config, plus the extension ID and session ID; if signed in, also your account ID.…

DontPayFull.comv2.1.43Chrome Web Store
45Risk

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

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Visited-Site Hostnames Sent to DontPayFull's Analytics Backend

When you visit a tracked store such as target.com or sephora.com, the extension reports the hostname to DontPayFull's events.dpf.cloud, gated by remote config, plus the extension ID and session ID; if signed in, also your account ID.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You finish loading a page on a store this extension tracks on behalf of DontPayFull, such as target.com or sephora.com.

No click or interaction with the extension itself is required -- an ordinary page load is enough.

The extension did this

The background service worker reports that page's hostname to DontPayFull's own analytics backend, events.dpf.cloud.

The same test with a site outside DontPayFull's tracked-store catalog -- we used Wikipedia -- produced no such request.

02EvidenceFIELD TABLE
Fields inside every page-view event:
FieldValueWhy it matters
Page hostname
target.comThe domain of the page you're on, with any leading 'www.' stripped.
Extension ID
iddgmfjgflgejeafamidllamlmchohjbA constant identifier for this extension -- the same value for every install, not specific to you.
Account ID
884213 (illustrative -- this session was not signed in, so no real value was captured)Added only while you're signed in to your DontPayFull/Shopilo account -- ties this and every other tracked page view to your identity.
Store/session ID
4821 (illustrative)An internal ID correlating events to the store and browser tab -- not a persistent identity.
Extension version
2.1.43The exact build number of the extension sending the report.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://events.dpf.cloud/automatic-coupons/events/
Observed during dynamic analysis immediately after navigating to target.com (also observed with url: "sephora.com" on a separate navigation in the same session -- both on DontPayFull's tracked-store list). A control navigation to en.wikipedia.org, which is not on that list, produced no equivalent request in the same session.
Headers
Content-Typeapplication/json
Body
{
  "event": "page_view",
  "url": "target.com",
  "client_id": "iddgmfjgflgejeafamidllamlmchohjb",
  "version": "2.1.43",
  "extra": {}
}
04EvidenceCODE COMPARE
The code that does this

Gating, event construction, and X-Session obfuscation

What it actually does
Background: page-load listener (annotated)dist/background/bg.global.js
// Fires on every tab update; only proceeds once the page has
// finished loading AND the URL passes an internal store-recognition check.
chrome.tabs.onUpdated.addListener(async (tabId, { status }, tab) => {
  if (tab.url && await isRecognizedSite(tab.url) && status === "complete") {
    await reportPageView(tabId, tab.url);
  }
});
Content script: remote-config gate (annotated)dist/contentScripts/index.global.js
// Handles the TRACK_PAGE message the background worker just sent.
// eventsPageViewTrack is a value pulled from DontPayFull's remote config
// (loaded via GLOBAL_CONFIG) -- it can be "supported", "all", or unset.
onMessage(Messages.EXTENSION.TRACK_PAGE, () => {
  if (isExtensionLoading.value) return;
  // "supported": only send if THIS site is on DontPayFull's own store list
  if (eventsPageViewTrack.value === "supported" && isStoreSupported.value) {
    track(Events.PAGE_VIEW);
  }
  // "all": send for every site, no store-list check
  if (eventsPageViewTrack.value === "all") {
    track(Events.PAGE_VIEW);
  }
  return true;
});
Background: event record construction (annotated)dist/background/bg.global.js
// Turns the active tab's URL into just its hostname, then attaches
// the extension's own ID, version, and a session-correlation hash.
async function createEventObject(eventName, storeId = "", opts = {}) {
  const tabInfo = opts.tabId ? await getTabInfoById(opts.tabId) : null;
  let hostname = "";
  if (tabInfo && tabInfo.url) {
    hostname = new URL(tabInfo.url).hostname.replace("www.", "");   // <-- the page you visited, reduced to its domain
  }
  const defaults = await getDefaultObject(eventName, storeId, opts.tabId, hostname);
  const extra = getExtraData(eventName, opts);
  return { ...defaults, extra };
}

async function getDefaultObject(eventName, storeId, tabId, hostname) {
  const clientId = chrome.runtime.id;   // constant extension ID, same for every install
  return {
    client_id: clientId,
    sid: storeId,
    version: chrome.runtime.getManifest().version,
    session_group: hashOf(`${clientId}--${storeId}-${tabId ?? 0}`),
    release_type: getReleaseType(),
    event: eventName,
    url: hostname,          // <-- the visited hostname, in the clear
  };
}
Background: account-ID attach (annotated)dist/background/bg.global.js
// Reads your DontPayFull account ID out of the auth cookie's JWT payload
// and stamps it onto the event object about to be sent.
async function attachUserIdToLog(eventObject) {
  let userId = await getUserIdFromAuthCookie();
  const cachedId = await getCachedUserId();
  if (!userId && cachedId && cachedId !== "undefined") {
    userId = cachedId;
    await refreshCachedUserId();
  }
  if (userId) {
    eventObject.extra.uid = userId;   // <-- ties this page view to your account
  }
}
Background: X-Session obfuscation (annotated)dist/background/bg.global.js
// Copies the event object, drops any empty fields, stamps a fingerprint
// timestamp, then encodes it into a header using base64 + a randomized
// letter-shift cipher. This is obfuscation, not encryption -- there is no
// key and no server-side lookup. The shift amount and stripped-padding
// count are appended in the clear as the header's last two characters, so
// anyone holding the header value can reverse it (see the artifact block).
async function addSessionHeader(headers, eventOpts) {
  const eventCopy = await createEventObject(eventOpts);
  Object.keys(eventCopy).forEach((key) => {
    if (!eventCopy[key] || key === "extra") delete eventCopy[key];
  });
  eventCopy.fingerprint = Date.now() / 1000;
  headers["X-Session"] = obfuscate(JSON.stringify(eventCopy));
}

function obfuscate(jsonText) {
  let base64 = btoa(encodeURIComponent(jsonText));
  let paddingRemoved = 0;
  if (base64.endsWith("==")) { base64 = base64.slice(0, -2); paddingRemoved = 2; }
  else if (base64.endsWith("=")) { base64 = base64.slice(0, -1); paddingRemoved = 1; }

  const shift = 3 + Math.floor(Math.random() * 6);         // random shift, 3-8
  const padded = randomChars(4) + base64 + randomChars(4); // random padding, both ends
  return caesarShift(padded, shift) + paddingRemoved + shift;
}
05EvidenceARTIFACT
Reproduce it yourself

Decodes the X-Session header value the extension attaches to every request to events.dpf.cloud, so you can see exactly what it's reporting about you without trusting our summary.

RequiresNode.js 16+ (for the global atob function)
dontpayfull-xsession-decode.js · js
#!/usr/bin/env node
// dontpayfull-xsession-decode.js
//
// Decodes the X-Session request header the extension attaches to every
// request to events.dpf.cloud / api.dontpayfull.com. The header is not
// encrypted -- it's a base64-encoded copy of the same JSON event object
// already sitting in the plaintext request body, obfuscated with a
// randomized letter-shift (Caesar) cipher whose shift amount and stripped
// base64 padding length are appended as the last two characters.
//
// Usage:
//   node dontpayfull-xsession-decode.js '<value of the captured X-Session header>'
//
// No dependencies -- plain Node.js.

function unshift(str, shift) {
  const c = ((shift % 26) + 26) % 26;
  return str
    .split("")
    .map((ch) => {
      if (/[a-z]/i.test(ch)) {
        const code = ch.charCodeAt(0);
        if (code >= 65 && code <= 90) {
          return String.fromCharCode(((code - 65 - c + 26) % 26) + 65);
        }
        if (code >= 97 && code <= 122) {
          return String.fromCharCode(((code - 97 - c + 26) % 26) + 97);
        }
      }
      return ch;
    })
    .join("");
}

function decodeXSession(value) {
  if (typeof value !== "string" || value.length < 6) {
    throw new Error("Value too short to be a valid X-Session header.");
  }
  // Last char: the random shift amount (3-8) used by the extension's own
  // encoder (function no() -> s() in bg.global.js). Second-to-last char:
  // how many "=" base64 padding chars were stripped before shifting (0-2).
  const shift = parseInt(value.slice(-1), 10);
  const paddingRemoved = parseInt(value.slice(-2, -1), 10);
  const shiftedBody = value.slice(0, -2);

  const unshifted = unshift(shiftedBody, shift);

  // The encoder wraps the base64 payload with 4 random alnum characters on
  // each side before shifting (function n() in bg.global.js) -- strip them.
  const base64NoPad = unshifted.slice(4, unshifted.length - 4);
  const base64 = base64NoPad + "=".repeat(paddingRemoved);

  const jsonText = decodeURIComponent(atob(base64));
  return JSON.parse(jsonText);
}

const input = process.argv[2];
if (!input) {
  console.error("Usage: node dontpayfull-xsession-decode.js '<X-Session header value>'");
  process.exit(1);
}

try {
  const decoded = decodeXSession(input);
  console.log(JSON.stringify(decoded, null, 2));
} catch (err) {
  console.error("Failed to decode: " + err.message);
  process.exit(1);
}
How to run it
  1. 1
    node dontpayfull-xsession-decode.js '<X-Session header value captured from your own browser's network tab>'
06EvidenceTHIRD PARTY LIST
Hosts involved:
  • events.dpf.cloud

    Receives the page-view event (hostname, extension ID, account ID if signed in) as plaintext JSON plus an obfuscated X-Session header. Operated by DontPayFull, the developer.

  • api.dontpayfull.com

    Serves the remote config (including the events_page_view_track = "supported"/"all" setting) and the store-recognition list that gates whether a given site's page views get tracked.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

Account-linked coupon activity sent to events.dpf.cloud

When the coupon flow runs, the extension reads the DontPayFull auth cookie, extracts the account id from that token, and adds it as a user id on coupon logs.

It posts activity/page context to events.dpf.cloud; testing did not reach checkout

01EvidenceCAUSE EFFECT
What actually happens
You did this

You use the automatic coupon feature on a checkout page.

The relevant code path starts from the Apply Coupons button in the content script.

The extension did this

The extension labels coupon activity with an account id from the DontPayFull authentication cookie.

It then prepares event and cycle logs for events.dpf.cloud.

02EvidenceFIELD TABLE
Fields the code adds to coupon-event and coupon-cycle logs
FieldValueWhy it matters
Your account-linked id
account id 123456 (illustrative)This ties coupon activity back to the same DontPayFull account identifier instead of leaving it as an anonymous browser event.
Shopping site context
example-store.com (illustrative)This shows which store or page host was active when the coupon event was created.
Coupon session
sid 8421, tab 2990810 (illustrative)This lets separate coupon attempts on the same browser be grouped into one application flow.
Coupon event name
start_autoapplyThis records what you did in the coupon interface, such as starting automatic application or copying a code.
Coupon-cycle details
index 2 of 6, session stopped by final screen (illustrative)This records coupon attempts, result steps, timing, version, and stop reason for the automatic coupon flow.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://events.dpf.cloud/automatic-coupons/events/
No request body was captured because dynamic testing did not reproduce the supported checkout coupon-application trigger.
Headers
Content-Typeapplication/json
04EvidenceNETWORK CAPTURE
Captured request
POSThttps://events.dpf.cloud/automatic-coupons/cycles
No request body was captured because dynamic testing did not reproduce the supported checkout coupon-application trigger.
Headers
Content-Typeapplication/json
05EvidenceCODE COMPARE
The code that does this

The shipped code reads the auth cookie, adds uid, and posts coupon logs

What it actually does
Auth cookie constant and uid attachment in the deobfuscated background bundledist/background/bg.global.js
AUTH_COOKIE: {
  url: "https://www.dontpayfull.com/",
  name: "AUTH_BEARER_DPF"
}

function h(g) {
  var _;
  if (!g) return !1;
  const p = t(g.value);
  return !(!p || !p.data || !((_ = e(p.data)) != null && _.id))
}
async function f() {
  try {
    return await C.cookies.get(Z.AUTH_COOKIE)
  } catch (g) {
    return ze.error("getAuthCookie", g), g.message && g.message.includes(Kr.NO_HOST_PERMISSIONS_COOKIE) && await i(P.EXTENSION.SET_PERMISSIONS_REMOVED_SCREEN, {
      visibility: !0
    }), null
  }
}
async function A() {
  var p;
  const g = await f();
  if (g != null && g.value) {
    const _ = t(g == null ? void 0 : g.value);
    return (p = e(_.data)) == null ? void 0 : p.id
  }
}
async function T(g) {
  let p = await A();
  const _ = await r();
  !p && _ && _ !== "undefined" && (p = _, await o()), p && (g.extra.uid = p)
}
return {
  openAuthWindow: c,
  closeAuthWindow: d,
  handleRemovedAuthWindow: l,
  logOut: u,
  extractAuthInfoFromCookie: h,
  attachUserIdToLog: T,
  getAuthCookie: f
}
Cycle log POST in the deobfuscated background bundledist/background/bg.global.js
class Li {
  constructor(e) {
    this.codeApplyInfo = {}, this.code = "", this.error = "", this.index = -1, this.maxIndex = -1, this.sid = -1, this.extensionID = "", this.stoppedBy = X.NO_STOPPED, this.version = "0.0.0", this.releaseType = "public", this.tabID = -1, this.codeApplyInfo = e.storage.currentCodeApplyInfo, this.code = e.storage.currentCode, this.index = e.storage.currentIndex, this.error = e.storage.lastError, this.maxIndex = e.storage.maxIndex, this.sid = e.storage.sid, this.extensionID = e.extensionID, this.stoppedBy = e.storage.stoppedBy, this.version = e.extVersion, this.releaseType = e.releaseType, this.tabID = e.tabID
  }
  getLog() {
    var o, i, a;
    var e, n, s;
    const r = `${this.extensionID}--${this.sid}-${this.tabID}`;
    return {
      client: {
        id: this.extensionID || ""
      },
      tid: Array.isArray(this.codeApplyInfo.trainings) && this.codeApplyInfo.trainings[0].id ? this.codeApplyInfo.trainings[0].id : -1,
      tv: this.codeApplyInfo.trainingVersion || "",
      session: this.codeApplyInfo.session || "",
      index: this.index > -1 ? this.index : -1,
      index_max: (o = this.maxIndex) != null ? o : -1,
      sid: (i = this.sid) != null ? i : -1,
      code: (a = (e = this.codeApplyInfo.codeInfo) == null ? void 0 : e.hash) != null ? a : this.code || "",
      code_type: (n = this.codeApplyInfo.codeInfo) == null ? void 0 : n.type,
      is_code_hash: !!((s = this.codeApplyInfo.codeInfo) != null && s.hash),
      steps: this.codeApplyInfo.steps || {},
      started_at: this.codeApplyInfo.startedAt || new Date("01/01/1970").toISOString(),
      ended_at: this.codeApplyInfo.endedAt || new Date("01/01/1970").toISOString(),
      last_error: this.error || "",
      session_stopped_by: this.stoppedBy || "",
      version: this.version || it.EXT_VERSION,
      session_group: ee.hashStr(r),
      release_type: this.releaseType || it.RELEASE_TYPE
    }
  }
  sendLog() {
    if (Di() || !this.codeApplyInfo.startedAt && !this.codeApplyInfo.endedAt) return Promise.resolve();
    const e = this.getLog();
    return fetch("https://events.dpf.cloud/automatic-coupons/cycles", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(e)
    })
  }
}
Event log POST and Apply Coupons trigger in the deobfuscated content scriptdist/contentScripts/index.global.js
async function t(o) {
  return await xm().attachUserIdToLog(o), Xc(ws.EVENTS_LOG).request({
    method: "post",
    postData: o
  })
}

function n({
  url: o,
  body: r
}) {
  return Xc(o).request({
    method: "post",
    postData: r
  })
}
return {
  sendCouponReview: e,
  sendEventLog: t,
  sendGA4Log: n
}

if (!D.length) {
  t(ve.START_AUTOAPPLY), n(vn.APPLY_COUPONS, {
    location: !0
  });
  const T = await Q();
  if (T && T.preventLoad) return Kt().setMainScreenVisibility(!0)
}
r(), D.length ? (x ? c(D, f.value) : A(D), v.value = !1) : s(), l(!0), _i().applyHopURL(), Bo().removeStandDownOnInteract(), mo().closeAutoApplyPopup(), p(!1), await fr().startApplyingSession()
06EvidenceTHIRD PARTY LIST
Remote hosts receiving the coupon telemetry
  • events.dpf.cloud

    Receives automatic coupon event logs and coupon-cycle logs that include account-linked uid fields and coupon-flow context.

  • api.dontpayfull.com

    Receives extension API requests for coupon configuration, store resolution, active codes, expired codes, trainings, deals, and related stores.

Updated 21 September 2026jfoanacamkbfibjbidbmeobmnndfgpca