[微技术贴]用chrome查看Amex卡的消费奖励类别统计
利用 Amex API 抓取多卡消费奖励类别与剩余额度统计。
1. 关键信息
- 需使用 Chrome 桌面版,打开目标卡主页,展开下拉框,选中目标卡。
- 需通过 F12 → Console 粘贴并运行指定 JavaScript 代码。
- 结果在新标签页以表格形式显示,包含 category、Points、Cash、remainingAmount 等。
- 默认查询当年 YTD;可修改 yr、queryStart、queryEnd 查询指定日期(最大一年范围)。
- 支持显示使用统计数据日期;新版/旧版界面需切换代码版本。
- token 获取方式可能因 UI/A-B 测试变化;若失败可尝试从
window.__INITIAL_STATE__或 network beacon 定位。 - 部分联名卡/COBAND 卡(如 Hilton Aspire、Surpass、BBP)不支持。
2. 羊毛/优惠信息
- 可统计各类奖励(Bonus、Refer、ADJUSTMENT、INDUSTRY_CATEGORY)。
- 可查看剩余额度(remainingAmount),便于管理超市/类别额度。
- 可估算 Refer 使用数量(通过 Bonus 变化)。
- 无具体卡种 SUB、报销、积分兑换比例、折扣码、限时活动、酒店/航司奖励兑换、银行开户奖励信息。
3. 最新动态
- 代码持续更新以适配界面变更(#42、#49、#57)。
- API 曾短暂失效后恢复(#47、#48)。
- 查询范围仍限于一年内,与 Reward Activity 范围一致(#44)。
4. 争议或不同意见
- 界面版本差异导致 token 获取失败(#27、#31、#62)。
- 部分用户误选“所有账户”导致无法抓取(#65)。
- 新版 Tiles 界面与旧版 List 界面兼容问题(#62、#67)。
5. 行动建议
- 仅限 Amex 主卡使用,联名卡/COBAND 卡请换用旧版或放弃。
- 遇到错误优先检查选卡状态与界面版本,必要时从历史记录恢复旧版。
- 如遇 API 失效,可等待 Amex 更新样本后再尝试。
受到旁边楼启发,略微研究了一下amex的API后发现可以手动抓取各种卡的消费类别统计,包括bonus奖励(大部分来自开卡,副卡,refer)
最近发现通过奖励项目的变化时间能知道账号升降机准确生效日期
使用说明
以PC版chrome为基础
登录amex网页并切换至想要统计的卡,如果账号里只有一张卡则可跳过
(再)点击一次切换卡的按钮,会弹出下拉框列表,卡多的账号还需要点击View All以显示全部卡。注意,此下拉框必须保持显示可见状态直至代码运行结束
按F12调出开发者工具,切换至console页面
粘贴代码并按回车
结果将在新的浏览器页面显示
备注1,统计结果默认为calendar year to date。举例如果改到2022,需要把首行定义为yr = 2022
Update 9/13/25
即使amex界面大改版,代码依然没有无需改动。但amex限制了历史查询时间,现在无法超过一年(跟reward activity查询范围一样)
Update 11/28/24
代码没有改动, 只是突然发现希尔顿和万豪的卡也能用这个api了
Update 9/27/24
新版本只适用于新amex界面,还是旧界面的用历史记录的版本
Update 4/5/24
更新匹配amex新的接口要求
Update 7/9/23
改进代码逻辑
Update 3/16/23
在报告页面增加显示使用统计数据的日期
Update 3/15/23
发现通过css-r4d5p3来获取token只适用于含有副卡的账户。改进后代码会先尝试使用该字段,若找不到则会重新读取完整页面来匹配token。
增加了fetch请求的次数,来应对某些时候无返回或超时的情况。
改进统计起始和结束日期的输入。
代码在此
yr = new Date().getFullYear();
queryStart = yr + "-01-01";
queryEnd = yr + "-12-31";
// read current active card token
cls = '[aria-controls="simple-switcher-listbox"]';
target = document.querySelector(cls);
img = target.getElementsByTagName('img')[0];
cardnum = target.querySelector('[data-testid="simple_switcher_display_number_val"]')
name = img.getAttribute('alt') + ' (' + cardnum.innerHTML + ')';
const run = +(target.getAttribute('aria-expanded')=='true');
if (run) {
token = target.getAttribute('aria-activedescendant');
token = token.slice(token.lastIndexOf('-')+1);
} else {
console.log('Please expand card switcher list before running');
}
//get card product data
for (var i = 0; i < 3*run; ++i) {
try {
cp = await fetch("https://functions.americanexpress.com/ReadLoyaltyBenefitsCardProduct.v1", {
"body": JSON.stringify({
"accountTokens":[token],
"cardNames":[],
"productType":"AEXP_CARD_ACCOUNT"}),
"method": "POST",
"mode": "cors",
"credentials": "include",
"headers": {
"accept": "application/json",
"content-type": "application/json",
"one-data-correlation-id": (Math.random() + 1).toString(36).substring(2)
},
}).then(res => res.json());
break;
} catch(err) {
setTimeout(() => { console.log(err.message); }, 5000);
}
}
try {
cardName = cp.cardDetails[0].cardName;
} catch(err) {
cardName = '';
}
// request reward summary for startDate to endDate, default ytd
for (var i = 0; i < 3*run; ++i) {
try {
res = await fetch("https://functions.americanexpress.com/ReadLoyaltyTransactionSummaries.v1", {
"body": JSON.stringify({
"accountToken":token,
"productType":"AEXP_CARD_ACCOUNT",
"startDate":queryStart,
"endDate":queryEnd,
"periodType":"CALENDAR_PERIOD",
"category":["REWARD"],
"summariesBy":["category","transactionType"],
"transactionType":["INDUSTRY_CATEGORY","BONUS","ADJUSTMENT"],
"includeSuppCards":true,
"cardProductName":cardName,
"summariesFor":"CARD_NUMBER"}),
"method": "POST",
"mode": "cors",
"credentials": "include",
"headers": {
"accept": "application/json",
"content-type": "application/json",
"one-data-correlation-id": (Math.random() + 1).toString(36).substring(2) }
}).then(res => res.json());
break;
} catch(err) {
setTimeout(() => { console.log(err.message); }, 5000);
}
}
// display in new tab
function display(res, img, name) {
if (!('summary' in res)) {
console.log(res.error);
console.log(name);
console.log(token);
return;
}
if (res.status['code'] != '0000') {
console.log(res.status);
console.log(name);
console.log(token);
return;
}
tab = window.open('about:blank', '_blank');
_table_ = tab.document.createElement('table'),
_tr_ = tab.document.createElement('tr'),
_th_ = tab.document.createElement('th'),
_td_ = tab.document.createElement('td');
_table_.style.border = '1px solid black';
_th_.style.border = '1px solid black';
_td_.style.border = '1px solid black';
// Builds the HTML Table out of myList json data from Ivy restful service.
function buildHtmlTable(arr) {
var table = _table_.cloneNode(false),
columns = addAllColumnHeaders(arr, table);
for (var i=0, maxi=arr.length; i < maxi; ++i) {
var tr = _tr_.cloneNode(false);
for (var j=0, maxj=columns.length; j < maxj ; ++j) {
var td = _td_.cloneNode(false);
cellValue = arr[i][columns[j]];
td.appendChild(document.createTextNode(arr[i][columns[j]] || ''));
tr.appendChild(td);
}
table.appendChild(tr);
}
return table;
}
// Adds a header row to the table and returns the set of columns.
// Need to do union of keys from all records as some records may not contain
// all records
function addAllColumnHeaders(arr, table)
{
var columnSet = [],
tr = _tr_.cloneNode(false);
for (var i=0, l=arr.length; i < l; i++) {
for (var key in arr[i]) {
if (arr[i].hasOwnProperty(key) && columnSet.indexOf(key)===-1) {
columnSet.push(key);
var th = _th_.cloneNode(false);
th.appendChild(document.createTextNode(key));
tr.appendChild(th);
}
}
}
table.appendChild(tr);
return columnSet;
}
data = JSON.parse(JSON.stringify(res.summary));
// modify data for better display
for (i = 0;i < data.length; i++) {
delete data[i]['benefitId'];
delete data[i]['summariesBy'];
delete data[i]['hasTracker'];
delete data[i]['totalAmount'];
if ('pointCount' in data[i]['summariesTotal'][0]) {
data[i]['Points'] = data[i]['summariesTotal'][0]['pointCount'];
}
if ('cash' in data[i]['summariesTotal'][0]) {
data[i]['Cash'] = data[i]['summariesTotal'][0]['cash']['amount'];
}
delete data[i]['summariesTotal'];
if ('tracker' in data[i]) {
data[i]['remainingAmount'] = data[i]['tracker']['remainingAmount']['value'];
}
delete data[i]['tracker'];
}
tab.document.body.append(name);
tab.document.body.appendChild(tab.document.createElement('br'));
tab.document.body.appendChild(tab.document.createElement('br'));
_img_ = tab.document.createElement('img');
_img_.setAttribute('class', img['alt']);
_img_.setAttribute('src', img['src']);
_img_.setAttribute('width', 300);
tab.document.body.appendChild(_img_);
tab.document.body.appendChild(tab.document.createElement('br'));
tab.document.body.appendChild(tab.document.createElement('br'));
tab.document.body.append('Summary from ' + res['period']['startDate'] + ' to ' + res['period']['endDate']);
tab.document.body.appendChild(buildHtmlTable(data));
tab.document.title = name;
}
if (run) display(res, img, name);
效果图集
百夫长
【引用自 Ditto】:
Screen Shot 2023-03-03 at 12.52.59 AM1920×610 98.5 KB
个人金
普金946×385 94.6 KB
玫瑰金
【引用自 RoyWright】:
image1631×388 159 KB
商金
商金1338×638 234 KB
商白
商白1240×410 117 KB
BBP
BBP1053×340 74.9 KB
BBC
bbc752×341 59.9 KB
ED
ed1065×338 60.5 KB
BCP
BCP1169×409 102 KB
钛金预定吧
好强zs
这个很不戳啊,改成油猴脚本。
不會編碼是不是就是21世紀的文盲 完了
文盲多好,活得潇洒没有负担
留名zszs
好家伙。。火钳刘明
很强,不过要怎么使用这结果?
好像很硬核
能再简单描述下使用场景吗?
卷起来了
赞赞赞
火钳刘明
Screen Shot 2023-03-03 at 12.52.59 AM1920×610 98.5 KB
得一批
只有联名卡
另一个钛金限定?
不过这个抓出来和官网上写的不是一样的么?
嚯 整挺好
火钳刘明
泥潭果然人才济济
【引用自 twjeric】:
很强,不过要怎么使用这结果?
【引用自 zxrxhzmn】:
能再简单描述下使用场景吗?
很多时候东西先做出来,用途再慢慢发掘。
比如,看bonus可以估计本年度refer用了多少。又如BCP超市,有显示remainingAmount,方便查看本年度剩余额度
【引用自 EDC】:
不过这个抓出来和官网上写的不是一样的么?
如果官网有显示是一样的,但官网很多卡都没有
听说过没用过,有需要下次再学习一下
image1631×388 159 KB
简单快捷
mac chrome 不work? css-r4d5p3找不到
css-r4d5p3 已经没了?
这个有点意思,玫瑰金卡的返回里面有超市年度25000统计,但是普通金的却没有显示。
【引用自 xh91】:
mac chrome 不work?
这就不清楚了,我没有mac可以测试
【引用自 eleboson】:
css-r4d5p3 已经没了?
或许是忘记开下拉框?或者像上面一样是mac?
可能我卡多于4张?
product-row很容易找到,name和image都在(显然的,因为网页上显示着呢),但是没有任何长得像token的东西。试了一下click以后看network但是看不出是怎么传token的。
image1522×1117 76 KB
你的div顺序跟我的不一样,我的是role list \ css-bcbh4b \ product-row。你也是pc的chrome吗?我也不太懂为啥网页生成会不一样。
不过关于token,手动找可以看
window.__INITIAL_STATE__,或者看xhr里面经常出现的beacon的payload数据
另外你看的应该不会是副卡吧?副卡没有token
是PC chrome,主卡。可能不同用户就是会有不同UI的A/B test?
但是确实在beacon里(request里,不是response)找到token了,效果不错
image1636×238 27.4 KB
【引用自 eleboson】:
可能不同用户就是会有不同UI的A/B test?
我换了几个号后发现确实有你那个顺序的,但是依然有显示css-r4d5p3,原因不明。小破工具凑合着用
突然想起其实可以试试chatgpt,手动就基本上分步骤google
2012年黑卡大佬
Blue Cash Everyday 不work,直接粘代码显示undefined (找不到aria-labelledby)
我自己把token粘贴进去,出来BCE的图,但是下边数据还是Platinum的
image1542×722 236 KB
代码用不了,求教这个报错是啥原因?
同样遇到这个问题
Uncaught TypeError: Cannot read properties of undefined (reading ‘getAttribute’)
at :7:55
刚申的卡也碰到了同样的问题,于是改进了代码逻辑,这个问题应该是修好了,有空帮忙试用一下哈
@eleboson @MasterCard @Reaper-17 @SatoriKomeiji
可以了!
大佬牛 想问一下有办法查看去年 而非calendar ytd的结果吗
可以,已经更新。你正好提醒了我要修一下日期的bug
没发现啥新问题,感谢更新。
这个接口好像失效了,连Amex自己的显示都没了。
@amexrat 动作真快
类别奖励确实没了,但起码bonus还留着,还是可以看看每个卡refer了多少。
要等amex网页出新样本才能更新了
刚刚又能用了,结果啥都不用改
真好用!感谢楼主
这个弹窗… 不知道为什么
我用没问题,你这是什么卡?
hilton aspire + surpass
cobrand不支持啊
对不起 我捞了。。。。
biz gold无副卡失败, 手动找了token 替换就可以了.
VM291:76 Uncaught TypeError: Cannot read properties of undefined (reading ‘axp-myca-root’)
at :75:36
更新了,因为amex网页有update
有这个error:
image1011×127 4.36 KB
你是不是开了其他amex插件
没有别的插件
image785×847 43.7 KB
image799×334 12.4 KB
你截图看看选卡的下拉框长啥样
image923×426 57.9 KB
现在是tiles了,不是老的list
我的界面还是下拉框,不是这样方格的,测试不了
我发现了,你是不是选了all accounts, 你要先选中想看的那一张卡去主界面
image1910×209 8.79 KB
我现在也报错,是选的想看的那张卡,也打开了所有的卡,之前还好好的
amex文艺复兴了,你试试从历史记录里面找回Update 4/5/24的版本
最新版和旧版代码现在都用不了了
我刚刚试了一楼的还可以啊,就是卡名字显示不对,统计还是在的
如果你确定步骤都没错的话,把console的错误贴出来看看
而且amex老弄a/b test界面,很难debug
image1377×159 7.62 KB
步骤就是进入特定卡的主页,展开下拉框,输入代码
我今天乱跑的是碰到一次这个error,但再刷新就好了
试试最新油猴版吧
可以了,感谢!