DEV Community

Cover image for I added 20 lines of code to stop my ERP from lying to me
Michel Faure
Michel Faure

Posted on

I added 20 lines of code to stop my ERP from lying to me

Comic strip — Michel sees a +9,318 line bump on his dashboard, hesitates, builds a watcher, gets the verdict

Hook

April 14th, 6:47 AM. My dashboard proudly announces a jump of 9,318 lines since yesterday. Of those 9,318, there are 5,037 that come from a SQL dump of already existing migrations. A technical export, not a line of new work. And yet the counter climbs, the valuation gauge shifts, and the "100K lines" achievement blinks green. I look at it for five seconds, coffee in hand. I understand that my own tool is lying to me with my consent. Worse: it's been doing it for three weeks, and I knew.

A few days earlier, Antoine had dropped by my office at a quarter to eight, hand on the doorframe. The former director, seventy-three years old, retiring in September. He didn't sit down. « Michel, combien vaut la maison aujourd'hui, dis-moi ? »Michel, what is the house worth today, tell me? I served him back a sentence that said nothing. « Évidemment. Bon, on avance. »Right. Let's move on. He walked out. The question stayed.


If you have 30 seconds. Measuring the value of internal software with lines × day-rate produces a number that diverges from reality as AI drives down the cost of writing. This article explains why I coded my own valuation instrument rather than delegate to a firm, the economic thesis that justifies it (a singular good needs a judgment device), and the twenty-line guardrail that keeps my counter from lying. Useful if you run internal software without a market price.

The price void

I run a ceramics school in Paris and the greater Paris area, six sites, several hundred students. For twenty-nine days, I've been coding — alone, with Claude Code — the business ERP that replaces our stack of tools. The system is called Rembrandt. At the time of writing, it contains 91,000 lines of TypeScript, 377 commits over four weeks, 16 documented architecture decisions. I'm not a developer by training.

An object like this never meets its price. No one buys a vertical ERP for a six-site art school on a market that doesn't exist. And production cost doesn't say much anymore either, because it's been divided by ten in eighteen months and keeps falling. In that void between a price that doesn't exist and a cost that no longer means anything, some measure has to stand in for a compass. By default, it's the line counter multiplied by a senior day-rate. Everyone knows the equation. It's seductive because it gives you a number, and a number makes the object exist as an asset rather than a side-project.

For three weeks, I watched my dashboard climb with that equation in my gut. Until the morning of April 14th.

The detour through the outside

One Monday, in a Paris meeting room, we had signed with a well-known European ERP vendor. Annual licenses, a five-figure consulting package, tacit renewal. Everyone was smiling. What no one read out loud was the billing grid for custom developments: per line of code produced. Technical annex, page 14. One line = one unit of value. We initialed.

Three days later, rereading the contract in my office, I understood — a little late — that the metric produces the code as much as it measures it: when you pay per line, you receive lines. I called the vendor. I asked where the prepaid scope ended and where billing-per-line started. The answer was polite and circular. To this day the vendor refuses any refund and the negotiation is still open.

It was the following Saturday, five days after signing, that I opened Claude Code for the first time.

The weekend that flipped

I'm not telling you about that Saturday for the sake of narrative. I'm telling you because it contains, in seed form, the mistake I was about to make against myself.

I flipped because the LOC metric no longer held at the vendor. A line of code billed as a unit of value, in a world where writing a line costs ten times less than it did two years ago. It doesn't take much reflection to see that this unit no longer holds on a vendor's side.

Forty-eight hours later, I had something running. A Supabase schema, three Next.js routes, a working authentication page. Nothing spectacular. Just proof that the alternative existed, and that it fit in a weekend.

It takes a little more to understand that you're applying to yourself the very metric you buried on the vendor's side. And yet that's exactly what my dashboard had been doing for twenty-one days, with my blessing. lines_total × 15 € somewhere deep inside a function, and a gauge that climbed on its own.

Three drifts the counter can't see

The first drift is the simplest. Production cost falls, the counter rises, the gap widens mechanically. By 2028, I could display 200,000 lines for a real cost of a few tens of thousands of euros. No accountant would sign off on that without raising an eyebrow. No buyer would pay that without an audit. The metric lies louder and louder, and it lies all the more loudly the longer you let it rise.

The second drift is more subtle. Of the 91,000 lines, about 10,000 do routine CRUD on contacts and forms, replaceable in one morning by a SaaS at 100 euros a month. Other bundles of 10,000 lines encode the logic of four catch-up periods per year across six sites with Qualiopi certification rules that no one else has ever needed to formalize. Same volume, real values incomparable. The counter sees bytes where it should see the commoditizable and the singular separately.

The third drift is the one that tipped me over. Rembrandt's real patrimony is not in the code. It's in about 3,000 historicized contacts, 5,000 qualified leads, 800 active enrollments, three years of reconciled financial history, and sixteen architecture decisions that crystallize why we do things this way and not otherwise. None of that weighs a single line of code. All of that weighs a significant share of what someone would pay to take the tool over.

