Skip to content

Instantly share code, notes, and snippets.

@dapper-gh
Last active March 27, 2025 13:12
Bookmarklet: Add Amex Offer to Multiple Cards

Bookmarklet: Add Amex Offer to Multiple Cards

Why?

Ordinarily, adding an Amex Offer to one of your cards will prevent it from being added to other cards, even if it was listed on multiple cards. This bookmarklet allows you to add Amex Offers to all the cards on which it is listed, bypassing this restriction. You can use this for particular profitable offers or 20% off a 60 USD purchase of National Geographic Wines, if that's your jam.

How do I use it?

Copy the text from the ao.txt file. Create a bookmark in your browser and paste in what you just copied as the URL. Some browsers will remove the javascript: text at the start of the bookmark--it is essential that you add this back manually if this happens. Browers do this because javascript: URLs can be dangerous, so only use this script if you trust it. Log into Your American Express Dashboard. Click your new bookmark and wait a few seconds for the script to do its thing. You will eventually see a list of all offers across all your cards. Click "Add" for the offers you want to add to all available cards.

How does it work?

The core restriction-bypassing part of this bookmarklet simply sends multiple requests to https://functions.americanexpress.com/CreateCardAccountOfferEnrollment.v1 at the same time and with the same client-provided timestamp. You can view the full source in the file named amex-load-offer.js.

Doesn't CardPointers charge 72 USD per year for this feature?

Yes. For that extra 72 USD, you get developer support and a whole host of other user-friendly features. This bookmarklet gives you the basics and nothing else. If you would feel bad paying nothing for the bookmarklet, you can donate to me in one of the following ways:

If none of those methods work, but you still want to give me money, send me an email at my Zelle address and we can work something out.

