WTWilly Tai
Case studyAI Governance & Assurance

The plug button that has to say a name out loud.

Group intercompany balances routinely fail to eliminate at close, and the difference gets "plugged" to a suspense account to hit the deadline. The plug then ages invisibly until an auditor or a restatement finds it. This workbench does the reconciliation the way it should have been done all along: deterministic code pairs 400 due-to/due-from documents across a EUR parent and a USD subsidiary, recomputes every retranslation from the real ECB reference rate, and classifies whatever fails as timing, rate, missing counterpart or rounding. The residual can still be plugged. What it cannot be is anonymous: the plug button demands a named approver, and the plug stays on the face of the dashboard as an aged open item.

94.0%
of IC value auto-eliminated
By code, with FX recomputed. Synthetic ledgers, real ECB rates.
394/400
documents paired
On counterparty + reference + transaction-currency amount.
6
residual items, all owned
€2.40M, aged 1 to 78 days, each with a named controller.
0
anonymous plugs
A plug needs a name before it is allowed to exist.

Figures from a synthetic two-entity close (Meridian, Q4 FY2025) computed against real ECB reference rates. They demonstrate the mechanism, not a client outcome.

01 / The pain, measured

Out of balance, out of sight, and re-litigated by email every month.

The intercompany step of a group close is still run like correspondence. Each entity accountant emails a due-to/due-from balance listing. A group accountant pastes them into a matrix spreadsheet. Differences are chased through email threads with screenshots attached. The FX retranslation is argued about because each side applied a different rate, or the same rate to a different date. And at the deadline, whatever is still open is journaled to an "IC out of balance" suspense account so the consolidation ties.

The survey evidence says this is the norm rather than the exception. Deloitte's intercompany accounting survey of 81 companies found about 30% reporting significant out-of-balance positions that get plugged at close, and 54% running the process manually with no counterparty visibility. The plug is not a neutral shortcut. It builds restatement risk on the balance sheet, it breaks the transfer-pricing documentation trail that tax authorities expect to reconcile to the ledgers, and it is precisely the population a group auditor samples. The same dispute then resurfaces the following month, because nothing recorded who owed the answer.

02 / Why a model alone makes it worse

"Ask AI to clear the difference" is a request to hide it fluently.

An intercompany difference is a disagreement between two legal entities about money. Handing that to a language model without hard boundaries fails in specific, predictable ways:

So the design question is a boundary question: which parts are deterministic reconciliation, which small part is language work, and which single action needs a human name attached before it can happen.

03 / The trust boundary

Code decides what eliminates. The model only drafts the awkward email.

Every pairing, every retranslation and every classification is computed deterministically. The engine pairs lines on counterparty + document reference + transaction-currency amount, recomputes each EUR retranslation from the ECB reference rate for the booking date (it checks the stored rate against the published one, and believes the published one), and classifies every failure by rule. The language model sits strictly downstream: it drafts the dispute note to the counterparty controller, citing the specific line pairs, the ECB series and date, and the rule classification. It cannot reclassify an item and it cannot change an amount. The one consequential action, plugging the residual, is gated behind a named human approver and lands on a hash-chained ledger.

DETERMINISTIC · pairs, recomputes FX, classifies, DECIDES MODEL drafts only · a HUMAN owns the plug Ledger 1000EUR parent · 399 lines Ledger 2100USD subsidiary · 397 lines ECB ratesEXR.D.USD.EUR · real Pairpartner · ref · amount Recompute FXbooking + closing rate Classifyeliminate · timingrate · missing Dispute notedrafts only · cited Plug gatenamed approver Append-only, hash-chained ledgerevery pairing · classification · plug approval · agent id + human id per entry · tamper-evident · replayable
fig.1 · the trust boundary: deterministic code decides what eliminates; the model drafts; a named human owns the plug

The decision function is small enough to read in one screen, and that is deliberate. It handles the absent counterpart with the cutoff rule, recomputes the retranslation from the published rate, and assigns an owner as part of the classification itself:

the deterministic decision · app.jsthis code, and never a model, decides what eliminates
function classify(p, s) {
  if (s === null) {                                   // no counterpart at all
    const cls = p.posting_date >= L.cutoff_start ? "TIMING" : "MISSING";
    return { cls, owner: L.entities[other(p.company)].controller };
  }
  const dTxn = Math.abs(p.amount_usd - s.amount_usd); // rounding noise?
  if (p.func_ccy !== "USD") {                         // recompute the retranslation
    const ecb = rateOnOrBefore(p.posting_date);       // the rate code derives,
    const expected = p.amount_usd / ecb.rate;         // never the stored one
    if (Math.abs(p.amount_func - expected) > L.tolerance.fx_eur) {
      return { cls: "RATE", owner: L.entities[p.company].controller, ecb };
    }
  }
  return { cls: "ELIMINATE", rounding: dTxn > 0, dTxn };
}
Ownership is assigned by rule, in the same branch that classifies. The model is never on this path; it receives the output and writes a note about it.
04 / Worked example

One stored rate is wrong, and €39,182.19 of payable appears from nowhere.

