Founder & AI engineer · Germany & India

I build the thing,
then I ship it.

I’m Nikhilvarma, a founder and AI engineer. I build AI products end to end. Ganymede decides which delinquent borrower is worth an agent-minute, then coaches the call that follows. Witness turns a photograph of a returned gearbox part into an ISO failure record linked to the batch that made it. FirstChair reads how ChatGPT, Gemini and Perplexity describe a law firm against its competitors. Before all of it I spent eighteen months inside a US fintech, rebuilding a monolith into event-driven services for 500+ concurrent users and going from data engineer to lead developer. 6 products are live below, withseven investigations and a peer-reviewed paper behind them.

Shipped & live · 6

Things I’ve shipped

Not prototypes and not screenshots. Six products running in production right now, each with the build written up: what it does, how it is put together, and what each decision cost.

08 · Ganymede

08 · Shipped & live · 2026

Ganymede

Which delinquent borrower is worth an agent-minute, and what to say once the call starts.

A collections team has more accounts in arrears than it has hours to call them. Ganymede decides which accounts are worth calling, then helps the agent through the call that follows. It ranks by the money a call is expected to recover rather than by the probability the borrower defaults. Those two orderings are not the same, and the difference is most of the value. The outcome of each call becomes the label that retrains the model that picked it.

The questionA risk score sorts borrowers by how likely they are to get worse. A collections team does not need that list. It needs the list of calls that recover the most money per agent-minute. How much does the difference between those two lists actually cost?

How it’s put together

  1. 01

    Panel

    • Loan-level servicing history
    • Monthly borrower panel
    • Contact events by channel

    Real dates, real delinquency transitions.

  2. 02

    Score

    • L1 trajectory: does this worsen
    • L2 self-cure: does it fix itself
    • L4 promise kept
    • Calibration, then reason codes

    Two questions, not one.

  3. 03

    Allocate

    • Uplift over self-cure
    • Weighted by exposure
    • Under a capacity constraint
    • Do not contact is a scored action

    Expected value, never probability.

  4. 04

    Coach

    • Tier 1 renders under a millisecond
    • Tier 2 waits for the next pause
    • Branches on borrower state

    Two tiers, set by the measured gap.

  5. 05

    Learn

    • Every decision logs its arm and propensity
    • Promise resolved against payment
    • Drift monitors on input and score
One loop, two lenses. The risk lens picks the conversation. The coach lens shapes it. The outcome of that conversation is the label both lenses retrain on. Split them and each half degrades: a queue nobody knows how to work, or advice with no idea who it is talking to.

Rough sketch

Allocator studio

Agent desk

Evidence

The allocator studio leads with both queues side by side, because the comparison is the finding. A single ranked list would hide it.

What happens, step by step

  1. Build the panel. Loan-level servicing history becomes a monthly borrower panel. Calendar dates are the point: a timing feature from a source without real dates is refused at the feature layer rather than caught in review.
  2. Score two questions, not one. L1 asks whether the account worsens over the next ninety days. L2 asks whether it recovers without contact. An agent reads those numbers at face value, so calibration is the gate rather than AUC.
  3. Rank by money, not by risk. The allocator maximises expected recovered value per agent-minute: uplift over self-cure, weighted by exposure, under the capacity the team actually has. Probability never sorts the queue.
  4. Coach inside the measured gap. 328 inter-turn gaps from a real call give a median of 479 ms and a p25 of 292 ms. The budget is 300 ms because that is what the distribution allows. A hint that misses the gap arrives after the moment it was for.
  5. Log the decision, then resolve it. Every score, hint and override is written down with its experiment arm and its propensity. When the payment arrives or does not, the promise resolves and both lenses retrain on the result.

A decision that shaped it

Rank by expected valueinstead of Rank by probability of default

Risk-ranking calls a 1,289 euro account early and never reaches a 1.93 million euro one anywhere in the capacity sweep. Twelve agent-minutes cost more than the whole uplift on the small account is worth. At 15% capacity the value ordering recovers 59% more using roughly half the contacts. At 60% the edge falls to 2.4%, because with enough agents to call everyone the ordering stops mattering. The gain lives exactly where the constraint is real.

  • Python
  • Polars
  • LightGBM
  • scikit-learn
  • Calibration
  • Uplift modelling
  • LLM orchestration
  • Vercel
09 · Witness

09 · Shipped & live · 2026

Witness

A photograph of a returned gearbox part becomes an ISO failure record linked to the batch that made it.

