Is Rakuten: Get Cash Back For Shopping safe?
Code analysis indicates Rakuten captures the full HTML of your order confirmation pages and sends it to third-party servers.
When an order confirmation page loads, the extension reads the complete innerHTML of the page and POSTs it to capture.ecbsn.com and capture.fillr-tech.com. Requests include a hardcoded developer key. Order confirmation pages typically contain itemized purchase details, shipping addresses, and personal identifiers. This behavior has not been directly verified through dynamic analysis.
Who publishes itEbates Performance Marketing Inc., d/b/a Rakuten Rewards - no other listings under this identity, 8 shared hostnames
Ebates Performance Marketing Inc., d/b/a Rakuten Rewards - no other listings under this identity, 8 shared hostnames
What this publisher told the store about itself, and the other listings that told it the same thing.
Shared hosts - 8 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.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Rakuten Uploads Full Order Page HTML to Two Third-Party Servers
During an active shopping trip, a purchase makes the extension read the order confirmation HTML (name, address, items, total) and upload it to capture.ecbsn.com and capture.fillr-tech.com via a hardcoded shared API key.
You complete a purchase on any merchant site where you activated a Rakuten shopping trip.
The trigger requires an active shopping trip tracking number (TTN), meaning Rakuten was already involved in tracking your browsing session at that merchant.
Rakuten reads the entire HTML of your order confirmation page and uploads it to two third-party servers.
Your order details, including name, shipping address, items, and total, are packaged as gzip-compressed JSON and sent to both Rakuten's analytics backend and a Fillr data capture service using a hardcoded developer key.
| Field | Value | Why it matters | |
|---|---|---|---|
Full order page HTML | <!DOCTYPE html><html><head>...</head><body>...Order #A1B2C3D4 confirmed. Shipping to Jane Smith, 42 Maple St, Portland OR 97201...</body></html> | The full HTML of your checkout confirmation page: name, delivery address, items, quantities, prices, order number. | |
Order confirmation URL | https://www.amazon.com/gp/buy/thankyou/handlers/display.html?ie=UTF8&checkoutSessionId=abc123 | The full URL of the order confirmation page, sent as content_source in the upload payload. | |
Rakuten Member ID | rr_7482910 | Your Rakuten account identifier, included in the ecbsn.com upload payload as rr_member_id. | |
Store Name and ID | Amazon / store_id: 1234 | The merchant name and internal Rakuten store identifier, included in both upload payloads. |
merchant.js: page classifier triggers innerHTML capture
// Check feature flag (OCPDOM_CAPTURE_EABLED — note: typo in source)
const captureEnabled = featureFlags.isEnabled(FLAGS.OCPDOM_CAPTURE_EABLED);
// Fire only when: page is an order confirmation, capture not yet done,
// and an active Rakuten shopping trip tracking number exists
if (pageType === 'OrderConfirm' && captureEnabled &&
!this.OCPDOMCaptured && this.merchant.session.shoppingTrip.ttn) {
// Capture the ENTIRE page HTML
const html = document.documentElement.innerHTML;
if (html) {
// Fire event to background service worker
sendEvent('OCPDOMCapture', {
content: html, // full page HTML
merchant: this.merchant,
detail: pageType
});
this.OCPDOMCaptured = true; // prevent duplicate sends
}
}// Upload #1: POST to Rakuten analytics backend (ecbsn.com)
handleCaptureOrderPage({ url, tabId, data }) {
const { storeName, storeId, session } = data.merchant;
this.postData(this.deps.captureOrderUrl, {}, {
tenant_id: this.deps.tenantId,
source_name: 'toolbar',
store_name: storeName,
store_id: storeId,
shopping_trip_id: session.shoppingTrip.ttn,
rr_member_id: this.deps.getMemberId(), // Rakuten account ID
content_source: url, // order confirmation URL
content_type: 'order confirmation',
content_format: 'html',
content: data.content // full page HTML
});
}
// Upload #2: POST to Fillr third-party capture (fillr-tech.com)
handleOCPDOMCapture({ url, tabId, data }) {
const ebToken = this.deps.getEBToken();
if (this.deps.fillrOCPDOMUploadUrl && ebToken) {
const headers = {
'x-dev-key': this.deps.domUploadDevKey, // hardcoded: f401eb0b18d9ca5c20ad6e16574b0e27
ebtoken: ebToken
};
this.postData(this.deps.fillrOCPDOMUploadUrl, headers, {
tracking_ticket: session.shoppingTrip.ttn,
content: data.content // same full page HTML
});
}
}
// Both uploads use gzip compression
async postData(url, extraHeaders = {}, payload) {
const compressed = await gzip(JSON.stringify(payload));
return await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Encoding': 'gzip',
...extraHeaders
},
body: compressed
});
}- capture.ecbsn.com
Rakuten's order capture analytics backend (Rakuten/Ebates). Receives full page HTML plus member ID, shopping trip ID, store details, and order URL.
- capture.fillr-tech.com
Fillr's third-party data capture service. Receives a copy of the order page HTML via a hardcoded developer key. Fillr is an autofill/data company; appears to be a data partnership.
Reads the Rakuten extension source files and confirms all three components of the order capture mechanism are present: the innerHTML trigger in merchant.js, the dual-POST handler in bg.js, and the hardcoded Fillr dev key.
'use strict';
const fs = require('fs');
const path = require('path');
const EXT_DIR = process.argv[2] || './extracted';
const bgContent = fs.readFileSync(path.join(EXT_DIR, 'js/bg.js'), 'utf8');
const merchantContent = fs.readFileSync(path.join(EXT_DIR, 'js/content/merchant.js'), 'utf8');
const extract = (content, pattern) => {
const m = content.match(pattern);
return m ? m[1] : '(NOT FOUND)';
};
const CAPTURE_ORDER = extract(bgContent, /CAPTURE_ORDER:"([^"]+)"/);
const FILLR_OCP_DOM_UPLOAD_DEVKEY = extract(bgContent, /FILLR_OCP_DOM_UPLOAD_DEVKEY:"([^"]+)"/);
const FILLR_OCP_DOM_UPLOAD_URL = extract(bgContent, /FILLR_OCP_DOM_UPLOAD_URL:"([^"]+)"/);
const hasHandleCaptureOrderPage = bgContent.includes('handleCaptureOrderPage');
const hasHandleOCPDOMCapture = bgContent.includes('handleOCPDOMCapture');
const hasGzip = bgContent.includes('Content-Encoding') && bgContent.includes('gzip');
const hasDevKeyHeader = bgContent.includes('x-dev-key');
const hasInnerHTMLCapture = merchantContent.includes('document.documentElement.innerHTML');
const hasOrderConfirmTrigger = merchantContent.includes('OrderConfirm');
const EXPECTED_DEV_KEY = 'f401eb0b18d9ca5c20ad6e16574b0e27';
const keyMatch = FILLR_OCP_DOM_UPLOAD_DEVKEY === EXPECTED_DEV_KEY;
console.log('=== Rakuten Claim 1122: Order Confirmation Page HTML Capture ===\n');
console.log('Hardcoded endpoints extracted from js/bg.js:');
console.log(' CAPTURE_ORDER: ' + CAPTURE_ORDER);
console.log(' FILLR_OCP_DOM_UPLOAD_URL: ' + FILLR_OCP_DOM_UPLOAD_URL);
console.log(' FILLR_OCP_DOM_UPLOAD_DEVKEY: ' + FILLR_OCP_DOM_UPLOAD_DEVKEY);
console.log(' Dev key matches expected: ' + keyMatch);
console.log();
console.log('Handler presence in js/bg.js:');
console.log(' handleCaptureOrderPage: ' + hasHandleCaptureOrderPage);
console.log(' handleOCPDOMCapture: ' + hasHandleOCPDOMCapture);
console.log(' gzip encoding: ' + hasGzip);
console.log(' x-dev-key header: ' + hasDevKeyHeader);
console.log();
console.log('Trigger in js/content/merchant.js:');
console.log(' innerHTML capture: ' + hasInnerHTMLCapture);
console.log(' OrderConfirm classifier: ' + hasOrderConfirmTrigger);
console.log();
if (keyMatch && hasHandleCaptureOrderPage && hasHandleOCPDOMCapture && hasInnerHTMLCapture) {
console.log('RESULT: CONFIRMED — all claim components present.');
console.log(' 1. merchant.js reads innerHTML on OrderConfirm pages with active TTN.');
console.log(' 2. bg.js POSTs HTML to ' + CAPTURE_ORDER);
console.log(' 3. bg.js POSTs same HTML to ' + FILLR_OCP_DOM_UPLOAD_URL);
console.log(' using hardcoded dev key: ' + FILLR_OCP_DOM_UPLOAD_DEVKEY);
console.log(' 4. Both uploads are gzip-compressed JSON.');
} else {
console.log('RESULT: INCONCLUSIVE — one or more components not found.');
}
- 1node rakuten-ocp-capture-verify.js /path/to/chhjbpecpncaggjpdakmflnfcopglcmi/extracted
Merchant site visits sent to two Rakuten analytics endpoints on every navigation
Navigating to any site on Rakuten's tracked merchant list sends your hostname and a session ID to Segment (events.engager.ecbsn.com) and messaging (api.rakuten.com).
Six POSTs captured across three sites; Segment uses a hardcoded key.
You navigate to a website on Rakuten's merchant tracking list, such as amazon.com, walmart.com, or ebay.com.
The extension sends your hostname to two Rakuten-controlled batch endpoints: a Segment analytics endpoint and a messaging endpoint.
This fires once per domain per 30-minute window. No shopping trip or click-through is required.
| Field | Value | Why it matters | |
|---|---|---|---|
Merchant hostname | amazon.com | The domain of the merchant site you are visiting, for example amazon.com. | |
Toolbar ID | 465221942 | A numeric identifier assigned to your Rakuten extension install, used to link events back to your account. | |
Anonymous ID | a3f71b2c-84de-4e9c-bc12-3f9a11e70d55 | A persistent session identifier that Rakuten uses to correlate browsing events across navigations. | |
User agent | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 | Your browser name and version string, used for device fingerprinting. | |
Extension version | 5.1.0 | The version of the Rakuten extension installed. |
| Field | Value | Why it matters | |
|---|---|---|---|
Page URL | amazon.com/ | The full URL of the merchant page you visited. | |
Page host | amazon.com | The hostname of the page you are on. | |
Installation ID | b2e44a91-3f87-4c01-a812-9e3c5d70b124 | A persistent identifier tied to your extension installation. | |
User agent | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 | Your browser name and version string. |
| Content-Type | application/json |
| Authorization | Basic UlJrenJjdEV3dG1ndDhQZ1lXa0Y2U3pONjJvYUFpSUQ6 |
{
"batch": [
{
"type": "track",
"event": "Visit NP Page",
"properties": {
"host": "amazon.com"
},
"context": {
"browser_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"app_version": "5.1.0",
"browser": "chrome",
"toolbarid": 465221942
},
"anonymousId": "a3f71b2c-84de-4e9c-bc12-3f9a11e70d55",
"writeKey": "RRkzrctEwtmgt8PgYWkF6SzN62oaAiID",
"sentAt": "2026-04-18T13:29:47.212Z"
}
],
"sentAt": "2026-04-18T13:29:47.213Z"
}The code that fires both tracking calls on every matched merchant navigation:
// Fires on every handleDocumentComplete (navigation) event.
async function trackNonPartnerPage(url) {
const domain = parseDomain(url)?.domain;
if (!domain) return;
if (!settings.get('trackNP')) return; // feature flag must be enabled
if (!self.EBATES) return;
// Lazy-load tracked domain list from remote config
if (!domainSet) domainSet = await DomainList.getDomains();
if (!domainSet.has(domain)) return;
// Dedup: skip if already sent within the last 30 minutes
if (!lastVisitMap) lastVisitMap = await storage.get('np-domains-last-visit') || new Map();
lastVisitMap.forEach((ts, d) => {
if (ts + 1_800_000 < Date.now()) lastVisitMap.delete(d); // expire old entries
});
if (!lastVisitMap.has(domain)) {
// Send to Segment analytics
EBATES.segment.track('Visit NP Page', { host: domain });
// Send to Rakuten holistic messaging
const fullUrl = `https://${domain}`;
EBATES.holistic.track(Events.WebsiteVisited, {
data: { website_visit_id: generateUUID() },
url: fullUrl
});
}
lastVisitMap.set(domain, Date.now());
await storage.set('np-domains-last-visit', lastVisitMap);
}// The Segment analytics client, configured with a hardcoded write key.
const segmentClient = new SegmentAnalytics({
key: config.get('SEGMENT_KEY'), // 'RRkzrctEwtmgt8PgYWkF6SzN62oaAiID'
host: config.get('SEGMENT_HOST'), // 'https://events.engager.ecbsn.com'
});
// The POST that transmits queued events:
fetch(`${this.host}/v1/batch`, {
method: 'POST',
headers: {
Authorization: `Basic ${btoa(this.writeKey + ':')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(batch),
});- events.engager.ecbsn.com
Rakuten's self-hosted Segment analytics endpoint. Receives 'Visit NP Page' events including merchant hostname, toolbar ID, and anonymous ID. ecbsn.com is a Rakuten-owned domain.
- api.rakuten.com
Rakuten's core API. Receives 'Website Visited' holistic events including page.url, page.host, and installation ID via the /message/v2/regions/USA/messages/batch path.
Failed Cash-Back Activations Logged to a Third-Party Datadog Account
When a Cash Back activation is interrupted (closed tab, timeout, back/forward nav, or page error), the extension logs the failure to its own analytics and to Datadog, including store ID, redirect chain, and user agent, via a hardcoded key.
You click a Cash Back activation link and the merchant redirect is interrupted: it times out, you close/reload the tab, navigate back/forward, or the destination errors.
This is a common, everyday interaction, no unusual behavior on your part is required.
The extension records the failed activation to its own analytics and separately to a third-party Datadog account, including your user agent and the merchant's store ID.
This is a warn-level event named 'ActivationStatus', delivered in a batched POST alongside any other queued log entries.
| Field | Value | Why it matters | |
|---|---|---|---|
Merchant store ID | 1234 | Rakuten's internal identifier for the merchant you were trying to buy from. | |
Affiliate redirect URLs | https://click.linksynergy.com/deeplink?id=Xa1Bc2De3Fg&mid=1234&murl=https%3A%2F%2Fwww.nike.com%2F,https://www.nike.com/ | The full chain of tracking URLs the extension routed you through on the way to the merchant, joined into one string. | |
Failure reason | closed tab | Why the activation didn't complete, tab closed, redirect timed out, page navigated back, or a page error. | |
Browser user agent | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 | Your browser name, version, and OS string, used for device identification. | |
Extension page URL | chrome-extension://chhjbpecpncaggjpdakmflnfcopglcmi/ | The address of the extension's own internal page, sent as the log's 'view.url' field on every entry. |
| Content-Type | application/json |
[
{
"ddsource": "browser",
"message": "ActivationStatus",
"status": "warn",
"date": 1755298187212,
"http": {
"useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
},
"origin": "logger",
"view": {
"referrer": "chrome-extension://chhjbpecpncaggjpdakmflnfcopglcmi/",
"url": "chrome-extension://chhjbpecpncaggjpdakmflnfcopglcmi/"
},
"service": "rr-button",
"store_id": "1234",
"redirect_chain": "https://click.linksynergy.com/deeplink?id=Xa1Bc2De3Fg&mid=1234&murl=https%3A%2F%2Fwww.nike.com%2F,https://www.nike.com/",
"reason": "closed tab"
}
]bg.js builds the activation-failure log payload; DdLogger batches and POSTs it to Datadog
// Called with a reason of 'timed out', 'closed tab', 'reload tab',
// 'forward_back', or 'page error' whenever an affiliate redirect
// doesn't complete cleanly.
function handleActivationFailure({ tabId, reason, extra }) {
const tab = pendingActivations.get(tabId);
if (!tab) return;
const storeId = tab.storeId;
const merchant = self.EBATES.merchants.get(storeId);
const trip = merchant?.session.shoppingTrip.getCurrentState();
const payload = {
store_id: storeId,
redirect_chain: tab.urls.toString(), // every URL in the redirect hop
reason,
...extra,
...(trip ? {
activation_type: trip.type,
attribution_source_previous_click: trip.source,
tracking_ticket: trip.ttn,
} : {}),
};
// Sent to Rakuten's own Segment pipeline...
self.EBATES.segment.track('Activation Failure', payload);
// ...AND to the third-party Datadog logger, same payload.
ddLogger.warn('ActivationStatus', payload);
self.EBATES.settings.affiliates.clear(tabId);
}class DdLogger {
globalContext = {};
batch = [];
warn(message, fields = {}) {
return this.addToBatch(message, fields, 'warn');
}
async addToBatch(message, fields, status) {
if (this.disabled) return;
const entry = {
...fields,
...this.globalContext,
ddsource: 'browser',
message,
date: Date.now(),
http: { useragent: navigator.userAgent }, // added to EVERY log entry
status,
origin: 'logger',
view: {
referrer: `${self.location.origin}/`, // the extension's own page URL
url: `${self.location.origin}/`,
},
};
// ddIgnoreList can suppress specific messages; ActivationStatus is not on it
this.batch.push(entry);
await this.fetching;
this.send();
}
fetch(batch) {
return fetch(
buildUrl('https://browser-intake-datadoghq.com/api/v2/logs', {
ddsource: 'browser',
ddtags: 'api:fetch,datacenter:us',
'dd-api-key': config.get('DATADOG_TOKEN'), // hardcoded, same for every install
'dd-evp-origin': 'browser',
'dd-request-id': generateRequestId(),
}),
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(batch) }
);
}
}- browser-intake-datadoghq.com
Datadog's log-ingestion endpoint. Receives 'ActivationStatus' with your user agent, page URL, store ID, and redirect chain, separate from Rakuten's own analytics.
+1 more finding not shown
Where it sends data
Destinations our analysis observed Rakuten 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.
- capture.ecbsn.com
Rakuten sends data to capture.ecbsn.com. 2 other extensions we have analysed send data here.
- capture.fillr-tech.com
Rakuten sends data to capture.fillr-tech.com. One other extension we have analysed sends data here.