Inspiration

One of us manages client accounts for small moving companies every day, and they all lose money the same way. Quotes are given over the phone and miss the fifth floor with no elevator. Deposits are chased by text message. The final invoice is typed by hand after the move, the extra hours turn into an argument, and the money shows up weeks later, if at all.

None of this needs a bigger company. It needs one assistant that listens to the customer, prices the job fairly, and then lets PayPal do the collecting. Our rule for that assistant: AI understands and explains. A person approves. PayPal moves the money.

What it does

MoveDesk runs the whole quote-to-cash cycle for a small mover.

  1. Chat intake. A customer describes the move in their own words, in English or Arabic, at any hour. Claude pulls out the job details (rooms, floors, elevator, distance, packing, contact). Every field is validated on its own; anything invalid is dropped and asked again.
  2. Estimate. A gradient boosting model predicts the hours with a P10 to P90 range and the drivers behind it ("stairs, no elevator: +1.3 h").
  3. Price. The company's own rate card turns hours into money in plain Decimal code. The AI never computes a price.
  4. Owner approval. The owner sees the price lines, what adds time and every assumption the agent made. Nothing is sent until they click Approve.
  5. Deposit. PayPal Invoicing creates and sends a 25% deposit invoice with a QR code, and the customer's chat shows a Pay deposit button.
  6. Booked. When PayPal's webhook says the invoice is paid, MoveDesk verifies the signature and reads the invoice back from PayPal before booking the job.
  7. Final invoice. The real hours produce the final invoice: overtime at the rate card price, and 5% off if paid within 3 days.
  8. Collections. If the discount window closes unpaid, MoveDesk cancels that invoice in PayPal and reissues it at full price. A second model predicts who is likely to pay late and picks the reminder schedule. Cash paid to the crew is recorded against the PayPal invoice.
  9. Owner copilot. The owner asks "who owes me money?" and gets an answer from live data. The copilot can only propose actions as buttons; it cannot change anything by itself.

The price is plain arithmetic, never a model output:

$$ \text{total} = h_b \cdot r + 2.20 \cdot \max(0,\ d - 30) + 1.50 \cdot b \cdot \mathbb{1}[\text{packing}] $$

where \( h_b \) is billable hours (half-hour steps, 3-hour minimum), \( r \) the crew's hourly rate, \( d \) the distance in km and \( b \) the number of boxes. Example: 5.5 h at $95 gives $522.50, a $130.63 deposit, and after 1.5 h of overtime a final invoice of $534.37, or $507.65 with the early-pay discount.

A Play the story button walks all ten steps on the hosted demo, with the customer's chat updating live beside the owner's board.

How we built it

  • One small app: a single FastAPI process (Python, Pydantic v2), one SQLite file, and static pages served by FastAPI. The owner board uses AG Grid Community. No separate frontend build.
  • PayPal: a direct REST client for Invoicing v2 (create, send, get, QR code, record payment, remind, cancel) and Webhooks v1 signature verification. OAuth tokens are cached, and every write carries a PayPal-Request-Id. A built-in simulator implements the same interface, so the app runs end to end with no keys and switches to the sandbox when credentials are set.
  • AI:
    • Claude tool use with a forced tool schema for intake. Customer text is wrapped and marked as data. A rule-based extractor takes over if there is no key or Claude fails.
    • Claude copilot with four read tools and one propose_action tool, capped at six rounds. Proposals are checked against the job's status before they appear as buttons. It answers offline if Claude is unavailable.
    • scikit-learn: a gradient boosting regressor with P10/P90 quantile models for hours, and a classifier for the chance of paying on time.
  • Safety: a job is paid only when a GET of that exact invoice returns PAID or MARKED_AS_PAID. Webhook events are de-duplicated, transitions only run from the expected state (a double approve returns 409 and creates one invoice), and owner routes need an admin key when real PayPal keys are set.
  • Hosting: the whole app ships from one render.yaml on Render. The free tier resets SQLite when it sleeps, so the demo reseeds on every boot and every judge gets a fresh company with jobs at every stage.
  • Tests: [49] automated tests cover quote math, approval gates, duplicate and forged webhooks, discount reissue, reminder schedules, prompt injection and the copilot's propose-only rule.

[HONESTY LINE, keep ONE: "The hosted demo runs the built-in PayPal simulator so judges can try it without accounts; we also ran the deposit flow on the real PayPal sandbox (invoice INV2-...)." OR "The hosted demo and video use the built-in PayPal simulator; the live sandbox client is in the repo and tested against a fake PayPal server."]

Challenges we ran into

  • The invoice note trap. PayPal's /send ignores a custom note, so the explanation goes into detail.note when the draft is created.
  • Two shapes for one webhook. The paid event puts the invoice id at resource.id; older samples nest it. The handler reads both, and both are tested.
  • Two shapes for one QR code. The docs describe the QR response as Base64 text in one place and JSON in another. The client accepts both, and the deposit still goes out with a pay link if the QR call fails.
  • Cancel and pay at the same moment. A customer can pay the discounted invoice just as we cancel it. MoveDesk reads the status first (paid wins), and a late payment on a replaced invoice is flagged for the owner instead of lost.
  • Free hosting that forgets. Render's free tier wipes SQLite on sleep, so we made auto-seeding on boot a feature.

Accomplishments that we're proud of

  • The full Invoicing lifecycle in one product: deposit, QR, verified payment, final invoice with discount, cancel and reissue, cash recording and reminders.
  • No AI output can set a price, approve a quote, send an invoice or mark a job paid.
  • It works with no keys at all: the simulator, rule-based intake and offline copilot keep the whole story running, clearly labelled.
  • A duration model with a held-out error of 0.81 hours against 2.61 hours for a rooms-only baseline, on the dataset in the repo.

What we learned

  • Collections is where small businesses lose money, and timing matters as much as the invoice itself.
  • Giving the copilot only a "propose" tool was simpler and safer than policing an agent that can act.
  • A simulator with the same interface as the live client let us build and test the whole loop before touching the sandbox.

What's next for MoveDesk

  • Card-on-file deposits and calendar scheduling.
  • A rate card editor for each company.
  • Distance from addresses instead of asking the customer.
  • Postgres, and retraining the models on a real company's job history.

Built With

Share this project:

Updates

Submission history