-
-
call or text, ask if you'll make payroll, and watch the answer get made. 02
-
Palletbay has 28,509 across 204 open invoices.
-
The owner asks, "Am I going to make payroll these next two weeks?" Span checks cash in and out, then asks the one thing it can't know.
-
The honest answer: $385 short. Our payment model scores every invoice, and Span suggests texting three reliable late payers.
-
Reminders go out with pay links. Customer agents, simulated from real payment history, negotiate plans within the owner's rules.
-
Customers pay through a link (simulated Visa sandbox): four payments, $709 collected.
-
The gap closes from −43. The forecast re-runs on every payment and keeps each revision in TimescaleDB.
-
What if Evergreen doesn't pay? Span re-forecasts without them ($238 short) and picks three other accounts to chase.
-
A one-page report for the accountant, built from the same events as the canvas. Print it or download the PDF.
-
Under the hood: a trained payment model (MAE 1.25 days, AUC 0.968) does the numbers, Gemini writes the words, one event stream feeds all.
Inspiration
What it does
How we built it
Challenges we ran into
Accomplishments that we're proud of
What we learned
What's next for Span
Span: About the Project
What inspired us
Every small-business owner ends up asking the same question late at night:
"Am I going to make payroll on Friday?"
The answer is already in their books, but it's spread across the bank balance, a pile of unpaid invoices, each customer's payment habits, and the bills coming due. Turning that into a yes or a no has usually meant an accountant they can't afford, and the answer arrives after Friday has passed.
Intuit's challenge asked for work that "used to be too slow or costly to run." For a 10-person warehouse, few things are slower or more costly than an honest cash forecast. We wanted the owner to call a phone number, ask out loud, and watch the answer get worked out. We didn't want a black-box "the AI says no." We wanted their own invoices scored one by one on screen, so they could see why.
We also didn't want Span to stop at bad news. If the answer is "no," the obvious next question is "so what do I do?", and Span should go do it.
How we built it
1. The books
We loaded Intuit's 100-company QuickBooks dataset into Postgres (TimescaleDB locally, Tiger Data in production):
| Table | Rows |
|---|---|
| Invoices | 84,451 |
| Customer payments | 143,616 |
| Bills | 39,563 |
| Payroll runs | 25,733 |
| Bank transactions | 234,610 |
From that we built a derived invoice_facts table that records, for each invoice, what was actually paid, how late it was, and whether it ended as paid, open, partial or written_off.
2. The fast lane: a payment model we trained
An LLM shouldn't be guessing when a customer will pay. Each open invoice \(i\) has an open balance \(a_i\) and a horizon of \(h\) days until payday. We trained gradient-boosted trees with scikit-learn's HistGradientBoostingClassifier on all 86 companies that issue invoices. The model estimates:
$$ p_i(h) = \Pr\big(\text{invoice } i \text{ is paid within } h \text{ days} \mid \text{customer history, age, amount, …}\big) $$
A second classifier estimates the chance an invoice is never paid, \(\Pr(\text{write-off}_i)\). Features include each customer's own track record: historical days late, share paid on time, and partial-payment rate.
We evaluated on a held-out 60-day window:
- AUC for "paid within 30 days": 0.968
- Mean absolute error on days late: 1.25 days, compared with 1.36 for a "this customer's average" baseline
- Write-off AUC: 0.795 (5-fold cross-validation, only 46 positive examples)
Scoring all 204 of Palletbay's open invoices takes under 100 ms.
3. The forecast
Let \(B\) be today's bank balance, \(\mathcal{B}\) the unpaid bills due by the target date, \(D\) any owner draw due, and \(P\) the payroll due by that date. Cash on payday before payroll is:
$$ C = B \;+\; \sum_{i \in \text{open}} a_i \, p_i(h) \;-\; \sum_{b \in \mathcal{B}} \text{amount}_b \;-\; D $$
The answer is the gap:
$$ \text{gap} = \max(0,\; P - C) $$
Expected value alone hides risk, so we also compute a pessimistic case that only counts invoices the model considers reliable:
$$ C_{\text{pess}} = B + \sum_{i \,:\, p_i(h) \ge 0.7} a_i \, p_i(h) - \sum_{b \in \mathcal{B}} \text{amount}_b - D $$
That's what lets Span give answers like "Barely, and $X short if the slow payers slip." For our demo company, Palletbay Storage & Warehousing:
$$ C = \$12{,}890.06, \qquad P = \$13{,}275.00, \qquad \text{gap} = \$384.94 $$
So the honest answer is: "No. About $385 short."
4. The slow lane: Gemini
Gemini acts as the planner. It routes the intent, calls tools (forecast_cash, rank_chase, defer_bill, draft_reminder, …), picks which levers close the gap, writes the spoken answer, and writes the narrative for the final report. Every Gemini output has a deterministic template fallback, so the product works end to end with no API key.
5. Events first
This architectural choice paid off more than any other. Every visible step the agent takes is an event written to Postgres: node.create, node.update, decision, speech, report.ready. The events are fanned out over server-sent events (SSE). Three things read that one stream:
- The live canvas (Vite + React 19, designed in Paper), which draws the reasoning as a workflow.
- Deterministic replay:
?run=<id>replays any run with its original timing, andnpm run recordturns a replay into the demo video, stills and thumbnail. - The report, a one-page JSON/PDF rebuilt from the same events.
Because all three read the same events, the voice, the screen and the PDF can't disagree.
6. Closing the loop
When the owner says "yes," Span:
- drafts reminders grounded in each invoice and the customer's history, and sends them through Photon
- classifies replies as
will_pay,dispute,wants_planorother - negotiates with Fetch.ai uAgents: customer agents, simulated from each customer's real payment history, go back and forth with Span's agent for at most four rounds, under rules the owner sets (installments only if the customer pays at least 80% of invoices, discounts capped at 5%, disputes escalate to the owner)
- takes payment through a pay link on the Visa sandbox or Solana devnet USDC
- recomputes \(C\) on every payment, so the gap on screen shrinks in real time
In the demo, Evergreen's agent agrees to two installments of $578.10. The first lands on the Visa sandbox and the cushion turns green.
Sentry traces every request and every model call. Devin built the pay page, the report generator, the demo recorder and the report page in parallel while we worked on the core loop.
Challenges we faced
The forecast was wrong, and it looked right
Our first bill query filtered on
$$ \text{as_of} \le \text{due_date} \le \text{target} $$
which quietly dropped every overdue unpaid bill. Palletbay had four overdue bills totaling $1,598.55. Leaving them out pushed \(C\) above \(P\):
$$ \text{gap}_{\text{buggy}} = \max(0,\; 384.94 - 1{,}598.55) = 0 $$
So Span said "you're fine" when the true answer was "you're $385 short." The output looked completely plausible. We caught it on a Sentry trace of the payroll run by comparing the bill total against the books. The fix includes any unpaid bill issued by today and due on or before the target date. A regression test now asserts the reconciled total of $10,866.86.
The dataset fought back
Payroll cadence (weekly, biweekly, irregular), partial payments and write-offs hidden in journal entries (bad_debt, client_default) each broke a naive rollup. We only trusted a number once it reconciled against invoice-level facts.
Real time vs. a spoken answer
The owner is on the phone and can't wait for 204 invoices to be scored and explained. Instead of computing everything and then responding, the run streams events as it works: the spoken answer goes out as soon as the forecast exists, while the canvas keeps filling in the per-invoice scores behind it. The math takes milliseconds. The pacing is there so a human can follow it.
Negotiating with agents that don't exist yet
Real customers don't have AI agents today, so the other side of the negotiation has to be simulated, and we didn't want that to turn into theater. Each customer agent's behavior comes from that customer's real payment history in the dataset. Every round is an event so it can be audited afterward, and every simulated channel (texts, replies, card rail) is labeled as simulated in the UI. We never fake a delivery.
Honesty under a deadline
The easiest demo is a scripted one that always says "yes, you're fine." Ours has to say "no" on stage, and that only works if every number traces back to dataset rows. Holding to that, with no modified rows, reconciled totals and labeled simulations, cost us time. It's also why we trust the demo.
What we learned
- Showing the reasoning changes the product. Owners don't trust "the model says no." They trust seeing their own invoices scored, with the customers they already worry about flagged in red.
- Use a trained model for numbers and an LLM for language. The payment model gives calibrated probabilities with measurable error. Gemini is excellent at planning, wording and explaining, and it shouldn't be the thing deciding whether Evergreen pays by the 20th.
- An events-first design pays for itself. One stream gave us the live canvas, replayable demos, auto-recorded video and a report that always matches the screen.
- Plausible-looking output is the most dangerous kind. Our worst bug produced a clean, confident, wrong answer. Tracing and reconciliation tests caught what eyeballing never would have.
- Give every AI call an honest fallback. Deterministic templates kept the whole loop working while API keys were still on their way.
What's next for Span
- Real phone calls end to end: Gemini Live and ElevenLabs over Photon/Twilio media streams.
- Agentverse registration, so ASI:One can find and talk to Span directly.
- Writing back to QuickBooks: posting payments, deferrals and payment plans through Intuit's API.
- More questions: "Should I take this order?", "Can I afford this hire?", "Who should I chase today?"
Built With
- caddy
- devin
- docker
- elevenlabs
- fastapi
- fetch.ai
- gemini
- imessage
- intuit
- openrouter
- paper
- photon
- postgresql
- python
- react
- scikit-learn
- sentry
- server-sent-events
- tiger-data
- timescaledb
- typescript
- uagents
- vite
- web-speech-api
Log in or sign up for Devpost to join the conversation.