A returned gearbox part arrives with a complaint and almost nothing else. Someone photographs it, writes one sentence in a spreadsheet, and puts the part in a bin. When the same damage appears on a later batch, nobody can prove it, because the first record said "worn" and the second said "pitting" and neither cited a standard. Witness makes the record the product. A photograph becomes a record with a damage mode, a clause number, a severity, a cause and a link to the batch that made the part. The model suggests. The inspector decides.

The questionThe failure was never the hard part. The record was. How do you turn a photograph and a one-line complaint into evidence a warranty claim can stand on?

How it’s put together

  1. 01

    Intake

    • EXIF read
    • Provenance scored
    • Perceptual hash against the tenant's assets
    • A re-sent photo is caught as a duplicate

    Before anything is believed.

  2. 02

    Enrol

    • The tenant's own known-good photos
    • A coreset is trained from them
    • Threshold calibrated leave-one-image-out

    Per part family, per tenant.

  3. 03

    Detect

    • PatchCore-style memory bank
    • Nearest-neighbour distance
    • Anomaly score and heat map
    • No database credentials in the function

    Abnormal against normal, nothing more.

  4. 04

    Classify

    • ISO 15243 for bearings
    • ISO 10825 for gear teeth
    • An off-list code is discarded
    • Confidence gate at 0.70

    A forced choice, never free text.

  5. 05

    Decide

    • High confidence files itself
    • Low confidence queues for review
    • Inspector confirms or corrects
    • Model suggestion kept beside it, immutable

    The human call is authoritative.

  6. 06

    Attribute

    • Mode, severity and cause as three axes
    • Finding linked to batch and supplier
    • Warranty report
    • Label export as model and human pairs
The confidence gate is the one place the software decides whether a person is needed. Everything before it is a pipeline stage that does one job and hands off. Everything after it is a human decision that the system records and never overwrites.

Rough sketch

Review queue

Finding detail

Insights cube

The review queue is the product's centre of gravity. It is the only screen where a person changes what the system believes.

What happens, step by step

  1. Read the photograph before trusting it. EXIF gives a provenance score. A perceptual hash is checked against the tenant's other assets, so the same part photographed twice and sent twice is recognised rather than counted twice.
  2. Learn what good looks like. Enrolment is the tenant's own photographs of undamaged parts, per part family. There is no synthetic defect data anywhere in the system, because a generated defect teaches the model the generator.
  3. Ask only whether the part is abnormal. Stage one is a memory bank of normal patch features scored by nearest-neighbour distance. It answers one question. It does not name the damage.
  4. Force the answer onto a published standard. Stage two picks one code from the part family's ISO catalogue. It cannot invent a code, because an invented one is discarded rather than stored. The finding carries a clause number, so it traces back to the document.
  5. Send the unsure ones to a person. Above the gate the record files itself. Below it the record waits in the review queue. The threshold is visible on the page rather than buried in a config file.
  6. Record the mechanism, not only the surface. The same pit can come from fatigue at end of life or from a contaminant dent that started it early. The damage mode looks identical. The batch-level action does not, so attribution is a separate axis.
  7. Close the loop. Every confirmed record becomes a model-and-human pair in the label export. The training data is the inspector's work, collected as a by-product of doing it.

A decision that shaped it

Classify against ISO 15243 and ISO 10825instead of A house taxonomy of damage names

A house taxonomy is a private opinion. A warranty claim needs a clause number from a published standard, and two inspectors using the same catalogue disagree far less than two inspectors writing prose. The standard also makes the record portable to a customer who never saw this tool.

  • TypeScript
  • Next.js
  • React
  • Supabase
  • Postgres RLS
  • Python
  • NumPy
  • Computer vision
  • Vercel
10 · Bud

10 · Shipped & live · 2026

Bud

Habit tracking that borrows from games instead of from spreadsheets.

Bud is a habit tracker that borrows the parts games already solved: visible progress, a reward that lands immediately, and a cost for slipping that does not wipe out your history. There is no account and no sign-up: it opens instantly and your streak lives in your own browser. It exists because the honest problem with habit trackers is not that they lack features, it is that opening them is itself a habit most people fail at.

The questionWhy do people abandon habit trackers in the second week, and can the tracker's own design be the fix rather than the cause?

How it’s put together

  1. 01

    Open

    • Static HTML and JS
    • No build step
    • No auth wall

    The number that matters is time-to-first-interaction.

  2. 02

    State

    • Habits and streaks in browser storage
    • No account, no sync, no server copy

    Yours, on your device.

  3. 03

    Loop

    • Immediate reward on completion
    • Long-arc progress
    • Forgiving decay on a miss

    Where the actual design work is.

  4. 04

    Surface

    • Today's list
    • Streak and progress state
    • History
