Is TripChipper (TravelArrow) - Travel Better safe?
TripChipper is high risk. Searching stays on Airbnb, TravelArrow reads each listing's ID, price, dates, guests, POSTing them to Whimstay via a hardcoded bearer token in the bundle. Test confirmed live auth; a lower quote triggers a deal pill via a CJ affiliate link.…
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Airbnb search data forwarded to Whimstay API via hardcoded credential
Searching stays on Airbnb, TravelArrow reads each listing's ID, price, dates, guests, POSTing them to Whimstay via a hardcoded bearer token in the bundle.
Test confirmed live auth; a lower quote triggers a deal pill via a CJ affiliate link.
You search for accommodation on Airbnb.
Any search on one of 43 Airbnb country domains (e.g. airbnb.com, airbnb.co.uk, airbnb.de) triggers the intercept.
The extension reads listing details from the search results and forwards them to a third-party pricing API.
airbnb-interceptor.js runs in the MAIN world and intercepts the Airbnb StaysSearch GraphQL response to extract and transmit per-listing data.
| Content-Type | application/json |
| Authorization | Bearer eyJ0eXBlIjoiV2hpbXN0YXktdG9rZW4iLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjI3NTQyMiIsInJlcXVlc3RfaWQiOiIySGl0RFI2emROSjRBQkFVL25oOWxnPT1fI19zb3l6NXZxTXN5WG12WkxwZ0tlTkFnPT0iLCJzY29wZSI6WyJUUkFWRUxMRVIiXSwiaXNzIjoiV2hpbXN0YXktQVBJIiwic3ViIjoid2hpbXN0YXlfMkhpdERSNnpkTko0QUJBVS9uaDlsZz09XyNfc295ejV2cU1zeVhtdlpMcGdLZU5BZz09IiwiaWF0IjoxNzY3ODU2ODEwLCJleHAiOjI1NTYyNTY4MTAsImF1ZCI6Imh0dHBzOi8vd2hpbXN0YXkuY29tIn0.N7A9WH-Uhj1AThIMR7utVZZfhj8HUuckwZTcHWTbj3s |
{
"check_in": "2026-07-10",
"check_out": "2026-07-15",
"airbnb_property_id": "45812736",
"airbnb_pricing": 1250,
"no_of_adults": 2,
"no_of_childs": 0,
"no_of_pets": 0
}| Field | Value | Why it matters | |
|---|---|---|---|
Airbnb property ID | 45812736 | Uniquely identifies the specific rental you looked at. | |
Check-in date | 2026-07-10 | The travel date you entered in the search form. | |
Check-out date | 2026-07-15 | Your departure date from the search form. | |
Nightly price (Airbnb) | 1250 | The per-night price Airbnb showed you for this listing. | |
Adult guest count | 2 | Number of adults you specified in the search. | |
Child guest count | 0 | Number of children from your search filters. | |
Pet count | 0 | Number of pets from your search filters. |
Payload construction and POST, content.js lines 97349-97412
// Hardcoded Whimstay API endpoint and 25-year bearer token
const WHIMSTAY_ENDPOINT = 'https://apiprodv2.whimstay.com/whimstayAPI/v1/travel-arrow/pricing';
const WHIMSTAY_TOKEN = '<JWT: id=275422, scope=TRAVELLER, exp=2051-01-02>';
const CJ_AFFILIATE_BASE = 'https://www.anrdoezrs.net/click-100950458-17169403';
// For each Airbnb listing in search results:
async function checkWhimstayPrice(listing, searchContext) {
const propertyId = extractPropertyId(listing); // from GraphQL relay ID
const airbnbPrice = extractNightlyPrice(listing, searchContext.checkIn, searchContext.checkOut);
const payload = {
check_in: searchContext.checkIn,
check_out: searchContext.checkOut,
airbnb_property_id: propertyId,
airbnb_pricing: airbnbPrice,
no_of_adults: searchContext.adults,
no_of_childs: searchContext.children,
no_of_pets: searchContext.pets
};
const response = await fetch(WHIMSTAY_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${WHIMSTAY_TOKEN}`
},
body: JSON.stringify(payload)
});
const result = await response.json();
// If Whimstay price is lower, wrap their URL in a CJ affiliate link
if (result.whimstay_pricing < airbnbPrice) {
return { ...result, whimstay_url: `${CJ_AFFILIATE_BASE}?sid=ta-airbnb&url=${encodeURIComponent(result.whimstay_url)}` };
}
}- apiprodv2.whimstay.com
Receives per-listing POSTs with Airbnb property ID, pricing, dates, guest count. Whimstay is an independent third-party rental marketplace unrelated to Airbnb.
- www.anrdoezrs.net
Commission Junction affiliate redirect domain. Clicks on an injected Whimstay deal pill route here, attributing the referral to TravelArrow publisher 100950458.
- api.travelarrow.io
TravelArrow's own backend. Receives click-event telemetry (pillShown, pillClicked, whimstayLinkClicked) tied to the injected Airbnb UI elements.
Sends a test POST to the Whimstay pricing endpoint using the hardcoded bearer token extracted from the extension bundle. Confirms the credential is live and demonstrates the exact request the extension makes per Airbnb listing.
#!/usr/bin/env node
// Reproducer: verify the hardcoded Whimstay bearer token in TravelArrow v10.0.4
// Extracted from: content.js line 97308
const ENDPOINT = 'https://apiprodv2.whimstay.com/whimstayAPI/v1/travel-arrow/pricing';
const TOKEN = 'eyJ0eXBlIjoiV2hpbXN0YXktdG9rZW4iLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjI3NTQyMiIsInJlcXVlc3RfaWQiOiIySGl0RFI2emROSjRBQkFVL25oOWxnPT1fI19zb3l6NXZxTXN5WG12WkxwZ0tlTkFnPT0iLCJzY29wZSI6WyJUUkFWRUxMRVIiXSwiaXNzIjoiV2hpbXN0YXktQVBJIiwic3ViIjoid2hpbXN0YXlfMkhpdERSNnpkTko0QUJBVS9uaDlsZz09XyNfc295ejV2cU1zeVhtdlpMcGdLZU5BZz09IiwiaWF0IjoxNzY3ODU2ODEwLCJleHAiOjI1NTYyNTY4MTAsImF1ZCI6Imh0dHBzOi8vd2hpbXN0YXkuY29tIn0.N7A9WH-Uhj1AThIMR7utVZZfhj8HUuckwZTcHWTbj3s';
// Test payload matching the schema the extension uses
const payload = {
check_in: '2026-08-01',
check_out: '2026-08-07',
airbnb_property_id: '45812736', // replace with a real Airbnb listing ID to test matching
airbnb_pricing: 1200,
no_of_adults: 2,
no_of_childs: 0,
no_of_pets: 0
};
// Decode and display the token claims
const parts = TOKEN.split('.');
const claims = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
console.log('JWT claims:', JSON.stringify(claims, null, 2));
console.log('Token expires:', new Date(claims.exp * 1000).toISOString());
console.log();
const resp = await fetch(ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${TOKEN}`
},
body: JSON.stringify(payload)
});
console.log('HTTP status:', resp.status);
const body = await resp.json();
console.log('Response:', JSON.stringify(body, null, 2));
// A 200 with apiStatus=ERROR (code 221009 = no mapping for property ID) still
// confirms the credential authenticated — a 401/403 would indicate it had been revoked.
if (resp.status === 200) {
console.log('\n[CONFIRMED] Token authenticated against live Whimstay API.');
} else {
console.log('\n[CHECK] Unexpected status — token may have been revoked.');
}- 1Requires Node.js 18+ (native fetch).
- 2Run: node check-whimstay-token.mjs.
- 3A 200 response with apiStatus=ERROR errorCode=221009 confirms the credential is live. A 401 or 403 indicates Whimstay has revoked the token.
Cross-Site Checkout Tracker Reports Merchant And Payment URLs To TravelArrow
TripChipper registers webRequest for every load on every site.
Visiting a cashback merchant then checkout on any of 55+ hardcoded processors sent the merchant, prior page, and checkout URL to api.travelarrow.io.
Undisclosed in the listing.
You browse a shopping or travel site the extension treats as a cashback partner, then continue on to a payment provider's hosted checkout page.
The tracker inspects the destination host of every top-level page load in the browser, not only pages on cashback-partner sites.
The extension's background service worker reports the merchant site and the checkout destination to TravelArrow's server.
It registers Chrome's webRequest API for every website (host permissions cover http://*/* and https://*/*) to watch main-frame page loads across the whole browser.
| Content-Type | application/x-www-form-urlencoded |
{
"group": "cashback",
"type": "HostedCheckoutOffDomainDiscovered",
"service": "target.com",
"url": "https://checkout.stripe.com/",
"meta": {
"merchantDomain": "target.com",
"fromUrl": "https://www.target.com/",
"toUrl": "https://checkout.stripe.com/",
"toHost": "checkout.stripe.com",
"matchedKeyword": "checkout",
"isKnownPaymentHost": true
}
}Registering a browser-wide webRequest listener and reporting matched merchant→checkout transitions
class HostedCheckoutTracker {
static register() {
if (this.registered) return;
this.registered = true;
this.refreshConfigCache();
this.warmDomainsCache();
this.warmHeuristicsCache();
const filter = { urls: ["<all_urls>"], types: ["main_frame"] };
chrome.webRequest.onBeforeRequest.addListener(details => {
this.isEnabledSync() && this.handleBeforeRequest(details);
}, filter);
chrome.webRequest.onBeforeRedirect.addListener(details => {
this.isEnabledSync() && this.handleBeforeRedirect(details);
}, filter);
chrome.webRequest.onCompleted.addListener(details => {
this.isEnabledSync() && this.handleCompleted(details);
}, filter);
chrome.webRequest.onErrorOccurred.addListener(details => {
this.isEnabledSync() && this.chains.delete(details.requestId);
}, filter);
log("[HostedCheckoutTracker] webRequest listeners registered");
}
}static async fireOffDomainDiscovery(match) {
const dedupeKey = `${match.tabId}|${normalizeHostForDedupe(match.offHost)}|${match.matchedKeyword}`;
const lastFired = this.recentDiscoveryKeys.get(dedupeKey);
if (lastFired && Date.now() - lastFired < 30_000) return; // 30s de-dupe
this.recentDiscoveryKeys.set(dedupeKey, Date.now());
log(`[HostedCheckoutTracker] Discovery: ${match.merchantDomain} -> ${match.offHost}`);
await createEvent({
group: "cashback",
type: "HostedCheckoutOffDomainDiscovered",
service: match.merchantDomain,
url: match.offUrl,
important: true,
meta: {
merchantDomain: match.merchantDomain,
fromUrl: match.merchantUrl,
toUrl: match.offUrl,
toHost: match.offHost,
matchedKeyword: match.matchedKeyword,
isKnownPaymentHost: match.isKnownPaymentHost
}
});
}
// createEvent() — base64-encodes the JSON and POSTs it as a single form field
async function createEvent(event) {
const account = await getAccount();
const payload = {
...event,
accountId: account?.id ?? null,
meta: { ...event.meta, version: chrome.runtime.getManifest().version, name: "chromeDesktop" }
};
const encoded = btoa(new TextEncoder().encode(JSON.stringify(payload))
.reduce((s, b) => s + String.fromCodePoint(b), ""));
await fetch(`${config.api.url}/v3/events`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ payload: encoded }),
credentials: "omit"
});
}| Field | Value | Why it matters | |
|---|---|---|---|
Site you were shopping on | target.com | The merchant or travel site the extension recognized from its cashback-partner list. | |
Page you left | https://www.target.com/ | The exact page on that site you navigated away from. | |
Checkout page you reached | https://checkout.stripe.com/ | The hosted checkout page you landed on after leaving the merchant site. | |
Payment processor | checkout.stripe.com | The hostname of the checkout page, matched against a built-in list of 55+ known payment processors. | |
Trigger keyword | checkout | The word in the URL or page that told the extension this was a checkout flow. | |
Known-processor flag | true | Whether the destination matched the extension's built-in payment-host list, as opposed to an unrecognized checkout page. |
- api.travelarrow.io
TravelArrow's own backend. Receives the merchant site, the page left, and the checkout URL for every matched navigation, as a base64-encoded event.