Back to blog
Tutorial6 min read

How to Clean Up PayNow and NETS Transactions in Google Sheets: 12 Patterns

By JX · Personal Finance
How to Clean Up PayNow and NETS Transactions in Google Sheets: 12 Patterns

Last updated: 19 October 2026

A PayNow row rarely tells you who you paid. It gives you a name fragment, a reference code and a date, so a month of DBS, OCBC or UOB transactions imported into Google Sheets ends up looking like PAYNOW-OUT TAN WEI MING REF 88213 and NETSQR 8821 KOPITIAM. Below are the 12 descriptor patterns behind those strings, plus the exact formulas that turn each one into a clean merchant name and a category you can total.

Scope: Singapore bank exports (DBS, OCBC, UOB), covering PayNow, NETS, FAST, GIRO and card rows. Exact strings differ a little between banks, so read the examples as shapes rather than literal matches.

If you would rather not build the formula chain yourself, CalmExpense runs this cleanup on your CSV inside your own Google Sheet. Free tier, no bank login.

What a PayNow row is actually telling you

Every descriptor is built from four parts, in this order: a payment rail, an identity, a reference, and sometimes a location or terminal code. The rail tells you how the money moved. The identity is the part you care about. The rest is noise that breaks your sorting, your lookups and your category totals.

Once you see the four parts, the cleaning job stops being guesswork. You strip the rail, keep the identity, and drop the codes.

The 12 patterns you will run into

#What shows upWhat it isWhat to strip
1PAYNOW-OUT TAN WEI MING REF 88213Transfer to a personRail prefix, REF code
2PAYNOW TRANSFER 91234567Transfer to a mobile numberRail prefix, 8-digit number
3PAYNOW UEN 202312345KPayment to a business by UENRail prefix, UEN
4PAYNOW QR HAWKER STALL 03QR payment at a stallRail prefix, outlet code
5NETSQR 8821 KOPITIAMNETS QR at a terminalNETSQR, terminal digits
6NETS POS 004512 FAIRPRICEDebit card at a terminalNETS POS, terminal number
7NTUC FP 1234 SGContactless card purchaseStore number, SG suffix
8GRAB*TRIP 8821 SGApp or online card chargeAsterisk code, SG suffix
9GIRO COLLECTION AIA 0021Recurring GIRO deductionRail prefix, billing code
10FAST TRF TO 001-234567-8Transfer to an account numberRail prefix, account number
11ATM CASH WITHDRAWAL 0512Cash withdrawalLocation code
12REFUND NTUC FP 1234Reversal of an earlier chargeKeep the merchant, flag as refund

Rows 1 to 4 are where most of the mess comes from. PayNow carries the recipient's registered name, which is a person's name for a hawker stall paying by mobile, and a company name for a proper business. There is no merchant category in the file. You have to infer it.

Step 1: strip the rail and the codes

Put the raw descriptor in column A, starting at A2. In B2, this single formula handles patterns 1 to 8 and 10 to 12:

=PROPER(TRIM(REGEXREPLACE(REGEXREPLACE(REGEXREPLACE(REGEXREPLACE(A2,"(?i)^(PAYNOW(-OUT|-IN)?|NETSQR|NETS POS|NETS|FAST TRF TO|GIRO COLLECTION|ATM CASH WITHDRAWAL|POS|IBANKING)\s*",""),"\s+(REF|TRF|TRANSFER)[: ]?[A-Z0-9]{4,}$",""),"\s+\d{4}$",""),"\s+SG$","")))

Read it from the inside out. The first REGEXREPLACE removes the rail. The second removes a trailing reference code. The third removes a four-digit terminal or store number. The fourth removes the SG country suffix. TRIM collapses the leftover double spaces, and PROPER fixes the shouting.

Copy it down the column. Then check the rows where the result still looks wrong, because those are the ones worth a manual fix.

Step 2: a lookup table for repeat offenders

Ten merchants usually account for most of your rows. Build a small table on a second tab with the raw string in one column and the name you want in the next, then wrap the cleaner in a lookup:

