An isometric 3D illustration clustered in the center of a wide magenta-pink banner with subtle geometric patterns and the Wolfpack Digital logo at the top center. The illustration depicts a modern digital and physical payment system, featuring a central smartphone with a yellow screen displaying a digital wallet interface with a balance of "$215.8." Below the phone, a stack of gold coins, a payment card terminal with a large yellow 'PAY' button, and an inserted yellow credit card labeled "BANK" are arranged. To the right, a dark wallet sits behind the setup, and an unrolling physical paper receipt with transaction details is visible. Floating above are two security icons: a yellow speech bubble with a shield and checkmark, and a green speech bubble with a checkmark. The color palette is composed of pink, yellow, gold, and dark grey, with clean lines and a minimalist aesthetic.

How we audit fintech codebases in the AI era (and what still needs a human)

blog post publisher

Victor Motogna

Head of Web Development

Reading time: 11 min

Published: Aug 31, 2026

Key takeaways

  • Audit-readiness is the development team's responsibility. AI writing more of the code concentrates that responsibility, it does not dilute it.
  • An audit is too big for one prompt. It becomes reliable when you decompose it into small steps, each one explicitly deterministic or LLM-driven, coordinated by a deterministic orchestrator.
  • LLM steps flag risks, with file and line references, under a strict schema. Deterministic steps reproduce the flags. No repro, no finding.
  • A well-built loop tightens with every run: recurring AI flags become static checks. That feedback step is the difference between AI tooling and AI slop.
fintech
AI
code audit
engineering
compliance

Audit-readiness is still a developer responsibility

Fintech products carry other people's money and identity. So it was always our job to ship them with a sound security, privacy, and traceability base: an auditor showing up should be boring. That duty does not transfer to the model when the model writes the code. The less of the code we type, the more of the verification we own.

And the gaps that matter are rarely exotic. At Wolfpack Digital, integrating with national financial institutions has often meant working with legacy systems whose APIs were designed for internal use only. Their side can't give you a modern security base, so yours has to carry it: webhook validation through API keys, IP matching, certificate pinning.

Another common trap is mobile security the backend never mirrors: the app has 3DS and biometrics, but the endpoint behind it accepts the data without validating a token, so anyone calling the API directly bypasses every check the app makes. If someone on your team can manually unblock a customer from a dashboard, that action needs a paper trail, for posterity and for proof.

An audit touches far more than this: infrastructure, vendor contracts, incident process. We can't cover it all in one article. So today we focus on the code, and on one question: how do you decompose "are we audit-ready?" into something you can verify, now that LLMs do a lot of the code-writing?

What is a fintech audit loop?

A fintech audit loop is a repeatable verification workflow that breaks an audit into small checks, runs the mechanical ones as static checks and the judgment ones as schema-constrained LLM reviews, and requires every LLM flag to be reproduced before it counts as a finding. It is a loop because results feed back: recurring flags become new static checks for the next run.

The design principle behind it: no repro, no finding. An LLM step never emits a pass or a fail. It flags a risk with a location, and the flag becomes a finding only when something deterministic reproduces it: a failing test, a command anyone can rerun, or a static check that already covers it. Auditors, regulators, and due-diligence teams accept proof, not model output. And so should you. This is also where the loop differs from most "AI code review" tools, which hand you verdicts you can't rerun or are very hard to follow.

The loop, and the rule that shapes it

Everything in the loop follows one decomposition rule, applied to every audit question:

Three-step decomposition rule for audit questions. Step 1: can it be phrased as a pattern over code, schema or config? Then it's a static check — no LLM, binary result, runs in CI. Step 2: does it need intent or cross-component reasoning? Then it's an LLM step — narrowest possible question, schema-constrained output, reproducible answer. Step 3: does an LLM flag recur across runs or projects? Then codify it as a static check. Corollary: fuzzy steps get deterministic validation of their output, with every returned location mechanically checked to exist.

"Is any money column a float?" is grep with taste, so it stays deterministic. "What happens if this exact webhook payload arrives twice, concurrently?" needs reading three files and reasoning about a race, so it goes to an LLM, phrased as one narrow question rather than "review my code". Every fuzzy step gets deterministic validation of its output, so a hallucinated file path dies at the door.

