How to Validate Accounting Data Before an AI Assistant Triggers a Write Operation

How to Validate Accounting Data Before an AI Assistant Triggers a Write Operation

October 31, 2026
9 min read
0 views
0

An AI assistant should propose an accounting change, not decide whether its own proposal is safe to execute. Before a write reaches the ledger, deterministic code should validate the request, verify it against authoritative records, and bind any required human approval to the exact operation being submitted.

A balanced journal entry is a useful starting point. It is not proof that the right business, accounts, period, or supporting document has been selected. A valid-looking entry can still put the wrong transaction in the wrong books.

The practical engineering problem is therefore larger than checking JSON. It is building a boundary between generated suggestions and authorized financial changes.

Key Takeaways

  • Treat model-generated tool arguments as untrusted input, even when they match a published schema.

  • Validate structure, monetary representation, accounting rules, and business context separately.

  • Bind approval to an immutable request representation; edits require a new review.

  • Enforce authorization, period locks, and duplicate protection on the server, not only in the assistant.

  • Test rejection paths and ambiguous outcomes before enabling live writes.

Start With a Small, Explicit Write Contract

An assistant asked to record an expense might generate a journal-entry proposal containing a business identifier, posting date, currency, source-document reference, and debit and credit lines. Each field needs a defined meaning.

Avoid accepting a free-text instruction such as “book this receipt appropriately” at the final write boundary. Interpretation belongs upstream. The write service should receive structured values it can inspect and reject.

The MCP tools specification describes tools with input schemas and recommends input validation, access controls, and human oversight. A schema constrains the shape of a call; it does not establish that an accounting decision is correct.

For an accounting-specific example, developers evaluating LedgerMCP’s accounting MCP server should inspect the actual tool schemas and write behavior before building an approval layer. The example below is an independent teaching format, not that service’s API or a tested integration with it.

Separate Four Kinds of Validation

1. Structural checks

Require known fields and reject unsupported ones. Validate dates as calendar dates, not just strings that resemble dates. Check identifier types before looking up their values. Bound the number of lines and the size of incoming requests to limit accidental or abusive oversized submissions.

Use a fixed representation for money. This example uses positive integer cents for a demonstration ledger that supports only USD. It deliberately does not implement currencies with different minor-unit rules, foreign-exchange accounting, or fractional-cent calculations. Python booleans need explicit rejection because they are a subtype of integers.

2. Accounting checks

For the simplified contract below, each line must have a valid side, an allowed account, and a positive amount. Total debits must equal total credits. The supported accounts and their posting eligibility must come from the selected business, not a global list or the model’s memory.

Balanced entries remain insufficient. An expense incorrectly posted to equipment can balance perfectly. Source evidence, accounting policy, and qualified review still determine classification.

3. Business-context checks

Verify that the authenticated user may write to the selected business, the posting period is open, and the source document belongs to that business. Read these facts from authoritative systems. Do not trust a supplied field that says the user is authorized or the period is unlocked.

Also define what happens when the source changes. If an invoice is corrected after review, its former approval should not authorize a different amount. Where possible, include a source revision or content hash in the approved proposal and verify it during execution.

4. Execution checks

The final service must defend against concurrent requests, stale approvals, and retries. Client-side validation is helpful feedback, not an enforcement boundary. The ledger must independently reject invalid or unauthorized writes even if a caller bypasses the assistant entirely.

A Runnable Python Validation Example

Save the following as validate_entry.py and run python3 validate_entry.py. It uses only Python’s standard library and makes no network requests or real accounting changes.

import copy
import hashlib
import json
import re
from datetime import date
def validate(entry, context):
if type(entry) is not dict:
raise ValueError("Entry must be an object")
fields = {"business_id", "date", "currency", "source_id", "lines"}
if set(entry) != fields:
raise ValueError("Unexpected or missing entry fields")
if entry["business_id"] != context["business_id"]:
raise ValueError("Wrong business")
if not context["can_write"]:
raise ValueError("Write permission required")
value = entry["date"]
if type(value) is not str or not re.fullmatch(r"\d{4}-\d{2}-\d{2}", value):
raise ValueError("Date must be YYYY-MM-DD")
posted = date.fromisoformat(value)
if not context["open_from"] <= posted <= context["open_to"]:
raise ValueError("Posting date outside permitted period")
if entry["currency"] != "USD":
raise ValueError("Demo supports USD only")
source = entry["source_id"]
if type(source) is not str or source not in context["sources"]:
raise ValueError("Unknown source document")
lines = entry["lines"]
if type(lines) is not list or not 2 <= len(lines) <= 100:
raise ValueError("Require 2 to 100 lines")
totals = {"debit": 0, "credit": 0}
for line in lines:
if type(line) is not dict or set(line) != {"account_id", "side", "cents"}:
raise ValueError("Invalid line fields")
account, side, cents = line["account_id"], line["side"], line["cents"]
if type(account) is not str or account not in context["accounts"]:
raise ValueError("Account not available for posting")
if type(side) is not str or side not in totals:
raise ValueError("Invalid side")
if type(cents) is not int or not 0 < cents <= 100_000_000:
raise ValueError("Amount must be positive integer cents within demo limit")
totals[side] += cents
if totals["debit"] != totals["credit"]:
raise ValueError("Entry does not balance")
encoded = json.dumps(entry, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
context = {
"business_id": "demo_business",
"can_write": True,
"open_from": date(2026, 10, 1),
"open_to": date(2026, 10, 31),
"sources": {"receipt_001"},
"accounts": {"office_expense", "bank"},
}
entry = {
"business_id": "demo_business",
"date": "2026-10-15",
"currency": "USD",
"source_id": "receipt_001",
"lines": [
{"account_id": "office_expense", "side": "debit", "cents": 12500},
{"account_id": "bank", "side": "credit", "cents": 12500},
],
}
reviewed_hash = validate(entry, context)
assert len(reviewed_hash) == 64
bad_cases = []
bad = copy.deepcopy(entry)
bad["lines"][1]["cents"] = 12499
bad_cases.append(bad)
bad = copy.deepcopy(entry)
bad["lines"][0]["cents"] = True
bad_cases.append(bad)
bad = copy.deepcopy(entry)
bad["business_id"] = "other_business"
bad_cases.append(bad)
bad = copy.deepcopy(entry)
bad["date"] = "2026-09-30"
bad_cases.append(bad)
for bad in bad_cases:
try:
validate(bad, context)
except ValueError:
pass
else:
raise AssertionError("Invalid request was accepted")
changed = copy.deepcopy(entry)
for line in changed["lines"]:
line["cents"] = 13000
assert validate(changed, context) != reviewed_hash
print("PASS: valid entry accepted; four invalid cases rejected; edit changes hash")

The context is hard-coded solely to make the example reproducible. In production, it must be loaded by trusted server code for the authenticated principal and business. The validator checks source membership, not receipt authenticity or agreement between the receipt and proposed amount.

The hash identifies this Python serialization of the proposal. It is not a signature, permission grant, or universal cross-language canonicalization standard. An attacker who can alter both a proposal and an unprotected approval record defeats that design.

Bind Approval to the Reviewed Proposal

Present the reviewer with the business, posting date, source document, accounts, total, and reason for the entry. Store approval in a protected server-side record with the proposal hash, approver identity, expiration, and status.

At execution, reconstruct the proposal using the same serialization rules and compare its hash with the stored approval. Reject modifications rather than silently approving a nearby amount or a different account. Revalidate current permissions and period status because either may have changed since review.

Do not ask the model to generate its own approval token. Approval must come from the authorized person or a separately enforced policy. The model’s statement that “the user approved this” is not evidence.

Make Retries Safe Without Hiding Errors

Assign a stable idempotency key to the approved business operation. Persist the key, request hash, and result together with the accounting write in an atomic transaction where the architecture permits it. Reusing the same key with different content should produce a conflict, not a new posting.

If an external service owns the ledger, use its documented idempotency mechanism and an explicit recovery workflow. After a timeout, query the operation’s status before attempting another write. A missing response does not establish that nothing happened.

Likewise, consuming approval and writing the ledger cannot be two uncoordinated steps that allow simultaneous requests to spend the same authorization twice. Design the transaction boundary, uniqueness constraints, and recovery states together.

Test What Must Never Be Accepted

The included checks are a small demonstration, not a complete test suite. Add tests for unknown fields, missing fields, invalid dates, unsupported currencies, negative amounts, excessive line counts, unauthorized accounts, and unavailable source documents.

Then test the service around the validator: expired approvals, revoked access, closed periods, changed source revisions, repeated keys, and concurrent submissions. Verify that logs contain useful identifiers and outcomes without unnecessarily duplicating sensitive financial documents.

Finally, test a balanced but substantively wrong entry. It should reach an exception or human-review path, not be described as correct simply because the sums match. The boundary is successful when it knows both what it can prove and what it cannot.

Technical References

Frequently Asked Questions3 FAQs

Quick answers to common questions about this topic

No. Schemas check structure and selected constraints. They do not independently prove authorization, correct classification, source authenticity, or compliance with an organization’s accounting policies.

No. Equal debits and credits prevent one kind of inconsistency. Both sides can still use the wrong amount, business, date, or accounts.

Not as written. It is an isolated validator, without authentication, persistent approvals, source verification, idempotency storage, or a posting adapter. Those controls require implementation and testing before live use.

Was this article helpful?Vote to let the author know
0
Share this article

Loading comments...

Related Articles

How Businesses Can Benefit from Real-World Asset Tokenization

How Businesses Can Benefit from Real-World Asset Tokenization

Real-world asset tokenization uses blockchain technology to represent assets digitally, helping businesses explore new funding opportunities, improve asset management, automate transactions, and increase transparency. The article explains its key benefits, practical applications, and important legal and security considerations.

0 views
Read More
Read full article: How Businesses Can Benefit from Real-World Asset Tokenization
How to Develop a BEP20 Token for Your Blockchain Project

How to Develop a BEP20 Token for Your Blockchain Project

Blockchain tokens are used across a wide range of applications, including digital platforms, decentralized finance (DeFi), gaming ecosystems, and community-driven projects. For businesses planning to launch a token on BNB Smart Chain, the BEP20 standard provides a framework for creating fungible tokens that can interact with compatible applications and services.

17 views
Read More
Read full article: How to Develop a BEP20 Token for Your Blockchain Project
Jeff Geschwind: The ASCO’s Guiding Principles for the Responsible Use of AI

Jeff Geschwind: The ASCO’s Guiding Principles for the Responsible Use of AI

The American Society of Clinical Oncology (ASCO) has served as a world-leading professional organization for physician/scientists and other oncology professionals involved in the care of patients with cancer since 1964. The ASCO maintains a depth of resources and tracks the latest oncology news, in addition to advocating for important policies and initiatives that impact the field of oncology.

26 views
Read More
Read full article: Jeff Geschwind: The ASCO’s Guiding Principles for the Responsible Use of AI
Axi (formerly AxiTrader) alternatives: four brokers to compare

Axi (formerly AxiTrader) alternatives: four brokers to compare

Guide to compares four brokers against Axi using only the details each firm publishes on its own website, starting with the one that matches Axi's app, MetaTrader and copy trading set-up while adding markets Axi's international site doesn't list.

18 views
Read More
Read full article: Axi (formerly AxiTrader) alternatives: four brokers to compare