In a regulated bank, a journal is not just an accounting entry — it's a control. Under SOX, the person who prepares a journal cannot be the person who approves it, and a journal that posts without an approver of record is an audit finding. A generic GL will happily post any journal that balances. This overlay makes preparer ≠ approver a deterministic hard gate the ledger can never bypass — a self-approved journal is blocked from posting even though it balances — and every approval transition an immutable audit row. The period close inherits the same discipline: it cannot complete while a single journal in the period is still unapproved.

The problem — and the money

The buyer is the bank's controller or SOX-compliance lead who signs the 302/404 attestation. A material journal posted without segregation of duties — or a period closed with unapproved entries in it — is not a reconciliation nuisance, it's a control deficiency, with remediation cost and restatement exposure measured in millions. A vanilla GL enforces exactly one invariant: debits equal credits. That is necessary but nowhere near sufficient for a regulated close. The payoff here is a refusal: a perfectly balanced journal whose approved_by equals its prepared_by is blocked from posting — the books stay clean, and the attestation holds. In finserv, the deterministic gate is the feature.

This is the GL suite + the finserv overlay. The six base skills — core, journal-entry, period-close and the rest — are unchanged. The overlay only adds three columns, one audit table, and two deterministic gates layered over the balance gate and the close. Same bucket, one copy of the rows.

What finserv adds

A small, additive diff on top of the suite (skill: gl/finserv → the gl/overlays/finserv contract):

  • Segregation-of-duties columns — on gl/core journals (prepared_by, approved_by — the hard-gate input — and approval_state draft|pending|approved), added with ALTER TABLE … ADD COLUMN over the pg wire. A column-add, never a second copy.
  • One tablejournal_approvals (key approval_id): the immutable segregation-of-duties trail — one append-only row per approval transition (actor, action, prev/new state, evidence).
  • Two gate changes — journal-entry's balance gate (Σdebit = Σcredit) still runs, but a deterministic SoD hard gate layers over it: a journal may post only when approved_by IS NOT NULL AND approved_by <> prepared_by AND approval_state = 'approved'. And period-close gains a sign-off gate: a period cannot close while any journal in it is still unapproved.
  • Presetsrequire_dual_control = true; require_close_signoff = true; a self-approved journal and a dual-controlled one seeded so the gate is visible on day one.

Apply the diff — the ALTERs, the audit table, and the seed that drives the gate:

You

In the gl bucket, apply the finserv overlay: ALTER journals ADD prepared_by, approved_by, approval_state; create journal_approvals; then stage two balanced draft journals — J2 (prepared_by=alice, approved_by=alice, self-approved) and J3 (prepared_by=alice, approved_by=bob, approval_state approved).

DODIL MCP tools called
data_pgdata_table_createdata_table_upsert
Agent

Added prepared_by/approved_by/approval_state to journals (ALTER over pg — the master, not a copy; all three land nullable). Created journal_approvals. Staged J2 (self-approved: alice/alice) and J3 (dual-controlled: alice/bob, approved) as balanced drafts — the inputs the SoD gate reads.

Now the payoff — the SoD hard gate blocking a balanced self-approved journal, and the guarded posting path:

You

In gl, compute each draft journal's gate verdict: blocked when approved_by is null, or approved_by = prepared_by, or approval_state <> 'approved', else post_ok — carrying the per-journal imbalance. Then post only the ones the gate clears with a guarded UPDATE, and show which journal actually flipped to posted.

DODIL MCP tools called
data_sqldata_pg
Agent

J2 (alice/alice, imbalance 0.00) = blocked: preparer=approver (SoD) — balanced but self-approved, so it never posts. J3 (alice/bob, approved) = post_ok. The guarded UPDATE flipped only J3 to posted; J2 stayed draft. journal_approvals holds the transitions.

The balance gate still runs — a journal that doesn't balance is rejected exactly as in the base journal-entry skill. But balancing is no longer sufficient to post: a balanced self-approved journal is held. The hard gate is a deterministic SQL predicate, not a judgement call; that's the point.