There are many ways to build such a loop, and that's the beauty of it. For our use case right now, we want to build a workflow that only covers a few cases. I know fintech projects are very different, and some of these features are only the surface, but we need to start from a solid base in order to be able to build a complete audit. Applied to a simple codebase, the rule produces seven phases under one orchestrator:

Seven-phase fintech audit loop under a deterministic orchestrator that handles sequencing, schema validation with retry, majority voting across runs, budgets and an append-only findings log. Phase 0 repo scan (deterministic): stack detection, file and dependency inventory, DB schema dump. Phase 1 discovery (LLM validated deterministically): money map, write paths, external boundaries, PII map, each item resolved to file and line. Phase 2 static checks (deterministic): the C1–C15 check pack, binary and control-mapped, cheap enough to run in CI on every push. Phase 3 review (LLM): questions R1–R5, three runs with a majority vote, producing flags rather than verdicts. Phase 4 reproduce (deterministic): a failing test, a rerunnable command, or a match to a static check; anything else becomes needs_human. Phase 5 report (deterministic): append-only log, deduplicated and diffed against the previous run. Phase 6 codify (LLM plus human approval): a flag class seen twice or more becomes a new static check, C16 onward, feeding back into phase 2.

The orchestrator is boring deterministic code, and that is the point. It owns sequencing, schema validation with retries, running each review question three times with a majority vote, budgets, and an append-only findings log: the audit's own audit trail. The LLM appears in exactly two places. Discovery agents each answer one inventory question (where is money represented, what are the write paths, where are the external boundaries, where does PII live), and every returned location is mechanically checked to exist. Review agents each answer one risk question over one slice of that inventory. Everything else, from the C1-C15 static check pack to reproduction and reporting, is plain code you could run in CI.

So what does a finding look like? Here is the double-credit case. The review step emits a flag under the finding schema:

{
"id": "R1-004",
"flag": "webhook handler credits balance with no dedupe on event id",
"file": "payments/webhooks.py",
"line": 87,
"control_ref": "PCI-DSS / PSD2",
"proof": null,
"status": "flagged"
}

That flag alone proves nothing. So the reproduce step turns it into a test (in Rails this would be a short RSpec case, in Node a Jest one, etc.):

test "same webhook delivered twice credits once":
post /webhooks/psp, payload(event_id: "evt_1", amount: 50)
post /webhooks/psp, payload(event_id: "evt_1", amount: 50)
expect account.balance == 50 # fails: balance is 100

The test fails, so the flag is proven, and status moves to proven with the test attached. What can't be reproduced goes to a needs_human pile that is counted and shows up in the report.

Lifecycle of a flag. A schema-valid flag from review questions R1–R5 must be reproduced in exactly one of three ways to become a proven finding with proof attached: P1 a failing test that is run and witnessed, P2 a command anyone can rerun, or P3 a match against an existing static check. A flag that cannot be reproduced moves to needs_human, where it is counted and reported but never dropped.

One more thing closes the loop. The second time a flag class shows up, it should have been a static check, so the codify step drafts one (a semgrep rule, an AST query, a SQL assertion), a human reviews it, and it joins the pack. This way, each run needs the LLM for less.

What should we build this as? The options we weighed

There is no single obvious shape for a workflow like this, and we went back and forth on it. The same loop could live in your editor, in a script, in your pipeline, or behind a dashboard. Each of those is a real option with real trade-offs, so it is worth walking through them the way we did, rather than jumping to a conclusion.

Option 1 - a Claude Code subagent pack. The audit lives where developers already work: a set of subagents in the repo, one per review question, invoked from the editor. This is the cheapest option to build by far. There is no infrastructure, prompt iteration is immediate, and a developer can point R1 at a webhook handler mid-task and get a useful answer in a minute. But the model owns the control flow, and that is the problem. Two runs explore the codebase differently and surface different flags, there is no append-only log unless you bolt one on, and a chat transcript is not something you can hand to an auditor as evidence. In our experience, this is a great prototyping bench and a poor system of record.

Option 2 - a plain script. A small CLI, written in whatever language your team already ships, that runs the phases in order and calls the LLM through the API the way it would call any function: schema in, JSON out, validated, retried on invalid output. The pros are exactly the properties an audit needs. Control flow is code, reruns are comparable, budgets and logs come naturally, and when an auditor asks "how did you produce this report?" the answer is "we ran this command, here is the log". The cons are real too. You build the plumbing yourself (validation, retries, the majority vote), prompt iteration is slower than in an editor, and someone has to remember to run it.

Option 3 - wired into CI/CD. Run the audit as a pipeline stage and every push gets checked, which sounds like the endgame. For the static checks it is: they are fast, free, and binary, so there is no reason they shouldn't gate a PR like any linter. The full loop is a different story. Discovery and review need whole-repo context, not a diff, and they cost real tokens, so running them per push is slow, expensive, and noisy. There is also the risk that finding reports on every PR trains developers to scroll past it within a week.

Option 4 - a hosted service. Scheduled runs, history across projects, a dashboard, findings diffed release over release. For an agency this is attractive, and honestly it is where a workflow like this wants to end up. But it is the heaviest build, and it front-loads the hardest problem: fintech clients handing repo access to an audit service is a security conversation in itself, and you inherit compliance obligations of your own the moment you store their findings. Building the platform before the loop has proven itself on real codebases is effort in the wrong order.

So we landed on a hybrid, and the split follows the decomposition rule from earlier. Subagents are where prompts get born, because iteration speed wins during design. The script is where the loop lives, because control flow must be code. CI is where the static check pack runs, because rules are cheap and cadence is free. And the hosted service stays parked until the loop has earned it. Your constraints may weigh these differently, and that is fine: the comparison is the part worth stealing, not our answer.

The script at the center of it fits in about twenty lines:

manifest = scan(repo) # phase 0
inventory = validate(llm(D1..D4, manifest)) # phase 1: unresolvable items dropped
results = run_checks(C1..C15, inventory) # phase 2
flags = []
for q in R1..R5:
answers = [llm(q, slice(inventory, q)) for run in 1..3]
flags += majority(answers) # phase 3: disagreement lowers confidence
findings, needs_human = reproduce(flags) # phase 4: P1 test | P2 command | P3 check
log.append(dedupe(findings + results)) # phase 5: append-only, diffed vs last run
for f in recurring(log):
draft_check(f) -> human_review # phase 6: approved checks join C16+

Every line that touches the model is wrapped the same way: schema validation with retry, a token budget, and a hard rule that nothing unvalidated flows downstream.

Keeping the loop honest

A loop like this degrades in two directions, and we guard against both explicitly.

Deterministic steps drift toward fuzziness. The temptation shows up the day a static check gets hard to write: the AST query is fiddly, so someone suggests just asking the model. Don't. The moment a check calls an LLM, it stops being evidence you can hand to an auditor and becomes an opinion with a config file. Our test is blunt: run the check pack twice on the same commit, and the output must be byte-identical. So tool versions are pinned, checks take no network calls, and the check pack is treated like production code: each check has fixtures of known-bad snippets it must catch and known-good ones it must pass.

LLM steps drift toward slop. Structure here is enforced, never requested. Every review step writes into the finding schema, and output that fails validation is retried, not parsed leniently: a finding without a resolvable file and line does not exist. Prompts are versioned artifacts in the repo, and they get regression tests too. The setup for that is a small seeded codebase with known planted bugs: if a prompt change makes R1 miss the double-credit it used to catch, that change does not merge. On top sit the runtime guards: three runs with a majority vote, and a watched needs_human rate. When that rate climbs, the fix is almost always the same: the question got too broad, so split it.

What still needs a human (and the mistakes teams make)

The loop verifies what exists. It does not judge what should exist. Whether your ledger model fits your settlement flow, what your reconciliation design leaves unreconciled, whether your risk scoring would hold up in front of a regulator, which early architecture choice breaks first at 10x: those questions stay with a senior reviewer, and no amount of codifying absorbs them. In our experience, even the paper-trail question is judgment first: a tool can verify that your support dashboard logs an actor, but only a person can decide whether that log would answer what an investigator actually asks.

Still, the common traps are not subtle. A few patterns to avoid:

- Treating LLM output as a verdict - shipping raw model opinion as an "AI audit". That is the slop pattern, and it is the one this loop exists to prevent.

- Refusing LLMs entirely - grep does not find race conditions, and pretending it does is the same abdication in the other direction.

- Trusting green CI - tests assert intended behavior; audits hunt unintended behavior. These are different jobs.

- Auditing once - scale moves the goalposts. The loop is a cadence, not an event. And compliance platforms like Vanta or Drata cover infrastructure and paperwork; your code is still yours.

When to run this

Run the full loop on a codebase takeover, before PSP onboarding, before due diligence, or after an incident. Run the static checks in CI from day one: they are just rules, and they are cheap. A classic manual audit is enough when you are pre-MVP, no real money moves, and the codebase is readable in a day.

For priorities: money-path checks first, because errors there compound silently. Audit trail second, because you cannot reconstruct it after the fact. Reconciliation third, because it is the net that catches whatever the first two miss.

Your loop will look different from ours, and that is fine. The checks are stack-specific and swappable; the decomposition rule is the part to keep. We're building this as a base workflow we tailor per project, and a follow-up article will show a full run on a real codebase, findings included. I hope this article helps you make your product audit-ready before someone else checks it for you.

Do you have a fintech product that needs this kind of look under the hood? Get in touch with us, and let's talk more!

Frequently asked questions

A repeatable verification workflow that breaks an audit into small checks. The mechanical ones run as static checks, the judgment ones run as schema-constrained LLM reviews, and every LLM flag has to be reproduced before it counts as a finding. It loops because results feed back, so recurring flags become new static checks for the next run.
An LLM step never emits a pass or a fail. It flags a risk with a location, and that flag only becomes a finding once something deterministic reproduces it: a failing test, a command anyone can rerun, or a static check that already covers it. Auditors and due-diligence teams accept proof, not model output.
If the question can be phrased as a pattern over code, schema or config, it stays deterministic. "Is any money column a float?" is grep with taste. If it needs intent or cross-component reasoning, such as what happens when the same webhook payload arrives twice concurrently, it goes to an LLM as one narrow question.
Victor Motogna

Written by

Victor Motogna

Head of Web Development

Victor Motogna is the Head of Web Development at Wolfpack Digital, leading the web development team and driving innovation in scalable, secure web applications. With a Bachelor's in Computer Science and a Master's in High Performance Computing & Big Data Analytics, he brings deep technical expertise and a forward-thinking approach to building enterprise-grade solutions.


As both a technical leader and hands-on contributor, Victor works across the full technology stack including Ruby on Rails, Vue.js, Nuxt.js, JavaScript, and Python, with extensive experience in DevOps frameworks and cloud infrastructure (Azure, AWS, Kubernetes). His role extends beyond traditional web development—he plays a key part in architecting AI-powered features, training machine learning models, and ensuring AI integration delivers genuine business value rather than following trends.


Victor's leadership philosophy centers on balancing technical excellence with practical delivery. He excels at translating complex technical concepts into clear business language, architecting solutions that strike the right balance between technical sophistication and MVP speed, and staying ahead of rapid technological change. His approach emphasizes building stable, secure end-to-end solutions while constantly seeking smarter, more efficient development processes.


A frequent speaker at technology conferences across Europe, Victor shares insights on modern web development practices, AI integration strategies, cloud architecture, and building high-performing development teams. His writing draws on real-world experience delivering 250+ digital products and reflects his commitment to using technology to create meaningful solutions that improve people's lives.


Through his blog contributions, Victor explores topics at the intersection of web development, AI, and entrepreneurship, focusing on practical implementation strategies, technology decision-making, and fostering knowledge exchange within development teams.


Areas of expertise: Web application architecture, Ruby on Rails development, Vue.js/Nuxt.js, AI integration, machine learning model training, DevOps and cloud infrastructure, team leadership, full-stack development, technical strategy, scalable systems design.

View profile