Skip to content

Part 2 of 7 · Subscription audit bot series ~5 min read

How the transaction feed arrives

This system reads money movements, which makes the intake the part worth being careful about. Everything here is designed around one rule that never bends: whatever the feed is, it is read-only, and the system holds nothing that could move money or change a subscription.

Key takeaways

  • Three lanes: a CSV drop, an emailed statement, and a read-only Open Banking feed.
  • Read-only is a hard boundary. No payment scope, no admin credentials to anything.
  • Every line gets a fingerprint, so overlapping exports cannot double a charge.
  • The merchant string is kept raw as well as cleaned, because cleaning loses information.
  • Foreign-currency charges keep both amounts; grouping needs the billed one.

Three lanes, one boundary

Three transaction feed lanes converging on one row shapeThree boxes stacked on the left. CSV drop, a monthly export placed in a folder. Emailed statement, arriving as an attachment from the card provider. And Open Banking feed, an API with a read-only scope. Their arrows are labelled file, attach and api, converging on One transaction row holding the date, the amount, the merchant string, the card and the currency. Below it, connected by a downward arrow, is the Grouper, which finds what repeats. A note says whatever the lane, the scope is read, and nothing here can move money.CSV dropmonthly exportfileEmailed statementfrom the card providerattachOpen Banking feedread-only scopeapiOne transaction rowdate, amount, merchant,card, currencyGrouperfind what repeatsWhatever the lane, the scope is read. Nothing here can move money.
Fig 1. Three ways a feed arrives and one row shape. The Open Banking lane is the only one with a credential attached, and its scope is read-only by design and by grant.
  • Database
  • App integration
  • Networking
  • Analytics

Why read-only is stated so heavily

Because the obvious next feature is not read-only. Once a system can see that a subscription should be cancelled, the natural request is for it to cancel it, and that requires either payment-level access to the card or admin credentials to a dozen third-party services. Either turns a small useful tool into a serious piece of security surface for a saving that a person can realise in ninety seconds by clicking cancel themselves.

So the boundary is stated in the design rather than left as an implementation detail. The IAM roles in Part 7 have no write path to anything financial, the Open Banking grant requests only the transactions scope, and there is no credential store for the audited services because there is nothing to store.

The row, and the fingerprint

date         2026-07-02
amount       14.40           in the billed currency
currency     GBP
orig_amount  17.99           if the charge was in another currency
orig_ccy     USD
merchant_raw SP * PROJTOOL   exactly as it appeared
merchant     projtool        cleaned, for grouping
card         ****4417
fingerprint  sha256(date|amount|merchant_raw|card)

The fingerprint is the whole answer to overlapping exports, which happen constantly. Somebody re-downloads a statement to check something and drops it in the folder. A monthly export includes three days of the previous month. An Open Banking feed backfills. All three would otherwise double charges and destroy the grouping, because a subscription that appears to be charged twice a month is a different pattern.

Why the raw merchant string is kept

Cleaning a merchant string loses information, and sometimes the lost information is the answer. SP * PROJTOOL cleans to projtool, which is right and useful for grouping. But SP * is a payment-processor prefix, and knowing that the charge went through that processor is occasionally the only way to work out what a merchant actually is. So both are stored, grouping uses the clean one, and the question sent to a human quotes the raw one — because that is what they will recognise from their own statement.

Foreign currency, and why it matters here

  • Group on the original amount, not the billed one. A $17.99 subscription bills at £14.40 one month and £14.02 the next because the rate moved. Grouping on the billed figure sees two different subscriptions; grouping on the original sees one.
  • Report the billed amount. The annual figure in the question is what actually left the account, because that is the number the owner is being asked to justify.
  • A currency change is a signal. A subscription that stops being billed in dollars and starts being billed in pounds usually means the provider opened a local entity — and frequently means the price changed at the same time.
  • Never guess a rate. If the export does not carry the original amount, the system uses the billed amount with a wider grouping tolerance rather than inventing a conversion.

Next: how a set of similar-looking charges becomes one subscription.

All posts