Last active
March 31, 2026 19:45
-
-
Save raxityo/c82abe49b77d5feaeb5cf6b69f0e0d9d to your computer and use it in GitHub Desktop.
Clip all available BJ's coupons
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name BJ's Coupon Clipper | |
| // @namespace Violentmonkey Scripts | |
| // @match https://www.bjs.com/ | |
| // @grant none | |
| // @version 1.0 | |
| // @author @raxityo | |
| // @description 3/24/2024, 11:01:30 PM | |
| // ==/UserScript== | |
| /** | |
| * Clip all available BJ's coupons | |
| * - Login to BJ's website | |
| * - Open dev console and paste this function | |
| * - Run it! Example `await clipAllOffers()` | |
| * @return {Promise<T>} | |
| */ | |
| (async function clipAllOffers() { | |
| const membershipNumber = localStorage.getItem("x_MembershipNumber"); | |
| const zipcode = JSON.parse( | |
| localStorage.getItem("clubDetailsForClubId") | |
| ).postalCode; | |
| await fetch( | |
| "https://api.bjs.com/digital/live/api/v1.0/member/available/offers", | |
| { | |
| method: "post", | |
| credentials: "include", | |
| body: JSON.stringify({ | |
| membershipNumber, | |
| zipcode, | |
| category: "", | |
| isPrev: false, | |
| isNext: true, | |
| pagesize: 500, | |
| searchString: "", | |
| indexForPagination: 0, | |
| brand: "" | |
| }) | |
| } | |
| ) | |
| .then((r) => r.json()) | |
| .then(([{ availableOffers }]) => { | |
| // Intentionally doing sequential requests to avoid hammering the backend | |
| availableOffers.forEach(async ({ offerId, storeId }) => { | |
| await fetch( | |
| `https://api.bjs.com/digital/live/api/v1.0/store/${storeId}/coupons/activate?zip=07302&offerId=${offerId}`, | |
| { | |
| credentials: "include" | |
| } | |
| ); | |
| }); | |
| }); | |
| })().then(); |
Author
@raxityo Great idea. Thanks for the tip and the code!
I've done some improvements to the script, including adding a button on the page. Thank you for the script:
// ==UserScript==
// @name BJ's Coupon Clipper with Live Count
// @namespace Violentmonkey Scripts
// @match *://*.bjs.com/*
// @grant none
// @version 1.5
// @author @raxityo
// @description Adds a button that shows a live count of coupons being clipped on any BJ's website subdomain.
// ==/UserScript==
(function() {
'use strict';
async function clipAllOffers() {
button.disabled = true;
button.style.backgroundColor = "#ccc";
button.style.cursor = "not-allowed";
try {
const membershipNumber = localStorage.getItem("x_MembershipNumber");
const clubDetails = localStorage.getItem("clubDetailsForClubId");
if (!membershipNumber || !clubDetails) {
console.error("Missing membership or club details in localStorage.");
button.textContent = "Missing login/club info";
return;
}
const zipcode = JSON.parse(clubDetails).postalCode;
const response = await fetch("https://api.bjs.com/digital/live/api/v1.0/member/available/offers", {
method: "POST",
credentials: "include",
body: JSON.stringify({
membershipNumber,
zipcode,
category: "",
isPrev: false,
isNext: true,
pagesize: 500,
searchString: "",
indexForPagination: 0,
brand: ""
})
});
const data = await response.json();
if (!data || !Array.isArray(data) || !data[0]?.availableOffers) {
console.error("Unexpected response structure.", data);
button.textContent = "Unexpected response";
return;
}
const offers = data[0].availableOffers;
const total = offers.length;
if (total === 0) {
button.textContent = "No coupons available";
return;
}
for (let i = 0; i < total; i++) {
button.textContent = `Clipping coupon ${i + 1} of ${total}`;
const { offerId, storeId } = offers[i];
await fetch(
`https://api.bjs.com/digital/live/api/v1.0/store/${storeId}/coupons/activate?zip=07302&offerId=${offerId}`,
{
credentials: "include"
}
);
}
button.textContent = "Coupons clipped successfully!";
} catch (error) {
console.error("Error clipping coupons:", error);
button.textContent = "Error clipping coupons";
}
}
const button = document.createElement("button");
button.textContent = "Clip All Coupons";
button.style.position = "fixed";
button.style.top = "10px";
button.style.right = "10px";
button.style.zIndex = "10000";
button.style.padding = "10px 15px";
button.style.backgroundColor = "#007bff";
button.style.color = "#fff";
button.style.border = "none";
button.style.borderRadius = "5px";
button.style.cursor = "pointer";
button.style.boxShadow = "0 2px 6px rgba(0,0,0,0.3)";
button.addEventListener("click", clipAllOffers);
function addButton() {
if (!document.body.contains(button)) {
document.body.appendChild(button);
}
}
const observer = new MutationObserver(addButton);
observer.observe(document.body, { childList: true, subtree: true });
addButton();
})();
I think they changed their API or introduced throttling or something recently.
I used to just run something like this on their coupons page:
const allClipButtons = document.querySelectorAll('[name="clipToCard"]'); allClipButtons.forEach((button)=> {button.click();});
@trevorknight you can go ahead and try this one: https://greasyfork.org/en/scripts/528560-bj-s-coupon-clipper-with-live-count-fast-batching/code
Made an update to include BATCH_SIZE and BATCH_DELAY. Feel free to modify as needed. It worked for me a couple days ago. Let me know if this doesn't work
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment

@TheMarf I have just converted this to a UserScript, so you can use it in FireFox or other browsers using plugins such as Tampermonkey or ViolentMonkey and installing the script as a UeserScript. Doing so would automatically run the script whenever you visit bjs.com.