A trial-balance line classifier for the LodgeiT chart of accounts. Send ledger
lines with an entity type and a section; get back an SBRM code with a status of
accepted_fact or draft_fact. Rules run first, the model second — and a
draft is the engine refusing to guess.
Unlike the deterministic calculators, Fano has a model in the path. It is not there to be trusted blindly: a small language-free classifier proposes a code, a layer of hand-authored LodgeiT word rules and structural firewalls check that proposal against the section and entity you declared, and anything the two do not agree on is returned as a draft for a human to confirm in one click — never silently accepted. The classifier is diagnostic; the rules and the refusals are the contract.
https://fano-engine-79859053141.australia-southeast1.run.app
POST /ingest/trial_balance
GET /v1/info
GET /openapi.json
Post a whole trial balance in one call: an entity_structure for the payload and a
list of lines. Each line carries its description, the source_topology
(the ledger section you are filing it under), and an upstream prediction
(predicted_code, confidence) plus an amount. The declared
entity and section are inputs, not decoration — the model is masked to leaves
within the section you declared, and the firewall checks the returned code against that section and
entity. GET /v1/info returns the version, the access mode, the limits, and the reason
codes; GET /openapi.json is the full schema.
company, trust, partnership, sole_trader, super_fund. Declared once for the whole payload.current_assets, non_current_assets, current_liabilities, non_current_liabilities, equity, revenue, expenses. The model is masked to leaves within it.sbrm_<n> (use sbrm_0000 if you have none). Echoed back as operator_hint_predicted_code; it does not steer the classification.operator_hint_confidence.One sole-trader line that accepts, one that drafts — both from a single balanced payload. These are live responses from the service above.
curl -s -X POST \
"https://fano-engine-79859053141.australia-southeast1.run.app/ingest/trial_balance" \
-H "Content-Type: application/json" \
-d '{
"entity_structure": "sole_trader",
"lines": [
{ "description": "Business Cheque Account", "predicted_code": "sbrm_0000",
"source_topology": "current_assets", "confidence": 0.5, "amount": 5000.0 },
{ "description": "Owner Capital", "predicted_code": "sbrm_3100",
"source_topology": "equity", "confidence": 0.5, "amount": -5000.0 }
]
}'
{
"status": "success",
"equilibrium_valid": true,
"results": [
{
"description": "Business Cheque Account",
"predicted_code": "sbrm_1137",
"confidence": 1.0,
"cascade_topology": "current_assets",
"operator_hint_predicted_code": "sbrm_0000",
"operator_hint_source_topology": "current_assets",
"operator_hint_confidence": 0.5,
"fano_status": "accepted_fact",
"quarantine_reason": "keyword_rule:sbrm_1137"
},
{
"description": "Owner Capital",
"predicted_code": "sbrm_3130",
"confidence": "0.97",
"cascade_topology": "equity",
"operator_hint_predicted_code": "sbrm_3100",
"operator_hint_source_topology": "equity",
"operator_hint_confidence": 0.5,
"fano_status": "draft_fact",
"quarantine_reason": "rule_model_disagreement (rule=sbrm_3130, model=sbrm_3121)"
}
]
}
Reading it back: Business Cheque Account is an
accepted_fact — a LodgeiT word rule owned the line and returned
sbrm_1137 (Cash > Bank Accounts), reason
keyword_rule:sbrm_1137; you can post it. Owner Capital is a
draft_fact — the rule leaf (sbrm_3130, Sole Trader – Capital Introduced)
and the model leaf (sbrm_3121) disagree, so the engine shows the rule candidate, names
the disagreement in quarantine_reason, and hands the line to an operator rather than
guess between two plausible equity leaves. Both outcomes are correct behaviour; the draft is the
point.
Every line carries a fano_status of accepted_fact or
draft_fact, and a quarantine_reason that names why. The reasons are a
fixed set — an agent can branch on the prefix:
draft_fact with both candidates shown. Drafts are contractual — treat them as review work, not as failures, and never post a draft unreviewed.Open access — no key, no signup — bounded by request guards rather than authentication:
| Guard | Value | On breach |
|---|---|---|
| Lines per request | 500 | 413 — payload too large; split the trial balance. |
| Body size | 512 KB | 413 — payload too large. |
| Rate | 60 / minute / IP | 429 — back off and retry after the window. |
| Malformed request | — | 422 — the body names the field. Do not retry unchanged. |
| Unbalanced payload | net ≠ 0 | 400 — the lines do not net to zero. |
The service is capped at three instances, so sustained heavy load will queue. A 413
means the request was too big to accept; a 429 means you were too fast. Both are the
engine holding a boundary, not a fault on your side.
An Apache-2.0 kit with the pinned OpenAPI snapshot, the SBRM lexicon, canonical fixtures, and CI that fails when the live contract drifts from the snapshot: