Ledgira Manual, as shipped in the download
Ledgira — User Manual
Bank and credit-card statements turned into transactions you can reconcile and export. Version 3.1.4 [src: app/controllers/api.php:20].
About this manual
Every statement here was written by reading Ledgira's own source, and each one carries the file it came from in a bracketed src marker. Paths are relative to the folder this docs directory sits in. If a sentence disagrees with the application, the application is right and this manual has a bug — the markers exist so you can check rather than take our word for it.
Nothing is described that the code does not do. Where something is deliberately limited, that is said plainly rather than left out.
What Ledgira is
A statement goes in as a PDF or an image. A vision model reads it. What comes out is a canonical set of transactions with signed integer-cent amounts, arithmetic a bookkeeper can check, and an export your accounting tool will take [src: app/src/Statement.php:3].
The model's job is reading the paper. Everything after that — the signing, the reconciliation, the running-balance check and the exports — is deterministic and happens with no network call [src: app/src/Statement.php:9].
Money and date parsing are reused verbatim from the same tested engine the rest of the catalogue uses; only the statement-specific work lives here [src: app/src/Statement.php:12].
The sign convention — the one rule everything builds on
A credit is positive. A debit is negative [src: app/src/Statement.php:22].
For a current or savings account that reads the way you would expect: a deposit or interest payment increases the balance and is positive; a withdrawal, card purchase or fee decreases it and is negative [src: app/src/Statement.php:25].
For a credit card, the same rule is applied to what you owe. A purchase, interest charge or fee increases the balance owed and is therefore positive; a payment or refund decreases it and is negative [src: app/src/Statement.php:28].
That is deliberate rather than a quirk. It is the standard double-entry treatment of a liability account, and it is what makes one reconciliation identity hold for both kinds of statement [src: app/src/Statement.php:30]:
opening_balance + Σ(signed amounts) == closing_balance
Direction is derived from the sign, not stored alongside it [src: app/src/Statement.php:89]. The two cannot disagree because there is only one of them.
Reading what the model returned
Direction tokens are normalised from whatever a model emits — debit, dr, d, withdrawal, money out, paid out, w/d, -, and the credit equivalents cr, deposit, money in, paid in, + [src: app/src/Statement.php:57], with a prefix fallback so "debit transaction" and "credit adj." also land correctly [src: app/src/Statement.php:64].
Amounts resolve to signed cents. An explicit direction wins; where there is none, the sign the amount was printed with is trusted — so (45.00) and -45.00 are debits and 45.00 is a credit [src: app/src/Statement.php:74]. A value with no parseable number at all returns nothing rather than zero [src: app/src/Statement.php:76].
Both statement layouts are handled: a single amount column with a direction, and separate debit and credit columns [src: app/src/Statement.php:118].
Account numbers are reduced to their last four digits [src: app/src/Statement.php:106], so a full account or card number is not carried around.
Reconciliation
Ledgira adds up the signed amounts, adds them to the opening balance, and compares the result with the stated closing balance [src: app/src/Statement.php:184].
Three outcomes, and the third is the one that matters:
| Status | Meaning |
|---|---|
reconciled | The arithmetic lands on the closing balance, within tolerance |
discrepant | It does not, and you are told by exactly how much |
incomplete | An opening or closing balance is missing, so no claim is made |
It never says "reconciled" without both balances. Without them the status is incomplete and the comparison simply is not attempted [src: app/src/Statement.php:195].
The delta is reported as computed minus stated, and the source spells out how to read the sign: a positive delta means the transactions add up to more than the closing balance says [src: app/src/Statement.php:182].
The running-balance check
Separately, Ledgira verifies the statement's own running-balance column against the transactions [src: app/src/Statement.php:225], flagging each inconsistent row with what it expected, what the statement stated, and the difference.
It works two ways and picks automatically. With an opening balance it checks absolutely — each running figure should equal the opening plus everything up to that row. Without one it checks the deltas — each running figure should differ from the previous by exactly that row's amount [src: app/src/Statement.php:219].
Rows with no stated running balance are skipped, because they carry no claim to verify [src: app/src/Statement.php:221]. Nothing is invented to fill a gap.
This is the check that finds a single mis-read digit. The totals can reconcile while one row is wrong and another is wrong by the opposite amount; the running column will not.
Status
Four statuses [src: app/src/Statement.php:43], moved by a pure transition rule [src: app/src/Statement.php:270]. A fresh extraction always lands on extracted from whatever state it was in; review is only meaningful once extracted; an unknown event changes nothing.
Exporting
CSV to RFC 4180, QIF for both bank and credit-card account types, and JSON [src: app/src/Statement.php:17]. The QIF writer emits the account-type header your accounting package expects [src: app/src/Statement.php:345].
Users and roles
Three roles [src: app/src/Perms.php:20]. The source names the asset it is protecting, and it is not the data:
the LLM API key. Extraction spends the buyer's own AI credits, so whoever holds the key holds the spend; and whoever can delete a statement can remove the evidence a set of books was built from. Reviewing and categorising, by contrast, is the daily bookkeeping job [src: app/src/Perms.php:5].
This table was produced by executing the product's own permission function against every permission the routes ask for.
| Permission | viewer | member | admin |
|---|---|---|---|
dashboard.view | yes | yes | yes |
statements.view | yes | yes | yes |
transactions.view | yes | yes | yes |
exports.run | yes | yes | yes |
transactions.edit | — | yes | yes |
rules.manage | — | yes | yes |
statements.upload | — | yes | yes |
extraction.run | — | — | yes only |
statements.delete | — | — | yes |
settings.view / settings.manage | — | — | yes |
team.view / team.manage | — | — | yes |
audit.view | — | — | yes |
Note the gap between uploading and extracting. A bookkeeper can upload a statement, but only an admin can run the extraction that spends AI credits [src: app/controllers/app.php:567]. That separation is the whole point of the role map here.
A viewer can run exports — that is deliberate, since exporting is how an accountant gets the data out without being able to change it.
Grants below admin are enumerated rather than wildcarded [src: app/src/Perms.php:14].
Backups are for an administrator only
Both backup downloads require the settings permission, which only an administrator holds [src: app/controllers/api.php:209], [src: app/controllers/api.php:221]. Until 3.1.4 they were gated by sign-in alone, so a viewer could download the whole database; a viewer or member now gets a 403, and the Backups card on the Security page is shown only to administrators.
A backup contains every table with only credential columns redacted, so every transaction, the team roster and the audit trail come out in full — which is why it is an administrator's download.
The API and agent access
Six tools are exposed to an agent, of which one writes [src: app/controllers/v3.php:22]: listing statements, statement detail, listing transactions, category totals and export rows on the read side; updating a transaction on the write side.
The instructions given to an agent state two things worth repeating:
Direction is derived from the sign and the two cannot be set independently [src: app/controllers/v3.php:19].
Extraction is not available to an agent at all — it spends the account owner's AI credits and stays a deliberate human action [src: app/controllers/v3.php:20].
What Ledgira does not do
It does not let an agent spend your money [src: app/controllers/v3.php:20], and it does not let a bookkeeper do it either [src: app/controllers/app.php:567].
It does not connect to your bank. It reads statements you already have.
It does not claim a reconciliation it cannot prove. Missing an opening or closing balance produces incomplete, not a pass [src: app/src/Statement.php:195].
It does not invent a running balance for a row that does not state one [src: app/src/Statement.php:221].
It does not do your bookkeeping. It produces canonical, checked, exportable transactions; what they mean in your accounts is yours.