How I Manage My Credit Card Spending With Open Banking
So why would anyone want to do this? For me it was when I was experienced enough to learn that credit cards aren’t the devil and if you use them properly, you can be a lot smarter with your money. Whether its rewards, cashback or to benefit from Section 75. I got to the point where I was paying for everything with my Amex (to get that juicy cashback). However, it was hard to keep a track of how much I have left to spend for the rest of the month.
Monzo Plus does have a feature to show your balances but I wanted something that was a bit more restrictive and really didn’t let me go over my budget. I already have a direct debit set up, was there a way to set aside how much my balance is so when it came to the payment day, the money could come out of there? Monzo has these things called pots where you can set aside money. If I could get a way to move money to a pot when I spend on my Amex, we’re in business.
Version 1
After some googling, I came across forums where people were using the IFTTT integration with Monzo to move money into a pot. Awesome! I had to figure out how I could trigger a webhook to move money to a pot. This is where I learned about webhooks and HTTP requests and in the process came up with a solution to run an iOS Shortcut every time I spent money. This would perform a GET request to my IFTTT Webhook endpoint which moved money into my pot! This worked but it was too loborious. I needed to fully automate this.
Version 2
Version 1 worked, but I wanted something smarter. Less manual, more automated. Something that just works in the background without me needing to run a Shortcut every time I tapped my card.
So I upgraded the system using Pipedream, Cloudflare Workers, and ntfy for notifications. Here’s how I set it up:
1. Catching the Spend
I used Pipedream to hook into my Amex account via GoCardless Bank Account Data (formerly Nordigen). The workflow triggers every time there’s a new transaction.
To avoid unnecessary triggers, I added a basic filter to ignore lines like PAYMENT RECEIVED - THANK YOU
.
If it’s an actual spend, I forward two key things to my Cloudflare Worker:
- the amount
{{steps.trigger.event.transactionAmount.amount}}
- the remittance line
{{steps.trigger.event.remittanceInformationUnstructured}}
(e.g. “TESCO FUEL WATFORD”)
I also fire a quick push notification via ntfy - just because I like seeing when stuff happens.
Show code: Pipedream POST to Cloudflare Worker
await fetch("https://my-worker.potzo.workers.dev", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
amount: steps.trigger.event.transactionAmount.amount,
line: steps.trigger.event.remittanceInformationUnstructured
})
});
2. Moving the Money
My Cloudflare Worker handles the actual transfer into the Monzo pot. Based on the amount, it decides whether to deposit or withdraw (in case of refunds), and sends the relevant request to Monzo.
Show code: Move money into Monzo pot
const moveMoney = async (amount) => {
const moveType = amount < 0 ? "deposit" : "withdraw";
const sourceType = amount < 0 ? "source_account_id" : "destination_account_id";
const depositAmount = Math.abs(amount);
const makeId = (length) => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
return Array.from({ length }, () => chars.charAt(Math.floor(Math.random() * chars.length))).join('');
}; // you just have to something like this!
const headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": `Bearer ${accessToken}`
};
const dedupeId = makeId(20);
const fData = new URLSearchParams();
fData.append(sourceType, SOURCE_ACCOUNT_ID);
fData.append("dedupe_id", dedupeId);
fData.append("amount", depositAmount.toString());
const res = await fetch(`https://api.monzo.com/pots/${POT_ID}/${moveType}`, {
method: "PUT",
headers,
body: fData
});
return await res.json();
};
3. Matching the Transaction
To keep my Monzo feed tidy, I fetch the Monzo transaction (by getting the most recent transaction that matches the amount and time), and then annotate it with the original Amex description.
Show code: Find matching Monzo transaction
const getTransaction = async () => {
const d = new Date();
d.setSeconds(0, 0);
const since = d.toISOString();
const res = await fetch(`https://api.monzo.com/transactions?account_id=${SOURCE_ACCOUNT_ID}&since=${since}`, {
headers: { "Authorization": `Bearer ${accessToken}` }
});
const { transactions } = await res.json();
const match = transactions.find(txn => txn.amount === amount);
return match?.id;
};
Show code: Annotate Monzo transaction
const amexAnnotate = async (transactionId, message) => {
const headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": `Bearer ${accessToken}`
};
const fData = new URLSearchParams();
fData.append("metadata[notes]", message);
const res = await fetch(`https://api.monzo.com/transactions/${transactionId}`, {
method: "PATCH",
headers,
body: fData
});
return await res.json();
};
Now my Monzo feed shows exactly what each Amex pot transfer was for. It’s clean, and makes it obvious at a glance.
4. Maintenance? Barely.
The only “manual” bit is refreshing the open banking connection every 90 days on Pipedream - this is a neccessary evil and even that takes 2 minutes. Once that’s done, everything else is fully automated.
This version has been running smoothly for a while now. For me, it’s solved the annoying “how much do I owe Amex this month?” problem, without me overspending or needing to remember anything. If you’re someone who uses Monzo and Amex (or any rewards credit card) and wants a cleaner budgeting flow, this might be something you’d want too.
Version 3?
Now that it’s working well for me, I’m seeing if others would find this useful too. If that’s you, I’m building a version for the public!
I’ll be in touch when it’s ready to roll out more widely.