Local-first by construction. There is no server in this diagram because there is no server in the product: the browser holds the state and the reward loop reads from it directly.

Rough sketch

A fresh day

Streak building

After a miss

One screen, three states: the app never navigates, it just responds. A fresh day, a streak building, and the day after a miss: decayed but not reset to zero.

What happens, step by step

  1. Open it. No login, no splash, no loading state worth naming. The app is small enough that it is simply there.
  2. Mark something done. The reward fires at the moment of completion, not at the end of the week, and not once the habit has paid off in real life. That gap is where most habits die.
  3. Watch the long arc. A second, slower progression runs underneath the daily one, so a good week is visible as something other than a bigger number.
  4. Miss a day. Progress decays. It does not reset. Losing three weeks of work for one missed Tuesday teaches you to stop opening the tracker, which is the opposite of the goal.

A decision that shaped it

No framework, no build stepinstead of React or Svelte with a bundler

A tracker that takes four seconds to open is a tracker you stop opening. The entire application is smaller than a typical framework runtime, and the cost of that choice, hand-written DOM updates, is one I actually pay, in a codebase this size, willingly.

  • JavaScript
  • Local-first state
  • Vercel
11 · Cartwise

11 · Shipped & live · 2026

Cartwise

Photograph a receipt, get a nutrition summary. The data entry is the whole problem.

Cartwise turns a supermarket receipt into a nutrition summary. You photograph the receipt, it reads the lines, works out what each abbreviated item actually is, and tells you what came into the house on that shop. It does not ask you to log anything, because the receipt was already written for you, and the reason nutrition apps get abandoned is not motivation, it is that logging is a chore you have to remember at the exact moment you least want to.

The questionCan the data-entry step be removed entirely, by using a record the shop already produced, instead of asking a person to reproduce it by hand?

How it’s put together

  1. 01

    Capture

    • Receipt photo upload
    • Text extraction
    • Line splitting

    The input already exists. That is the whole bet.

  2. 02

    Resolve

    • Truncated and abbreviated line items
    • Retailer-specific shorthand
    • Match to a known product

    The genuinely hard part.

  3. 03

    Enrich

    • Nutrition lookup per matched item
    • Quantity and unit normalisation
  4. 04

    Report

    • Per-shop summary
    • History across shops
    • What entered the house, not what was eaten
Everything hard is in the middle lane. The nutrition arithmetic at the end is trivial; turning MLK SEMI 2PT into semi-skimmed milk, two pints, is where accuracy is won or lost.

Rough sketch

Upload

Parsed items

Summary

Upload, review the parse, then the summary. The review step exists because the parse is the part that can be wrong.

What happens, step by step

  1. Photograph the receipt. The one action the user takes. Everything after this is the app's problem.
  2. Read the lines. The receipt is split into individual line items with quantities and prices.
  3. Resolve each line to a real product. This is the accuracy bottleneck. Receipt lines are truncated, abbreviated and retailer-specific: MLK SEMI 2PT has to become semi-skimmed milk, two pints, and every retailer abbreviates differently.
  4. Attach nutrition. Once a line is resolved to a product, the nutrition data is a lookup. This step is easy, which is exactly why the previous one matters.
  5. Summarise the shop. The output describes what entered the house over that shop, deliberately not what was eaten, which is a different and much harder question the app does not pretend to answer.

A decision that shaped it

The receipt as the inputinstead of Manual food logging

Receipts are generated automatically, itemised, and exist whether or not the user decides to create them. Making the receipt the input means logging stops being a habit that has to be maintained, which is the failure mode, not a feature gap.

  • Next.js 15
  • TypeScript
  • Prisma
  • Vercel
12 · FirstChair

12 · Shipped & live · 2026

FirstChair

How do ChatGPT, Gemini and Perplexity describe your law firm versus your competitors?

FirstChair watches how AI assistants talk about a law firm. When someone asks ChatGPT, Gemini or Perplexity to recommend a lawyer in their city, the answer they get is now part of that firm's reputation, and until recently nobody was measuring it. The tool asks each model the same questions repeatedly and reports what proportion of the time the firm shows up, where in the answer, and how it is described relative to named competitors.

The questionA firm can rank first on Google and still be invisible, or actively misdescribed, when a client asks an assistant instead. How do you measure something whose answer changes every time you ask?

