Amex QFX Statement Downloader

Automatically download QFX statements from Amex

// ==UserScript==
// @name         Amex QFX Statement Downloader
// @namespace    Violentmonkey Scripts
// @license      MIT
// @version      1.1
// @description  Automatically download QFX statements from Amex
// @match        https://*.americanexpress.com/*
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// ==/UserScript==
(function () {
'use strict';

// src/common.ts
async function getElement(selector, POLL_INTERVAL) {
  return new Promise((resolve) => {
    const check = setInterval(() => {
      let el;
      if (typeof selector === "string") {
        el = document.querySelector(selector);
      } else {
        el = Array.from(selector).map((s) => document.querySelector(s)).find(Boolean);
      }
      if (el) {
        clearInterval(check);
        resolve(el);
      }
    }, POLL_INTERVAL);
  });
}
function formatDateYYYYdMMdDD(date) {
  return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
}
function GM_xmlhttpRequest_promise(pack) {
  return new Promise((resolve, reject) => {
    GM_xmlhttpRequest({ ...pack, onload: resolve, onerror: reject });
  });
}
async function easyDownload({ content, name, saveAs = true }) {
  await GM_download_promise({
    url: URL.createObjectURL(new Blob([content.trim()], { type: "application/x-qfx" })),
    name,
    saveAs
  });
}
function GM_download_promise(option) {
  const { url, name, saveAs } = option;
  return new Promise((resolve, reject) => {
    GM_download({ url, name, saveAs, onload: resolve, onerror: reject });
  });
}

// src/amex/lib.ts
var BANK_ID = "amex";
var LOGGER_prefix = `[${BANK_ID} Downloader]`;
var POLL_INTERVAL = 500;
async function addDownloadButton() {
  const container = await getElement(["div.DynamicLayout"], POLL_INTERVAL);
  const CLASS = "my-download-btn";
  if (container.querySelector(`.${CLASS}`))
    return;
  const btn = document.createElement("button");
  btn.textContent = "Download QFX";
  btn.className = CLASS;
  btn.style.cssText = `
            padding: 8px 12px;
            margin: 0px 0px 12px 0px;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            width: fit-content;
        `;
  btn.addEventListener("click", async () => {
    try {
      await fireDownloadProcess();
    } catch (err) {
      console.error(`[${BANK_ID} Downloader] Error:`, err);
    }
  });
  container.insertBefore(btn, container.firstChild);
}
async function fireDownloadProcess() {
  const a = document.querySelector('a[title="Download your Statements"]');
  if (!a || !a.href) {
    console.error(`${LOGGER_prefix} Failed to find download link`);
    return;
  }
  const url = a.href;
  const match = url.match(/[?&]account_key=([^&]+)/);
  if (!match || match.length < 2 || !match[1]) {
    console.error(`${LOGGER_prefix} Failed to extract account_key`);
    return;
  }
  const accountKey = match[1];
  console.log(`${LOGGER_prefix} Extracted account_key:`, accountKey);
  const [payload, endDate] = await buildPayload(accountKey);
  const U = new URL("https://global.americanexpress.com/api/servicing/v1/financials/documents");
  payload?.forEach((value, key) => U.searchParams.append(decodeURIComponent(key), decodeURIComponent(value)));
  const { status, responseText: content } = await GM_xmlhttpRequest_promise({
    method: "GET",
    url: U.toString() + "&=",
    headers: {}
  });
  if (status !== 200) {
    throw new Error(`Request failed: ${status} ${content}`);
  }
  const acctIdMatch = content.match(/<ACCTID>(.*?)<\/ACCTID>/);
  if (!acctIdMatch || acctIdMatch.length < 2 || !acctIdMatch[1]) {
    console.error(`${LOGGER_prefix} Failed to extract ACCTID`);
    return;
  }
  let acctId = acctIdMatch[1].replace(/[^a-zA-Z0-9]/g, "");
  console.log(`${LOGGER_prefix} Extracted ACCTID:`, acctId);
  await easyDownload({
    content,
    name: `${BANK_ID}_${acctId}_${endDate}_80.qfx`,
    saveAs: true
  });
}
async function buildPayload(accountKey) {
  const payload = new URLSearchParams;
  const endDate = new Date;
  const startDate = new Date(endDate);
  startDate.setDate(endDate.getDate() - 80);
  const eStr = formatDateYYYYdMMdDD(endDate);
  payload.append("end_date", eStr);
  payload.append("start_date", formatDateYYYYdMMdDD(startDate));
  payload.append("file_format", "quicken");
  payload.append("limit", "3000");
  payload.append("status", "posted");
  payload.append("account_key", accountKey);
  payload.append("client_id", "AmexAPI");
  return [payload, eStr];
}

// src/amex/index.ts
try {
  addDownloadButton();
  const observer = new MutationObserver(addDownloadButton);
  observer.observe(document.body, { childList: true, subtree: true });
} catch (err) {
  console.error("[WF Downloader] Error:", err);
}


})();