DEV Community

Cover image for I thought my finance agent was smart until AMZN Mktp US quietly wrecked 3 months of reports
Lars Winstand
Lars Winstand

Posted on • Originally published at standardcompute.com

I thought my finance agent was smart until AMZN Mktp US quietly wrecked 3 months of reports

I thought my finance agent was smart until AMZN Mktp US quietly wrecked 3 months of reports

AMZN Mktp US showed up in a bank alert.

My agent confidently labeled it software.

That one bad guess polluted three months of category totals before I noticed.

The annoying part: the automation looked great.

  • Gmail alert comes in
  • n8n picks it up
  • GPT-5.4 extracts merchant + amount into JSON
  • PostgreSQL stores the row
  • Google Sheets updates the dashboard

Clean pipeline. Valid JSON. No crashes.

Also wrong.

The purchase was random household stuff from Amazon, not software. But because I built the agent like a tiny know-it-all instead of a skeptical assistant, it kept making the same kind of mistake on messy merchant strings.

That changed how I think about finance agents.

Honestly, it changed how I think about agent design in general.

The first finance agent I'd actually trust is not the one that auto-categorizes everything. It's the one that says: I am not sure about this one.

While looking at other personal automation setups, I ran into this r/openclaw thread about running OpenClaw on a VPS with Coolify. Different use case, same lesson: just because you can wire an LLM into your life does not mean the autonomous version is the good version.

The real bug: merchant strings are garbage

Humans can often infer what AMZN Mktp US means from memory.

A model cannot, at least not reliably.

That string could mean:

  • books
  • batteries
  • HDMI cables
  • office supplies
  • groceries
  • a coffee grinder

If you force GPT-5.4, Claude Opus 4.6, or Grok 4.20 to pick a category every time, you are not building intelligence.

You are building a confident error generator.

That was my mistake. I optimized for autonomy first.

The original workflow

This was the first version:

Gmail bank alert
  -> n8n email trigger
  -> GPT-5.4 extraction + categorization
  -> PostgreSQL insert
  -> Google Sheets reporting
Enter fullscreen mode Exit fullscreen mode

And the payload looked something like this:

{
  "merchant": "AMZN Mktp US",
  "amount": 47.18,
  "date": "2026-07-14",
  "category": "software"
}
Enter fullscreen mode Exit fullscreen mode

Nothing broke.

That is exactly why this class of workflow is dangerous.

The JSON was valid. The pipeline succeeded. Structured Outputs did their job.

But valid JSON is not the same thing as correct judgment.

If you build automations in n8n, Make, Zapier, OpenClaw, or a custom OpenAI-compatible stack, this is the failure mode to watch for.

The scary workflows are not the ones that crash loudly.

They are the ones that run perfectly while being wrong.

Why full autonomy is the wrong goal for finance data

A fully autonomous categorizer feels elegant because it removes friction.

  • no review queue
  • no interrupts
  • no human approval step
  • no messy edge-case handling

That design is worse for finance data.

One wrong label compounds.

If an LLM misreads a support ticket, maybe somebody gets a slightly awkward reply.

If an LLM mislabels recurring transactions, your reporting layer becomes fiction.

That is a much more expensive failure.

The better pattern: extract reliably, classify cautiously

After testing a few setups, I stopped caring about "which model is smartest?" and started caring about workflow boundaries.

My conclusion:

  • GPT-5.4 was strong at consistent extraction from bank-alert emails
  • Claude Opus 4.6 was often better when I wanted reasoning about uncertainty
  • Grok 4.20 was fine, but not better for this task than a stricter extraction-plus-triage flow

The winner was not a single model.

The winner was the workflow.

The losing design was:

classify every charge automatically
Enter fullscreen mode Exit fullscreen mode

The winning design was:

extract reliably, classify only when confidence is high, send the rest to review
Enter fullscreen mode Exit fullscreen mode

Less magical.

Much more useful.

The redesign: bank-alert emails first, PostgreSQL second, spreadsheet last

The fix was not "add more intelligence."

The fix was "reduce the surface area."

Step 1: only watch bank-alert emails

I stopped trying to ingest every possible financial signal.

Now the workflow only watches a specific Gmail label or sender pattern.

Not the whole inbox.
Not CSV dumps.
Not forwarded receipts from five sources.

For a personal setup, this can be as simple as:

Gmail label: bank-alerts
Enter fullscreen mode Exit fullscreen mode

In n8n, that means a narrower trigger.

In Make, Zapier, OpenClaw, or a custom worker, same idea: reduce noisy inputs before the model ever sees them.

Step 2: PostgreSQL becomes the system of record

This mattered more than any prompt tweak.

I started storing this shape first:

create table transactions (
  id bigserial primary key,
  raw_email_id text unique not null,
  merchant_string text not null,
  amount numeric(12,2) not null,
  txn_timestamp timestamptz not null,
  proposed_category text,
  confidence_score numeric(4,3),
  ambiguity_flag boolean not null default false,
  review_status text not null default 'pending',
  final_category text,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);
Enter fullscreen mode Exit fullscreen mode

That one decision fixed a lot:

  • no repeated parsing of the same transaction
  • no spreadsheet pretending to be a database
  • no reclassification loops
  • clear separation between proposed and approved category

The spreadsheet became a view.

Not the brain.

What should be automatic vs sent to review?

