Is Shopilo: Automatic Coupon Finder by DontPayFull safe?
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.…
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
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.
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 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.
| Field | Value | Why it matters | |
|---|---|---|---|
Page hostname | target.com | The domain of the page you're on, with any leading 'www.' stripped. | |
Extension ID | iddgmfjgflgejeafamidllamlmchohjb | A 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.43 | The exact build number of the extension sending the report. |
| Content-Type | application/json |
{
"event": "page_view",
"url": "target.com",
"client_id": "iddgmfjgflgejeafamidllamlmchohjb",
"version": "2.1.43",
"extra": {}
}Gating, event construction, and X-Session obfuscation
// 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);
}
});// 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;
});// 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
};
}// 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
}
}// 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;
}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.
#!/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);
}
- 1node dontpayfull-xsession-decode.js '<X-Session header value captured from your own browser's network tab>'
- 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.
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
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 labels coupon activity with an account id from the DontPayFull authentication cookie.
It then prepares event and cycle logs for events.dpf.cloud.
| Field | Value | Why 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_autoapply | This 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. |
| Content-Type | application/json |
| Content-Type | application/json |
The shipped code reads the auth cookie, adds uid, and posts coupon logs
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
}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)
})
}
}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()- 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.