Take document IC-2025-4051, a trade invoice for USD 666,782.04. Both entities booked it, and the pair matches to the cent in transaction currency. But the parent translated its EUR payable at a stored rate of 1.0801, a stale vintage that survived in a rate table. The ECB reference rate for the booking date, 2025-11-06, is 1.1533. The engine recomputes: USD 666,782.04 at 1.1533 is EUR 578,151.43, against EUR 617,333.62 booked. The parent's ledger overstates the intercompany payable by EUR 39,182.19, an amount that corresponds to no transaction at all.

LineLedgerTxn (USD)Rate appliedEUR bookedEUR recomputedClass
P-00511000 · due-to666,782.041.0801 stored617,333.62578,151.43Rate
S-00512100 · due-from666,782.04USD functionaln/an/aMatched in txn ccy
P-03991000 · due-to435,972.871.1553 · agrees377,367.67377,367.67Missing counterpart · abstained on cause

The classification lands on L. Verhoeven, the controller of the entity that misbooked, and the model drafts the dispute note: it cites P-0051 and S-0051, names the ECB series and date, states both EUR figures and the difference, and closes with the one sentence that defines its role: this note cannot change either amount. For IC-2025-4399, the 78-day-old item with no counterpart anywhere in the subsidiary's 397 open lines, the note abstains on the cause because neither ledger carries evidence of one.

Then comes the button. The residual of €2,400,851.86 sits above the register with "Plug it" next to it. Press it and the app asks for a name, because someone has to own the plug on the face of the dashboard. Approve it and the amount does the opposite of vanishing: it becomes PLUG-2025-Q4, an open item with an owner and an age, carrying all six differences still itemised beneath it. A toggle shows how most groups do it instead, the same amount sliding silently into suspense account 219900 with no owner, no age and the line pairs discarded. That journal is rendered for contrast and is never executed.
05 / What the run produced

The numbers, and the limits stated flatly.

Across the 400 synthetic documents, the engine auto-eliminated 394 pairs worth €37,613,345.64 of the €40,014,197.50 total intercompany value, which is 94.0%, with every retranslation recomputed from the ECB reference rate rather than taken from either ledger. Nine of those pairs differed by cents and were auto-cleared under the $0.05 rounding tolerance, $0.25 in total. The residual is €2,400,851.86 across six items, aged 1 to 78 days: two timing gaps at cutoff, two wrong-rate bookings, two missing counterparts. Each carries a named owner and a drafted, cited dispute note. A replay re-pairs all 400 documents and reproduces every classification and every cent, with the ledger hash chain intact.

Where the data comes from, honestly. The FX component is real: 64 daily observations of the ECB's US dollar/Euro reference rate for Q4 2025, series EXR.D.USD.EUR.SP00.A, reproduced with source acknowledgement as the ECB requires. The two ledgers are synthetic, generated by a fixed-seed script (gen_data.py) so a third party can re-run the seed and obtain byte-identical data, identical classifications and identical hashes. The 94.0% match rate is therefore a property of the seeded faults, and it is illustrative. The mechanism is what the demo proves.

The honest limits. This is two entities and one currency pair; real groups run dozens of entities, netting hubs and multiple GAAPs. The classification rules are simplified: no partial deliveries, no intercompany profit in inventory, no disputed pricing logic. Transfer-pricing implications are flagged on the open items, and they are analysed no further than that. And the workbench cannot make two controllers agree; it can only make their disagreement precise and impossible to lose. The month-end argument still happens. It just happens once, over cited line pairs, instead of every month over screenshots.

AI Control Ledger · entryintegrity verified · replay 400/400 to the cent
seq        : 5
action     : classify
subject    : IC-2025-4051
agent      : anti-plug-engine v1          human : none
class      : RATE        eur_at_close : 567,474.08      age : 55 d
owner      : L. Verhoeven
prev_hash  : 52fbf2b5
hash       : fe901744    ← = fnv1a(prev_hash + canonical(entry))
Threat model: append-only. Reclassify this entry to ELIMINATED without re-hashing, the quiet edit a suspense plug relies on, and Verify breaks the chain at exactly #5. The Tamper button on the live app demonstrates it.

Why the button demands a name.

Every close I have seen has a drawer where the differences go, and the drawer is always anonymous. So when I built the intercompany step, I spent the automation budget where it belongs, on the deterministic 94%: pairing, recomputing the FX from the published ECB rate, classifying by rule, replaying to the cent. That part should never have been manual. But the design decision I care about is the residual 6%. The tempting move is to let software make the leftover disappear, and that is the restatement machine again with better tooling. So the plug is allowed to exist, because deadlines are real, and it is required to carry a human name and an age on the face of the dashboard. The model is confined to drafting the dispute note from cited line pairs, and where the evidence runs out, the note says so instead of guessing. If a tool like this feels uncomfortable, that is the discomfort of a difference you can no longer lose.

Press the button yourself.

The pairing run, the ECB recompute, the residual register with its six owners, the dispute-note drafts, the named-approver plug gate, the suspense anti-pattern for contrast, and the tamper-and-replay ledger all run as a working, offline application.

Open the live workbench →Runs entirely in your browser: no network, no data leaves the page. Synthetic ledgers, real ECB reference rates.