// by dapper
// to minify:
// terser amex-load-offer.js --mangle -c unsafe_arrows=true,unsafe_comps=true,unsafe_math=true,unsafe_methods=true,unsafe_proto=true,keep_fargs=false,passes=3,drop_debugger=false -f quote_style=3,wrap_func_args=false,ascii_only=true,ecma=2023 | sed 's/\%/\%25/g ; s/^\!/javascript:void /'
(async function() {
// The URL of the American Express dashboard.
const DASHBOARD_HREF = "https://global.americanexpress.com/dashboard";
// Gets the __INITIAL_STATE__ variable, as given by American Express.
async function getRawInitialState() {
const response = await fetch(DASHBOARD_HREF);
const text = await response.text();
const parsedDoc = new DOMParser().parseFromString(text, "text/html");
const initialState = parsedDoc.getElementById("initial-state");
const stateJSON = initialState.innerHTML.match(/\_\_INITIAL\_STATE\_\_\s*=\s*([^\n]+)\s*;(?:\n|$)/)[1];
return JSON.parse(JSON.parse(stateJSON));
}
// Parses an array of the form ["prop1", "value1", "prop2", "value2"] into an object, starting at the given offset.
// Will optionally use a function to transform the values.
function parseArrayIntoObject(array, offset, valueMapper = v => v) {
const outObject = {};
for (let i = offset; i < array.length; i += 2) {
outObject[array[i]] = valueMapper(array[i + 1]);
}
return outObject;
}
// Turns an __INITIAL_STATE__-formatted variable into a more usable format.
function parseRawInitialState(state) {
if (Array.isArray(state) && state[0] === "~#iM") {
return parseArrayIntoObject(state[1], 0, parseRawInitialState);
}
if (Array.isArray(state) && state[0] === "^ ") {
return parseArrayIntoObject(state, 1, parseRawInitialState);
}
if (Array.isArray(state)) {
return state.map(value => parseRawInitialState(value));
}
return state;
}
// Makes a request to a functions.americanexpress.com API.
async function makeFunctionRequest(path, body) {
const response = await fetch("https://functions.americanexpress.com/" + path, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
credentials: "include",
body: JSON.stringify(body)
});
const json = await response.json();
return json;
}
// Pads a number with a leading zero, if necessary, to create a two-digit number.
function padTens(num) {
return num.toString().padStart(2, "0");
}
// Returns the timezone of the browser in a way that American Express likes.
function getUserOffset() {
const tz = new Date().getTimezoneOffset();
const tzSign = tz >= 0 ? "-" : "+";
const hours = padTens(Math.floor(Math.abs(tz) / 60));
const minutes = padTens(Math.abs(tz) % 60);
return tzSign + hours + ":" + minutes;
}
// Creates a date string in the American Express format.
function getDateTimeWithOffset() {
const date = new Date();
const year = date.getFullYear();
const month = padTens(date.getMonth() + 1);
const day = padTens(date.getDay());
const hour = padTens(date.getHours());
const minute = padTens(date.getMinutes());
const second = padTens(date.getSeconds());
const offset = getUserOffset();
return `${year}-${month}_${day}T${hour}:${minute}:${second}${offset}`;
}
// Creates a <td> element with the specified content (either text or HTML).
function createTd(content, isHTML = false) {
const td = document.createElement("td");
if (isHTML) {
td.innerHTML = content;
} else {
td.textContent = content;
}
return td;
}
// Creates the display text for a card, e.g. "Business Platinum 12345".
function createCardDescription(cardID) {
return `${cardIDMeta[cardID].product.description.replace(/\s*(?:Card|Honors|Bonvoy|SkyMiles)\s*/g, " ").replace(/\s+/g, " ")} ${cardIDMeta[cardID].account.display_account_number}`;
}
// Turns an array of card IDs into text displaying the list of cards, e.g. "Business Platinum 12345, Business Gold 23456 (2 cards)".
// If "standalone," will use angle brackets to represent special values or messages.
function createCardList(cards, standalone) {
if (cards.length) {
return `${cards.map(cardID => createCardDescription(cardID)).join(", ")} (${cards.length} card${cards.length === 1 ? "" : "s"})`;
} else {
return standalone ? "<no cards>" : "no cards";
}
}
if (location.hostname !== "global.americanexpress.com" || !["/dashboard", "/overview"].includes(location.pathname)) {
if (confirm(`You must be on ${DASHBOARD_HREF} to use this bookmarklet. Would you like to go there now?`)) {
location.href = DASHBOARD_HREF;
}
return;
}
const initialState = parseRawInitialState(await getRawInitialState());
const cardIDMeta = initialState.modules["axp-myca-root"].products.details.types.CARD_PRODUCT.productsList;
const cardIDs = Object.keys(cardIDMeta);
const userOffset = getUserOffset();
const offerIDMap = new Map();
for (const cardID of cardIDs) {
const response = await makeFunctionRequest("ReadCardAccountOffersList.v1", {
accountNumberProxy: cardID,
locale: "en-US",
source: "STANDARD",
typeOf: "MERCHANT",
status: ["ELIGIBLE", "ENROLLED"],
offerRequestType: "LIST",
userOffset
});
// todo: The request above errors out when using a non-AO-eligible card, and there should be some way to detect that in the initial state.
// For now, we just use ?? [] to ignore the card.
for (const offer of response?.offers ?? []) {
if (!offer.achievement_type) continue;
const offerDescriptor = offerIDMap.get(offer.id) ?? {enrolled: [], cards: []};
offerDescriptor.offer = offer;
if (offer.status === "ENROLLED") offerDescriptor.enrolled.push(cardID);
else offerDescriptor.cards.push(cardID);
offerIDMap.set(offer.id, offerDescriptor);
}
}
const marginStyle = " style=\"margin:4px\"";
const ui = document.createElement("div");
ui.innerHTML = `<p style="text-align:center"><input type=button value=Close class=close-btn${marginStyle}><input type=button value="Add All" class=add-all-btn${marginStyle}></p><table${marginStyle}><thead><tr><th scope=col>Description</th><th scope=col>Available On</th><th scope=col>Add</th></thead><tbody></tbody></table><p style="text-align:center"><input type=button value=Close class=close-btn ${marginStyle}></p>`;
ui.style.position = "fixed";
ui.style.width = ui.style.height = "100%";
ui.style.top = ui.style.left = "0";
ui.style.backgroundColor = "rgba(255,255,255,0.9)";
ui.style.zIndex = "999";
ui.style.overflow = "auto";
ui.style.padding = "8px";
const tbody = ui.getElementsByTagName("tbody")[0];
const closeButtons = ui.getElementsByClassName("close-btn");
for (const closeButton of closeButtons) {
closeButton.addEventListener("click", function() {
if (ui.parentElement) {
document.documentElement.style.overflow = "auto";
ui.parentElement.removeChild(ui);
}
});
}
const addAllButton = ui.getElementsByClassName("add-all-btn")[0];
addAllButton.addEventListener("click", function() {
if (this.parentElement) this.parentElement.removeChild(this);
const interval = setInterval(_ => {
const {done, value} = offerExecutors.values().next();
if (done) {
clearInterval(interval);
}
if (value) {
offerExecutors.delete(value);
value();
}
}, 500);
});
let offerExecutors = new Set();
for (const offer of offerIDMap.values()) {
const tr = document.createElement("tr");
tr.appendChild(createTd(`${offer.offer.name}: ${offer.offer.short_description}`, false));
tr.appendChild(createTd(createCardList(offer.cards, true), false));
if (offer.enrolled.length) {
const messageTd = createTd(`<already enrolled on ${createCardList(offer.enrolled, false)}>`, false);
tr.appendChild(messageTd);
tbody.appendChild(tr);
} else {
const buttonTd = createTd(`<input type=button value=Add${marginStyle}>`, true);
const executor = (function() {
offerExecutors.delete(executor);
const parent = this.parentElement;
if (parent) parent.removeChild(this); else return;
const requestDateTimeWithOffset = getDateTimeWithOffset();
const userOffset = getUserOffset();
for (const card of offer.cards) {
makeFunctionRequest("CreateCardAccountOfferEnrollment.v1", {
accountNumberProxy: card,
identifier: offer.offer.id,
locale: "en-US",
requestDateTimeWithOffset,
userOffset
}).then(data => {
if (parent.children.length) {
parent.appendChild(document.createTextNode(", "));
}
const span = document.createElement("span");
span.style.fontFamily = "monospace";
if (data.isEnrolled) {
span.style.color = "#0CC00C";
span.textContent = `success (${createCardDescription(card)})`;
} else {
span.style.color = "#C00C0C";
span.textContent = `error: ${data.explanationMessage} (${createCardDescription(card)})`;
}
parent.appendChild(span);
});
}
}).bind(buttonTd.children[0]);
offerExecutors.add(executor);
buttonTd.children[0].addEventListener("click", executor);
tr.appendChild(buttonTd);
tbody.prepend(tr);
}
}
for (let i = 0; i < tbody.children.length; i += 2) {
tbody.children[i].style.backgroundColor = "rgba(0,0,0,0.2)";
}
document.documentElement.style.overflow = "hidden";
document.body.appendChild(ui);
})();
javascript:void async function(){const e="https://global.americanexpress.com/dashboard";function t(e,t,n=e=>e){const o={};for(let s=t;s<e.length;s+=2)o[e[s]]=n(e[s+1]);return o}async function n(e,t){const n=await fetch("https://functions.americanexpress.com/"+e,{method:"POST",headers:{"Content-Type":"application/json"},credentials:"include",body:JSON.stringify(t)});return await n.json()}function o(e){return e.toString().padStart(2,"0")}function s(){const e=(new Date).getTimezoneOffset();return(0>e?"+":"-")+o(Math.floor(Math.abs(e)/60))+":"+o(Math.abs(e)%2560)}function r(){const e=new Date;return`${e.getFullYear()}-${o(e.getMonth()+1)}_${o(e.getDay())}T${o(e.getHours())}:${o(e.getMinutes())}:${o(e.getSeconds())}${s()}`}function a(e,t=!1){const n=document.createElement("td");return t?n.innerHTML=e:n.textContent=e,n}function l(e){return`${i[e].product.description.replace(/\s*(?:Card|Honors|Bonvoy|SkyMiles)\s*/g," ").replace(/\s+/g," ")} ${i[e].account.display_account_number}`}function c(e,t){return e.length?`${e.map(e=>l(e)).join(", ")} (${e.length} card${1===e.length?"":"s"})`:t?"<no cards>":"no cards"}if("global.americanexpress.com"!==location.hostname||!["/dashboard","/overview"].includes(location.pathname))return void(confirm(`You must be on ${e} to use this bookmarklet. Would you like to go there now?`)&&(location.href=e));const i=function e(n){return Array.isArray(n)&&"~#iM"===n[0]?t(n[1],0,e):Array.isArray(n)&&"^ "===n[0]?t(n,1,e):Array.isArray(n)?n.map(t=>e(t)):n}(await async function(){const t=await fetch(e),n=await t.text(),o=(new DOMParser).parseFromString(n,"text/html").getElementById("initial-state").innerHTML.match(/\_\_INITIAL\_STATE\_\_\s*=\s*([^\n]+)\s*;(?:\n|$)/)[1];return JSON.parse(JSON.parse(o))}()).modules["axp-myca-root"].products.details.types.CARD_PRODUCT.productsList,d=Object.keys(i),u=s(),p=new Map;for(const e of d){const t=await n("ReadCardAccountOffersList.v1",{accountNumberProxy:e,locale:"en-US",source:"STANDARD",typeOf:"MERCHANT",status:["ELIGIBLE","ENROLLED"],offerRequestType:"LIST",userOffset:u});for(const n of t?.offers??[]){if(!n.achievement_type)continue;const t=p.get(n.id)??{enrolled:[],cards:[]};t.offer=n,"ENROLLED"===n.status?t.enrolled.push(e):t.cards.push(e),p.set(n.id,t)}}const f=" style=\"margin:4px\"",h=document.createElement("div");h.innerHTML=`<p style="text-align:center"><input type=button value=Close class=close-btn${f}><input type=button value="Add All" class=add-all-btn${f}></p><table${f}><thead><tr><th scope=col>Description</th><th scope=col>Available On</th><th scope=col>Add</th></thead><tbody></tbody></table><p style="text-align:center"><input type=button value=Close class=close-btn ${f}></p>`,h.style.position="fixed",h.style.width=h.style.height="100%25",h.style.top=h.style.left="0",h.style.backgroundColor="rgba(255,255,255,0.9)",h.style.zIndex="999",h.style.overflow="auto",h.style.padding="8px";const m=h.getElementsByTagName("tbody")[0],y=h.getElementsByClassName("close-btn");for(const e of y)e.addEventListener("click",function(){h.parentElement&&(document.documentElement.style.overflow="auto",h.parentElement.removeChild(h))});h.getElementsByClassName("add-all-btn")[0].addEventListener("click",function(){this.parentElement&&this.parentElement.removeChild(this);const e=setInterval(()=>{const{done:t,value:n}=g.values().next();t&&clearInterval(e),n&&(g.delete(n),n())},500)});let g=new Set;for(const e of p.values()){const t=document.createElement("tr");if(t.appendChild(a(`${e.offer.name}: ${e.offer.short_description}`,!1)),t.appendChild(a(c(e.cards,!0),!1)),e.enrolled.length){const n=a(`<already enrolled on ${c(e.enrolled,!1)}>`,!1);t.appendChild(n),m.appendChild(t)}else{const o=a(`<input type=button value=Add${f}>`,!0),c=function(){g.delete(c);const t=this.parentElement;if(!t)return;t.removeChild(this);const o=r(),a=s();for(const s of e.cards)n("CreateCardAccountOfferEnrollment.v1",{accountNumberProxy:s,identifier:e.offer.id,locale:"en-US",requestDateTimeWithOffset:o,userOffset:a}).then(e=>{t.children.length&&t.appendChild(document.createTextNode(", "));const n=document.createElement("span");n.style.fontFamily="monospace",e.isEnrolled?(n.style.color="#0CC00C",n.textContent=`success (${l(s)})`):(n.style.color="#C00C0C",n.textContent=`error: ${e.explanationMessage} (${l(s)})`),t.appendChild(n)})}.bind(o.children[0]);g.add(c),o.children[0].addEventListener("click",c),t.appendChild(o),m.prepend(t)}}for(let e=0;e<m.children.length;e+=2)m.children[e].style.backgroundColor="rgba(0,0,0,0.2)";document.documentElement.style.overflow="hidden",document.body.appendChild(h)}();
@rrggrrggrrgg
Copy link

rrggrrggrrgg commented Mar 27, 2025

Have an offer on 3 of my cards. The applet won't show the Add option because it reports them already added to another account-- however those are authorized user accounts. Can the Add button be exposed for this case? I modified the Applet to force this and it worked for 2 cards. Then I manually added the 3rd and amex allowed it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment