FinTech / Payments Confidential payments operator May 14, 2026
Closing a client-side audit gap on a live payments console, before the regulator found it.
A live payments console had grown over four years into a system where the compliance team could not reliably answer the question a regulator was about to ask. The client-side audit instrumentation was partial, inconsistent across screens, and shipped through the same channel as the API. We rebuilt the audit layer in place, in parallel with the existing system, without disturbing the operations team's daily work. Cutover happened on a Tuesday. The first regulator query landed three months later. The new evidence answered it in four hours.
Outcomes
- Client-side audit coverage across six regulated surfaces: 62% → 100% of audit-sensitive actions instrumented
- First post-launch regulator query answered in under four hours, against an internal estimate of six weeks if the rebuild had not happened
- Zero lost audit events across nine recorded network drops in the first quarter, courtesy of the durable client-side queue
- Time-to-evidence for compliance queries: down from a quarter-long investigation to an SQL filter against the new audit store
Services involved
The first time we saw the codebase, a senior engineer on the client’s team walked us through the payments operations console. It was a single-page React application processing about 180,000 settlements a day across three corridors. The walkthrough was punctuated by small caveats. “This screen captures the user’s action but doesn’t record what they saw.” “This one logs the click but the audit event has the wrong session ID half the time.” “This one is fine, I think, but we haven’t checked since the redesign last year.”
By the end of the hour we had counted six regulated surfaces, four different audit-instrumentation patterns across them, and one persistent uncertainty about whether the compliance team could actually answer a regulator query about a payment that had been settled six months earlier. The engagement brief was short. The compliance team had been notified that a regulator was likely to come asking about activity in the previous quarter. The team’s confidence in the existing audit data ranged from “probably fine” on the newest screen to “we honestly don’t know what we’d find” on the oldest.
We had fourteen weeks. The timeline was set by the regulator’s expected query window, not by the build. We could not pause the operations team — there was no maintenance window long enough to take the system offline — and we could not change the API contracts because they were shared with an internal posting layer and three external partner banks. This is exactly the shape of work our frontend modernization service is designed for, and this writeup is what we did inside those constraints.
What we found in the audit week
The client gave us read-only access to the entire frontend codebase and a sample of audit events from the previous six months. The first two days were just reading. We mapped every audit-sensitive action in the console — every state change a regulator might later ask about — onto a spreadsheet with columns for the action, the surface, the instrumentation pattern, the captured payload, and the storage destination.
By the end of the week the spreadsheet had 41 rows.
Three of those rows were uninstrumented entirely. The actions happened, the API recorded them, but nothing on the frontend captured what the operator had seen at the moment they acted. Eleven were instrumented but through the same HTTP call as the API, so an audit event was only recorded if the API call succeeded. That meant if the operator clicked Approve during a network blip, the audit record showed they had never tried. The remaining 27 were instrumented through a separate channel but used four different payload shapes across five years of accumulated patterns, with no unifying schema and three different retention policies in three different storage backends.
We presented the spreadsheet to the compliance lead on Friday. The conversation that followed was the one we usually have at this stage. The compliance lead had not realised the coverage was that uneven. The engineering lead had not realised the compliance team would consider it a problem. The CFO had not realised any of it.
The architecture we proposed
We wrote a short scoping document the following Monday. Two pages. It described an audit layer that lived on a separate channel from the API, behind a durable client-side queue, writing to immutable storage on a retention policy owned by compliance rather than by product.
The decision the team had to make was whether to retrofit the new pattern incrementally — screen by screen, leaving the others on their old instrumentation in the meantime — or to land it as a single client-side module that every surface would migrate to behind the existing UI.
The incremental approach kept the existing code in place and rewrote each instrumentation hook in turn. It was lower risk in the small but higher risk in the aggregate. Every screen would be on a slightly different state of the migration at any given time, and a regulator query during the migration window would have to be answered against a mixed estate. The single-module approach replaced the instrumentation across all six surfaces at once. Higher risk on the cutover day, lower risk thereafter.
We recommended the single-module approach. The compliance lead agreed for a reason we had not initially weighted enough: the audit data was about to become evidence in a regulator file, and a mixed estate during the query window would have required the team to explain in writing why different screens had different coverage at different points in time. The single-module approach made that explanation unnecessary.
The build, weeks two through twelve
What matters here is not the module’s internals — they are not unusual — but the engagement-specific decisions, because most of the build was mechanical once the design was settled.
The audit channel was a separate HTTP transport from the API, posting batched events to a write-only endpoint we stood up in the first sprint. The endpoint accepted events asynchronously, did not propagate errors back to the UI, and wrote to an object store with bucket-level immutability. The events were buffered in an IndexedDB queue inside the application, drained by a Service Worker that batched and retried with backoff. The queue was cleared on confirmed receipt from the endpoint, not on send. We wrote roughly 1,400 lines of code for the module and 800 lines of test code that ran against a deterministic clock so we could verify ordering and idempotency.
Two engagement-specific decisions are worth recording.
The first was how to capture the visible state at click time on the older two screens, both of which computed display values inside the React render function. Lifting the render-time computation out of the render took six engineer-days on the oldest screen and four on the slightly less old one. We did it because the alternative — re-running the render synchronously to extract the state — was already known to produce wrong values on screens where the underlying data was being mutated by an in-flight WebSocket update. The team had hit that exact bug in the existing instrumentation eight months earlier and spent a week debugging it before they realised the captured “audit state” did not match what the operator had actually been looking at.
The second decision was about staleness. The settlement feed had no notion of a revision number — values arrived with timestamps, but a stale value could look identical to a fresh one if no update had been pushed in the interim. We added a server-side counter that incremented on every authoritative settlement update and propagated it through the WebSocket envelope to the client. The change touched five backend services, two of which were owned by partner banks. The negotiation took longer to settle than the code took to write. The negotiation was worth it for the same reason the compliance lead had agreed to the single-module approach: the day a regulator asked whether the operator had been looking at stale data when they approved a high-value settlement, the answer needed to be a number, not a story.
By the end of week ten the module was running in production against shadow traffic. The new audit data was being written to the new endpoint alongside the existing audit data. We compared coverage. The new instrumentation covered 100% of the audit-sensitive actions on the spreadsheet. The old instrumentation covered 62% — slightly worse than the team’s intuition, slightly better than the engineering lead’s pessimistic estimate.
The cutover
The cutover was the part we lost most sleep over. The new module had been running in shadow for two weeks. The audit data looked right. The endpoint was stable. The IndexedDB queue had been exercised against three simulated network drops without losing events.
We cut over on a Tuesday morning, the lowest-volume window of the week. The compliance team was on standby. The operations team was on standby. We had three engineers paired with three operations users for the first hour. The switchover was a feature flag, atomic at session start.
What we expected was a quiet morning. What we got was a quiet morning, except for one issue we had not anticipated. One of the older screens used an unusual focus-management pattern that interacted with the new audit hook in a way that briefly delayed focus return after form submission by about 280 milliseconds. Nobody on the operations team noticed, but our own instrumentation flagged it on the third user. We rolled out a fix four hours later. The fix was three lines.
The rest of that Tuesday and the following Wednesday were uneventful. By Friday the team had confirmed the audit endpoint had been ingesting 100% of the events the module captured, and the IndexedDB queue had been clearing inside the worker’s backoff window in every recorded case. The old instrumentation was left in place for a further four weeks as a fallback, then archived.
The first post-launch regulator query
The query arrived eleven weeks after cutover. The regulator wanted to know what an operator had seen on the screen at 14:42:18 IST on a specific day in the previous quarter, when they had approved a partial settlement that had subsequently been disputed by the receiving bank.
The compliance lead’s first instinct, six months earlier, would have been to escalate the query to engineering and expect a multi-week investigation against logs that probably did not contain the relevant evidence. With the new audit data, the compliance team ran an SQL filter against the audit store, found the session, pulled the relevant events, and reconstructed the screen state at the moment of the action. The captured state showed the settlement amount, the destination account, the partner bank’s reply code, the live FX rate, the operator’s last-seen revision number for each field, and the fact that the FX rate had refreshed two seconds before the operator approved. The screen had not been stale. The operator had acted on current data. The dispute was a downstream reconciliation issue, not an operator error.
The compliance team responded to the regulator the same afternoon. Four hours, end to end. Before the rebuild, the internal estimate for the same query had been six to eight weeks, and the answer would have been a written explanation rather than a structured reconstruction.
What we would do differently
Two things, neither of them surprising in retrospect.
We would propose the data-revision counter earlier in the timeline, not as a sub-task of the module build. The counter required cross-team negotiation we had not initially scoped for, and it almost slipped past the cutover date. On the next engagement of this shape, we have proposed the counter as a Week-1 dependency, with the partner-bank conversations starting on Day 2 rather than Week 5.
We would also pair more aggressively with the operations users during the design phase. We held two structured sessions with them, which was enough to confirm the audit-sensitive actions but not enough to catch the focus-management interaction that bit us on cutover day. On the next engagement we have built in a half-day shadow with each operator persona before the architecture is finalised.
What the team owns now
The new module is theirs. The audit endpoint is theirs. The Service Worker is theirs. The retention policy is theirs — we wrote the first draft; the compliance lead now owns it. We handed over a printed reading list of the design decisions, a recorded walkthrough, and a Friday afternoon Q&A. The team has shipped four feature updates to the audit module since cutover. We have not been involved in any of them.
If you are sitting with audit coverage you are not confident in — or you have a regulator query window approaching and you don’t yet know what your data will and won’t answer — we run a 20-minute Architecture Review where we look at your current instrumentation, your retention posture, and your priorities, and send back a written diagnosis. No deck. Just a roadmap.
02. Newsletter
More on regulated FinTech engineering, monthly.
Practitioner notes on the work behind real FinTech systems. One short essay or case study a month. No marketing, no sequences. Reply to talk.
Prefer RSS? Subscribe to the feed →
Adjacent to your problem?
The right next step is a 20-minute review.
We look at your stack, listen to your constraints, and send you a written diagnosis with a suggested next step. No deck. No pitch.