And the close inherits the same discipline — a period cannot close while it holds an unapproved journal:

You

In gl, check whether period 2026-09 can close: count its journals still draft and unapproved (approved_by null, or approved_by = prepared_by, or approval_state <> 'approved'). Then void the unapproved ones and re-check.

DODIL MCP tools called
data_sqldata_pg
Agent

Period 2026-09 had 2 unapproved journals (J2 self-approved + J4 no approver) -> 'close blocked: unapproved journals'. After voiding both (status='rejected'), 0 remain -> close_ok, and the trial balance stayed exactly 0.00.

Scaffold it — the one-shot

With the DODIL MCP connected, one prompt composes the suite and this overlay:

Scaffold a GL for my bank — the GL suite plus the finserv overlay.
 
Base: the full gl suite (chart of accounts + account-hierarchy graph + journal-entry / period-close /
financial-reporting / subledger-reconciliation / multi-currency) on one bucket, money DECIMAL(18,2),
idempotent ON CONFLICT posting.
 
Then apply the finserv overlay on the SAME bucket:
1. ALTER journals ADD prepared_by, approved_by, approval_state (pg wire — the master, not a copy).
2. Create journal_approvals (key approval_id) — an append-only row per approval transition.
3. Layer a DETERMINISTIC segregation-of-duties hard gate over the balance gate: a journal may post ONLY
   when approved_by is set, differs from prepared_by, and approval_state='approved' — a self-approved
   journal is blocked even when it balances.
4. Add a close sign-off gate: a period cannot close while any journal in it is unapproved.
Seed the demo above and show the hard gate holding a balanced self-approved journal.

Verify

Every result below was live-validated on 2026-09-03 (org IHDIASH, throwaway bucket glovlfinserv0903, torn down after):

# columns land (nullable, over the pg wire)
dodil data pg -b "$BUCKET" "ALTER TABLE journals ADD COLUMN approved_by VARCHAR"   # + prepared_by, approval_state
# the hard gate holds a balanced self-approved journal: J2 (alice/alice) = blocked (SoD)
dodil data sql -b "$BUCKET" "SELECT journal_id, prepared_by, approved_by, approval_state FROM journals WHERE status='draft' ORDER BY journal_id"
# the guarded post flips only the dual-controlled one: J2 stays draft, J3 -> posted
dodil data sql -b "$BUCKET" "SELECT journal_id, status FROM journals WHERE journal_id IN (2,3) ORDER BY journal_id"
# the close sign-off gate + trial balance stays exactly 0.00
dodil data sql -b "$BUCKET" "SELECT CAST(SUM(balance) AS VARCHAR) AS trial_balance FROM ledger"   # -> "0.00"

The ALTER-adds, the journal_approvals trail, the segregation-of-duties hard gate blocking a balanced self-approved journal, the guarded posting path, and the close sign-off gate are all proven live — with the trial balance holding at exactly 0.00 throughout (ledger: Cash 1000000.00, AR 40000.00, Common Stock -1000000.00, Product Revenue -40000.00).

Connect your tools

Everything the overlay wrote lives in the one DataK3 bucket, reachable by your own stack — a GRC / audit tool reads journal_approvals and journals.approval_state over the Postgres wire; a controller's close dashboard runs the sign-off gate as a plain SQL query before locking the period. data connect gl prints the endpoints. Full, live-validated walkthrough: Connect your tools.

Composes

This page is not a fork of the GL — it is a composition:

  • Base: the six GL suite skills (gl/core, gl/journal-entry, gl/period-close, …) — the system of record, unchanged.
  • Overlay: gl/overlays/finserv — the additive diff above (the segregation-of-duties columns, the journal_approvals trail, the SoD hard gate over the balance gate, the close sign-off gate).

Read the base to learn the mechanics; this overlay is the small, industry-specific diff on top.