This is the actual design question.

Not:

  • Which model is best?
  • How do I make it 100% autonomous?
  • How do I get prettier JSON?

The useful question is:

Where is certainty high enough to automate, and where should ambiguity stop the line?

My current rule set is simple.

Auto-approve

  • clearly named merchants with stable history
  • recurring charges I already approved before
  • alerts with enough context to map confidently

Send to review

  • vague strings like AMZN Mktp US
  • first-time merchants
  • pending transactions with incomplete descriptions
  • anything where the model explanation sounds like a guess

This review checkpoint is the whole product.

Without it, the agent is just a spreadsheet vandal with good formatting.

A practical classification contract

If I were implementing this cleanly today, I would force the model to return uncertainty explicitly.

Something like:

{
  "merchant": "AMZN Mktp US",
  "amount": 47.18,
  "date": "2026-07-14",
  "proposed_category": null,
  "confidence_score": 0.41,
  "ambiguity_flag": true,
  "reason": "Amazon marketplace charges span many categories and the merchant string lacks item-level detail"
}
Enter fullscreen mode Exit fullscreen mode

That is a much better result than fake precision.

Example n8n decision logic

This is the kind of branching logic I wish I had started with.

const confidence = $json.confidence_score ?? 0;
const ambiguous = $json.ambiguity_flag === true;
const firstSeenMerchant = $json.first_seen_merchant === true;

if (ambiguous || confidence < 0.85 || firstSeenMerchant) {
  return [{ ...$json, route: "review" }];
}

return [{ ...$json, route: "auto_approve" }];
Enter fullscreen mode Exit fullscreen mode

Then route review to:

  • a Slack message
  • a small internal review UI
  • an Airtable queue
  • a Notion database
  • even a simple Google Sheet tab if you must

The point is not elegance.

The point is preventing silent corruption.

Why this architecture also cuts token usage

This was the nice surprise.

The best way I found to reduce LLM usage was not better prompting.

It was better boundaries.

1. Narrow triggers

Watching only bank-alert emails means fewer model calls.

2. Extract once

Store structured results and stop asking the model to rediscover the same facts.

3. Reuse state

PostgreSQL can answer "have I seen this merchant before?" faster and cheaper than another LLM call.

4. Treat ambiguity as valid output

If the model is unsure, stop there.

Do not force a category just so the pipeline can feel complete.

This matters even more if you're paying per token

A lot of developers accidentally build expensive agent loops like this:

new email arrives
  -> call model
  -> write row
  -> re-read spreadsheet
  -> reclassify old rows
  -> regenerate summaries
  -> call model again for cleanup
Enter fullscreen mode Exit fullscreen mode

That is how a tiny automation turns into a cost leak.

If you are running agents continuously in n8n, Make, Zapier, OpenClaw, or a custom OpenAI-compatible workflow, architecture matters more than prompt cleverness.

And if you're stuck on per-token billing, every unnecessary reprocessing step hurts twice:

  • more cost
  • more chances for the model to be confidently wrong

This is exactly why flat-rate compute is attractive for always-on automations. When you're iterating on routing, retries, review queues, and structured extraction, the last thing you want is token anxiety every time you improve the workflow.

I've been thinking about this more while testing OpenAI-compatible setups that can swap providers without rewriting everything. Standard Compute is interesting in that context because it gives you an OpenAI-compatible endpoint with flat monthly pricing, which fits the way developers actually build agents: lots of retries, lots of orchestration, lots of background runs, and no patience for surprise billing.

Minimal architecture I would recommend now

If I had to rebuild this from scratch, I would keep it boring.

Gmail bank alerts
  -> n8n trigger
  -> LLM extraction
  -> PostgreSQL insert
  -> confidence / ambiguity gate
  -> auto-approve known-good cases
  -> review queue for messy cases
  -> spreadsheet/dashboard as read-only view
Enter fullscreen mode Exit fullscreen mode

That design is:

  • cheaper
  • calmer
  • easier to debug
  • much harder to poison quietly

Quick implementation sketch

Example review query

select
  id,
  merchant_string,
  amount,
  txn_timestamp,
  proposed_category,
  confidence_score,
  ambiguity_flag
from transactions
where review_status = 'pending'
order by txn_timestamp desc;
Enter fullscreen mode Exit fullscreen mode

Example merchant history lookup

select final_category, count(*) as usage_count
from transactions
where merchant_string = $1
  and final_category is not null
group by final_category
order by usage_count desc;
Enter fullscreen mode Exit fullscreen mode

Example approval update

update transactions
set final_category = $2,
    review_status = 'approved',
    updated_at = now()
where id = $1;
Enter fullscreen mode Exit fullscreen mode

The broader lesson for agent builders

This is not really about personal finance.

It is about agent architecture.

The same rule applies to:

  • invoice routing
  • lead qualification
  • support triage
  • procurement workflows
  • internal ops automations

When the input is messy, uncertainty needs to be a first-class output.

If your agent is forced to sound certain all the time, it will eventually lie to you in a very polished way.

That is the bug.

The old version of my finance agent looked better in a demo.

The new version is the one I would actually trust.

The best way to reduce LLM usage in a finance agent is not to make it more autonomous. It's to make it more skeptical: watch only bank-alert emails, store structured results in PostgreSQL, and send ambiguous charges to review before one bad label poisons months of data.

Top comments (0)