OnisAI · Shipped

Screenshot in, scored decision out

A multi-tenant operations platform for rideshare drivers. It ingests an offer as an image, parses it out of noisy OCR, scores it against a model of what the job actually pays, and carries it through its lifecycle with a full audit trail.

Software Developer & AI Automation · Contract Oct 2025 – Jun 2026 onisai.com Live demo GitHub

A rideshare offer arrives on a countdown. The screen shows a fare and two distances, but never the number that decides whether the job is worth taking: what it pays per hour once the unpaid miles to reach the passenger and the fixed overhead of every job are taken out of it. There is not enough time to work that out by hand, so drivers fall back on rules of thumb that are wrong in both directions.

There is a working demo of the scoring engine at onisai.com/demo, and a longer technical writeup at onisai.com/engineering. The demo runs the production scoring module in the browser against synthetic offers.
OnisAI scoring demo: a queue of offers with pay and pickup verdicts, and a breakdown panel explaining one decision
The scoring engine running in the browser. Each offer carries a pay verdict and a pickup verdict, and the panel explains why — trip band, the thresholds it was judged against, and what the unpaid pickup costs. Try it at onisai.com/demo.

What I worked on

Built with a cross-functional team. My contribution was the OCR pipeline, the scoring engine, backend components for parsing, validation, summarisation and reporting, and dashboard UI work.

ResultHow
~98% OCR accuracyMulti-stage validation with fallback logic
~90% less manual processingEnd-to-end capture → structured JSON
~1.2s end to endLow-latency pipeline from capture to scored verdict
9 metrics per offerScoring isolated as a pure function, no I/O

The OCR figure is the one that mattered commercially. A parser that is right most of the time is worse than useless when each mistake costs money, which is why the fallback path and the manual correction loop exist at all.

Three decisions worth explaining

Pickup cost is relative, not absolute

A two-mile pickup is wasteful for a three-minute hop and trivial for a forty-minute motorway run. So the thresholds are not fixed. The engine classifies the trip into a band first — short, medium or long — then judges the pickup against that band.

There is a deliberate asymmetry inside each band: a CLOSE verdict requires both distance and time to be within bounds, while SLIGHTLY FAR accepts either. Distance and time diverge under traffic, and a pickup that is short in miles but long in minutes is a traffic problem rather than a distance one. It should degrade to a warning, not pass as clean.

This is also the idea the corridor engine later generalises: once you accept that context changes what a number means, the next question is what the route between those two points passes through.

Charge the overhead before judging the rate

Nominal hourly rate pretends a job ends the instant the passenger steps out. Every job really costs a couple of minutes of waiting, loading and repositioning, and that overhead is proportionally brutal on short trips. Charging it first is what stops the engine over-rewarding quick hops that feel lucrative and are not.

scoring.js
hourly_adj = price / max(trip_min + overheadMinutes, 1) * 60

The overhead and both accept/reject thresholds are environment-configurable rather than hard-coded, because the right numbers differ by city, vehicle and driver. The defaults are starting points, not truths.

Degrade, do not crash

The capture path is the one thing that must never fail: an offer is gone in seconds and will not be shown again. So Cloud Vision is an optional dependency — required lazily, with the load error cached and a structured failure returned rather than thrown.

A missing OCR provider therefore degrades one request instead of taking down the process. A bad parse is recoverable too: the trip lands in offered_parse_failed with its raw OCR text intact, and the driver corrects the fields by hand. Those corrections are then rescored through the same function as the original parse rather than patched into the stored row — otherwise a trip can end up displaying a fare and a verdict that contradict each other.

An event log, not a status column

Trips move through offered, accepted and completed, with rejected and cancelled as terminal alternatives, and invalid transitions are rejected explicitly rather than silently succeeding. Every transition appends to an event log, and each event carries a snapshot of the prior state.

That one choice makes undo nearly free: the undo path reads the last status event, restores the previous state wholesale, and appends an event of its own. No reverse state machine to write and keep correct, and no history destroyed — the undo is itself a record.

It matters because a driver tapping the wrong button in a moving vehicle is not an edge case, it is routine. Undo had to be one tap and impossible to get wrong.

Closing the loop

The platform's quoted trip time is an estimate; what actually happened is knowable only afterwards, and the system holds both numbers. On completion it compares elapsed time against the original estimate and grades the difference into a traffic level.

The grading is not a plain threshold ladder. Long trips absorb small absolute overruns — three minutes late on a forty-minute journey is noise, not congestion — so any trip of twenty minutes or more landing within ±10% of estimate is treated as normal flow regardless of the absolute delta. Judging delay in raw minutes would systematically over-report congestion on exactly the long trips the engine most wants to recommend.

This is the feedback loop that made the corridor work possible: once every completed trip produces a data point about how a route behaved at a given hour, evidence-based routing stops being hypothetical.