How it’s put together

  1. 01

    Prompt set

    • Firm and competitor names
    • City and practice-area framing
    • Held constant across runs

    Fixed, so runs are comparable over time.

  2. 02

    Sample

    • ChatGPT
    • Gemini
    • Perplexity
    • N runs per prompt

    Repeatedly, not once.

  3. 03

    Extract

    • Mentioned or not
    • Position in the answer
    • Framing and sentiment
    • Competitors named alongside
  4. 04

    Aggregate

    • Mention rate across runs
    • Typical position
    • Movement over time

    A distribution, never a rank.

The scheduler and the sampler are the product. A version of this that queries once and prints a rank would be quicker to build and would be reporting noise.

Rough sketch

Firm overview

Model detail

The dashboard leads with the distribution, because that is the finding. A single number would be the wrong headline.

What happens, step by step

  1. Fix the questions. A firm, its named competitors, a city and a practice area become a fixed prompt set. Changing the questions between runs would make the time series meaningless, so they are held constant.
  2. Ask every model, repeatedly. Each prompt goes to ChatGPT, Gemini and Perplexity, many times. One measurement of a non-deterministic system is an anecdote.
  3. Read each answer. For every response: was the firm mentioned at all, in what position, in what framing, and who else appeared alongside it.
  4. Report a distribution. Mentioned in 7 of 10 runs, usually third is a real answer. Ranked third is a fabrication, and it is the answer a naive version of this product would give.
  5. Track the movement. Because the models change underneath without notice or changelog, the useful output is the trend across scheduled runs rather than any single reading.

A decision that shaped it

Sample repeatedly and report a distributioninstead of One query, one rank, like an SEO tool

Model output is non-deterministic and the underlying model is unversioned. The same prompt returns different text run to run, and the model itself changes without a changelog. A single measurement is noise presented as a number. This is the whole product; getting it wrong makes everything else decoration.

  • Next.js
  • TypeScript
  • Supabase
  • LLM orchestration
  • Vercel
13 · Knock

13 · Shipped & live · 2026

Knock

Local services booking, built end to end.

Knock is a booking site for local services: a plumber, a cleaner, a tutor. A customer finds a provider, sees which times are genuinely free, and takes one. The provider sees the same calendar from the other side and manages what arrives. Almost none of the work is in the pages you can see. It is in making sure two people can never both walk away believing they hold the same eleven o'clock on Thursday.

The questionHow do you let two strangers book against one calendar at the same instant without telling either of them something untrue?

How it’s put together

  1. 01

    Browse

    • Provider directory
    • Service detail
    • Availability calendar

    Read-heavy, cacheable, and allowed to be slightly stale.

  2. 02

    Claim

    • Slot claim request
    • Conflict check against held slots
    • Accept or reject, never maybe

    The single write path. Everything contends here on purpose.

  3. 03

    State

    • Slot records
    • Booking records
    • Provider profiles and hours

    Slot is authoritative. The booking record is downstream of it.

  4. 04

    Manage

    • Customer appointments
    • Provider queue
    • Cancel and release
The slot is the object with state; a booking is what happens when a claim on a slot succeeds. Every write funnels through the claim path, which is the only code in the system allowed to say yes.

Rough sketch

Provider detail

Provider queue

Customer-side booking and the provider's queue, at the level of what sits where.

What happens, step by step

  1. Find someone. The customer browses providers by service, and opens one to see hours and availability.
  2. See real availability. The calendar renders from the provider's own working hours minus everything already held, in the customer's timezone rather than the provider's.
  3. Claim a slot. Choosing a time submits a claim, not a booking. The claim either wins the slot or is refused. There is no intermediate state a customer could misread as confirmed.
  4. Both sides update. A won claim writes the booking and removes the slot from every other customer's view. The provider's queue picks it up from the same record.
  5. Manage afterwards. Either side can cancel, which releases the slot back into availability rather than deleting history.

A decision that shaped it

Model the slot, not the bookinginstead of A bookings table with a uniqueness constraint bolted on

If the booking is the object, two simultaneous requests are both valid until a constraint rejects one, and by then the interface has already told both customers yes. Making the slot the thing with state means the contention is visible in the design instead of surfacing as a database error.

Data & research · 7

Questions worth measuring

Every figure below came out of the analysis, including the one that found nothing.

Currently

Studying, building, and looking

M.Sc. Big Data & Business Analytics at FOM Hochschule, through August 2027. Shipping products alongside it. Open to data, software and AI engineering roles anywhere in Germany, and to full-time work in India.