=IFERROR(VLOOKUP(A2,Lookups!$A:$B,2,FALSE), <the cleaner formula>)

The lookup wins when it matches, the formula handles everything else. This is the same idea behind the formulas I use to categorize 1,200 transactions, applied one layer earlier.

Step 3: assign a category

With clean names in column B and amounts in column D, category in C2:

=IFS(
  REGEXMATCH(B2,"(?i)fairprice|ntuc|cold storage|sheng siong|giant|redmart"),"Groceries",
  REGEXMATCH(B2,"(?i)kopitiam|hawker|food court|caifan|restaurant|mcdonald|kfc|subway|toast box"),"Food/Dining",
  REGEXMATCH(B2,"(?i)grab|gojek|tada|comfortdelgro|smrt|simplygo|transitlink"),"Transport",
  REGEXMATCH(B2,"(?i)anytime fitness|fitness first|gym"),"Gym Membership",
  REGEXMATCH(B2,"(?i)youtube|google one|icloud|adobe|microsoft 365"),"YouTube Premium",
  REGEXMATCH(B2,"(?i)uniqlo|shopee|lazada|decathlon|ikea"),"Shopping",
  TRUE,"Uncategorized")

Order matters. "Grab" appears in ride receipts and in food delivery, so decide which one you want and put that rule first. Then total each category:

=SUMIF($C$2:$C$500,"Groceries",$D$2:$D$500)

If you have not exported the file yet, start with how to export DBS, OCBC and UOB transactions to Google Sheets. Card rows from PDF statements need a different import step.

Why this changes your numbers, not just your column widths

Small PayNow transfers are the easiest spending to lose track of, because each one is too small to notice and too messy to categorize. Thirty of them at S$18 each is S$540 in a month. Against the S$1,986 median monthly expense for a Singapore household, that is 27% of your spending sitting in a column you cannot total.

The failure is quiet. Your Groceries total looks fine, your Food/Dining total looks fine, and the gap hides in Uncategorized. You then set next month's budget from numbers that were never complete. Cleaning the descriptors first is the only way the SUMIF totals mean anything.

Where CalmExpense fits

The product is a dashboard that sits on top of your own Google Sheet. You import the CSV, it cleans the local merchant strings, and the charts read from the sheet in your Drive. Nothing goes to a server the developer controls, and there is no bank login to hand over.

Two limits worth knowing before you try it. There is no mobile app yet, so you use it in a browser through Google Sheets. Import is manual CSV, not a live bank feed. If you are weighing that trade against a subscription tool, four options compared covers the alternatives honestly.

Pricing is S$29.90 one-time for Pro, with a free tier that covers the cleaning and the basic dashboard. YNAB runs about S$147 a year, which is the comparison that matters if you plan to track for more than a few months.

FAQ

Why does my PayNow row show a person's name instead of the shop? Because PayNow identifies the recipient by their registered PayNow name, which for a small stall is often the owner's personal name. You cannot recover the shop name from the file. Add it to your lookup table once and it stays fixed.

Do I need clean names before categorizing? Yes. The category formula matches on merchant text, so an uncleaned string like PAYNOW-OUT TAN WEI MING REF 88213 will never match "hawker" or "kopitiam". Clean first, then categorize.

What about PAYNOW-IN rows? They are incoming transfers, so they belong in an income column, not an expense column. The formula above strips the prefix the same way, which lets you separate them with a simple filter on the rail.

Will this work with OCBC and UOB exports? The rail names are the same across the three banks. The reference and terminal codes sit in slightly different places, so test the formula on 20 rows from each bank before you copy it down the whole column.


The cleanup takes about 20 minutes the first time and near zero after that, because the lookup table does the repeat work. That is the whole argument for keeping your ledger in a sheet you own: the rules stay where you can read them.

See your own spending this clearly

CalmExpense turns your bank statement into a clean dashboard inside your own Google Sheets. No bank linking, no subscription. One payment of $29.90.

Start your free trial