-
-
Proyección a 30 días con banda pesimista. La nómina del 15 sep queda descubierta por $37,784, detectado 2 días antes
-
Escalera de liquidez: 4 peldaños por costo real. Crédito hasta el final. Nómina y SAT bloqueados. Faltante: $37,784.
-
Adelantar una factura 9 días cierra el 100% de la brecha. Residual $0: no se necesita crédito.
-
Saldo y obligaciones en vivo vía Capital One Nessie; cobranza desde CFDI. Clasificado TIMING GAP, no déficit estructural.
-
Cada factura cruzada contra su depósito real. Plazo por cliente: 15, 38 y 45 días. Medido, no supuesto
-
Si la escalera no alcanza: crédito puente comparado por CAT, con perfil armado desde CFDI y Nessie.
-
Panorama diario: cobrado, por cobrar, días al incumplimiento y cuánto cierra cada peldaño.
Beel
Cash flow forecasting and intelligent collection engine for Mexican SMBs.
Inspiration
One of us has family running as medium distributors in Bajio, with 22 employees, payroll on the 15th and the 30th, no exceptions. They sell to hardware stores on net-30 terms, which in practice means the hardware store pays whenever they feel like it.
So every couple of weeks,there's this ritual: they open the bank app and they do the arithmetic in their head. They try to figure out if the money coming in will land before the money going out. The account then sends real financial information on the 5th of the next month, which is too late to be useful for anything.
When the math doesn't work out, their options are a corporate card at around 45% APR or calling the clients to beg. Nobody builds software for that moment. The fintech apps that they have installed are all trying to sell the card.
We wanted to build something that calls the clients first.
What it does
Beel takes two inputs: the CFDI XMLs exported from the SAT portal, and the bank account. No passwords, no scraping, no fake authentication screen.
From there, it reconciles the invoices against actual bank transactions. Sometimes the CFDI reference show up in the transaction description and the match is easy. Usually it doesn't, so we fall back to matching on the amount tolerance and a settlement date window. Whatever doesn't match becomes an open receivable or payable.
Then it learns how each customer actually pays.Not net-30 because that's what the invoice says. The median lag between the issue and the settlement for that specific customer across the history. Ferreteria Lopez pays in 4 days. Someone else takes 55. The forecast uses real numbers.
With that, it projects 30 days of cash and finds the first payment you can't cover. Payroll and taxes are marked as hard deadlines because you can't move them.
The part we cared about the most is what happens next. Before showing you any credit, Beel goes down a ladder: Can you collect an invoice a few days early from someone who reliably pays fast? can you move money from the reserve? can soft payables slide a week? Credit lines only appear if none of those closes the gap and when they're sorted by effective cost, not by whoever pays us the most.
And if the gap isn't a timing problem and if the business is structured to lose money, it won't show what loans at all. It shows a counterfactual instead: here's the month where the interest payments finish you off.
That's the whole idea, really. A timing gap and a structural gap look identical in a bank account and only one of them should be solved with borrowed money.
There's also stress lab with presets(weak month, cost shock, collections delay) so you can see which shock your business actually can't absorb.
cashₜ = cashₜ₋₁ + ∑ collectionsₜ - ∑ obligationsₜ
A breach is the first day where cash drops below your safety buffer on hard deadline obligations. Collections in that sum are already discounted by how reliably each customer pays, so a chronically late client never pops the curve up.
How we built it
Backend is Python on FastAPI. We wrote the CFDI 4.0 parser, the reconciliation logic, and the learned-terms module ourselves, each one runnable standalone so we could check them independently instead of debugging through the UI. Bank data comes from the Capital One Nessie API balance, deposits, purchases, withdrawals, recurring bills pulled on sync.
The engine is plain Python with no ML. Everything a business owner sees traces back to a specific invoice or a specific transaction. That was a deliberate constraint: if we can't explain the number, she won't act on it, and a recommendation nobody acts on is worthless.
batch = parse_dir(cfdi_directory) reconciled = reconcile(batch, deposits, purchases) terms = learn_terms(reconciled.matched) tuned = apply_terms(reconciled, terms)
tuned.expected_collections(HORIZON_DAYS) # {date: amount}, already risk-discounted tuned.open_receivables
Storage is Postgres on Tiger Cloud, and this part ended up taking most of the second day. CFDI documents carry RFCs and personal data under Mexico's LFPDPPP, and the whole product is one database holding several businesses' bank positions, so we treated tenant isolation as the actual engineering problem rather than a checkbox:
Row level security with FORCE on every table, plus a separate app_api database role so the API isn't the table owner Tenant scope set per transaction with SET LOCAL, so a pooled connection can't carry one request's tenant into the next current_tenant() returns NULL when unset, so a forgotten scope denies everything instead of exposing everything Raw CFDI XML encrypted with AES-256-GCM before storage, with the invoice UUID as additional authenticated data so a blob can't be moved to another row and still decrypt Argon2id for passwords, writes logged to an audit hypertable, and only the last four digits of an account number ever stored
We wrote down what each of those does not cover, too. The encryption key lives in the API process, so it protects a leaked backup and not an API compromise. The audit table records writes and not reads, because Postgres has no SELECT trigger. Saying that out loud is the difference between a control and theatre.
Front end is React 18 with TypeScript, Vite, Tailwind v4, Recharts for the forecast curve and health gauge, TanStack Query for data, Zustand for session.
We also seeded three complete mock scenarios CFDI batches with matching Nessie transactions which turned out to be the best decision we made. The forecast engine and the front end got built at the same time against a fixed contract instead of one person waiting on the other.
Challenges we ran into
Every row-level security policy we wrote was doing nothing, and nothing told us. Postgres doesn't apply the RLS to a table owner. Tiger Cloud gives us the tsdadmin, which owns everything we created, and the API also was used to connect us. The policies existed they validated. They filtered exactly zero rows. Cross-tenant reads just succeed with no error and no warning. We've only found it because we went looking .
Fixing it took two changes, and neither was sufficient alone, FORCE ROW and LEVEL SECURITY so the owner obeys its own policies and a separates the least privileged role so the API isn't the owner either. Then registration broke , because with the FORCE on, nothing can insert a tenant row before the tenant id is set. We solved that by pre-generating the id rather than adding a privileged function, which means there's no SECURITY DEFINER escape hatch anywhere in the schema.
That was the third bug of the same shape we hit. The other two were: A storage layout that put every tenant's file in one folder and a materialized aggregate that doesn't inherit RLS because a background worker builds it. All three looked correct, raised nothing and returned other tenant's data. That failure mode is now the first thing we check for.
CFDIhas distinctions between PUE and PPD that we got wrong. At the first, PUE invoices were already paid, counting them as receivables. Inflated the forecast with money that had already arrived.
Accomplishments that we're proud of
The learn payment terms mostly tell someone to call Ferreteria Lopez: they pay in 4 days. That covers the role payroll only, but it works if the 4 days are real. Without it, the whole ladder is guesswork. Finding the RLS bug would have shifted silently. It is the kind of thing that Enzafintec refused to pay for: the engine declining to show the credit to a structurally unprofitable business, easy to skip, and anybody would have no reason it was missing.
Lastly, the full path works end-to-end: the CFDI XML uploaded and encrypted, a bank data sync for a 30-day curve with one specific thing to do: how to do the other side.
What we learned
What surprised us most is that a small business is not necessarily unprofitable when it needs to borrow. Often, it has a calendar problem rather than an earnings problem. These situations require completely different responses—and sometimes no lending product at all. We learned to identify what distinguishes a temporary liquidity gap from a structural deficit.
We also learned what a security control that fails open looks like from the inside: almost identical to one that works. There may be nothing unusual in the logs, nothing failing in the tests, and SQL that appears correct. We only discovered the issue by signing in as one tenant and deliberately attempting to access another tenant’s data. Explainability is not just a feature in this product; it determines whether people will trust and adopt it. Nobody calls a customer to request an early payment simply because an application told them to. They need to understand which invoice matters, why it matters, and how that action changes their forecast. Above all, we learned that building a strong idea, defining a clear scope, and organizing the team well can matter even more than the programming itself. Planning may feel slow and take significant time at the beginning, but it makes development much easier. It reduces confusion, helps everyone work toward the same objective, and allows the final product to come together much faster.
What's next for Beel !
The portfolio view is the obvious next thing. The same engine running across the bank's entire SMB book gives early warning on the whole portfolio, and that's valuable to the bank's like Capital One rather than just to the business. The adapter layer is already shaped for it.
Afterthat directly SAT and verfication integration: invoices come in continuously instead of being manually uploaded,push alerts when a breach is detected instead of a warning for someone to open the app,proper key management instead of an environment variable. Rate limiting on auth, real partner credit lines with real cost and data behind the ranking.
Built With
- aes-256-gcm
- argon2id
- css
- fastapi
- nessieapi
- postgresql
- psycopg
- python
- react
- recharts
- tailwind
- tanstackquery
- tigerdb
- timescaledb
- typescipt
- vite
- zutstand
Log in or sign up for Devpost to join the conversation.