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:

  1. The live canvas (Vite + React 19, designed in Paper), which draws the reasoning as a workflow.
  2. Deterministic replay: ?run=<id> replays any run with its original timing, and npm run record turns a replay into the demo video, stills and thumbnail.
  3. 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_plan or other
  • 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

Share this project:

Updates

Submission history