Hassan SalemNotebook · München
← Notebook
AI1 November 20266 min read

The Small Text Change That Gave 41 Customers a Free Month

A text change and a billing change can be the same line of code. Review AI pull requests by what they touch, not by the title.


"Hey, why did nobody get an invoice today?"

Monday, 09:20. Sven from ops, in our billing channel. On a normal Monday we see a small wave of first invoices, from all the trials that ended over the weekend. Today the number was zero.

I wrote back "checking". In engineer language this means "I have no idea".

Four innocent lines

Our billing is not complicated. A scheduled job runs every morning, finds the trials that ended, and converts them to paid. I looked at the logs. The job ran fine. It just found nobody to convert.

So either no trial ended this weekend, or the job thinks trials are longer than they really are.

I opened a session with the agent and gave it a narrow task:

Do not change any code.
Look at the git history of the last 7 days.
Find every commit that could change which accounts
the trial conversion job selects. Explain how.

It needed less than a minute. A commit from Thursday afternoon, titled "Update trial text for autumn campaign". The author was Lena, our new product person.

The diff was four lines. Three in the pricing page template, "14 days free" became "30 days free". And one line in Kotlin:

object Plans {
    // before Thursday
    // val TRIAL_PERIOD: Duration = Duration.ofDays(14)
    val TRIAL_PERIOD: Duration = Duration.ofDays(30)
}

The agent explained the rest without me asking. This constant is read by the pricing page, by the signup flow, and by the conversion job, which calculates the trial end as createdAt + TRIAL_PERIOD. So from Thursday evening, every account in trial got 16 more days. Silently. It listed 41 accounts whose trial should have ended between Thursday and today.

I was frustrated. Not at Lena. At four lines that looked so harmless that three people let them pass.

The agent did exactly what she asked

I did not write in Slack. I called Lena. She was nervous already, she had seen Sven's message.

Her story was simple. Marketing wanted a 30 day trial for the autumn campaign. Our CEO said yes. Lena opened the agent in the repo and wrote: "Change the trial from 14 to 30 days."

And the agent did exactly that. To be fair, it did it well. It did not hardcode "30" in the HTML. It found the single source of truth and changed it there, so the page and the signup stay consistent. A good engineer would do the same thing. If the request is "make the trial 30 days", this is the correct change.

The question nobody asked was the boring one: for new customers only, or also for everyone who is in trial right now?

The agent answered her question perfectly. Nobody asked the second question, and the second question was the one about money.

Then the pull request. Omar, our other engineer, approved it at 17:50 on Thursday, between two calls. The title said "text". Three of four lines were text. Merge, auto deploy, done.

On Monday Omar said it himself, before I could say anything: "I read the title, not the file list." I have done exactly this in my career more than once, so I just nodded.

What stayed with me was Lena's sentence: "I thought I was only changing the website." For her, the pricing page is the product. For the code, the pricing page and the billing job read the same number. Both views are correct. Only one of them knows where the money is.

Not my decision

The technical part was quick. The agent wrote a query for the 41 accounts with their original trial end dates. I checked five of them by hand against the signup dates. Correct.

Then there was a choice. Bill them today with a short apology, or give them the 30 days as if it was planned.

This is not an engineering decision, even if it feels like one when you are the person holding the query. So I took the list to our CEO with both options in three sentences each. He thought for maybe ten seconds. "Give them the month. It costs less than one angry email thread." Fair.

The real fix was in the code. The requirement was never "the trial is 30 days". It was "people who sign up during the campaign get 30 days". That is a fact about an account, not about the world. So we store it on the account at signup:

fun startTrial(account: Account, now: Instant) {
    account.trialEndsAt = now.plus(plans.currentTrialPeriod())
}

// conversion job
fun accountsToConvert(today: LocalDate) =
    accounts.findByTrialEndsAtBefore(today.atStartOfDay(UTC).toInstant())

Here the agent was very helpful. It wrote the migration that backfills trialEndsAt for existing accounts, and it suggested by itself to keep the 41 accounts as an explicit list in the migration, with a comment linking the decision. I asked for one more test: change the plan default, and an existing account keeps its end date. It wrote it, I made it fail once on purpose to be sure it tests the right thing, then green.

A safe lane, not a locked door

In standup somebody said the obvious thing: "Maybe non-engineers should not push code anymore."

I said no. Lena's text change was useful. She did in ten minutes what would have waited in my backlog for a week, and our site has fewer typos since she has access. The problem was never who wrote the change. The problem was what the change touched.

So we changed two things.

First, a CODEOWNERS file with branch protection. Anything in the billing package or the Plans object needs an engineer's approval. Templates and texts, anyone on the team can approve.

Second, we added a rule to our CLAUDE.md, the instructions file the agent reads at the start of every session:

If a change touches billing/, Plans, or anything that decides
what a customer pays or when, stop before editing.
Tell the user this affects existing customers, and ask:
is this for new customers only, or for everyone?

The week after, Lena tried the same request again on a test branch, just to see. The agent stopped and asked her the question. She typed "new customers only, obviously" and laughed. Obvious for her. Never obvious for the code.

What I would tell another team

  1. Read the file list before the diff, and the diff before the title. A "text change" that touches a .kt file in billing is not a text change. This takes five seconds.
  2. Protect by blast radius, not by person. CODEOWNERS on money, auth and data deletion. Let everything else stay open and fast.
  3. Write the business question into the agent instructions. Agents are good at "how", and they rarely ask "for whom" by themselves. Put the question where the agent reads it every time.
  4. Store decisions on the record. If marketing can change a value, the value belongs to the account at the moment of signup, not to a global constant.
  5. Separate the fix from the decision. I could fix the code. Whether 41 customers pay today was the CEO's call, and he made it in ten seconds once he had two clear options.

Lena still ships small changes every week. Her last pull request was a typo fix in the onboarding email, and the description said: "Only text. Checked the file list." Nobody asked her to write that.

How do you let non-engineers ship, without letting them ship the billing job?

Written by Hassan Salem in Munich — software engineer, slow German learner, compulsive note-taker. More about me →

Telc German B1 Exam Success Kit (20/80 Rule)

If this was useful

Telc German B1 Exam Success Kit (20/80 Rule)

The 20% of German that keeps showing up in the telc B1 exam. Learn it first and walk in already knowing what to expect. €25

Get it on Gumroad — €25