Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save aleung/78bab19c8151c0f85c362869698d4c9f to your computer and use it in GitHub Desktop.

Select an option

Save aleung/78bab19c8151c0f85c362869698d4c9f to your computer and use it in GitHub Desktop.
Export transactions from Wealthsimple to a CSV file for Actual Budget import
// ==UserScript==
// @name Wealthsimple Activity Export (Visible Chequing)
// @namespace https://leoliang.cn.eu.org
// @version 2025.12.28.4
// @description Directly export currently visible Chequing transactions to CSV
// @author Gemini / Shota Senga
// @match https://my.wealthsimple.com/app/activity*
// @icon https://www.google.com/s2/favicons?sz=64&domain=wealthsimple.com
// @grant none
// ==/UserScript==
(function () {
"use strict";
const SELECTORS = {
activityHeader: "//h1[contains(., 'Activity')]",
dateGroups: "h2",
};
init();
function init() {
const observer = new MutationObserver(() => {
const header = document.evaluate(SELECTORS.activityHeader, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (header && !document.getElementById('ws-export-container')) {
addExportButton(header);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function addExportButton(header) {
const container = document.createElement("div");
container.id = 'ws-export-container';
container.style.display = "inline-block";
const btn = document.createElement("button");
btn.innerText = "Export Visible Chequing";
btn.onclick = runImmediateExport;
btn.style.cssText = `
margin-left: 15px; padding: 6px 14px; background-color: #000;
color: #fff; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; font-size: 14px;
`;
container.appendChild(btn);
header.parentElement.appendChild(container);
}
function runImmediateExport() {
const status = createStatusBox();
updateStatus(status, 'Parsing visible transactions...');
try {
const transactions = extractVisibleTransactions();
if (transactions.length === 0) {
updateStatus(status, '⚠️ No Chequing items found on page');
setTimeout(() => status.remove(), 3000);
return;
}
const csv = generateCsv(transactions);
downloadCsv(csv);
updateStatus(status, `✅ Exported ${transactions.length} items`);
setTimeout(() => status.remove(), 2000);
} catch (err) {
console.error(err);
updateStatus(status, '❌ Error - Check Console');
setTimeout(() => status.remove(), 5000);
}
}
function extractVisibleTransactions() {
const results = [];
const h2s = Array.from(document.querySelectorAll(SELECTORS.dateGroups));
h2s.forEach(h2 => {
const dateStr = h2.innerText;
const dateObj = parseWsDate(dateStr);
const isoDate = dateObj.toISOString().split('T')[0];
// Traverse siblings until next h2 to find buttons
let nextEl = h2.nextElementSibling;
while (nextEl && nextEl.tagName !== 'H2') {
const buttons = nextEl.querySelectorAll('button[type="button"]');
buttons.forEach(btn => {
const pElements = Array.from(btn.querySelectorAll('p'));
const isChequing = pElements.some(p => p.innerText === "Chequing");
if (isChequing) {
// Payee is usually the first p with unmasked privacy rule
const payee = pElements[0]?.innerText || "Unknown Payee";
const amountRaw = pElements.find(p => p.innerText.includes('$'))?.innerText || "0";
results.push({
date: isoDate,
payee: payee,
amount: cleanAmount(amountRaw)
});
}
});
nextEl = nextEl.nextElementSibling;
}
});
return results;
}
function parseWsDate(str) {
const now = new Date();
const lower = str.toLowerCase();
if (lower.includes('today')) return new Date(now.setHours(0,0,0,0));
if (lower.includes('yesterday')) {
const y = new Date();
y.setDate(y.getDate() - 1);
return new Date(y.setHours(0,0,0,0));
}
const d = new Date(str);
if (isNaN(d.getTime())) return now;
return d;
}
function cleanAmount(val) {
return val.replace(/[−\u2212]/g, '-')
.replace(/[CAD\s$,]/g, '')
.trim();
}
function generateCsv(data) {
const headers = ["Date", "Payee", "Amount"];
const rows = [headers.join(",")];
data.forEach(t => {
const row = [
`"${t.date}"`,
`"${t.payee.replace(/"/g, '""')}"`,
`"${t.amount}"`
];
rows.push(row.join(","));
});
return rows.join("\n");
}
function downloadCsv(content) {
const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
const timestamp = new Date().toISOString().slice(0,10);
link.href = url;
link.download = `ws-export-${timestamp}.csv`;
link.click();
setTimeout(() => URL.revokeObjectURL(url), 100);
}
function createStatusBox() {
const s = document.createElement('div');
s.style.cssText = `
position: fixed; bottom: 20px; right: 20px; z-index: 10002;
background: #333; color: #fff; padding: 12px 20px;
border-radius: 8px; font-family: monospace; font-size: 12px;
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
`;
document.body.appendChild(s);
return s;
}
function updateStatus(el, text) { if (el) el.innerText = text; }
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment