Financial Analysis
The balance method, and why every CSV parser lied.
The CSV parsers broke. Not a little. Chase missed 42% of transactions on some statement periods. Columns shifted. Memo fields contained commas that broke field boundaries. When you discover that the foundation of your financial analysis is missing almost half the data, you have two choices: fix the parsers, or abandon the format entirely.
I abandoned it.
What replaced CSVs is a method built on the one artifact that every bank produces reliably: the monthly statement PDF. Years of Chase statements covering every entity in the business. Every dollar that entered or left, verified not by parsing individual transactions, but by reading the three numbers that the bank itself guarantees are correct: opening balance, total deposits, total withdrawals. The balance is the truth. Everything else is commentary.
Why CSVs Failed
CSV parsing seems simple until you realize the bank changes formats without notice. The parser that worked in January breaks in March because a new column appeared. Here is what actually happened:
CHASE BUSINESS CSV — FAILURE MODES
─────────────────────────────────────────────────────────────────
Problem 1: Column shifts
Columns shifted between statement periods. The parser
built for January's format silently misread March's.
No error thrown. No warning. Just wrong numbers.
Problem 2: Comma-in-field corruption
Memo fields contained commas that broke field
boundaries. Parser read "DEPOSIT, BUSINESS"
as two fields instead of one. Every transaction
with a comma in the description was either
split or dropped.
Problem 3: Format version changes
Date format changed between years. The parser
choked on the transition and silently dropped
records. You would not know unless you checked
the totals against the statement.
Result: Up to 42% of transactions missed in some
statement periods. Zero errors thrown.
The silent failure is the part that matters. The bank did not throw an error. It did not warn you. It parsed the CSV, returned results, and those results were missing transactions. You would not know unless you checked the totals against the statement. If you trusted the parser output and built a P&L from it, your expenses would be understated by hundreds of thousands of dollars. Your profit would look real. It was not.
The PDF Balance Method
Years of bank statement PDFs. Every month, every entity, January 2019 through December 2025. The method does not parse individual transactions at all. It extracts three numbers from each statement and verifies them against the printed closing balance.
import pdfplumber
import re
def extract_statement_balances(pdf_path: str) -> dict:
"""
Extract opening balance, total deposits, total withdrawals,
and closing balance from a bank statement PDF.
Uses pdfplumber for text extraction. Regex patterns match
the specific format each bank uses for summary sections.
Returns None for any field that cannot be reliably extracted.
"""
with pdfplumber.open(pdf_path) as pdf:
full_text = ""
for page in pdf.pages:
full_text += page.extract_text() or ""
# Chase statement summary patterns
patterns = {
"opening": r"Beginning Balance\s+\$?([\d,]+\.\d{2})",
"deposits": r"Deposits and Additions\s+\$?([\d,]+\.\d{2})",
"withdrawals": r"(?:Checks|Withdrawals|Electronic)\s.*?\$?([\d,]+\.\d{2})",
"closing": r"Ending Balance\s+\$?([\d,]+\.\d{2})",
}
result = {}
for field, pattern in patterns.items():
match = re.search(pattern, full_text)
if match:
result[field] = float(match.group(1).replace(",", ""))
else:
result[field] = None
return result
The verification step is what makes this work. Bank statements are legal documents. The printed balances are audited. The relationship between them is arithmetic:
def verify_statement(balances: dict) -> dict:
"""
Verify: opening + deposits - withdrawals = closing
If the computed closing matches the printed closing,
the extraction is correct. If it does not match,
the extraction failed and should not be trusted.
"""
if any(v is None for v in balances.values()):
return {"verified": False, "reason": "missing fields"}
computed = (
balances["opening"]
+ balances["deposits"]
- balances["withdrawals"]
)
printed = balances["closing"]
diff = abs(computed - printed)
# Allow $0.01 tolerance for rounding
if diff <= 0.01:
return {
"verified": True,
"computed_closing": computed,
"printed_closing": printed,
"difference": 0.00,
}
else:
return {
"verified": False,
"computed_closing": computed,
"printed_closing": printed,
"difference": round(diff, 2),
"reason": f"Off by ${diff:.2f} — extraction error",
}
If you can extract those three numbers reliably, you do not need to parse individual transactions at all. The balance IS the truth. The result: 100% of statements balance. Every one. Not because the code is perfect, but because the verification step catches every extraction error before it enters the dataset. A statement that does not balance gets flagged and manually reviewed. None required it.
The Chart of Accounts
The business runs through four entities and one bank. Not because four is the right number. Because real estate brokerage in multiple states requires a structure that routes commissions through the right licensed entity before they reach the operator. One bank account — Chase — handles all the actual money. The entities are the legal scaffolding around it.
THE CHART OF ACCOUNTS
─────────────────────────────────────────────────────────────────
ENTITY TYPE ROLE
─────────────────────────────────────────────────────────────────
Broker Services Marketing Corp Corporate entity. Handles
marketing spend, technology
costs, AI infrastructure.
Where the business lives.
Cloud Realty Chicago Brokerage Licensed brokerage for
Illinois placements. Holds
the broker license. Pays
the operator as licensee.
WalzKraft Chicago Brokerage Second licensed brokerage.
Same market, different
license arrangement.
Spirit Realty Out-of-State Licensed brokerage for
Brokerage placements outside Illinois.
Handles Texas, other states.
─────────────────────────────────────────────────────────────────
BANK: Chase (single business checking account)
The fund flow is what matters. When a placement closes out of state, the commission flows through the licensed brokerage in that state (Spirit Realty), which pays via ACH to the Chicago brokerage (Cloud Realty), which in turn pays the operator as a licensee through the marketing corporation (Broker Services). Three entities touched. One actual bank account received the money. If you do not understand the flow, you will see three transactions and think it is three separate payments. It is one payment moving through the legal structure.
When you search for a missing payment, you trace the flow. Which entity should have received it? Did the ACH clear? Did the intercompany transfer happen? The payment is not missing — it is stuck in one of the pipes.
Collections Automation
When a building owes commission, the balance method tells you the money is missing. But finding a missing payment is not the same as collecting it. Collections is a pipeline problem — decompose the debt into discrete tickets, age them, escalate automatically, and adjust recovery expectations as time passes.
COLLECTIONS TICKET LIFECYCLE
─────────────────────────────────────────────────────
STAGE 1: TICKET CREATION
Balance method detects: deposits < expected commission
→ Ticket created with: building ID, placement date,
expected amount, invoice reference
→ 4-tier verification runs automatically
→ If Tier 1 (bank records) confirms no deposit → active ticket
STAGE 2: AGING CLOCK
Each ticket carries an aging counter from creation date.
The aging clock drives every downstream decision:
0-30 days: "Normal" — building may be on standard pay cycle
→ Automated reminder via email
→ No escalation
30-60 days: "Overdue" — past any reasonable pay cycle
→ Second notice + phone follow-up trigger
→ Expected recovery adjusts downward
60-90 days: "At risk" — collection probability dropping fast
→ Escalation: direct contact with building manager
→ Revenue projection removes 65% of ticket value
90-180 days: "Critical" — most buildings that pay have paid by now
→ Final notice sequence
→ Revenue projection removes 82% of ticket value
180+ days: "Write-off candidate" — functionally non-collectible
→ Ticket moves to write-off review queue
→ Expected recovery: effectively zero
STAGE 3: RESOLUTION
Each ticket resolves to one of four states:
→ COLLECTED (bank deposit confirmed via Tier 1)
→ PARTIAL (amount received ≠ amount owed — investigate)
→ DISPUTED (building contests the placement or amount)
→ WRITTEN OFF (aged past recovery threshold)
The 4-tier verification from the previous section runs on every active ticket automatically. When a bank deposit appears that matches a ticket's expected amount, the ticket resolves. When a building disputes a placement, the ticket moves to the dispute queue where Tier 4 (contextual evidence — emails, CRM notes) becomes the deciding factor.
ESCALATION TRIGGERS (automated, not behavioral)
────────────────────────────────────────────────
The system does not rely on a human remembering to follow up.
Escalation is structural:
ticket.age > 30 AND ticket.status != COLLECTED
→ trigger: automated_reminder_email()
ticket.age > 60 AND ticket.status != COLLECTED
→ trigger: phone_followup_queue.add(ticket)
→ trigger: adjust_expected_recovery(ticket, 0.35)
ticket.age > 90 AND ticket.status != COLLECTED
→ trigger: escalation_contact(ticket.building_manager)
→ trigger: adjust_expected_recovery(ticket, 0.18)
ticket.age > 180 AND ticket.status != COLLECTED
→ trigger: writeoff_review_queue.add(ticket)
→ trigger: adjust_expected_recovery(ticket, 0.00)
The revenue projection updates in real time as tickets age.
The number on the balance sheet reflects what you will
actually collect, not what you are owed.
The Texas AR write-off section below shows this pipeline in action. When 27% of Texas receivables become non-collectible, that is not a surprise — it is the aging pipeline doing its job, adjusting expectations as tickets cross thresholds. The pipeline does not prevent non-payment. It prevents you from counting non-payment as an asset.
4-Tier Payment Verification
When a payment is disputed or missing, no single source tells you what happened. Four independent tiers, checked in order, build the complete picture.
TIER 1: DEFINITIVE SOURCES ────────────────────────── Bank records (Chase portal) → ACH timestamps, check numbers → Did the money actually move? → Search terms: entity name, Orig IDs, payment references CRM status = "Collected" or "Denied" → Ground truth from the operations layer → If CRM says denied, the deal is dead regardless of bank data AR Report → Accounts receivable aging, write-off status → How long has this been outstanding? TIER 2: STRONG EVIDENCE ──────────────────────── Invoice + Lease cross-reference → Invoice amount vs lease terms vs deposit → Is it the right amount? Do the numbers match? invoice_collected flag → Was the invoice marked as paid in the billing system? signed_lease + move_in date → Did the tenant actually move in? → A signed lease with no move-in = the deal fell through building_response → Did the building confirm the placement? TIER 3: SUPPORTING EVIDENCE ──────────────────────────── Accounting system → Revenue recognition, categorization, tax reporting → How was it recorded? Under what entity? verification_email → Email thread confirming placement and commission terms likelihood score → System-calculated probability of collection TIER 4: CONTEXTUAL EVIDENCE ──────────────────────────── Email threads, SMS conversations, CRM notes → What was promised vs what was delivered? dispute_raised flag → Is there an active dispute on this payment? data_sources provenance → Where did each piece of information originate?
Here is what this looks like in practice. A building owes $1,200 in commission for a placement. You run the four tiers:
PAYMENT INVESTIGATION: Building 4417, Unit 3R
──────────────────────────────────────────────
Tier 1 — Bank Records:
ACH deposit found: $1,200.00
Date: 2024-08-14
Reference: "BLDG 4417 COMM AUG"
Account: [ACCT] (Business Checking)
Status: CLEARED
Tier 2 — Invoice Cross-Reference:
Invoice #[INV-XXXX]: $1,200.00
Lease rent: $1,500/mo
Commission rate: 80% of first month
Expected: $1,200.00
Match: YES
Tier 3 — Accounting System:
Recorded amount: $1,100.00 ← DISCREPANCY
Category: "Commission Income"
Entity: [Operating Entity]
Tier 4 — Contextual Evidence:
Email thread (2024-07-28):
Building manager: "$100 discount
for late maintenance response"
CRM note: "Adjusted commission per
building agreement, -$100"
RESOLUTION:
Bank received $1,200. The accounting system recorded $1,100.
The $100 difference is a discount the building
applied for a maintenance issue. CRM note
confirms. The accounting system entry is correct (net amount).
Bank deposit was pre-adjustment.
No single source tells you this story.
Tier 1 says $1,200. Tier 2 says $1,200. Tier 3 says $1,100. If you stopped at Tier 1 or Tier 2, you would think The accounting system has a recording error. Tier 4 has the email that explains the $100 discount. Now you have the full story. The bank deposit was gross, the The accounting system entry was net, and the difference is documented in an email thread that no automated system would have found.
The Horizontal Truth Pattern
The 4-tier payment verification is a specific instance of a general pattern that applies across the entire business. When you cross-reference independent data sources, the consistent story across all of them is what you trust. Any single source can be wrong. The intersection of three or more sources rarely is.
def horizontal_verify(claim: str, sources: list[dict]) -> dict:
"""
Cross-reference a claim against independent data sources.
Each source is: {
"name": "bank_records",
"value": 1200.00,
"confidence": 0.95,
"timestamp": "2024-08-14"
}
Agreement threshold: 3+ sources must align.
If fewer than 3 agree, the claim is UNVERIFIED.
If sources contradict, return all versions
and flag for manual review.
"""
values = [s["value"] for s in sources]
unique_values = set(values)
if len(unique_values) == 1:
return {
"status": "VERIFIED",
"value": values[0],
"agreement": f"{len(sources)}/{len(sources)}",
"note": "All sources agree",
}
# Find majority value
from collections import Counter
counts = Counter(values)
majority_value, majority_count = counts.most_common(1)[0]
if majority_count >= 3:
outliers = [
s for s in sources
if s["value"] != majority_value
]
return {
"status": "VERIFIED_WITH_OUTLIERS",
"value": majority_value,
"agreement": f"{majority_count}/{len(sources)}",
"outliers": outliers,
"note": "Majority agree, investigate outliers",
}
return {
"status": "UNVERIFIED",
"values": dict(counts),
"note": "No majority — manual review required",
}
This pattern applies everywhere, not just payments:
DOMAIN SOURCES CROSSED THRESHOLD
──────────────────────────────────────────────────────────────────────────
Lead quality Twilio logs 3 of 4
CRM records
Conversation history
Placement outcome
Building value Voice call transcripts 3 of 4
ILS listing data
Commission actually paid
Agent notes from tours
Agent performance Messages sent 3 of 4
Responses received
Conversions
Revenue attributed
Tour verification CRM conversation notes 3 of 5
Voice AI call log
Google Calendar event
Building manager confirmation
Renter post-tour SMS
Payment status Bank records 3 of 4
Invoice/lease match
The accounting system Books entry
Email/SMS context
When you run a solo operation, every number has to survive cross-examination because there is no second pair of eyes to catch a mistake. The horizontal truth pattern is not a methodology. It is a survival mechanism.
Tour Verification: 5 Sources
The tour_schedule table is unreliable. Agents mark tours as completed that never happened. The motivation is simple: completed tours generate commission claims. A tour that never happened, marked "Completed" in the database, becomes a phantom placement that wastes collection effort on a building that owes nothing.
How bad is it? In January 2026, the tour_schedule table showed 390 completed tours at a single luxury building. The revenue table showed 8 placements at that building over its entire history. Only 8 placements ever at rents above $4,000. The tour_schedule table was inflated by a factor of 48.
TOUR VERIFICATION — 5 INDEPENDENT SOURCES
──────────────────────────────────────────
Source 1: CRM Conversation (textmessage table, 3.8M records)
→ Did the renter confirm the tour in text messages?
→ Look for: "yes I'll be there", "on my way",
"just left the tour", post-tour reactions
Source 2: Voice AI Call Log (openphone_log, 610K records)
→ Did the building confirm availability for the tour?
→ Voice AI calls buildings to verify units are still
available before sending a renter. If the call
never happened, the tour probably did not either.
Source 3: Google Calendar Event
→ Was a calendar event created with the tour details?
→ Calendar events are created by the scheduling system,
not by agents. Harder to fabricate.
Source 4: Building Manager Confirmation
→ Did the building manager acknowledge the tour?
→ Asana ticket update, email, or call log showing
the building expected the renter.
Source 5: Renter Post-Tour SMS
→ Did the renter text after the scheduled tour time?
→ "I liked the place" or "not what I expected" or
even "I couldn't find parking" — any post-tour
message is evidence the tour occurred.
VERIFICATION RULE:
3 of 5 confirm → Tour happened.
2 of 5 confirm → Probable. Flag for review.
1 of 5 confirm → Unlikely. Do not count.
0 of 5 confirm → Did not happen.
If ONLY the CRM/tour_schedule says it happened
and no other source confirms → it probably did not.
The five sources are deliberately independent. A renter confirming by text (Source 1) does not cause a Voice AI call (Source 2). A calendar event (Source 3) does not generate a building confirmation (Source 4). Each source is created by a different system, at a different time, by a different actor. Fabricating agreement across all five would require coordinated fraud across multiple systems. Fabricating one database field takes thirty seconds.
When the verification runs and finds zero confirming sources for a "Completed" tour, that record gets flagged. It does not get deleted — deleting data is a different kind of sin. It gets marked as unverified and excluded from commission calculations, revenue projections, and performance reporting.
Texas AR Write-Off
27% of Texas accounts receivable becomes non-collectible. More than a quarter of the money owed by Dallas and Houston buildings will never arrive.
TEXAS AR AGING ANALYSIS — COLLECTION PROBABILITY CURVE
──────────────────────────────────────────────────────
AGING BUCKET % OF TOTAL AR COLLECTION EXPECTED RECOVERY
PROBABILITY (as % of bucket)
──────────────────────────────────────────────────────────────────
0-30 days ~22% 85% 85% of bucket
30-60 days ~17% 60% 60% of bucket
60-90 days ~11% 35% 35% of bucket
90-180 days ~20% 18% 18% of bucket
180+ days ~30% ~0% functionally zero
──────────────────────────────────────────────────────────────────
Write-off rate: 27% of total Texas AR (historical)
Effective recovery on 180+ days: functionally zero
The collection probability curve shows that roughly a
third of outstanding AR sits in the 180+ day bucket
where recovery is effectively zero. After adjusting
total company-wide AR for the aging curve, realistic
collectible AR drops by roughly 35-40%.
After 90 days, the probability of collection drops below 20%. After 180 days, it is effectively zero. The building has either changed management, disputed the placement, or simply decided that ignoring invoices is cheaper than paying them. There is no legal infrastructure to pursue $1,200 commission claims across state lines. The cost of litigation exceeds the debt.
The pattern is geographic. Chicago buildings pay at a higher rate because the relationships are older and the placements are higher volume. A building that owes you for ten placements a year pays because they need you for the next ten. A Dallas building that owes you for two placements has no ongoing relationship to protect. They ghost.
This is why the financial analysis tracks AR aging in real time. Not as an accounting exercise. As a decision tool. If a Texas building hits 60 days overdue, the system flags it for escalation. If it hits 90, the system reduces the expected recovery in revenue projections. If it hits 180, the system writes it off and stops counting it as an asset. The number on the balance sheet reflects what you will actually collect, not what you are owed.
What Happens When the PDF Parser Breaks
The balance method is not immune to failure. The PDF parser has its own blind spots, and they are worse than the CSV problems because they are invisible.
KNOWN PDF PARSER FAILURES (verified Jan 2026)
─────────────────────────────────────────────
ATM & Debit Card transactions: 0% captured
→ pdfplumber cannot extract text from the ATM/debit
section of Chase statements. The formatting uses
a table layout that the parser reads as whitespace.
Electronic Withdrawals: 5-98% captured
→ Varies by statement period. Some months the parser
captures every electronic withdrawal. Other months
it captures almost none. No pattern found.
Estimated missing expenses: significant
→ Over 7 years, the transaction-level extraction
missed a substantial amount of expenses. This is
why the BALANCE method exists — it does not rely
on parsing individual transactions.
The critical insight: the balance method and the transaction parser are two different systems that answer two different questions. The balance method tells you how much money moved in aggregate. It is 100% accurate because it reads summary numbers that the bank has already calculated. The transaction parser tries to tell you WHERE the money went — individual vendors, individual payments. It is between 0% and 98% accurate depending on the transaction type and statement format.
For aggregate analysis, the balance method is all you need. Total deposits minus total withdrawals equals net cash movement. You do not need to parse every individual vendor payment. You need the three numbers the bank guarantees: opening balance, deposits, withdrawals. The rest is commentary.
For payment investigations — "Did we pay a specific partner?" — you need the transaction parser, and you have to accept that it might miss the payment entirely. That is why Tier 1 of the payment verification sends you to the bank portal directly, not to the parsed data. The parser is a convenience. The portal is the source of truth.
Every Claim Needs a Source
When you are the only person reviewing the numbers, discipline has to be structural, not behavioral. Every financial claim the system produces follows a mandatory format. Not because it is best practice. Because the wrong format nearly led to a false fraud accusation.
WRONG:
"Only $4,801.70 was paid."
RIGHT:
"Found $4,801.70 across 4 of 84 statements searched.
Full verification requires searching remaining 80.
Confidence: PARTIAL — 4.8% of statements reviewed.
Source files: stmt_2022_04.pdf, stmt_2022_05.pdf,
stmt_2022_09.pdf, stmt_2022_10.pdf"
The investigation that birthed this rule involved a vendor relationship — they provided a service, we owed them a percentage. The accountant claimed a certain amount had been paid. The initial search found payments across 4 statements and nearly concluded that the accountant was wrong, that a significant portion was missing.
Then the full 84-statement search ran. Seven payments. The total matched what the accountant claimed. The accountant was right. The initial search had only covered 4.8% of the data and happened to miss three payments.
[PARTNER] — VERIFIED PAYMENTS (all 84 statements searched) ────────────────────────────────────────────────────────── STATEMENT SEARCH TERM FORMAT [Month 1] Orig CO Name:[partner alias] [Month 2] Orig ID:[numeric reference] [Month 3] Melio ... Descr:[partner alias] [Month 4] Orig CO Name:[partner alias] [Month 5] Melio ... Descr:[partner alias] [Month 6] Orig CO Name:[partner alias] [Month 7] Orig CO Name:[partner alias] ────────────────────────────────────────────────────────── 7 payments found across 84 statements (4.8% initial sample missed 3) KEY LESSON: The initial 4-statement search used ONE regex pattern. The full search used TWO — because the payment processor changed reference formats mid-stream. The difference between "the accountant stole money" and "the accountant was right" was a regex pattern.
Three of the seven payments used a different reference format — the payment processor changed how it labeled the recipient mid-stream. The initial search only used one pattern. The full search used both. The difference between accusing the accountant of theft and confirming they were right was a regex pattern.
Every financial claim now includes: source files searched, scope of search, what was NOT searched, and confidence level. If the scope is not 100%, the claim says so. Explicitly.
What This Teaches About Operating Solo
The financial analysis infrastructure is not about accounting. It is about trust.
When you are the only person, every number has to be verified because there is no one to catch your mistakes. The horizontal truth pattern exists because I got burned trusting single sources. The PDF balance method exists because I got burned trusting parsers. The 4-tier payment verification exists because I got burned trusting bank records alone. The mandatory claim format exists because I almost accused an accountant of fraud based on a 4.8% sample.
Every piece of this system is scar tissue from something that went wrong.
The lesson is structural, not behavioral. When you operate solo, discipline cannot be a personality trait — it has to be a system property. The verification runs whether you remember or not. The aging clock ticks whether you check or not. The cross-reference catches discrepancies whether you are looking or not. The system does not trust any single source, including the operator. That is what makes it work.
The financial analysis system watches all of this. Not with dashboards or charts. With years of bank statement PDFs, read one at a time, balanced to the penny, cross-referenced against every other source that has an opinion about where the money went. That is the method. It is slow. It is tedious. It is correct.
Source: Bank statement PDFs (Chase), pdfplumber extraction, accounting system, CRM records. Verified January 2026. Balance method: 100% of statements verified.