WTWilly Tai
Case studyAI Governance & AssuranceLive demo · synthetic data

The evidence binder that can prove what the auditor received.

The PBC list (Provided by Client: the auditor's request list for schedules, reconciliations and documents) is where reporting timelines actually die. Requests arrive as free-text emails, artifacts go back as attachments with no completeness check, and weeks later the auditor says an item was never received. Nobody can prove otherwise, so the controller re-sends, the clock restarts, and the overrun resurfaces at fee negotiation with no evidence on either side. This demo is the governed alternative: a binder where the model may only locate and quote, four deterministic gates decide what is deliverable, a named controller releases every handover, and a hash-chained manifest turns "you never sent us that" into a five-second recomputation.

74 → 74
requests, all accounted for
65 delivered, 9 explicitly open. Zero silent gaps, by construction of the mechanism.
4 gates
decide in code
Period, entity, TB tie-out within SGD 1, SHA-256. A failed gate cannot be overridden.
3
defective artifacts caught
Wrong period, wrong entity, a register S$4,820 off the TB. All blocked before delivery.
58s
median request to evidence
Computed from ledger timestamps inside the simulated run, against a 3-day email loop.

Synthetic binder · 74 requests vs 76 artifacts · seeded generator + deterministic engine → identical bindings, hashes and ledger on every run

01 · The scramble

Audit support is a logistics problem wearing an accounting costume.

The Financial Education & Research Foundation's audit fee survey put the average public company audit fee at USD 3.01 million, up 6.4% year on year, with ICFR effort the leading driver. Practitioner surveys in the same territory find 57% of companies saying audit support gets harder every year, and PBC delays are repeatedly cited as the top cause of reporting timeline slippage. Behind those numbers sits a workflow that has barely changed in twenty years. The auditor emails a PBC spreadsheet. The controller forwards line items to process owners. Owners email back attachments of assorted periods, entities and versions. Someone maintains a tracker that drifts from reality within days. Then, deep into fieldwork, the auditor re-requests items already sent, because on their side the attachment landed in a different inbox, or an older version, or nowhere.

02 · The tempting shortcut

Handing the model the binder makes the dispute worse, and politer.

The obvious 2026 move is to point a language model at the document store and let it answer the PBC list. That fails in a specific, predictable way. A model asked to find "the Q3 bank reconciliations" will find something: a Q2 reconciliation with a similar filename, last year's Q3, the right file for the wrong entity. Matching is exactly the kind of plausible-similarity task models are fluent at, and audit evidence is exactly the domain where plausible similarity is the failure mode. A near miss delivered with a confident cover note is worse than a gap, because the gap gets chased this week and the near miss gets discovered in March.

The three seeded defects in this demo are the three classic near misses. A covenant certificate renamed "as at 31 December" whose content is dated 30 September. An inventory listing labelled SGP02 whose entity field says SGP01. A fixed asset register that misses the trial balance by S$4,820.00 because a disposal was never removed. A retrieval model matches all three happily, since it reads labels. The gates read content, and content is where evidence lives.

The dividing line: the model is good at locating candidates in messy free text and terrible at warranting them. So it locates and quotes, and the warrant comes from code that recomputes the facts.
03 · The trust boundary, drawn through a handover

Locate is a suggestion. Deliver is a decision. Code sits between them.

Every system on this site draws the same boundary: deterministic code computes and decides, the model only locates facts and phrases language, and a named human gates anything consequential. Applied to the auditee-to-auditor handover, the pipeline looks like this:

PBC EVIDENCE BINDER · ONE REQUEST, GOVERNED Auditor request free text · "Q3 bank recs" MODEL · LOCATE + PHRASE matches claimed metadata, quotes request + artifact DETERMINISTIC · FOUR GATES period covers the window entity code matches TB tie-out, SGD 1 tolerance SHA-256 computed, recorded Controller named human clicks Deliver ABSTAIN → OPEN ITEMS "no matching artifact in binder" GATE FAILED → BLOCKED undeliverable · no human override Hash-chained ledger + delivery manifest agent ID + human ID on every entry · artifact SHA-256 per delivery · disputes answered by recomputation, in seconds
One request, governed: the model proposes with quotes, the gates decide, the controller releases, the chain remembers.

Two design choices carry the weight. First, the abstention rule is absolute: if no artifact meets the request's period, entity and scope, the model outputs "no matching artifact in binder" and the item moves to Open Items. It may not offer a near miss, because a visible gap gets chased and an invisible substitution gets audited. Second, a failed gate has no override. The controller's Deliver button simply does not exist for a binding the code refused, which converts "please just send it, we are late" from a pressure point into a non-event.

04 · The decision function

Deliverable is a computed property, so it is written as a computation.

This is the gate function from the shipped file, minus the display strings. Every proposed binding runs through it, the visitor's typed requests included. Note what is absent: no confidence score, no weighting, no way for eloquence to move the outcome.

// gates 1-3: pure decisions over the ARTIFACT'S ACTUAL FIELDS (the model
// matched on the claimed label; defects live in the gap between the two)
function runGates(req, a, tb) {
  const period = a.actual.p0 >= req.w0 && a.actual.p1 <= req.w1;
  const entity = a.actual.e === req.e;
  let tie = "n/a";                     // only when a TB linkage is declared
  if (a.tb === "0000") tie = Math.abs(rowSum(a.rows)) <= TIE_TOL;   // a TB ties to zero
  else if (a.tb) {
    const target = Math.abs(tb[req.e][a.tb].b);     // control account, cents
    tie = Math.abs(rowSum(a.rows) - target) <= TIE_TOL; // TIE_TOL = 100 = SGD 1
  }
  return [period, entity, tie];        // gate 4, SHA-256, is awaited by the caller
}
// a binding is deliverable only if no gate is false; there is no override arm
const deliverable = gates.every(g => g !== false);
05 · A worked morning, and the dispute that dies

Seventy-four requests in, seventy-four accounted for.

The seeded run processes the full register: 74 requests against a binder of 76 artifacts across two entities. The engine delivers 65 requests (73 artifact hashes on the manifest, since the quarterly bank reconciliation requests each carry three monthly files), blocks 3 where a gate failed, and abstains on 6 where the binder holds nothing responsive. The 422-entry ledger chains every step, and the median request-to-evidence time computes to 58 seconds from the ledger's own timestamps.

Why the defects are seeded where they are: handovers fail on labels that disagree with contents. A tracker checks that something was sent; the gates check that the right thing was sent, and the manifest proves it stayed the right thing.
06 · What the numbers are, and what they are not

True by construction, which is precisely the point being made.

The binder is synthetic and labelled so in the app. The 65/3/6 split exists because I seeded three realistic defects and six genuine gaps into a deterministic engine; the run reproduces identically on every load, hashes included. The 58-second median comes from simulated timestamps inside the run, so it demonstrates that turnaround becomes computable from the ledger rather than claiming any client achieved it. What the demo proves is the control architecture: zero silent gaps is a property of the mechanism, since every request terminates in exactly one of two visible states.

The honest limits are worth stating flatly. The matching model here is a deterministic stub over a small in-browser corpus, standing in for retrieval against a real DMS or SharePoint, where matching is harder and abstention thresholds need real tuning. The hash manifest proves integrity and sequence within the app, and it is nothing like legal non-repudiation: there are no trusted timestamps and no counterparty signatures, so a production version would want both. Real PBC lists run to hundreds of items with multi-entity scoping this demo only sketches at two entities. And the request-list shape borrows from a published PBC template (NAIHC's Prepared-by-Client list), with artifact row schemas modelled on the State of CT Open Expenditures ledger fields, both cited in-app; the figures inside are generated.

07 · See it run

Kill the dispute yourself, then try to sneak a near miss past the gates.

Click the dispute button and watch the proof replay. Then type your own request: watch it bind with the gates going green one by one, or hit the renamed covenant certificate and watch a gate go red, or ask for the valuation report and watch the model abstain instead of improvising. Tamper with an artifact and let the recompute find it.

▶ Open the live demo Runs entirely in your browser · synthetic data · seeded, reproducible run

Both of my audiences are standing at this handover.

Most of my demos govern what happens inside the company: controls tested, payments gated, reconciliations refereed. This one governs the seam between the controller and the audit partner, which is exactly the pair this site speaks to. The CA/CIA in me has chased these lists and knows how they end; the engineer in me can now make the binder prove itself. The mechanism, bind with recomputed evidence or abstain by name, plus a manifest that turns turnaround into data, is what I would want on either side of the table when the fee conversation starts. If your close is losing weeks to the PBC loop, this is the artifact that ends the argument about whose fault that is, and here it is running.