[微技术贴]用chrome查看Amex卡的消费奖励类别统计

受到旁边楼启发,略微研究了一下amex的API后发现可以手动抓取所有amex自家卡(该API无法支持cobrand)的消费类别统计,包括bonus奖励(大部分来自开卡,副卡,refer)
最近发现通过奖励项目的变化时间能知道账号升降机准确生效日期

使用说明
以PC版chrome为基础

  1. 登录amex网页并切换至想要统计的卡,如果账号里只有一张卡则可跳过
  2. (再)点击一次切换卡的按钮,会弹出下拉框列表,卡多的账号还需要点击View All以显示全部卡。注意,此下拉框必须保持显示可见状态直至代码运行结束
  3. 按F12调出开发者工具,切换至console页面
  4. 粘贴代码并按回车
  5. 结果将在新的浏览器页面显示

备注1,统计结果默认为calendar year to date。举例如果改到2022,需要把首行定义为yr = 2022

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);
效果图集
百夫长
个人金

玫瑰金
商金

商白

BBP

BBC

ED

BCP

89 个赞

钛金预定吧

2 个赞

好强zs

3 个赞

这个很不戳啊,改成油猴脚本。

6 个赞

不會編碼是不是就是21世紀的文盲 完了 :full_moon_with_face:

2 个赞

文盲多好,活得潇洒没有负担 :melting_face:

7 个赞

留名zszs

3 个赞

好家伙。。火钳刘明

很强,不过要怎么使用这结果?

好像很硬核
能再简单描述下使用场景吗?

1 个赞

卷起来了

1 个赞

赞赞赞:+1:

2 个赞

火钳刘明

1 个赞

11 个赞

:cow:得一批

3 个赞

只有联名卡:sleepy:

另一个钛金限定?

1 个赞

不过这个抓出来和官网上写的不是一样的么? :face_with_monocle:

2 个赞

嚯 整挺好 :partying_face:

1 个赞

火钳刘明

2 个赞