The guardrail that settled it

The day after April 14th, I added a twenty-line guardrail to the snapshot cron. The idea is simple: any abnormal bump in lines_total must produce a warning before being cashed in as "progress".

// app/api/cron/compute-valorisation-donnees/route.ts
const { data: last7 } = await admin
  .from('valorisation_snapshots')
  .select('lines_total')
  .order('snapshot_date', { ascending: false })
  .limit(7)

const avg = last7.reduce((a, r) => a + r.lines_total, 0) / last7.length
const delta = loc.lines_total - (last7[0]?.lines_total ?? loc.lines_total)

if (delta > 3 * Math.max(avg * 0.02, 500)) {
  await postSlack(
    `:warning: abnormal bump lines_total: +${delta} ` +
    `(7-day avg ~${Math.round(avg * 0.02)}). ` +
    `Verify before counting as value.`
  )
}
Enter fullscreen mode Exit fullscreen mode

It isn't sophisticated. It's twenty lines of TypeScript that call a Slack webhook. But those twenty lines say something the previous twenty-one days weren't saying: an automatic counter that feeds into a value calculation must have a watcher. Without a watcher, the metric becomes an oracle that believes itself. That's exactly what had happened with the commercial vendor. That's exactly what I was about to do to myself.

I was also thinking about Antoine as I wrote this guardrail. He won't ask twice, and I don't want to hand him a number I haven't built myself. « Vous êtes sûr ? »Are you sure? — is a short question that demands, behind it, a method that can stand on its own.

The judgment device

There's an economic thesis, discreet but useful, that says singular goods — those without a market because they are unique and judged qualitatively rather than compared quantitatively — need a judgment device to circulate, defend themselves, and be valued. Lucien Karpik formalized it for wines, books, doctors. It applies word for word to a custom ERP. No market produces its price. It's the device that produces its discussable value.

What is at stake, then, in coding one's own valuation module, is not decorative. The instrument doesn't observe a pre-existing value that would be lying around, ready to be read. It manufactures the value as defensible: every euro it displays must be justifiable by a transparent method and a traceable source. That's what makes the object defensible before an accountant, a tax administration, a potential buyer. Without the device, there is no value — there's a director's gut feeling who happens to have coded a lot.

What it looks like in code

In concrete terms, the valorisation module fits into a snapshots table and four dimension tables. The heart of the consolidated API looks like this:

// lib/valorisation/compute.ts
export type Dimension = {
  id: 'saas' | 'usage' | 'donnees' | 'strategique'
  low: number | null
  high: number | null
  source: string        // table or method of origin
  refreshed_at: string  // ISO date
}

export function consolidate(dims: Dimension[]) {
  const present = dims.filter(d => d.low !== null && d.high !== null)
  return {
    value_low:  present.reduce((a, d) => a + (d.low  ?? 0), 0),
    value_high: present.reduce((a, d) => a + (d.high ?? 0), 0),
    dims_used:  present.map(d => d.id),
  }
}
Enter fullscreen mode Exit fullscreen mode

Three things this snippet says clearly. We sum the dimensions, we don't take the max, we don't weight. We keep track of which dimensions were used in each snapshot, so we can later explain why an interval moved. We accept null: if a dimension isn't yet instrumented, it doesn't break the calculation, it steps aside honestly.

The detail of the four dimensions deserves its own article, and that's the next one in the series. Here I'm only trying to say what I understood on the morning of April 14th. A measurement instrument you give yourself is not one more dashboard. It's the gesture by which a priceless object becomes an asset you can talk about. As long as the instrument is wrong, the asset remains a side-project telling itself stories. When the instrument begins to hold, the object begins to exist.

That's probably what you lose when you delegate the measurement, and what you regain by spending three hours on a Saturday coding yourself what stares at you every morning.

I'll have to go back to Antoine with the number. Not to convince him. To have the method that holds, the day he asks the question one last time.

What you can copy into your own project

Full snippets (the consolidate pattern, the cron guardrail, the valorisation_snapshots schema) in the series' companion repo, MIT license: github.com/michelfaure/rembrandt-samples.

Three directly applicable moves if you run internal software:

  1. A guardrail on any automatic counter that feeds a value calculation. The twenty-line Slack snippet above is the minimal example: detect abnormal bumps before they are cashed in as "progress"
  2. A consolidation structure summing several dimensions (the consolidate(dims) pattern), rather than a single metric. The detail of the four dimensions I use is covered in the next article
  3. A dated snapshot in the database (valorisation_snapshots with snapshot_date UNIQUE) that gives you a defensible history, auditable three months later

And a discipline: if you can't explain your valuation figure to a third party in ten minutes with traceable sources, your instrument doesn't hold. It doesn't matter how beautiful it is.

And you — how do you measure the value of your internal tool? I read the comments.


Companion code: rembrandt-samples/valorisation/ — the consolidate(dims) pattern and the 20-line Slack guardrail, MIT, copy-pastable.

Top comments (0)